From fb7003a5acb0c7b70905d616a28768bd468f98f6 Mon Sep 17 00:00:00 2001 From: Patrick Franz Date: Sat, 27 Jan 2024 08:52:51 +0100 Subject: [PATCH 1/1] Import plasma-discover_5.27.10.1.orig.tar.xz [dgit import orig plasma-discover_5.27.10.1.orig.tar.xz] --- .git-blame-ignore-revs | 2 + .gitignore | 30 + .gitlab-ci.yml | 12 + .kde-ci.yml | 32 + CMakeLists.txt | 128 + DiscoverVersion.h.in | 14 + LICENSES/BSD-3-Clause.txt | 26 + LICENSES/CC0-1.0.txt | 121 + LICENSES/GPL-2.0-only.txt | 319 ++ LICENSES/GPL-2.0-or-later.txt | 319 ++ LICENSES/GPL-3.0-only.txt | 625 +++ LICENSES/LGPL-2.0-or-later.txt | 446 ++ LICENSES/LGPL-2.1-only.txt | 467 ++ LICENSES/LGPL-3.0-only.txt | 163 + LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + LICENSES/LicenseRef-KDE-Accepted-LGPL.txt | 12 + Messages.sh | 7 + cmake/FindPackageKitQt2.cmake | 35 + discover/AbstractAppsModel.cpp | 194 + discover/AbstractAppsModel.h | 58 + discover/CMakeLists.txt | 115 + discover/DiscoverDeclarativePlugin.cpp | 64 + discover/DiscoverDeclarativePlugin.h | 17 + discover/DiscoverObject.cpp | 657 +++ discover/DiscoverObject.h | 100 + discover/FeaturedModel.cpp | 89 + discover/FeaturedModel.h | 27 + discover/OdrsAppsModel.cpp | 27 + discover/OdrsAppsModel.h | 18 + discover/PaginateModel.cpp | 388 ++ discover/PaginateModel.h | 119 + discover/PowerManagementInterface.cpp | 160 + discover/PowerManagementInterface.h | 64 + discover/ReadFile.cpp | 98 + discover/ReadFile.h | 60 + discover/UnityLauncher.cpp | 70 + discover/UnityLauncher.h | 38 + discover/autotests/CMakeLists.txt | 22 + discover/autotests/DiscoverTest.qml | 125 + discover/autotests/PaginateModelTest.cpp | 154 + discover/autotests/appstreamUrl.qml | 11 + discover/autotests/categoryArg.qml | 12 + discover/autotests/install.qml | 41 + discover/autotests/missingResource.qml | 9 + discover/autotests/packageArgument.qml | 12 + discover/autotests/toplevels.qml | 110 + discover/autotests/updateandinstall.qml | 41 + discover/autotests/wrongInput.qml | 8 + discover/discover.schema | 329 ++ discover/discoversettings.kcfg | 8 + discover/discoversettings.kcfgc | 5 + discover/icons/128-apps-plasmadiscover.png | Bin 0 -> 4687 bytes discover/icons/16-apps-plasmadiscover.png | Bin 0 -> 464 bytes discover/icons/22-apps-plasmadiscover.png | Bin 0 -> 736 bytes discover/icons/32-apps-plasmadiscover.png | Bin 0 -> 965 bytes discover/icons/48-apps-plasmadiscover.png | Bin 0 -> 1422 bytes discover/icons/64-apps-plasmadiscover.png | Bin 0 -> 2111 bytes discover/icons/CMakeLists.txt | 11 + discover/icons/sc-apps-plasmadiscover.svg | 41 + discover/main.cpp | 225 + discover/org.kde.discover.appdata.xml | 294 ++ discover/org.kde.discover.desktop.cmake | 207 + discover/plasmadiscoverui.rc | 10 + discover/plasmauserfeedback.kcfg | 7 + discover/plasmauserfeedback.kcfgc | 5 + discover/qml/AboutPage.qml | 14 + discover/qml/ActionListItem.qml | 61 + discover/qml/AddSourceDialog.qml | 71 + discover/qml/AddonsView.qml | 72 + discover/qml/ApplicationDelegate.qml | 169 + discover/qml/ApplicationPage.qml | 947 ++++ discover/qml/ApplicationResourceButton.qml | 86 + discover/qml/ApplicationScreenshots.qml | 234 + discover/qml/ApplicationsListPage.qml | 252 + discover/qml/BrowsingPage.qml | 188 + discover/qml/ConditionalLoader.qml | 21 + discover/qml/ConditionalObject.qml | 26 + discover/qml/ConvertDiscoverAction.qml | 21 + discover/qml/DiscoverDrawer.qml | 201 + discover/qml/DiscoverInlineMessage.qml | 28 + discover/qml/DiscoverPage.qml | 57 + discover/qml/DiscoverWindow.qml | 473 ++ discover/qml/Feedback.qml | 81 + discover/qml/GridApplicationDelegate.qml | 68 + discover/qml/InstallApplicationButton.qml | 89 + discover/qml/InstalledPage.qml | 22 + discover/qml/LabelBackground.qml | 49 + discover/qml/LoadingPage.qml | 11 + discover/qml/ProgressView.qml | 127 + discover/qml/Rating.qml | 52 + discover/qml/ReviewDelegate.qml | 142 + discover/qml/ReviewDialog.qml | 80 + discover/qml/ReviewsPage.qml | 87 + discover/qml/SearchField.qml | 51 + discover/qml/SearchPage.qml | 59 + discover/qml/Shadow.qml | 35 + discover/qml/SourcesPage.qml | 308 ++ discover/qml/TopLevelPageData.qml | 17 + discover/qml/UpdatesPage.qml | 546 +++ discover/qml/WebflowDialog.qml | 55 + discover/qml/navigation.js | 52 + discover/resources.qrc | 42 + exporter/CMakeLists.txt | 3 + exporter/DiscoverExporter.cpp | 92 + exporter/DiscoverExporter.h | 34 + exporter/main.cpp | 51 + kcm/CMakeLists.txt | 33 + kcm/discoversettings.kcfg | 18 + kcm/discoversettings.kcfgc | 11 + kcm/kcm_updates.json | 115 + kcm/package/contents/ui/main.qml | 134 + kcm/updates.cpp | 54 + kcm/updates.h | 39 + kcm/updatessettings.kcfg | 23 + kcm/updatessettings.kcfgc | 11 + libdiscover/ApplicationAddonsModel.cpp | 157 + libdiscover/ApplicationAddonsModel.h | 56 + libdiscover/CMakeLists.txt | 72 + libdiscover/CachedNetworkAccessManager.cpp | 33 + libdiscover/CachedNetworkAccessManager.h | 20 + libdiscover/Category/CategoriesReader.cpp | 60 + libdiscover/Category/CategoriesReader.h | 19 + libdiscover/Category/Category.cpp | 401 ++ libdiscover/Category/Category.h | 124 + libdiscover/Category/CategoryModel.cpp | 110 + libdiscover/Category/CategoryModel.h | 41 + libdiscover/DiscoverBackendsFactory.cpp | 132 + libdiscover/DiscoverBackendsFactory.h | 32 + .../ReviewsBackend/AbstractReviewsBackend.cpp | 58 + .../ReviewsBackend/AbstractReviewsBackend.h | 63 + libdiscover/ReviewsBackend/Rating.cpp | 149 + libdiscover/ReviewsBackend/Rating.h | 45 + libdiscover/ReviewsBackend/Review.cpp | 135 + libdiscover/ReviewsBackend/Review.h | 71 + libdiscover/ReviewsBackend/ReviewsModel.cpp | 184 + libdiscover/ReviewsBackend/ReviewsModel.h | 78 + libdiscover/ScreenshotsModel.cpp | 113 + libdiscover/ScreenshotsModel.h | 51 + libdiscover/Transaction/AddonList.cpp | 70 + libdiscover/Transaction/AddonList.h | 39 + libdiscover/Transaction/Transaction.cpp | 149 + libdiscover/Transaction/Transaction.h | 206 + .../Transaction/TransactionListener.cpp | 150 + libdiscover/Transaction/TransactionListener.h | 65 + libdiscover/Transaction/TransactionModel.cpp | 217 + libdiscover/Transaction/TransactionModel.h | 74 + libdiscover/UpdateModel/UpdateItem.cpp | 73 + libdiscover/UpdateModel/UpdateItem.h | 77 + libdiscover/UpdateModel/UpdateModel.cpp | 385 ++ libdiscover/UpdateModel/UpdateModel.h | 91 + .../appstream/AppStreamIntegration.cpp | 29 + libdiscover/appstream/AppStreamIntegration.h | 33 + libdiscover/appstream/AppStreamUtils.cpp | 299 ++ libdiscover/appstream/AppStreamUtils.h | 49 + libdiscover/appstream/OdrsReviewsBackend.cpp | 413 ++ libdiscover/appstream/OdrsReviewsBackend.h | 97 + libdiscover/backends/CMakeLists.txt | 56 + .../backends/DummyBackend/CMakeLists.txt | 21 + .../backends/DummyBackend/DummyBackend.cpp | 191 + .../backends/DummyBackend/DummyBackend.h | 58 + .../backends/DummyBackend/DummyNotifier.cpp | 22 + .../backends/DummyBackend/DummyNotifier.h | 34 + .../backends/DummyBackend/DummyResource.cpp | 220 + .../backends/DummyBackend/DummyResource.h | 78 + .../DummyBackend/DummyReviewsBackend.cpp | 87 + .../DummyBackend/DummyReviewsBackend.h | 65 + .../DummyBackend/DummySourcesBackend.cpp | 84 + .../DummyBackend/DummySourcesBackend.h | 43 + .../DummyBackend/DummyTransaction.cpp | 81 + .../backends/DummyBackend/DummyTransaction.h | 29 + .../DummyBackend/dummy-backend-categories.xml | 84 + .../DummyBackend/tests/CMakeLists.txt | 4 + .../backends/DummyBackend/tests/DummyTest.cpp | 286 ++ .../backends/DummyBackend/tests/DummyTest.h | 36 + .../DummyBackend/tests/UpdateDummyTest.cpp | 156 + .../backends/FlatpakBackend/CMakeLists.txt | 45 + .../FlatpakBackend/FlatpakBackend.cpp | 1944 ++++++++ .../backends/FlatpakBackend/FlatpakBackend.h | 155 + .../FlatpakBackend/FlatpakFetchDataJob.cpp | 60 + .../FlatpakBackend/FlatpakFetchDataJob.h | 20 + .../FlatpakBackend/FlatpakJobTransaction.cpp | 103 + .../FlatpakBackend/FlatpakJobTransaction.h | 41 + .../FlatpakBackend/FlatpakNotifier.cpp | 155 + .../backends/FlatpakBackend/FlatpakNotifier.h | 53 + .../FlatpakBackend/FlatpakPermission.cpp | 71 + .../FlatpakBackend/FlatpakPermission.h | 46 + .../FlatpakRefreshAppstreamMetadataJob.cpp | 54 + .../FlatpakRefreshAppstreamMetadataJob.h | 30 + .../FlatpakBackend/FlatpakResource.cpp | 1029 ++++ .../backends/FlatpakBackend/FlatpakResource.h | 253 + .../FlatpakBackend/FlatpakSourcesBackend.cpp | 430 ++ .../FlatpakBackend/FlatpakSourcesBackend.h | 73 + .../FlatpakTransactionThread.cpp | 267 + .../FlatpakBackend/FlatpakTransactionThread.h | 77 + .../flatpak-backend-categories.xml | 551 +++ .../backends/FlatpakBackend/flatpak-helper.h | 15 + .../org.kde.discover-flatpak.desktop | 62 + .../org.kde.discover.flatpak.appdata.xml | 152 + .../FlatpakBackend/qml/FlatpakAttention.qml | 19 + .../FlatpakBackend/qml/FlatpakEolReason.qml | 23 + .../FlatpakBackend/qml/FlatpakOldBeta.qml | 68 + .../FlatpakBackend/qml/FlatpakRemoveData.qml | 37 + .../FlatpakBackend/qml/PermissionsList.qml | 39 + .../backends/FlatpakBackend/resources.qrc | 10 + .../sc-apps-flatpak-discover.svg | 8 + .../FlatpakBackend/tests/CMakeLists.txt | 1 + .../FlatpakBackend/tests/FlatpakTest.cpp | 188 + .../backends/FwupdBackend/CMakeLists.txt | 17 + .../backends/FwupdBackend/FwupdBackend.cpp | 471 ++ .../backends/FwupdBackend/FwupdBackend.h | 99 + .../backends/FwupdBackend/FwupdResource.cpp | 198 + .../backends/FwupdBackend/FwupdResource.h | 153 + .../FwupdBackend/FwupdSourcesBackend.cpp | 142 + .../FwupdBackend/FwupdSourcesBackend.h | 45 + .../FwupdBackend/FwupdTransaction.cpp | 129 + .../backends/FwupdBackend/FwupdTransaction.h | 34 + .../backends/KNSBackend/CMakeLists.txt | 12 + .../backends/KNSBackend/KNSBackend.cpp | 707 +++ libdiscover/backends/KNSBackend/KNSBackend.h | 112 + .../backends/KNSBackend/KNSResource.cpp | 270 + libdiscover/backends/KNSBackend/KNSResource.h | 77 + .../backends/KNSBackend/KNSReviews.cpp | 179 + libdiscover/backends/KNSBackend/KNSReviews.h | 53 + .../backends/KNSBackend/KNSTransaction.cpp | 104 + .../backends/KNSBackend/KNSTransaction.h | 34 + .../backends/KNSBackend/tests/CMakeLists.txt | 4 + .../KNSBackend/tests/KNSBackendTest.cpp | 170 + .../KNSBackend/tests/KNSBackendTest.h | 38 + .../KNSBackend/tests/testplasmoids.knsrc | 6 + .../AppPackageKitResource.cpp | 307 ++ .../PackageKitBackend/AppPackageKitResource.h | 56 + .../backends/PackageKitBackend/CMakeLists.txt | 43 + .../PackageKitBackend/LocalFilePKResource.cpp | 112 + .../PackageKitBackend/LocalFilePKResource.h | 44 + .../PKResolveTransaction.cpp | 59 + .../PackageKitBackend/PKResolveTransaction.h | 36 + .../PackageKitBackend/PKTransaction.cpp | 339 ++ .../PackageKitBackend/PKTransaction.h | 56 + .../PackageKitBackend/PackageKitBackend.cpp | 1049 ++++ .../PackageKitBackend/PackageKitBackend.h | 188 + .../PackageKitBackend/PackageKitMessages.cpp | 380 ++ .../PackageKitBackend/PackageKitMessages.h | 20 + .../PackageKitBackend/PackageKitNotifier.cpp | 363 ++ .../PackageKitBackend/PackageKitNotifier.h | 57 + .../PackageKitBackend/PackageKitResource.cpp | 497 ++ .../PackageKitBackend/PackageKitResource.h | 142 + .../PackageKitSourcesBackend.cpp | 189 + .../PackageKitSourcesBackend.h | 42 + .../PackageKitBackend/PackageKitUpdater.cpp | 810 +++ .../PackageKitBackend/PackageKitUpdater.h | 108 + .../org.kde.discover.packagekit.appdata.xml | 152 + .../packagekit-backend-categories.xml | 586 +++ .../PackageKitBackend/pk-offline-private.h | 31 + .../backends/PackageKitBackend/pkui.qrc | 7 + .../qml/DependenciesButton.qml | 60 + .../qml/PackageKitPermissions.qml | 30 + .../backends/RpmOstreeBackend/CMakeLists.txt | 16 + .../RpmOstreeBackend/OstreeFormat.cpp | 194 + .../backends/RpmOstreeBackend/OstreeFormat.h | 70 + .../RpmOstreeBackend/RpmOstreeBackend.cpp | 618 +++ .../RpmOstreeBackend/RpmOstreeBackend.h | 138 + .../RpmOstreeBackend/RpmOstreeNotifier.cpp | 390 ++ .../RpmOstreeBackend/RpmOstreeNotifier.h | 79 + .../RpmOstreeBackend/RpmOstreeResource.cpp | 488 ++ .../RpmOstreeBackend/RpmOstreeResource.h | 118 + .../RpmOstreeSourcesBackend.cpp | 90 + .../RpmOstreeSourcesBackend.h | 28 + .../RpmOstreeBackend/RpmOstreeTransaction.cpp | 408 ++ .../RpmOstreeBackend/RpmOstreeTransaction.h | 112 + .../rpm-ostree-backend-categories.xml | 13 + .../backends/SnapBackend/CMakeLists.txt | 20 + .../backends/SnapBackend/SnapBackend.cpp | 278 ++ .../backends/SnapBackend/SnapBackend.h | 85 + .../backends/SnapBackend/SnapResource.cpp | 535 ++ .../backends/SnapBackend/SnapResource.h | 88 + .../backends/SnapBackend/SnapTransaction.cpp | 125 + .../backends/SnapBackend/SnapTransaction.h | 37 + .../SnapBackend/libsnapclient/CMakeLists.txt | 13 + .../libsnapclient/SnapAuthHelper.cpp | 68 + .../libsnapclient/SnapMacaroonDialog.cpp | 89 + .../libsnapclient/SnapMacaroonDialog.ui | 146 + .../libsnapclient/config-paths.h.cmake | 1 + .../org.kde.discover.libsnapclient.actions | 106 + .../org.kde.discover.snap.appdata.xml | 150 + .../SnapBackend/qml/ChannelsButton.qml | 41 + .../SnapBackend/qml/PermissionsButton.qml | 47 + libdiscover/backends/SnapBackend/snapui.qrc | 7 + .../backends/SteamOSBackend/CMakeLists.txt | 23 + .../SteamOSBackend/SteamOSBackend.cpp | 230 + .../backends/SteamOSBackend/SteamOSBackend.h | 68 + .../SteamOSBackend/SteamOSResource.cpp | 199 + .../backends/SteamOSBackend/SteamOSResource.h | 68 + .../SteamOSBackend/SteamOSTransaction.cpp | 123 + .../SteamOSBackend/SteamOSTransaction.h | 38 + .../com.steampowered.Atomupd1.xml | 310 ++ .../backends/SteamOSBackend/dbushelpers.cpp | 12 + .../backends/SteamOSBackend/dbushelpers.h | 17 + .../org.freedesktop.DBus.Properties.xml | 27 + libdiscover/config-paths.h.cmake | 1 + .../notifiers/BackendNotifierModule.cpp | 14 + libdiscover/notifiers/BackendNotifierModule.h | 81 + libdiscover/notifiers/CMakeLists.txt | 12 + .../resources/AbstractBackendUpdater.cpp | 65 + .../resources/AbstractBackendUpdater.h | 234 + libdiscover/resources/AbstractResource.cpp | 303 ++ libdiscover/resources/AbstractResource.h | 293 ++ .../resources/AbstractResourcesBackend.cpp | 154 + .../resources/AbstractResourcesBackend.h | 306 ++ .../resources/AbstractSourcesBackend.cpp | 40 + .../resources/AbstractSourcesBackend.h | 83 + libdiscover/resources/DiscoverAction.cpp | 75 + libdiscover/resources/DiscoverAction.h | 77 + libdiscover/resources/PackageState.cpp | 64 + libdiscover/resources/PackageState.h | 38 + libdiscover/resources/ResourcesModel.cpp | 441 ++ libdiscover/resources/ResourcesModel.h | 174 + libdiscover/resources/ResourcesProxyModel.cpp | 739 +++ libdiscover/resources/ResourcesProxyModel.h | 208 + .../resources/ResourcesUpdatesModel.cpp | 360 ++ libdiscover/resources/ResourcesUpdatesModel.h | 81 + libdiscover/resources/SourcesModel.cpp | 113 + libdiscover/resources/SourcesModel.h | 45 + .../resources/StandardBackendUpdater.cpp | 290 ++ .../resources/StandardBackendUpdater.h | 69 + libdiscover/resources/StoredResultsStream.cpp | 28 + libdiscover/resources/StoredResultsStream.h | 24 + .../discoverabstractnotifier.notifyrc | 583 +++ libdiscover/tests/CMakeLists.txt | 1 + libdiscover/tests/CategoriesTest.cpp | 64 + libdiscover/utils.h | 171 + logo.png | Bin 0 -> 4453 bytes notifier/BackendNotifierFactory.cpp | 39 + notifier/BackendNotifierFactory.h | 20 + notifier/CMakeLists.txt | 33 + notifier/DiscoverNotifier.cpp | 361 ++ notifier/DiscoverNotifier.h | 109 + notifier/NotifierItem.cpp | 100 + notifier/NotifierItem.h | 32 + notifier/UnattendedUpdates.cpp | 86 + notifier/UnattendedUpdates.h | 27 + notifier/main.cpp | 80 + .../org.kde.discover.notifier.desktop.cmake | 60 + po/ar/kcm_updates.po | 106 + po/ar/libdiscover.po | 3723 ++++++++++++++ po/ar/plasma-discover-notifier.po | 208 + po/ar/plasma-discover.po | 1600 ++++++ po/az/kcm_updates.po | 106 + po/az/libdiscover.po | 2605 ++++++++++ po/az/plasma-discover-notifier.po | 160 + po/az/plasma-discover.po | 1367 ++++++ po/bg/kcm_updates.po | 107 + po/bg/libdiscover.po | 2535 ++++++++++ po/bg/plasma-discover-notifier.po | 159 + po/bg/plasma-discover.po | 1222 +++++ po/bs/libdiscover.po | 3781 ++++++++++++++ po/bs/plasma-discover.po | 1498 ++++++ po/ca/kcm_updates.po | 110 + po/ca/libdiscover.po | 2547 ++++++++++ po/ca/plasma-discover-notifier.po | 161 + po/ca/plasma-discover.po | 1224 +++++ po/ca@valencia/kcm_updates.po | 110 + po/ca@valencia/libdiscover.po | 2549 ++++++++++ po/ca@valencia/plasma-discover-notifier.po | 161 + po/ca@valencia/plasma-discover.po | 1224 +++++ po/cs/kcm_updates.po | 105 + po/cs/libdiscover.po | 2443 ++++++++++ po/cs/plasma-discover-notifier.po | 160 + po/cs/plasma-discover.po | 1173 +++++ po/da/libdiscover.po | 4226 ++++++++++++++++ po/da/plasma-discover-notifier.po | 201 + po/da/plasma-discover.po | 1743 +++++++ po/de/kcm_updates.po | 109 + po/de/libdiscover.po | 4329 +++++++++++++++++ po/de/plasma-discover-notifier.po | 203 + po/de/plasma-discover.po | 1731 +++++++ po/el/libdiscover.po | 4268 ++++++++++++++++ po/el/plasma-discover-notifier.po | 193 + po/el/plasma-discover.po | 1671 +++++++ po/en_GB/kcm_updates.po | 105 + po/en_GB/libdiscover.po | 4144 ++++++++++++++++ po/en_GB/plasma-discover-notifier.po | 199 + po/en_GB/plasma-discover.po | 1660 +++++++ po/es/kcm_updates.po | 109 + po/es/libdiscover.po | 4211 ++++++++++++++++ po/es/plasma-discover-notifier.po | 203 + po/es/plasma-discover.po | 1734 +++++++ po/et/kcm_updates.po | 106 + po/et/libdiscover.po | 4209 ++++++++++++++++ po/et/plasma-discover-notifier.po | 197 + po/et/plasma-discover.po | 1734 +++++++ po/eu/kcm_updates.po | 110 + po/eu/libdiscover.po | 2736 +++++++++++ po/eu/plasma-discover-notifier.po | 200 + po/eu/plasma-discover.po | 1520 ++++++ po/fi/kcm_updates.po | 105 + po/fi/libdiscover.po | 4144 ++++++++++++++++ po/fi/plasma-discover-notifier.po | 203 + po/fi/plasma-discover.po | 1725 +++++++ po/fr/kcm_updates.po | 104 + po/fr/libdiscover.po | 3916 +++++++++++++++ po/fr/plasma-discover-notifier.po | 210 + po/fr/plasma-discover.po | 1756 +++++++ po/ga/libdiscover.po | 3474 +++++++++++++ po/ga/plasma-discover.po | 1416 ++++++ po/gl/kcm_updates.po | 107 + po/gl/libdiscover.po | 3868 +++++++++++++++ po/gl/plasma-discover-notifier.po | 201 + po/gl/plasma-discover.po | 1755 +++++++ po/he/libdiscover.po | 2643 ++++++++++ po/he/plasma-discover-notifier.po | 197 + po/he/plasma-discover.po | 1460 ++++++ po/hi/kcm_updates.po | 106 + po/hi/libdiscover.po | 2456 ++++++++++ po/hi/plasma-discover-notifier.po | 158 + po/hsb/libdiscover.po | 2478 ++++++++++ po/hsb/plasma-discover-notifier.po | 158 + po/hsb/plasma-discover.po | 1224 +++++ po/hu/kcm_updates.po | 108 + po/hu/libdiscover.po | 4172 ++++++++++++++++ po/hu/plasma-discover-notifier.po | 201 + po/hu/plasma-discover.po | 1668 +++++++ po/ia/kcm_updates.po | 107 + po/ia/libdiscover.po | 2651 ++++++++++ po/ia/plasma-discover-notifier.po | 185 + po/ia/plasma-discover.po | 1483 ++++++ po/id/kcm_updates.po | 107 + po/id/libdiscover.po | 2556 ++++++++++ po/id/plasma-discover-notifier.po | 158 + po/id/plasma-discover.po | 1238 +++++ po/ie/kcm_updates.po | 108 + po/ie/plasma-discover-notifier.po | 158 + po/it/kcm_updates.po | 107 + po/it/libdiscover.po | 4181 ++++++++++++++++ po/it/plasma-discover-notifier.po | 202 + po/it/plasma-discover.po | 1731 +++++++ po/ja/kcm_updates.po | 105 + po/ja/libdiscover.po | 2455 ++++++++++ po/ja/plasma-discover-notifier.po | 158 + po/ja/plasma-discover.po | 1209 +++++ po/ka/kcm_updates.po | 106 + po/ka/libdiscover.po | 2530 ++++++++++ po/ka/plasma-discover-notifier.po | 158 + po/ka/plasma-discover.po | 1225 +++++ po/kk/libdiscover.po | 3854 +++++++++++++++ po/kk/plasma-discover.po | 1513 ++++++ po/ko/kcm_updates.po | 105 + po/ko/libdiscover.po | 3715 ++++++++++++++ po/ko/plasma-discover-notifier.po | 195 + po/ko/plasma-discover.po | 1646 +++++++ po/lt/libdiscover.po | 3707 ++++++++++++++ po/lt/plasma-discover-notifier.po | 209 + po/lt/plasma-discover.po | 1604 ++++++ po/ml/libdiscover.po | 2479 ++++++++++ po/ml/plasma-discover-notifier.po | 160 + po/ml/plasma-discover.po | 1276 +++++ po/mr/libdiscover.po | 3164 ++++++++++++ po/mr/plasma-discover.po | 1507 ++++++ po/my/kcm_updates.po | 104 + po/my/libdiscover.po | 2529 ++++++++++ po/my/plasma-discover-notifier.po | 159 + po/my/plasma-discover.po | 1279 +++++ po/nb/libdiscover.po | 2534 ++++++++++ po/nb/plasma-discover-notifier.po | 160 + po/nb/plasma-discover.po | 1212 +++++ po/nds/libdiscover.po | 4064 ++++++++++++++++ po/nds/plasma-discover.po | 1447 ++++++ po/nl/kcm_updates.po | 106 + po/nl/libdiscover.po | 3739 ++++++++++++++ po/nl/plasma-discover-notifier.po | 202 + po/nl/plasma-discover.po | 1751 +++++++ po/nn/kcm_updates.po | 109 + po/nn/libdiscover.po | 2521 ++++++++++ po/nn/plasma-discover-notifier.po | 161 + po/nn/plasma-discover.po | 1221 +++++ po/pa/kcm_updates.po | 101 + po/pa/libdiscover.po | 3393 +++++++++++++ po/pa/plasma-discover-notifier.po | 191 + po/pa/plasma-discover.po | 1478 ++++++ po/pl/kcm_updates.po | 108 + po/pl/libdiscover.po | 4177 ++++++++++++++++ po/pl/plasma-discover-notifier.po | 204 + po/pl/plasma-discover.po | 1740 +++++++ po/pt/kcm_updates.po | 102 + po/pt/libdiscover.po | 2551 ++++++++++ po/pt/plasma-discover-notifier.po | 158 + po/pt/plasma-discover.po | 1236 +++++ po/pt_BR/kcm_updates.po | 107 + po/pt_BR/libdiscover.po | 2625 ++++++++++ po/pt_BR/plasma-discover-notifier.po | 162 + po/pt_BR/plasma-discover.po | 1388 ++++++ po/ro/kcm_updates.po | 108 + po/ro/libdiscover.po | 4101 ++++++++++++++++ po/ro/plasma-discover-notifier.po | 202 + po/ro/plasma-discover.po | 1604 ++++++ po/ru/kcm_updates.po | 108 + po/ru/libdiscover.po | 4192 ++++++++++++++++ po/ru/plasma-discover-notifier.po | 209 + po/ru/plasma-discover.po | 1770 +++++++ po/sk/kcm_updates.po | 105 + po/sk/libdiscover.po | 3836 +++++++++++++++ po/sk/plasma-discover-notifier.po | 205 + po/sk/plasma-discover.po | 1690 +++++++ po/sl/kcm_updates.po | 107 + po/sl/libdiscover.po | 2582 ++++++++++ po/sl/plasma-discover-notifier.po | 207 + po/sl/plasma-discover.po | 1280 +++++ po/sr/libdiscover.po | 1701 +++++++ po/sr/plasma-discover-notifier.po | 150 + po/sr/plasma-discover.po | 855 ++++ po/sr@ijekavian/libdiscover.po | 1701 +++++++ po/sr@ijekavian/plasma-discover-notifier.po | 150 + po/sr@ijekavian/plasma-discover.po | 855 ++++ po/sr@ijekavianlatin/libdiscover.po | 1702 +++++++ .../plasma-discover-notifier.po | 150 + po/sr@ijekavianlatin/plasma-discover.po | 855 ++++ po/sr@latin/libdiscover.po | 1702 +++++++ po/sr@latin/plasma-discover-notifier.po | 150 + po/sr@latin/plasma-discover.po | 855 ++++ po/sv/kcm_updates.po | 106 + po/sv/libdiscover.po | 3702 ++++++++++++++ po/sv/plasma-discover-notifier.po | 199 + po/sv/plasma-discover.po | 1739 +++++++ po/ta/kcm_updates.po | 105 + po/ta/libdiscover.po | 2513 ++++++++++ po/ta/plasma-discover-notifier.po | 157 + po/ta/plasma-discover.po | 1294 +++++ po/tg/libdiscover.po | 2510 ++++++++++ po/tg/plasma-discover-notifier.po | 171 + po/tg/plasma-discover.po | 1360 ++++++ po/tr/kcm_updates.po | 107 + po/tr/libdiscover.po | 2549 ++++++++++ po/tr/plasma-discover-notifier.po | 159 + po/tr/plasma-discover.po | 1220 +++++ po/ug/libdiscover.po | 3307 +++++++++++++ po/ug/plasma-discover.po | 1403 ++++++ po/uk/kcm_updates.po | 109 + po/uk/libdiscover.po | 3777 ++++++++++++++ po/uk/plasma-discover-notifier.po | 212 + po/uk/plasma-discover.po | 1781 +++++++ po/vi/libdiscover.po | 2683 ++++++++++ po/vi/plasma-discover.po | 1243 +++++ po/zh_CN/kcm_updates.po | 104 + po/zh_CN/libdiscover.po | 2465 ++++++++++ po/zh_CN/plasma-discover-notifier.po | 158 + po/zh_CN/plasma-discover.po | 1183 +++++ po/zh_TW/kcm_updates.po | 104 + po/zh_TW/libdiscover.po | 2542 ++++++++++ po/zh_TW/plasma-discover-notifier.po | 170 + po/zh_TW/plasma-discover.po | 1400 ++++++ update/CMakeLists.txt | 6 + update/DiscoverUpdate.cpp | 58 + update/DiscoverUpdate.h | 37 + update/main.cpp | 43 + 553 files changed, 323361 insertions(+) create mode 100644 .git-blame-ignore-revs create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .kde-ci.yml create mode 100644 CMakeLists.txt create mode 100644 DiscoverVersion.h.in create mode 100644 LICENSES/BSD-3-Clause.txt create mode 100644 LICENSES/CC0-1.0.txt create mode 100644 LICENSES/GPL-2.0-only.txt create mode 100644 LICENSES/GPL-2.0-or-later.txt create mode 100644 LICENSES/GPL-3.0-only.txt create mode 100644 LICENSES/LGPL-2.0-or-later.txt create mode 100644 LICENSES/LGPL-2.1-only.txt create mode 100644 LICENSES/LGPL-3.0-only.txt create mode 100644 LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 LICENSES/LicenseRef-KDE-Accepted-LGPL.txt create mode 100644 Messages.sh create mode 100644 cmake/FindPackageKitQt2.cmake create mode 100644 discover/AbstractAppsModel.cpp create mode 100644 discover/AbstractAppsModel.h create mode 100644 discover/CMakeLists.txt create mode 100644 discover/DiscoverDeclarativePlugin.cpp create mode 100644 discover/DiscoverDeclarativePlugin.h create mode 100644 discover/DiscoverObject.cpp create mode 100644 discover/DiscoverObject.h create mode 100644 discover/FeaturedModel.cpp create mode 100644 discover/FeaturedModel.h create mode 100644 discover/OdrsAppsModel.cpp create mode 100644 discover/OdrsAppsModel.h create mode 100644 discover/PaginateModel.cpp create mode 100644 discover/PaginateModel.h create mode 100644 discover/PowerManagementInterface.cpp create mode 100644 discover/PowerManagementInterface.h create mode 100644 discover/ReadFile.cpp create mode 100644 discover/ReadFile.h create mode 100644 discover/UnityLauncher.cpp create mode 100644 discover/UnityLauncher.h create mode 100644 discover/autotests/CMakeLists.txt create mode 100644 discover/autotests/DiscoverTest.qml create mode 100644 discover/autotests/PaginateModelTest.cpp create mode 100644 discover/autotests/appstreamUrl.qml create mode 100644 discover/autotests/categoryArg.qml create mode 100644 discover/autotests/install.qml create mode 100644 discover/autotests/missingResource.qml create mode 100644 discover/autotests/packageArgument.qml create mode 100644 discover/autotests/toplevels.qml create mode 100644 discover/autotests/updateandinstall.qml create mode 100644 discover/autotests/wrongInput.qml create mode 100644 discover/discover.schema create mode 100644 discover/discoversettings.kcfg create mode 100644 discover/discoversettings.kcfgc create mode 100644 discover/icons/128-apps-plasmadiscover.png create mode 100644 discover/icons/16-apps-plasmadiscover.png create mode 100644 discover/icons/22-apps-plasmadiscover.png create mode 100644 discover/icons/32-apps-plasmadiscover.png create mode 100644 discover/icons/48-apps-plasmadiscover.png create mode 100644 discover/icons/64-apps-plasmadiscover.png create mode 100644 discover/icons/CMakeLists.txt create mode 100644 discover/icons/sc-apps-plasmadiscover.svg create mode 100644 discover/main.cpp create mode 100644 discover/org.kde.discover.appdata.xml create mode 100644 discover/org.kde.discover.desktop.cmake create mode 100644 discover/plasmadiscoverui.rc create mode 100644 discover/plasmauserfeedback.kcfg create mode 100644 discover/plasmauserfeedback.kcfgc create mode 100644 discover/qml/AboutPage.qml create mode 100644 discover/qml/ActionListItem.qml create mode 100644 discover/qml/AddSourceDialog.qml create mode 100644 discover/qml/AddonsView.qml create mode 100644 discover/qml/ApplicationDelegate.qml create mode 100644 discover/qml/ApplicationPage.qml create mode 100644 discover/qml/ApplicationResourceButton.qml create mode 100644 discover/qml/ApplicationScreenshots.qml create mode 100644 discover/qml/ApplicationsListPage.qml create mode 100644 discover/qml/BrowsingPage.qml create mode 100644 discover/qml/ConditionalLoader.qml create mode 100644 discover/qml/ConditionalObject.qml create mode 100644 discover/qml/ConvertDiscoverAction.qml create mode 100644 discover/qml/DiscoverDrawer.qml create mode 100644 discover/qml/DiscoverInlineMessage.qml create mode 100644 discover/qml/DiscoverPage.qml create mode 100644 discover/qml/DiscoverWindow.qml create mode 100644 discover/qml/Feedback.qml create mode 100644 discover/qml/GridApplicationDelegate.qml create mode 100644 discover/qml/InstallApplicationButton.qml create mode 100644 discover/qml/InstalledPage.qml create mode 100644 discover/qml/LabelBackground.qml create mode 100644 discover/qml/LoadingPage.qml create mode 100644 discover/qml/ProgressView.qml create mode 100644 discover/qml/Rating.qml create mode 100644 discover/qml/ReviewDelegate.qml create mode 100644 discover/qml/ReviewDialog.qml create mode 100644 discover/qml/ReviewsPage.qml create mode 100644 discover/qml/SearchField.qml create mode 100644 discover/qml/SearchPage.qml create mode 100644 discover/qml/Shadow.qml create mode 100644 discover/qml/SourcesPage.qml create mode 100644 discover/qml/TopLevelPageData.qml create mode 100644 discover/qml/UpdatesPage.qml create mode 100644 discover/qml/WebflowDialog.qml create mode 100644 discover/qml/navigation.js create mode 100644 discover/resources.qrc create mode 100644 exporter/CMakeLists.txt create mode 100644 exporter/DiscoverExporter.cpp create mode 100644 exporter/DiscoverExporter.h create mode 100644 exporter/main.cpp create mode 100644 kcm/CMakeLists.txt create mode 100644 kcm/discoversettings.kcfg create mode 100644 kcm/discoversettings.kcfgc create mode 100644 kcm/kcm_updates.json create mode 100644 kcm/package/contents/ui/main.qml create mode 100644 kcm/updates.cpp create mode 100644 kcm/updates.h create mode 100644 kcm/updatessettings.kcfg create mode 100644 kcm/updatessettings.kcfgc create mode 100644 libdiscover/ApplicationAddonsModel.cpp create mode 100644 libdiscover/ApplicationAddonsModel.h create mode 100644 libdiscover/CMakeLists.txt create mode 100644 libdiscover/CachedNetworkAccessManager.cpp create mode 100644 libdiscover/CachedNetworkAccessManager.h create mode 100644 libdiscover/Category/CategoriesReader.cpp create mode 100644 libdiscover/Category/CategoriesReader.h create mode 100644 libdiscover/Category/Category.cpp create mode 100644 libdiscover/Category/Category.h create mode 100644 libdiscover/Category/CategoryModel.cpp create mode 100644 libdiscover/Category/CategoryModel.h create mode 100644 libdiscover/DiscoverBackendsFactory.cpp create mode 100644 libdiscover/DiscoverBackendsFactory.h create mode 100644 libdiscover/ReviewsBackend/AbstractReviewsBackend.cpp create mode 100644 libdiscover/ReviewsBackend/AbstractReviewsBackend.h create mode 100644 libdiscover/ReviewsBackend/Rating.cpp create mode 100644 libdiscover/ReviewsBackend/Rating.h create mode 100644 libdiscover/ReviewsBackend/Review.cpp create mode 100644 libdiscover/ReviewsBackend/Review.h create mode 100644 libdiscover/ReviewsBackend/ReviewsModel.cpp create mode 100644 libdiscover/ReviewsBackend/ReviewsModel.h create mode 100644 libdiscover/ScreenshotsModel.cpp create mode 100644 libdiscover/ScreenshotsModel.h create mode 100644 libdiscover/Transaction/AddonList.cpp create mode 100644 libdiscover/Transaction/AddonList.h create mode 100644 libdiscover/Transaction/Transaction.cpp create mode 100644 libdiscover/Transaction/Transaction.h create mode 100644 libdiscover/Transaction/TransactionListener.cpp create mode 100644 libdiscover/Transaction/TransactionListener.h create mode 100644 libdiscover/Transaction/TransactionModel.cpp create mode 100644 libdiscover/Transaction/TransactionModel.h create mode 100644 libdiscover/UpdateModel/UpdateItem.cpp create mode 100644 libdiscover/UpdateModel/UpdateItem.h create mode 100644 libdiscover/UpdateModel/UpdateModel.cpp create mode 100644 libdiscover/UpdateModel/UpdateModel.h create mode 100644 libdiscover/appstream/AppStreamIntegration.cpp create mode 100644 libdiscover/appstream/AppStreamIntegration.h create mode 100644 libdiscover/appstream/AppStreamUtils.cpp create mode 100644 libdiscover/appstream/AppStreamUtils.h create mode 100644 libdiscover/appstream/OdrsReviewsBackend.cpp create mode 100644 libdiscover/appstream/OdrsReviewsBackend.h create mode 100644 libdiscover/backends/CMakeLists.txt create mode 100644 libdiscover/backends/DummyBackend/CMakeLists.txt create mode 100644 libdiscover/backends/DummyBackend/DummyBackend.cpp create mode 100644 libdiscover/backends/DummyBackend/DummyBackend.h create mode 100644 libdiscover/backends/DummyBackend/DummyNotifier.cpp create mode 100644 libdiscover/backends/DummyBackend/DummyNotifier.h create mode 100644 libdiscover/backends/DummyBackend/DummyResource.cpp create mode 100644 libdiscover/backends/DummyBackend/DummyResource.h create mode 100644 libdiscover/backends/DummyBackend/DummyReviewsBackend.cpp create mode 100644 libdiscover/backends/DummyBackend/DummyReviewsBackend.h create mode 100644 libdiscover/backends/DummyBackend/DummySourcesBackend.cpp create mode 100644 libdiscover/backends/DummyBackend/DummySourcesBackend.h create mode 100644 libdiscover/backends/DummyBackend/DummyTransaction.cpp create mode 100644 libdiscover/backends/DummyBackend/DummyTransaction.h create mode 100644 libdiscover/backends/DummyBackend/dummy-backend-categories.xml create mode 100644 libdiscover/backends/DummyBackend/tests/CMakeLists.txt create mode 100644 libdiscover/backends/DummyBackend/tests/DummyTest.cpp create mode 100644 libdiscover/backends/DummyBackend/tests/DummyTest.h create mode 100644 libdiscover/backends/DummyBackend/tests/UpdateDummyTest.cpp create mode 100644 libdiscover/backends/FlatpakBackend/CMakeLists.txt create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakBackend.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakNotifier.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakNotifier.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakPermission.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakPermission.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakResource.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakResource.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.h create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.cpp create mode 100644 libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.h create mode 100644 libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml create mode 100644 libdiscover/backends/FlatpakBackend/flatpak-helper.h create mode 100644 libdiscover/backends/FlatpakBackend/org.kde.discover-flatpak.desktop create mode 100644 libdiscover/backends/FlatpakBackend/org.kde.discover.flatpak.appdata.xml create mode 100644 libdiscover/backends/FlatpakBackend/qml/FlatpakAttention.qml create mode 100644 libdiscover/backends/FlatpakBackend/qml/FlatpakEolReason.qml create mode 100644 libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml create mode 100644 libdiscover/backends/FlatpakBackend/qml/FlatpakRemoveData.qml create mode 100644 libdiscover/backends/FlatpakBackend/qml/PermissionsList.qml create mode 100644 libdiscover/backends/FlatpakBackend/resources.qrc create mode 100644 libdiscover/backends/FlatpakBackend/sc-apps-flatpak-discover.svg create mode 100644 libdiscover/backends/FlatpakBackend/tests/CMakeLists.txt create mode 100644 libdiscover/backends/FlatpakBackend/tests/FlatpakTest.cpp create mode 100644 libdiscover/backends/FwupdBackend/CMakeLists.txt create mode 100644 libdiscover/backends/FwupdBackend/FwupdBackend.cpp create mode 100644 libdiscover/backends/FwupdBackend/FwupdBackend.h create mode 100644 libdiscover/backends/FwupdBackend/FwupdResource.cpp create mode 100644 libdiscover/backends/FwupdBackend/FwupdResource.h create mode 100644 libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp create mode 100644 libdiscover/backends/FwupdBackend/FwupdSourcesBackend.h create mode 100644 libdiscover/backends/FwupdBackend/FwupdTransaction.cpp create mode 100644 libdiscover/backends/FwupdBackend/FwupdTransaction.h create mode 100644 libdiscover/backends/KNSBackend/CMakeLists.txt create mode 100644 libdiscover/backends/KNSBackend/KNSBackend.cpp create mode 100644 libdiscover/backends/KNSBackend/KNSBackend.h create mode 100644 libdiscover/backends/KNSBackend/KNSResource.cpp create mode 100644 libdiscover/backends/KNSBackend/KNSResource.h create mode 100644 libdiscover/backends/KNSBackend/KNSReviews.cpp create mode 100644 libdiscover/backends/KNSBackend/KNSReviews.h create mode 100644 libdiscover/backends/KNSBackend/KNSTransaction.cpp create mode 100644 libdiscover/backends/KNSBackend/KNSTransaction.h create mode 100644 libdiscover/backends/KNSBackend/tests/CMakeLists.txt create mode 100644 libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp create mode 100644 libdiscover/backends/KNSBackend/tests/KNSBackendTest.h create mode 100644 libdiscover/backends/KNSBackend/tests/testplasmoids.knsrc create mode 100644 libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp create mode 100644 libdiscover/backends/PackageKitBackend/AppPackageKitResource.h create mode 100644 libdiscover/backends/PackageKitBackend/CMakeLists.txt create mode 100644 libdiscover/backends/PackageKitBackend/LocalFilePKResource.cpp create mode 100644 libdiscover/backends/PackageKitBackend/LocalFilePKResource.h create mode 100644 libdiscover/backends/PackageKitBackend/PKResolveTransaction.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PKResolveTransaction.h create mode 100644 libdiscover/backends/PackageKitBackend/PKTransaction.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PKTransaction.h create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitBackend.h create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitMessages.h create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitNotifier.h create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitResource.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitResource.h create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.h create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp create mode 100644 libdiscover/backends/PackageKitBackend/PackageKitUpdater.h create mode 100644 libdiscover/backends/PackageKitBackend/org.kde.discover.packagekit.appdata.xml create mode 100644 libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml create mode 100644 libdiscover/backends/PackageKitBackend/pk-offline-private.h create mode 100644 libdiscover/backends/PackageKitBackend/pkui.qrc create mode 100644 libdiscover/backends/PackageKitBackend/qml/DependenciesButton.qml create mode 100644 libdiscover/backends/PackageKitBackend/qml/PackageKitPermissions.qml create mode 100644 libdiscover/backends/RpmOstreeBackend/CMakeLists.txt create mode 100644 libdiscover/backends/RpmOstreeBackend/OstreeFormat.cpp create mode 100644 libdiscover/backends/RpmOstreeBackend/OstreeFormat.h create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.cpp create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.h create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.cpp create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.h create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.h create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.cpp create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.h create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.cpp create mode 100644 libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.h create mode 100644 libdiscover/backends/RpmOstreeBackend/rpm-ostree-backend-categories.xml create mode 100644 libdiscover/backends/SnapBackend/CMakeLists.txt create mode 100644 libdiscover/backends/SnapBackend/SnapBackend.cpp create mode 100644 libdiscover/backends/SnapBackend/SnapBackend.h create mode 100644 libdiscover/backends/SnapBackend/SnapResource.cpp create mode 100644 libdiscover/backends/SnapBackend/SnapResource.h create mode 100644 libdiscover/backends/SnapBackend/SnapTransaction.cpp create mode 100644 libdiscover/backends/SnapBackend/SnapTransaction.h create mode 100644 libdiscover/backends/SnapBackend/libsnapclient/CMakeLists.txt create mode 100644 libdiscover/backends/SnapBackend/libsnapclient/SnapAuthHelper.cpp create mode 100644 libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.cpp create mode 100644 libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.ui create mode 100644 libdiscover/backends/SnapBackend/libsnapclient/config-paths.h.cmake create mode 100644 libdiscover/backends/SnapBackend/libsnapclient/org.kde.discover.libsnapclient.actions create mode 100644 libdiscover/backends/SnapBackend/org.kde.discover.snap.appdata.xml create mode 100644 libdiscover/backends/SnapBackend/qml/ChannelsButton.qml create mode 100644 libdiscover/backends/SnapBackend/qml/PermissionsButton.qml create mode 100644 libdiscover/backends/SnapBackend/snapui.qrc create mode 100644 libdiscover/backends/SteamOSBackend/CMakeLists.txt create mode 100644 libdiscover/backends/SteamOSBackend/SteamOSBackend.cpp create mode 100644 libdiscover/backends/SteamOSBackend/SteamOSBackend.h create mode 100644 libdiscover/backends/SteamOSBackend/SteamOSResource.cpp create mode 100644 libdiscover/backends/SteamOSBackend/SteamOSResource.h create mode 100644 libdiscover/backends/SteamOSBackend/SteamOSTransaction.cpp create mode 100644 libdiscover/backends/SteamOSBackend/SteamOSTransaction.h create mode 100644 libdiscover/backends/SteamOSBackend/com.steampowered.Atomupd1.xml create mode 100644 libdiscover/backends/SteamOSBackend/dbushelpers.cpp create mode 100644 libdiscover/backends/SteamOSBackend/dbushelpers.h create mode 100644 libdiscover/backends/SteamOSBackend/org.freedesktop.DBus.Properties.xml create mode 100644 libdiscover/config-paths.h.cmake create mode 100644 libdiscover/notifiers/BackendNotifierModule.cpp create mode 100644 libdiscover/notifiers/BackendNotifierModule.h create mode 100644 libdiscover/notifiers/CMakeLists.txt create mode 100644 libdiscover/resources/AbstractBackendUpdater.cpp create mode 100644 libdiscover/resources/AbstractBackendUpdater.h create mode 100644 libdiscover/resources/AbstractResource.cpp create mode 100644 libdiscover/resources/AbstractResource.h create mode 100644 libdiscover/resources/AbstractResourcesBackend.cpp create mode 100644 libdiscover/resources/AbstractResourcesBackend.h create mode 100644 libdiscover/resources/AbstractSourcesBackend.cpp create mode 100644 libdiscover/resources/AbstractSourcesBackend.h create mode 100644 libdiscover/resources/DiscoverAction.cpp create mode 100644 libdiscover/resources/DiscoverAction.h create mode 100644 libdiscover/resources/PackageState.cpp create mode 100644 libdiscover/resources/PackageState.h create mode 100644 libdiscover/resources/ResourcesModel.cpp create mode 100644 libdiscover/resources/ResourcesModel.h create mode 100644 libdiscover/resources/ResourcesProxyModel.cpp create mode 100644 libdiscover/resources/ResourcesProxyModel.h create mode 100644 libdiscover/resources/ResourcesUpdatesModel.cpp create mode 100644 libdiscover/resources/ResourcesUpdatesModel.h create mode 100644 libdiscover/resources/SourcesModel.cpp create mode 100644 libdiscover/resources/SourcesModel.h create mode 100644 libdiscover/resources/StandardBackendUpdater.cpp create mode 100644 libdiscover/resources/StandardBackendUpdater.h create mode 100644 libdiscover/resources/StoredResultsStream.cpp create mode 100644 libdiscover/resources/StoredResultsStream.h create mode 100644 libdiscover/resources/discoverabstractnotifier.notifyrc create mode 100644 libdiscover/tests/CMakeLists.txt create mode 100644 libdiscover/tests/CategoriesTest.cpp create mode 100644 libdiscover/utils.h create mode 100644 logo.png create mode 100644 notifier/BackendNotifierFactory.cpp create mode 100644 notifier/BackendNotifierFactory.h create mode 100644 notifier/CMakeLists.txt create mode 100644 notifier/DiscoverNotifier.cpp create mode 100644 notifier/DiscoverNotifier.h create mode 100644 notifier/NotifierItem.cpp create mode 100644 notifier/NotifierItem.h create mode 100644 notifier/UnattendedUpdates.cpp create mode 100644 notifier/UnattendedUpdates.h create mode 100644 notifier/main.cpp create mode 100644 notifier/org.kde.discover.notifier.desktop.cmake create mode 100644 po/ar/kcm_updates.po create mode 100644 po/ar/libdiscover.po create mode 100644 po/ar/plasma-discover-notifier.po create mode 100644 po/ar/plasma-discover.po create mode 100644 po/az/kcm_updates.po create mode 100644 po/az/libdiscover.po create mode 100644 po/az/plasma-discover-notifier.po create mode 100644 po/az/plasma-discover.po create mode 100644 po/bg/kcm_updates.po create mode 100644 po/bg/libdiscover.po create mode 100644 po/bg/plasma-discover-notifier.po create mode 100644 po/bg/plasma-discover.po create mode 100644 po/bs/libdiscover.po create mode 100644 po/bs/plasma-discover.po create mode 100644 po/ca/kcm_updates.po create mode 100644 po/ca/libdiscover.po create mode 100644 po/ca/plasma-discover-notifier.po create mode 100644 po/ca/plasma-discover.po create mode 100644 po/ca@valencia/kcm_updates.po create mode 100644 po/ca@valencia/libdiscover.po create mode 100644 po/ca@valencia/plasma-discover-notifier.po create mode 100644 po/ca@valencia/plasma-discover.po create mode 100644 po/cs/kcm_updates.po create mode 100644 po/cs/libdiscover.po create mode 100644 po/cs/plasma-discover-notifier.po create mode 100644 po/cs/plasma-discover.po create mode 100644 po/da/libdiscover.po create mode 100644 po/da/plasma-discover-notifier.po create mode 100644 po/da/plasma-discover.po create mode 100644 po/de/kcm_updates.po create mode 100644 po/de/libdiscover.po create mode 100644 po/de/plasma-discover-notifier.po create mode 100644 po/de/plasma-discover.po create mode 100644 po/el/libdiscover.po create mode 100644 po/el/plasma-discover-notifier.po create mode 100644 po/el/plasma-discover.po create mode 100644 po/en_GB/kcm_updates.po create mode 100644 po/en_GB/libdiscover.po create mode 100644 po/en_GB/plasma-discover-notifier.po create mode 100644 po/en_GB/plasma-discover.po create mode 100644 po/es/kcm_updates.po create mode 100644 po/es/libdiscover.po create mode 100644 po/es/plasma-discover-notifier.po create mode 100644 po/es/plasma-discover.po create mode 100644 po/et/kcm_updates.po create mode 100644 po/et/libdiscover.po create mode 100644 po/et/plasma-discover-notifier.po create mode 100644 po/et/plasma-discover.po create mode 100644 po/eu/kcm_updates.po create mode 100644 po/eu/libdiscover.po create mode 100644 po/eu/plasma-discover-notifier.po create mode 100644 po/eu/plasma-discover.po create mode 100644 po/fi/kcm_updates.po create mode 100644 po/fi/libdiscover.po create mode 100644 po/fi/plasma-discover-notifier.po create mode 100644 po/fi/plasma-discover.po create mode 100644 po/fr/kcm_updates.po create mode 100644 po/fr/libdiscover.po create mode 100644 po/fr/plasma-discover-notifier.po create mode 100644 po/fr/plasma-discover.po create mode 100644 po/ga/libdiscover.po create mode 100644 po/ga/plasma-discover.po create mode 100644 po/gl/kcm_updates.po create mode 100644 po/gl/libdiscover.po create mode 100644 po/gl/plasma-discover-notifier.po create mode 100644 po/gl/plasma-discover.po create mode 100644 po/he/libdiscover.po create mode 100644 po/he/plasma-discover-notifier.po create mode 100644 po/he/plasma-discover.po create mode 100644 po/hi/kcm_updates.po create mode 100644 po/hi/libdiscover.po create mode 100644 po/hi/plasma-discover-notifier.po create mode 100644 po/hsb/libdiscover.po create mode 100644 po/hsb/plasma-discover-notifier.po create mode 100644 po/hsb/plasma-discover.po create mode 100644 po/hu/kcm_updates.po create mode 100644 po/hu/libdiscover.po create mode 100644 po/hu/plasma-discover-notifier.po create mode 100644 po/hu/plasma-discover.po create mode 100644 po/ia/kcm_updates.po create mode 100644 po/ia/libdiscover.po create mode 100644 po/ia/plasma-discover-notifier.po create mode 100644 po/ia/plasma-discover.po create mode 100644 po/id/kcm_updates.po create mode 100644 po/id/libdiscover.po create mode 100644 po/id/plasma-discover-notifier.po create mode 100644 po/id/plasma-discover.po create mode 100644 po/ie/kcm_updates.po create mode 100644 po/ie/plasma-discover-notifier.po create mode 100644 po/it/kcm_updates.po create mode 100644 po/it/libdiscover.po create mode 100644 po/it/plasma-discover-notifier.po create mode 100644 po/it/plasma-discover.po create mode 100644 po/ja/kcm_updates.po create mode 100644 po/ja/libdiscover.po create mode 100644 po/ja/plasma-discover-notifier.po create mode 100644 po/ja/plasma-discover.po create mode 100644 po/ka/kcm_updates.po create mode 100644 po/ka/libdiscover.po create mode 100644 po/ka/plasma-discover-notifier.po create mode 100644 po/ka/plasma-discover.po create mode 100644 po/kk/libdiscover.po create mode 100644 po/kk/plasma-discover.po create mode 100644 po/ko/kcm_updates.po create mode 100644 po/ko/libdiscover.po create mode 100644 po/ko/plasma-discover-notifier.po create mode 100644 po/ko/plasma-discover.po create mode 100644 po/lt/libdiscover.po create mode 100644 po/lt/plasma-discover-notifier.po create mode 100644 po/lt/plasma-discover.po create mode 100644 po/ml/libdiscover.po create mode 100644 po/ml/plasma-discover-notifier.po create mode 100644 po/ml/plasma-discover.po create mode 100644 po/mr/libdiscover.po create mode 100644 po/mr/plasma-discover.po create mode 100644 po/my/kcm_updates.po create mode 100644 po/my/libdiscover.po create mode 100644 po/my/plasma-discover-notifier.po create mode 100644 po/my/plasma-discover.po create mode 100644 po/nb/libdiscover.po create mode 100644 po/nb/plasma-discover-notifier.po create mode 100644 po/nb/plasma-discover.po create mode 100644 po/nds/libdiscover.po create mode 100644 po/nds/plasma-discover.po create mode 100644 po/nl/kcm_updates.po create mode 100644 po/nl/libdiscover.po create mode 100644 po/nl/plasma-discover-notifier.po create mode 100644 po/nl/plasma-discover.po create mode 100644 po/nn/kcm_updates.po create mode 100644 po/nn/libdiscover.po create mode 100644 po/nn/plasma-discover-notifier.po create mode 100644 po/nn/plasma-discover.po create mode 100644 po/pa/kcm_updates.po create mode 100644 po/pa/libdiscover.po create mode 100644 po/pa/plasma-discover-notifier.po create mode 100644 po/pa/plasma-discover.po create mode 100644 po/pl/kcm_updates.po create mode 100644 po/pl/libdiscover.po create mode 100644 po/pl/plasma-discover-notifier.po create mode 100644 po/pl/plasma-discover.po create mode 100644 po/pt/kcm_updates.po create mode 100644 po/pt/libdiscover.po create mode 100644 po/pt/plasma-discover-notifier.po create mode 100644 po/pt/plasma-discover.po create mode 100644 po/pt_BR/kcm_updates.po create mode 100644 po/pt_BR/libdiscover.po create mode 100644 po/pt_BR/plasma-discover-notifier.po create mode 100644 po/pt_BR/plasma-discover.po create mode 100644 po/ro/kcm_updates.po create mode 100644 po/ro/libdiscover.po create mode 100644 po/ro/plasma-discover-notifier.po create mode 100644 po/ro/plasma-discover.po create mode 100644 po/ru/kcm_updates.po create mode 100644 po/ru/libdiscover.po create mode 100644 po/ru/plasma-discover-notifier.po create mode 100644 po/ru/plasma-discover.po create mode 100644 po/sk/kcm_updates.po create mode 100644 po/sk/libdiscover.po create mode 100644 po/sk/plasma-discover-notifier.po create mode 100644 po/sk/plasma-discover.po create mode 100644 po/sl/kcm_updates.po create mode 100644 po/sl/libdiscover.po create mode 100644 po/sl/plasma-discover-notifier.po create mode 100644 po/sl/plasma-discover.po create mode 100644 po/sr/libdiscover.po create mode 100644 po/sr/plasma-discover-notifier.po create mode 100644 po/sr/plasma-discover.po create mode 100644 po/sr@ijekavian/libdiscover.po create mode 100644 po/sr@ijekavian/plasma-discover-notifier.po create mode 100644 po/sr@ijekavian/plasma-discover.po create mode 100644 po/sr@ijekavianlatin/libdiscover.po create mode 100644 po/sr@ijekavianlatin/plasma-discover-notifier.po create mode 100644 po/sr@ijekavianlatin/plasma-discover.po create mode 100644 po/sr@latin/libdiscover.po create mode 100644 po/sr@latin/plasma-discover-notifier.po create mode 100644 po/sr@latin/plasma-discover.po create mode 100644 po/sv/kcm_updates.po create mode 100644 po/sv/libdiscover.po create mode 100644 po/sv/plasma-discover-notifier.po create mode 100644 po/sv/plasma-discover.po create mode 100644 po/ta/kcm_updates.po create mode 100644 po/ta/libdiscover.po create mode 100644 po/ta/plasma-discover-notifier.po create mode 100644 po/ta/plasma-discover.po create mode 100644 po/tg/libdiscover.po create mode 100644 po/tg/plasma-discover-notifier.po create mode 100644 po/tg/plasma-discover.po create mode 100644 po/tr/kcm_updates.po create mode 100644 po/tr/libdiscover.po create mode 100644 po/tr/plasma-discover-notifier.po create mode 100644 po/tr/plasma-discover.po create mode 100644 po/ug/libdiscover.po create mode 100644 po/ug/plasma-discover.po create mode 100644 po/uk/kcm_updates.po create mode 100644 po/uk/libdiscover.po create mode 100644 po/uk/plasma-discover-notifier.po create mode 100644 po/uk/plasma-discover.po create mode 100644 po/vi/libdiscover.po create mode 100644 po/vi/plasma-discover.po create mode 100644 po/zh_CN/kcm_updates.po create mode 100644 po/zh_CN/libdiscover.po create mode 100644 po/zh_CN/plasma-discover-notifier.po create mode 100644 po/zh_CN/plasma-discover.po create mode 100644 po/zh_TW/kcm_updates.po create mode 100644 po/zh_TW/libdiscover.po create mode 100644 po/zh_TW/plasma-discover-notifier.po create mode 100644 po/zh_TW/plasma-discover.po create mode 100644 update/CMakeLists.txt create mode 100644 update/DiscoverUpdate.cpp create mode 100644 update/DiscoverUpdate.h create mode 100644 update/main.cpp diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..0fe4e56 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# clang-format +67357bfc45da195f9d3397ddf5b650768705c18a diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..198d67c --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Ignore the following files +*~ +*.[oa] +*.diff +*.kate-swp +*.kdev4 +.kdev_include_paths +*.kdevelop.pcs +*.moc +*.moc.cpp +*.orig +*.user +.*.swp +.swp.* +Doxyfile +Makefile +avail +random_seed +/build*/ +CMakeLists.txt.user* +*.unc-backup* +/build*/ + +# LSP & IDE +/.clang-format +/compile_commands.json +.clangd +.cache +.idea +/cmake-build* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..bd1fb06 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +include: + - project: sysadmin/ci-utilities + file: + - /gitlab-templates/linux.yml + +# Commented out because they don't support appstreamqt yet, and it's probably fine +# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml +# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml +# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml diff --git a/.kde-ci.yml b/.kde-ci.yml new file mode 100644 index 0000000..205703f --- /dev/null +++ b/.kde-ci.yml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +Dependencies: +- 'on': ['@all'] + 'require': + 'frameworks/attica': '@latest' + 'frameworks/extra-cmake-modules': '@latest' + 'frameworks/karchive': '@latest' + 'frameworks/kauth': '@latest' + 'frameworks/kcmutils': '@latest' + 'frameworks/kcodecs': '@latest' + 'frameworks/kconfig': '@latest' + 'frameworks/kconfigwidgets': '@latest' + 'frameworks/kcoreaddons': '@latest' + 'frameworks/kcrash': '@latest' + 'frameworks/kdbusaddons': '@latest' + 'frameworks/kdeclarative': '@latest' + 'frameworks/ki18n': '@latest' + 'frameworks/kidletime': '@latest' + 'frameworks/kio': '@latest' + 'frameworks/knewstuff': '@latest' + 'frameworks/knotifications': '@latest' + 'frameworks/kservice': '@latest' + 'frameworks/kwidgetsaddons': '@latest' + 'frameworks/kwindowsystem': '@latest' + 'frameworks/kxmlgui': '@latest' + 'frameworks/purpose': '@latest' + 'frameworks/kuserfeedback': '@stable' + +Options: + require-passing-tests-on: [ 'Linux', 'FreeBSD'] diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..898c5f2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,128 @@ +project(discover) +set(PROJECT_VERSION "5.27.10") +set(PROJECT_VERSION_MAJOR 5) + +cmake_minimum_required(VERSION 3.16) + +set(QT_MIN_VERSION "5.15.2") +set(KF5_MIN_VERSION "5.102.0") +set(KDE_COMPILERSETTINGS_LEVEL "5.82") + +find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) + +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(ECMInstallIcons) +include(ECMMarkAsTest) +include(ECMAddTests) +include(GenerateExportHeader) +include(ECMQtDeclareLoggingCategory) +include(KDEClangFormat) +include(KDEGitCommitHooks) +find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} REQUIRED CONFIG COMPONENTS Widgets Test Network Concurrent DBus Quick) +find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG OPTIONAL_COMPONENTS WebView) + +find_package(PkgConfig REQUIRED) + +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED CoreAddons Config Crash DBusAddons I18n Archive XmlGui KIO Declarative KCMUtils IdleTime Notifications Purpose) +find_package(KF5Kirigami2 2.7.0) + +find_package(AppStreamQt5 1.0 CONFIG) +if (AppStreamQt5_FOUND) + add_definitions(-DDISCOVER_USE_STABLE_APPSTREAM) + set(DISCOVER_AppStreamQt_FOUND ${AppStreamQt5_FOUND}) + set(DISCOVER_AppStreamQt_VERSION ${AppStreamQt5_VERSION}) + set(DISCOVER_AppStreamQt_PACKAGE_NAME AppStreamQt5) +else() + find_package(AppStreamQt 0.15.3 CONFIG REQUIRED) + set(DISCOVER_AppStreamQt_FOUND ${AppStreamQt_FOUND}) + set(DISCOVER_AppStreamQt_VERSION ${AppStreamQt_VERSION}) + set(DISCOVER_AppStreamQt_PACKAGE_NAME AppStreamQt) +endif() +find_package(packagekitqt5 1.0.1 CONFIG) +find_package(KF5Attica 5.23 CONFIG) +find_package(KF5NewStuff 5.53 CONFIG) + +pkg_check_modules(Flatpak IMPORTED_TARGET flatpak>=0.11.8) +pkg_check_modules(Fwupd IMPORTED_TARGET fwupd>=1.5.0) +pkg_check_modules(Markdown IMPORTED_TARGET libmarkdown) +if(Markdown_FOUND AND Markdown_VERSION VERSION_GREATER_EQUAL 3) + add_definitions(-DMARKDOWN3) +endif() +pkg_check_modules(Ostree IMPORTED_TARGET ostree-1) +pkg_check_modules(RpmOstree IMPORTED_TARGET rpm-ostree-1) +find_package(KUserFeedback) + +list(APPEND CMAKE_AUTOMOC_MACRO_NAMES "DISCOVER_BACKEND_PLUGIN") + +set(CMAKE_CXX_STANDARD 17) +add_definitions(-DQT_NO_SIGNALS_SLOTS_KEYWORDS -DQT_NO_URL_CAST_FROM_STRING) + +configure_file(DiscoverVersion.h.in DiscoverVersion.h) + +add_subdirectory(libdiscover) +add_subdirectory(discover) +add_subdirectory(exporter) +add_subdirectory(update) + +option(WITH_KCM "Build and install the updates KCM" ON) +if(WITH_KCM) + add_subdirectory(kcm) +endif() + +option(WITH_NOTIFIER "Build and install the notifier plasmoid" ON) +if(WITH_NOTIFIER) + add_subdirectory(notifier) +endif() + +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(KF5Kirigami2 PROPERTIES + DESCRIPTION "KDE's lightweight user interface framework for mobile and convergent applications" + URL "https://techbase.kde.org/Kirigami" + PURPOSE "Required by discover qml components" + TYPE RUNTIME) +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(packagekitqt5 PROPERTIES + DESCRIPTION "Library that exposes PackageKit resources" + URL "https://www.freedesktop.org/software/PackageKit/" + PURPOSE "Required to build the PackageKit backend" + TYPE OPTIONAL) +set_package_properties(${DISCOVER_AppStreamQt_PACKAGE_NAME} PROPERTIES + DESCRIPTION "Library that lists Appstream resources" + URL "https://www.freedesktop.org" + PURPOSE "Required to build the PackageKit, Flatpak and Snap backends" + TYPE OPTIONAL) +add_feature_info(Flatpak Flatpak_FOUND + "Library that exposes flatpak repositories. Required to build the Flatpak backend" +) +add_feature_info(Fwupd Fwupd_FOUND "Exposes fwupd") +add_feature_info(Ostree Ostree_FOUND + "Library to manage ostree repository. Required to build the rpm-ostree backend" +) +add_feature_info(RpmOstree RpmOstree_FOUND + "rpm-ostree binary to manage the system. Required to build the rpm-ostree backend" +) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) + +# add clang-format target for all our real source files +file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) +kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) + +ecm_qt_install_logging_categories( + EXPORT DISCOVER + FILE discover.categories + DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} + ) + +ki18n_install(po) diff --git a/DiscoverVersion.h.in b/DiscoverVersion.h.in new file mode 100644 index 0000000..cf4c752 --- /dev/null +++ b/DiscoverVersion.h.in @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#ifndef DISCOVERVERSION_H +#define DISCOVERVERSION_H + +#include + +static QLatin1String version("@PROJECT_VERSION@"); + +#endif diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..0741db7 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,26 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000..0f3d641 --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,319 @@ +GNU GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C)< yyyy> + +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) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU 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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..1d80ac3 --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,319 @@ +GNU GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU 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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/LICENSES/GPL-3.0-only.txt b/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000..e142a52 --- /dev/null +++ b/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,625 @@ +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for them if you wish), that +you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs, and that you know you +can do these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And +you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. +Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a "modified version" of the earlier work +or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To "convey" a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A "Major Component", in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive +it, in any medium, provided that you conspicuously and appropriately publish +on each copy an appropriate copyright notice; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to "keep intact all notices". + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an "aggregate" if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + + 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + + 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. "Knowingly relying" +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from +the conditions of this License. If you cannot convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + + 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License "or any +later version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM +PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + + 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM +AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO +USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. END OF +TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU 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 . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + +This is free software, and you are welcome to redistribute it under certain +conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/LICENSES/LGPL-2.0-or-later.txt b/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000..5c96471 --- /dev/null +++ b/LICENSES/LGPL-2.0-or-later.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.1-only.txt b/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000..130dffb --- /dev/null +++ b/LICENSES/LGPL-2.1-only.txt @@ -0,0 +1,467 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the +successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +< one line to give the library's name and an idea of what it does. > + +Copyright (C) < year > < name of author > + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +< signature of Ty Coon > , 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-3.0-only.txt b/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000..bd405af --- /dev/null +++ b/LICENSES/LGPL-3.0-only.txt @@ -0,0 +1,163 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms +and conditions of version 3 of the GNU General Public License, supplemented +by the additional permissions listed below. + + 0. Additional Definitions. + + + +As used herein, "this License" refers to version 3 of the GNU Lesser General +Public License, and the "GNU GPL" refers to version 3 of the GNU General Public +License. + + + +"The Library" refers to a covered work governed by this License, other than +an Application or a Combined Work as defined below. + + + +An "Application" is any work that makes use of an interface provided by the +Library, but which is not otherwise based on the Library. Defining a subclass +of a class defined by the Library is deemed a mode of using an interface provided +by the Library. + + + +A "Combined Work" is a work produced by combining or linking an Application +with the Library. The particular version of the Library with which the Combined +Work was made is also called the "Linked Version". + + + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding +Source for the Combined Work, excluding any source code for portions of the +Combined Work that, considered in isolation, are based on the Application, +and not on the Linked Version. + + + +The "Corresponding Application Code" for a Combined Work means the object +code and/or source code for the Application, including any data and utility +programs needed for reproducing the Combined Work from the Application, but +excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without +being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility +refers to a function or data to be supplied by an Application that uses the +facility (other than as an argument passed when the facility is invoked), +then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure +that, in the event an Application does not supply the function or data, the +facility still operates, and performs whatever part of its purpose remains +meaningful, or + +b) under the GNU GPL, with none of the additional permissions of this License +applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header +file that is part of the Library. You may convey such object code under terms +of your choice, provided that, if the incorporated material is not limited +to numerical parameters, data structure layouts and accessors, or small macros, +inline functions and templates (ten or fewer lines in length), you do both +of the following: + +a) Give prominent notice with each copy of the object code that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the object code with a copy of the GNU GPL and this license document. + + 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, +effectively do not restrict modification of the portions of the Library contained +in the Combined Work and reverse engineering for debugging such modifications, +if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the Combined Work with a copy of the GNU GPL and this license +document. + +c) For a Combined Work that displays copyright notices during execution, include +the copyright notice for the Library among these notices, as well as a reference +directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + +0) Convey the Minimal Corresponding Source under the terms of this License, +and the Corresponding Application Code in a form suitable for, and under terms +that permit, the user to recombine or relink the Application with a modified +version of the Linked Version to produce a modified Combined Work, in the +manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + +1) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (a) uses at run time a copy of the Library +already present on the user's computer system, and (b) will operate properly +with a modified version of the Library that is interface-compatible with the +Linked Version. + +e) Provide Installation Information, but only if you would otherwise be required +to provide such information under section 6 of the GNU GPL, and only to the +extent that such information is necessary to install and execute a modified +version of the Combined Work produced by recombining or relinking the Application +with a modified version of the Linked Version. (If you use option 4d0, the +Installation Information must accompany the Minimal Corresponding Source and +Corresponding Application Code. If you use option 4d1, you must provide the +Installation Information in the manner specified by section 6 of the GNU GPL +for conveying Corresponding Source.) + + 5. Combined Libraries. + +You may place library facilities that are a work based on the Library side +by side in a single library together with other library facilities that are +not Applications and are not covered by this License, and convey such a combined +library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities, conveyed under the +terms of this License. + +b) Give prominent notice with the combined library that part of it is a work +based on the Library, and explaining where to find the accompanying uncombined +form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you +received it specifies that a certain numbered version of the GNU Lesser General +Public License "or any later version" applies to it, you have the option of +following the terms and conditions either of that published version or of +any later version published by the Free Software Foundation. If the Library +as you received it does not specify a version number of the GNU Lesser General +Public License, you may choose any version of the GNU Lesser General Public +License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether +future versions of the GNU Lesser General Public License shall apply, that +proxy's public statement of acceptance of any version is permanent authorization +for you to choose that version for the Library. diff --git a/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000..60a2dff --- /dev/null +++ b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt @@ -0,0 +1,12 @@ +This library 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 3 of +the license or (at your option) at any later version that is +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 as 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. diff --git a/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000..232b3c5 --- /dev/null +++ b/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the license or (at your option) any later version +that is 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 as defined in Section 6 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. diff --git a/Messages.sh b/Messages.sh new file mode 100644 index 0000000..85b9b11 --- /dev/null +++ b/Messages.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +$EXTRACTRC --context="Category" --tag-group=none --tag=Name `find libdiscover -name "*-categories.xml"` >> rc.cpp +$XGETTEXT rc.cpp `find libdiscover -name \*.cpp -o -name \*.qml` -o $podir/libdiscover.pot +$XGETTEXT `find discover -name \*.cpp -o -name \*.qml -o -name \*.js` -o $podir/plasma-discover.pot +$XGETTEXT `find notifier -name \*.cpp` -o $podir/plasma-discover-notifier.pot +$XGETTEXT `find kcm -name \*.cpp -o -name \*.qml` -o $podir/kcm_updates.pot diff --git a/cmake/FindPackageKitQt2.cmake b/cmake/FindPackageKitQt2.cmake new file mode 100644 index 0000000..8552a91 --- /dev/null +++ b/cmake/FindPackageKitQt2.cmake @@ -0,0 +1,35 @@ +# - Try to find the PackageKitQt2 library +# Once done this will define +# +# PACKAGEKITQT2_FOUND - system has the PackageKitQt2 library +# PACKAGEKITQT2_INCLUDEDIR - the PackageKitQt2 include directory +# PACKAGEKITQT2_LIBRARY - Link this to use the PackageKitQt2 +# +# SPDX-FileCopyrightText: 2010 Mehrdad Momeny +# SPDX-FileCopyrightText: 2010 Harald Sitter +# SPDX-FileCopyrightText: 2013 Lukas Appelhans +# +# SPDX-License-Identifier: BSD-3-Clause + +if (NOT WIN32) + find_package(PkgConfig) + pkg_check_modules(QPACKAGEKIT2 packagekit-qt2>=0.8) +endif() + +set(PackageKitQt2_FOUND FALSE) +if(QPACKAGEKIT2_FOUND) + find_library(PACKAGEKITQT2_LIBRARY NAMES packagekit-qt2 + HINTS ${QPACKAGEKIT2_LIBRARIES} + ) + + find_path(PACKAGEKITQT2_INCLUDEDIR PackageKit/packagekit-qt2/daemon.h + HINTS ${QPACKAGEKIT2_INCLUDEDIR} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(PackageKitQt2 DEFAULT_MSG PACKAGEKITQT2_LIBRARY PACKAGEKITQT2_INCLUDEDIR) + if(PACKAGEKITQT2_LIBRARY AND PACKAGEKITQT2_INCLUDEDIR) + mark_as_advanced(PACKAGEKITQT2_INCLUDEDIR PACKAGEKITQT2_LIBRARY) + set(PackageKitQt2_FOUND TRUE) + endif() +endif() diff --git a/discover/AbstractAppsModel.cpp b/discover/AbstractAppsModel.cpp new file mode 100644 index 0000000..f29acc9 --- /dev/null +++ b/discover/AbstractAppsModel.cpp @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AbstractAppsModel.h" + +#include "discover_debug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class BestInResultsStream : public QObject +{ + Q_OBJECT +public: + BestInResultsStream(const QSet &streams) + : QObject() + { + connect(this, &BestInResultsStream::finished, this, &QObject::deleteLater); + Q_ASSERT(!streams.contains(nullptr)); + if (streams.isEmpty()) { + QTimer::singleShot(0, this, &BestInResultsStream::clear); + } + + for (auto stream : streams) { + m_streams.insert(stream); + connect(stream, &ResultsStream::resourcesFound, this, [this](const QVector &resources) { + m_resources.append(resources.constFirst()); + }); + connect(stream, &QObject::destroyed, this, &BestInResultsStream::streamDestruction); + } + } + + void streamDestruction(QObject *obj) + { + m_streams.remove(obj); + clear(); + } + + void clear() + { + if (m_streams.isEmpty()) { + Q_EMIT finished(m_resources); + } + } + +Q_SIGNALS: + void finished(QVector resources); + +private: + QVector m_resources; + QSet m_streams; +}; + +AbstractAppsModel::AbstractAppsModel() +{ + connect(ResourcesModel::global(), &ResourcesModel::currentApplicationBackendChanged, this, &AbstractAppsModel::refreshCurrentApplicationBackend); + refreshCurrentApplicationBackend(); +} + +void AbstractAppsModel::refreshCurrentApplicationBackend() +{ + auto backend = ResourcesModel::global()->currentApplicationBackend(); + if (m_backend == backend) + return; + + if (m_backend) { + disconnect(m_backend, &AbstractResourcesBackend::fetchingChanged, this, &AbstractAppsModel::refresh); + disconnect(m_backend, &AbstractResourcesBackend::resourceRemoved, this, &AbstractAppsModel::removeResource); + } + + m_backend = backend; + + if (backend) { + connect(backend, &AbstractResourcesBackend::fetchingChanged, this, &AbstractAppsModel::refresh); + connect(backend, &AbstractResourcesBackend::resourceRemoved, this, &AbstractAppsModel::removeResource); + } + + Q_EMIT currentApplicationBackendChanged(m_backend); +} + +void AbstractAppsModel::setUris(const QVector &uris) +{ + if (!m_backend) + return; + + if (m_uris == uris) { + return; + } + m_uris = uris; + + QSet streams; + for (const auto &uri : uris) { + AbstractResourcesBackend::Filters filter; + filter.resourceUrl = uri; + streams << m_backend->search(filter); + } + if (!streams.isEmpty()) { + auto stream = new BestInResultsStream(streams); + acquireFetching(true); + connect(stream, &BestInResultsStream::finished, this, &AbstractAppsModel::setResources); + } +} + +static void filterDupes(QVector &resources) +{ + QSet found; + for (auto it = resources.begin(); it != resources.end();) { + auto id = (*it)->appstreamId(); + if (found.contains(id)) { + it = resources.erase(it); + } else { + found.insert(id); + ++it; + } + } +} + +void AbstractAppsModel::acquireFetching(bool f) +{ + if (f) + m_isFetching++; + else + m_isFetching--; + + if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) { + Q_EMIT isFetchingChanged(); + } + Q_ASSERT(m_isFetching >= 0); +} + +void AbstractAppsModel::setResources(const QVector &_resources) +{ + auto resources = _resources; + filterDupes(resources); + + if (m_resources != resources) { + // TODO: sort like in the json files + beginResetModel(); + m_resources = resources; + endResetModel(); + Q_EMIT appsCountChanged(); + } + + acquireFetching(false); +} + +void AbstractAppsModel::removeResource(AbstractResource *resource) +{ + int index = m_resources.indexOf(resource); + if (index < 0) + return; + + beginRemoveRows({}, index, index); + m_resources.removeAt(index); + endRemoveRows(); +} + +QVariant AbstractAppsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || role != Qt::UserRole) + return {}; + + auto res = m_resources.value(index.row()); + if (!res) + return {}; + + return QVariant::fromValue(res); +} + +int AbstractAppsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_resources.count(); +} + +QHash AbstractAppsModel::roleNames() const +{ + return {{Qt::UserRole, "application"}}; +} + +#include "AbstractAppsModel.moc" diff --git a/discover/AbstractAppsModel.h b/discover/AbstractAppsModel.h new file mode 100644 index 0000000..f2da44b --- /dev/null +++ b/discover/AbstractAppsModel.h @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2016-2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "resources/AbstractResourcesBackend.h" +#include + +class AbstractAppsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY isFetchingChanged) + Q_PROPERTY(bool isFetching READ isFetching NOTIFY isFetchingChanged) + Q_PROPERTY(AbstractResourcesBackend *currentApplicationBackend READ currentApplicationBackend NOTIFY currentApplicationBackendChanged) +public: + AbstractAppsModel(); + + void setResources(const QVector &resources); + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + QHash roleNames() const override; + AbstractResourcesBackend *currentApplicationBackend() const + { + return m_backend; + } + + bool isFetching() const + { + return m_isFetching != 0; + } + + virtual void refresh() = 0; + int count() const + { + return rowCount({}); + } + +Q_SIGNALS: + void appsCountChanged(); + void isFetchingChanged(); + void currentApplicationBackendChanged(AbstractResourcesBackend *currentApplicationBackend); + +protected: + void refreshCurrentApplicationBackend(); + void setUris(const QVector &uris); + void removeResource(AbstractResource *resource); + + void acquireFetching(bool f); + +private: + QVector m_resources; + int m_isFetching = 0; + AbstractResourcesBackend *m_backend = nullptr; + QVector m_uris; +}; diff --git a/discover/CMakeLists.txt b/discover/CMakeLists.txt new file mode 100644 index 0000000..17ac7f8 --- /dev/null +++ b/discover/CMakeLists.txt @@ -0,0 +1,115 @@ +add_subdirectory(icons) +if(BUILD_TESTING) + add_subdirectory(autotests) +endif() + +include_directories(${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/..) + +ecm_qt_declare_logging_category(plasma_discover_SRCS HEADER discover_debug.h IDENTIFIER DISCOVER_LOG CATEGORY_NAME org.kde.plasma.discover DESCRIPTION "discover" EXPORT DISCOVER) +kconfig_add_kcfg_files(plasma_discover_SRCS discoversettings.kcfgc GENERATE_MOC) + +if (TARGET KUserFeedbackCore) + kconfig_add_kcfg_files(plasma_discover_SRCS plasmauserfeedback.kcfgc GENERATE_MOC) +endif() + +add_executable(plasma-discover ${plasma_discover_SRCS} + main.cpp + DiscoverObject.cpp + DiscoverDeclarativePlugin.cpp + + AbstractAppsModel.cpp + OdrsAppsModel.cpp + FeaturedModel.cpp + PaginateModel.cpp + UnityLauncher.cpp + ReadFile.cpp + PowerManagementInterface.cpp + + DiscoverObject.h + DiscoverDeclarativePlugin.h + + FeaturedModel.h + PaginateModel.h + UnityLauncher.h + ReadFile.h + + + resources.qrc +) +add_executable(Plasma::Discover ALIAS plasma-discover) +set_target_properties(plasma-discover PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) + +target_link_libraries(plasma-discover PUBLIC + KF5::Crash + KF5::DBusAddons + KF5::I18n + KF5::ConfigGui + KF5::KIOCore + KF5::WindowSystem + KF5::Notifications + KF5::JobWidgets + Qt::Widgets + Qt::Quick + Discover::Common +) + +if (TARGET Qt::WebView) + target_link_libraries(plasma-discover PUBLIC Qt::WebView) + target_compile_definitions(plasma-discover PUBLIC -DWITH_QTWEBVIEW=1) +else() + target_compile_definitions(plasma-discover PUBLIC -DWITH_QTWEBVIEW=0) +endif() + +if (TARGET KUserFeedbackCore) + target_link_libraries(plasma-discover PRIVATE KUserFeedbackCore) + target_compile_definitions(plasma-discover PRIVATE WITH_FEEDBACK=1) +endif() + +install(TARGETS plasma-discover ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +# if (BUILD_DummyBackend) +# target_compile_definitions(plasma-discover PRIVATE $<$:QT_QML_DEBUG=1>) +# endif() + +# Standard desktop file accepts local files as input. +set(DesktopNoDisplay "false") +find_program(DPKG dpkg) +find_program(RPM rpm) +set(DesktopMimeType "") +if(DPKG) + set(DesktopMimeType "${DesktopMimeType}application/vnd.debian.binary-package;") +endif() +if(RPM) + set(DesktopMimeType "${DesktopMimeType}application/x-rpm;") +endif() +if(Flatpak_FOUND) + set(DesktopMimeType "${DesktopMimeType}application/vnd.flatpak;application/vnd.flatpak.repo;application/vnd.flatpak.ref;") +endif() +set(DesktopExec "plasma-discover %F") +configure_file(org.kde.discover.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.desktop) +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) + +# Support appstream:// URI +set(DesktopNoDisplay "true") +set(DesktopMimeType "x-scheme-handler/appstream;") +set(DesktopExec "plasma-discover %U") +configure_file(org.kde.discover.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.urlhandler.desktop) +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.urlhandler.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) + +# support snap:/ URI +set(DesktopNoDisplay "true") +set(DesktopMimeType "x-scheme-handler/snap;") +set(DesktopExec "plasma-discover %U") +configure_file(org.kde.discover.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.snap.desktop) +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.snap.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) + +if(EXISTS "/etc/debian_version") + set(DesktopNoDisplay "true") + set(DesktopMimeType "x-scheme-handler/apt") + set(DesktopExec "plasma-discover %U") + configure_file(org.kde.discover.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.apt.urlhandler.desktop) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.apt.urlhandler.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) +endif() + +install(FILES plasmadiscoverui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/plasmadiscover) +install(FILES org.kde.discover.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) diff --git a/discover/DiscoverDeclarativePlugin.cpp b/discover/DiscoverDeclarativePlugin.cpp new file mode 100644 index 0000000..8829a59 --- /dev/null +++ b/discover/DiscoverDeclarativePlugin.cpp @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "DiscoverDeclarativePlugin.h" +#include "ReadFile.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void DiscoverDeclarativePlugin::registerTypes(const char * /*uri*/) +{ + qmlRegisterType("org.kde.discover", 2, 0, "TransactionListener"); + qmlRegisterType("org.kde.discover", 2, 0, "ResourcesUpdatesModel"); + qmlRegisterType("org.kde.discover", 2, 0, "ResourcesProxyModel"); + qRegisterMetaType(); + + qmlRegisterType("org.kde.discover", 2, 0, "ReviewsModel"); + qmlRegisterType("org.kde.discover", 2, 0, "ApplicationAddonsModel"); + qmlRegisterType("org.kde.discover", 2, 0, "ScreenshotsModel"); + qmlRegisterType("org.kde.discover", 2, 0, "UpdateModel"); + qmlRegisterType("org.kde.discover", 2, 0, "ReadFile"); + + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "DiscoverAction", QStringLiteral("Use QQC Action")); + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "AbstractResource", QStringLiteral("should come from the ResourcesModel")); + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "AbstractSourcesBackend", QStringLiteral("should come from the SourcesModel")); + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "Transaction", QStringLiteral("should come from the backends")); + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "SourcesModelClass", QStringLiteral("should come from the backends")); + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "AbstractBackendUpdater", QStringLiteral("should come from the backends")); + qmlRegisterUncreatableType("org.kde.discover", 2, 0, "InlineMessage", QStringLiteral("should come from the backend")); + qmlRegisterAnonymousType("org.kde.discover", 1); + qmlRegisterAnonymousType("org.kde.discover", 1); + qmlRegisterAnonymousType("org.kde.discover", 1); + qmlRegisterAnonymousType("org.kde.discover", 1); + qmlRegisterAnonymousType("org.kde.discover", 1); + qmlRegisterAnonymousType("org.kde.discover", 1); + qmlRegisterSingletonInstance("org.kde.discover", 2, 0, "CategoryModel", CategoryModel::global()); + qmlRegisterSingletonInstance("org.kde.discover", 2, 0, "ResourcesModel", ResourcesModel::global()); + qmlRegisterSingletonInstance("org.kde.discover", 2, 0, "TransactionModel", TransactionModel::global()); + qmlRegisterSingletonInstance("org.kde.discover", 2, 0, "SourcesModel", SourcesModel::global()); + + qmlProtectModule("org.kde.discover", 2); +} diff --git a/discover/DiscoverDeclarativePlugin.h b/discover/DiscoverDeclarativePlugin.h new file mode 100644 index 0000000..4c1aac2 --- /dev/null +++ b/discover/DiscoverDeclarativePlugin.h @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include + +class DiscoverDeclarativePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") +public: + void registerTypes(const char *uri) override; +}; diff --git a/discover/DiscoverObject.cpp b/discover/DiscoverObject.cpp new file mode 100644 index 0000000..b18db10 --- /dev/null +++ b/discover/DiscoverObject.cpp @@ -0,0 +1,657 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "DiscoverObject.h" +#include "CachedNetworkAccessManager.h" +#include "DiscoverBackendsFactory.h" +#include "DiscoverDeclarativePlugin.h" +#include "FeaturedModel.h" +#include "OdrsAppsModel.h" +#include "PaginateModel.h" +#include "UnityLauncher.h" +#include + +// Qt includes +#include "discover_debug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// KDE includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// #include + +// DiscoverCommon includes +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef WITH_FEEDBACK +#include "plasmauserfeedback.h" +#endif +#include "PowerManagementInterface.h" +#include "discoversettings.h" + +class CachedNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory +{ + virtual QNetworkAccessManager *create(QObject *parent) override + { + return new CachedNetworkAccessManager(QStringLiteral("images"), parent); + } +}; + +class OurSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) +public: + void classBegin() override + { + } + void componentComplete() override + { + if (dynamicSortFilter()) + sort(0); + } +}; + +DiscoverObject::DiscoverObject(CompactMode mode, const QVariantMap &initialProperties) + : QObject() + , m_engine(new QQmlApplicationEngine) + , m_mode(mode) + , m_networkAccessManagerFactory(new CachedNetworkAccessManagerFactory) +{ + setObjectName(QStringLiteral("DiscoverMain")); + m_engine->rootContext()->setContextObject(new KLocalizedContext(m_engine)); + auto factory = m_engine->networkAccessManagerFactory(); + m_engine->setNetworkAccessManagerFactory(nullptr); + delete factory; + m_engine->setNetworkAccessManagerFactory(m_networkAccessManagerFactory.data()); + + qmlRegisterType("org.kde.discover.app", 1, 0, "UnityLauncher"); + qmlRegisterType("org.kde.discover.app", 1, 0, "PaginateModel"); + qmlRegisterType("org.kde.discover.app", 1, 0, "FeaturedModel"); + qmlRegisterType("org.kde.discover.app", 1, 0, "OdrsAppsModel"); + qmlRegisterType("org.kde.discover.app", 1, 0, "PowerManagementInterface"); + qmlRegisterType("org.kde.discover.app", 1, 0, "QSortFilterProxyModel"); +#ifdef WITH_FEEDBACK + qmlRegisterSingletonType("org.kde.discover.app", 1, 0, "UserFeedbackSettings", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new PlasmaUserFeedback(KSharedConfig::openConfig(QStringLiteral("PlasmaUserFeedback"), KConfig::NoGlobals)); + }); +#endif + qmlRegisterSingletonType("org.kde.discover.app", 1, 0, "DiscoverSettings", [](QQmlEngine *engine, QJSEngine *) -> QObject * { + auto r = new DiscoverSettings; + r->setParent(engine); + connect(r, &DiscoverSettings::installedPageSortingChanged, r, &DiscoverSettings::save); + connect(r, &DiscoverSettings::appsListPageSortingChanged, r, &DiscoverSettings::save); + return r; + }); + qmlRegisterAnonymousType("org.kde.discover.app", 1); + + qmlRegisterAnonymousType("org.kde.discover.app", 1); + qmlRegisterAnonymousType("org.kde.discover.app", 1); + qmlRegisterAnonymousType("org.kde.discover.app", 1); + + qmlRegisterUncreatableType("org.kde.discover.app", 1, 0, "DiscoverMainWindow", QStringLiteral("don't do that")); + + auto uri = "org.kde.discover"; + DiscoverDeclarativePlugin *plugin = new DiscoverDeclarativePlugin; + plugin->setParent(this); + plugin->initializeEngine(m_engine, uri); + plugin->registerTypes(uri); + + m_engine->setInitialProperties(initialProperties); + m_engine->rootContext()->setContextProperty(QStringLiteral("app"), this); + m_engine->rootContext()->setContextProperty(QStringLiteral("discoverAboutData"), QVariant::fromValue(KAboutData::applicationData())); + + connect(m_engine, &QQmlApplicationEngine::objectCreated, this, &DiscoverObject::integrateObject); + m_engine->load(QUrl(QStringLiteral("qrc:/qml/DiscoverWindow.qml"))); + + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + const auto objs = m_engine->rootObjects(); + for (auto o : objs) + delete o; + }); + auto action = new OneTimeAction( + [this]() { + bool found = DiscoverBackendsFactory::hasRequestedBackends(); + const auto backends = ResourcesModel::global()->backends(); + for (auto b : backends) + found |= b->hasApplications(); + + if (!found) { + QString errorText = i18n( + "Discover currently cannot be used to install any apps or " + "perform system updates because none of its app backends are " + "available."); + QString errorExplanation = xi18nc("@info", + "You can install some on the Settings page, under the " + "Missing Backends section." + "Also please consider reporting this as a packaging issue to " + "your distribution."); + QString buttonIcon = QStringLiteral("tools-report-bug"); + QString buttonText = i18n("Report This Issue"); + QString buttonUrl = KOSRelease().bugReportUrl(); + + if (KOSRelease().name().contains(QStringLiteral("Arch Linux"))) { + errorExplanation = xi18nc("@info", + "You can use pacman to " + "install the optional dependencies that are needed to " + "enable the application backends.Please note " + "that Arch Linux developers recommend using " + "pacman for managing software because " + "the PackageKit backend is not well-integrated on Arch " + "Linux."); + buttonIcon = QStringLiteral("help-about"); + buttonText = i18n("Learn More"); + buttonUrl = KOSRelease().supportUrl(); + } + + Q_EMIT openErrorPage(errorText, errorExplanation, buttonText, buttonIcon, buttonUrl); + } + }, + this); + + if (ResourcesModel::global()->backends().isEmpty()) { + connect(ResourcesModel::global(), &ResourcesModel::allInitialized, action, &OneTimeAction::trigger); + } else { + action->trigger(); + } +} + +DiscoverObject::~DiscoverObject() +{ + m_engine->deleteLater(); +} + +bool DiscoverObject::isRoot() +{ + return ::getuid() == 0; +} + +QStringList DiscoverObject::modes() const +{ + QStringList ret; + QObject *obj = rootObject(); + for (int i = obj->metaObject()->propertyOffset(); i < obj->metaObject()->propertyCount(); i++) { + QMetaProperty p = obj->metaObject()->property(i); + QByteArray compName = p.name(); + if (compName.startsWith("top") && compName.endsWith("Comp")) { + ret += QString::fromLatin1(compName.mid(3, compName.length() - 7)); + } + } + return ret; +} + +void DiscoverObject::openMode(const QString &_mode) +{ + QObject *obj = rootObject(); + if (!obj) { + qCWarning(DISCOVER_LOG) << "could not get the main object"; + return; + } + + if (!modes().contains(_mode, Qt::CaseInsensitive)) + qCWarning(DISCOVER_LOG) << "unknown mode" << _mode << modes(); + + QString mode = _mode; + mode[0] = mode[0].toUpper(); + + const QByteArray propertyName = "top" + mode.toLatin1() + "Comp"; + const QVariant modeComp = obj->property(propertyName.constData()); + if (!modeComp.isValid()) + qCWarning(DISCOVER_LOG) << "couldn't open mode" << _mode; + else + obj->setProperty("currentTopLevel", modeComp); +} + +void DiscoverObject::openMimeType(const QString &mime) +{ + Q_EMIT listMimeInternal(mime); +} + +void DiscoverObject::showLoadingPage() +{ + QObject *obj = rootObject(); + if (!obj) { + qCWarning(DISCOVER_LOG) << "could not get the main object"; + return; + } + + obj->setProperty("currentTopLevel", QStringLiteral("qrc:/qml/LoadingPage.qml")); +} + +void DiscoverObject::openCategory(const QString &category) +{ + showLoadingPage(); + auto action = new OneTimeAction( + [this, category]() { + Category *cat = CategoryModel::global()->findCategoryByName(category); + if (cat) { + Q_EMIT listCategoryInternal(cat); + } else { + openMode(QStringLiteral("Browsing")); + showError(i18n("Could not find category '%1'", category)); + } + }, + this); + + if (CategoryModel::global()->rootCategories().isEmpty()) { + connect(CategoryModel::global(), &CategoryModel::rootCategoriesChanged, action, &OneTimeAction::trigger); + } else { + action->trigger(); + } +} + +void DiscoverObject::openLocalPackage(const QUrl &localfile) +{ + if (!QFile::exists(localfile.toLocalFile())) { + showError(i18n("Trying to open inexisting file '%1'", localfile.toString())); + openMode(QStringLiteral("Browsing")); + return; + } + showLoadingPage(); + auto action = new OneTimeAction( + [this, localfile]() { + AbstractResourcesBackend::Filters f; + f.resourceUrl = localfile; + auto stream = new StoredResultsStream({ResourcesModel::global()->search(f)}); + connect(stream, &StoredResultsStream::finishedResources, this, [this, localfile](const QVector &res) { + if (res.count() == 1) { + Q_EMIT openApplicationInternal(res.first()); + } else { + QMimeDatabase db; + auto mime = db.mimeTypeForUrl(localfile); + auto fIsFlatpakBackend = [](AbstractResourcesBackend *backend) { + return backend->metaObject()->className() == QByteArray("FlatpakBackend"); + }; + if (mime.name().startsWith(QLatin1String("application/vnd.flatpak")) + && !kContains(ResourcesModel::global()->backends(), fIsFlatpakBackend)) { + openApplication(QUrl(QStringLiteral("appstream://org.kde.discover.flatpak"))); + showError(i18n("Cannot interact with flatpak resources without the flatpak backend %1. Please install it first.", + localfile.toDisplayString())); + } else { + openMode(QStringLiteral("Browsing")); + showError(i18n("Could not open %1", localfile.toDisplayString())); + } + } + }); + }, + this); + + if (ResourcesModel::global()->backends().isEmpty()) { + connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, action, &OneTimeAction::trigger); + } else { + action->trigger(); + } +} + +void DiscoverObject::openApplication(const QUrl &url) +{ + Q_ASSERT(!url.isEmpty()); + showLoadingPage(); + auto action = new OneTimeAction( + [this, url]() { + AbstractResourcesBackend::Filters f; + f.resourceUrl = url; + auto stream = new StoredResultsStream({ResourcesModel::global()->search(f)}); + connect(stream, &StoredResultsStream::finishedResources, this, [this, url](const QVector &res) { + if (res.count() >= 1) { + QPointer timeout = new QTimer(this); + timeout->setSingleShot(true); + timeout->setInterval(20000); + connect(timeout, &QTimer::timeout, timeout, &QTimer::deleteLater); + + auto openResourceOrWait = [this, res, timeout] { + auto idx = kIndexOf(res, [](auto res) { + return res->isInstalled(); + }); + if (idx < 0) { + bool oneBroken = kContains(res, [](auto res) { + return res->state() == AbstractResource::Broken; + }); + if (oneBroken && timeout) { + return false; + } + + idx = 0; + } + Q_EMIT openApplicationInternal(res[idx]); + return true; + }; + + if (!openResourceOrWait()) { + auto f = new OneTimeAction(0, openResourceOrWait, this); + for (auto r : res) { + if (r->state() == AbstractResource::Broken) { + connect(r, &AbstractResource::stateChanged, f, &OneTimeAction::trigger); + } + } + timeout->start(); + connect(timeout, &QTimer::destroyed, f, &OneTimeAction::trigger); + } else { + delete timeout; + } + } else if (url.scheme() == QLatin1String("snap")) { + openApplication(QUrl(QStringLiteral("appstream://org.kde.discover.snap"))); + showError(i18n("Please make sure Snap support is installed")); + } else { + const QString errorText = i18n("Could not open %1 because it " + "was not found in any available software repositories.", + url.toDisplayString()); + const QString errorExplanation = i18n("Please report this " + "issue to the packagers of your distribution."); + QString buttonIcon = QStringLiteral("tools-report-bug"); + QString buttonText = i18n("Report This Issue"); + QString buttonUrl = KOSRelease().bugReportUrl(); + Q_EMIT openErrorPage(errorText, errorExplanation, buttonText, buttonIcon, buttonUrl); + } + }); + }, + this); + + if (ResourcesModel::global()->backends().isEmpty()) { + connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, action, &OneTimeAction::trigger); + } else { + action->trigger(); + } +} + +class TransactionsJob : public KJob +{ +public: + void start() override + { + // no-op, this is just observing + + setTotalAmount(Items, TransactionModel::global()->rowCount()); + setPercent(TransactionModel::global()->progress()); + connect(TransactionModel::global(), &TransactionModel::lastTransactionFinished, this, &TransactionsJob::emitResult); + connect(TransactionModel::global(), &TransactionModel::transactionRemoved, this, &TransactionsJob::refreshInfo); + connect(TransactionModel::global(), &TransactionModel::progressChanged, this, [this] { + setPercent(TransactionModel::global()->progress()); + }); + refreshInfo(); + } + + void refreshInfo() + { + if (TransactionModel::global()->rowCount() == 0) { + return; + } + + setProcessedAmount(Items, totalAmount(Items) - TransactionModel::global()->rowCount() + 1); + auto firstTransaction = TransactionModel::global()->transactions().constFirst(); + Q_EMIT description(this, firstTransaction->name()); + } + + void cancel() + { + setError(KJob::KilledJobError /*KIO::ERR_USER_CANCELED*/); + deleteLater(); + } +}; + +bool DiscoverObject::quitWhenIdle() +{ + if (!ResourcesModel::global()->isBusy()) { + return true; + } + + if (!m_sni) { + auto tracker = new KUiServerV2JobTracker(m_sni); + + m_sni = new KStatusNotifierItem(this); + m_sni->setStatus(KStatusNotifierItem::Active); + m_sni->setIconByName("plasmadiscover"); + m_sni->setTitle(i18n("Discover")); + m_sni->setToolTip("process-working-symbolic", i18n("Discover"), i18n("Discover was closed before certain tasks were done, waiting for it to finish.")); + m_sni->setStandardActionsEnabled(false); + + connect(TransactionModel::global(), &TransactionModel::countChanged, this, &DiscoverObject::reconsiderQuit); + connect(m_sni, &KStatusNotifierItem::activateRequested, this, &DiscoverObject::restore); + + auto job = new TransactionsJob; + job->setParent(this); + tracker->registerJob(job); + job->start(); + connect(m_sni, &KStatusNotifierItem::activateRequested, job, &TransactionsJob::cancel); + + rootObject()->hide(); + } + return false; +} + +void DiscoverObject::restore() +{ + if (!m_sni) { + return; + } + + disconnect(TransactionModel::global(), &TransactionModel::countChanged, this, &DiscoverObject::reconsiderQuit); + disconnect(m_sni, &KStatusNotifierItem::activateRequested, this, &DiscoverObject::restore); + + rootObject()->show(); + m_sni->deleteLater(); + m_sni = nullptr; +} + +void DiscoverObject::reconsiderQuit() +{ + if (ResourcesModel::global()->isBusy()) { + return; + } + + m_sni->deleteLater(); + // Let the job UI to finalise properly + QTimer::singleShot(20, qGuiApp, &QCoreApplication::quit); +} + +void DiscoverObject::integrateObject(QObject *object) +{ + if (!object) { + qCWarning(DISCOVER_LOG) << "Errors when loading the GUI"; + QTimer::singleShot(0, QCoreApplication::instance(), []() { + QCoreApplication::instance()->exit(1); + }); + return; + } + + Q_ASSERT(object == rootObject()); + + KConfigGroup window(KSharedConfig::openConfig(), "Window"); + if (window.hasKey("geometry")) + rootObject()->setGeometry(window.readEntry("geometry", QRect())); + if (window.hasKey("visibility")) { + QWindow::Visibility visibility(QWindow::Visibility(window.readEntry("visibility", QWindow::Windowed))); + rootObject()->setVisibility(qMax(visibility, QQuickView::AutomaticVisibility)); + } + connect(rootObject(), &QQuickView::sceneGraphError, this, [](QQuickWindow::SceneGraphError /*error*/, const QString &message) { + KCrash::setErrorMessage(message); + qFatal("%s", qPrintable(message)); + }); + + object->installEventFilter(this); + connect(object, &QObject::destroyed, qGuiApp, &QCoreApplication::quit); + + object->setParent(m_engine); + connect(qGuiApp, &QGuiApplication::commitDataRequest, this, [this](QSessionManager &sessionManager) { + if (!quitWhenIdle()) { + sessionManager.cancel(); + } + }); +} + +bool DiscoverObject::eventFilter(QObject *object, QEvent *event) +{ + if (object != rootObject()) + return false; + + if (event->type() == QEvent::Close) { + if (!quitWhenIdle()) { + return true; + } + + KConfigGroup window(KSharedConfig::openConfig(), "Window"); + window.writeEntry("geometry", rootObject()->geometry()); + window.writeEntry("visibility", rootObject()->visibility()); + // } else if (event->type() == QEvent::ShortcutOverride) { + // qCWarning(DISCOVER_LOG) << "Action conflict" << event; + } + return false; +} + +QString DiscoverObject::iconName(const QIcon &icon) +{ + return icon.name(); +} + +void DiscoverObject::switchApplicationLanguage() +{ + // auto langDialog = new KSwitchLanguageDialog(nullptr); + // connect(langDialog, SIGNAL(finished(int)), this, SLOT(dialogFinished())); + // langDialog->show(); +} + +void DiscoverObject::setCompactMode(DiscoverObject::CompactMode mode) +{ + if (m_mode != mode) { + m_mode = mode; + Q_EMIT compactModeChanged(m_mode); + } +} + +class DiscoverTestExecutor : public QObject +{ +public: + DiscoverTestExecutor(QObject *rootObject, QQmlEngine *engine, const QUrl &url) + : QObject(engine) + { + connect(engine, &QQmlEngine::quit, this, &DiscoverTestExecutor::finish, Qt::QueuedConnection); + + QQmlComponent *component = new QQmlComponent(engine, url, engine); + m_testObject = component->create(engine->rootContext()); + + if (!m_testObject) { + qCWarning(DISCOVER_LOG) << "error loading test" << url << m_testObject << component->errors(); + Q_ASSERT(false); + } + + m_testObject->setProperty("appRoot", QVariant::fromValue(rootObject)); + connect(engine, &QQmlEngine::warnings, this, &DiscoverTestExecutor::processWarnings); + } + + void processWarnings(const QList &warnings) + { + for (const QQmlError &warning : warnings) { + if (warning.url().path().endsWith(QLatin1String("DiscoverTest.qml"))) { + qCWarning(DISCOVER_LOG) << "Test failed!" << warnings; + qGuiApp->exit(1); + } + } + m_warnings << warnings; + } + + void finish() + { + if (m_warnings.isEmpty()) + qCDebug(DISCOVER_LOG) << "cool no warnings!"; + else + qCDebug(DISCOVER_LOG) << "test finished successfully despite" << m_warnings; + qGuiApp->exit(m_warnings.count()); + } + +private: + QObject *m_testObject; + QList m_warnings; +}; + +void DiscoverObject::loadTest(const QUrl &url) +{ + new DiscoverTestExecutor(rootObject(), engine(), url); +} + +QQuickWindow *DiscoverObject::rootObject() const +{ + return qobject_cast(m_engine->rootObjects().at(0)); +} + +void DiscoverObject::showError(const QString &msg) +{ + QTimer::singleShot(100, this, [msg]() { + Q_EMIT ResourcesModel::global()->passiveMessage(msg); + }); +} + +void DiscoverObject::copyTextToClipboard(const QString text) +{ + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); +} + +void DiscoverObject::reboot() +{ + auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"), + QStringLiteral("/LogoutPrompt"), + QStringLiteral("org.kde.LogoutPrompt"), + QStringLiteral("promptReboot")); + QDBusConnection::sessionBus().asyncCall(method); +} + +void DiscoverObject::rebootNow() +{ + auto method = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"), + QStringLiteral("/org/freedesktop/login1"), + QStringLiteral("org.freedesktop.login1.Manager"), + QStringLiteral("Reboot")); + method.setArguments({true /*interactive*/}); + QDBusConnection::systemBus().asyncCall(method); +} + +QRect DiscoverObject::initialGeometry() const +{ + KConfigGroup window(KSharedConfig::openConfig(), "Window"); + return window.readEntry("geometry", QRect()); +} + +QString DiscoverObject::describeSources() const +{ + return rootObject()->property("describeSources").toString(); +} + +#include "DiscoverObject.moc" diff --git a/discover/DiscoverObject.h b/discover/DiscoverObject.h new file mode 100644 index 0000000..4851009 --- /dev/null +++ b/discover/DiscoverObject.h @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include + +#include + +class AbstractResource; +class Category; +class KStatusNotifierItem; +class QWindow; +class QQmlApplicationEngine; +class CachedNetworkAccessManagerFactory; +class TransactionsJob; + +class DiscoverObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(CompactMode compactMode READ compactMode WRITE setCompactMode NOTIFY compactModeChanged) + Q_PROPERTY(bool isRoot READ isRoot CONSTANT) + Q_PROPERTY(QRect initialGeometry READ initialGeometry CONSTANT) + +public: + enum CompactMode { + Auto, + Compact, + Full, + }; + Q_ENUM(CompactMode) + + explicit DiscoverObject(CompactMode mode, const QVariantMap &initialProperties); + ~DiscoverObject() override; + + QStringList modes() const; + + CompactMode compactMode() const + { + return m_mode; + } + void setCompactMode(CompactMode mode); + + bool eventFilter(QObject *object, QEvent *event) override; + + Q_SCRIPTABLE static QString iconName(const QIcon &icon); + + void loadTest(const QUrl &url); + + static bool isRoot(); + QQuickWindow *rootObject() const; + void showError(const QString &msg); + Q_INVOKABLE void copyTextToClipboard(const QString text); + QRect initialGeometry() const; + + QString describeSources() const; + Q_SCRIPTABLE void restore(); + +public Q_SLOTS: + void openApplication(const QUrl &app); + void openMimeType(const QString &mime); + void openCategory(const QString &category); + void openMode(const QString &mode); + void openLocalPackage(const QUrl &localfile); + + void reboot(); + void rebootNow(); + +private Q_SLOTS: + void switchApplicationLanguage(); + +Q_SIGNALS: + void openSearch(const QString &search); + void openApplicationInternal(AbstractResource *app); + void listMimeInternal(const QString &mime); + void listCategoryInternal(Category *cat); + + void compactModeChanged(DiscoverObject::CompactMode compactMode); + void unableToFind(const QString &resid); + void openErrorPage(const QString &errorMessage, const QString &errorExplanation, const QString &buttonText, const QString &buttonIcon, const QString &buttonURL); + +private: + void showLoadingPage(); + void integrateObject(QObject *object); + bool quitWhenIdle(); + void reconsiderQuit(); + QQmlApplicationEngine *engine() const + { + return m_engine; + } + + QQmlApplicationEngine *const m_engine; + + CompactMode m_mode; + QScopedPointer m_networkAccessManagerFactory; + KStatusNotifierItem *m_sni = nullptr; +}; diff --git a/discover/FeaturedModel.cpp b/discover/FeaturedModel.cpp new file mode 100644 index 0000000..c93d4d9 --- /dev/null +++ b/discover/FeaturedModel.cpp @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FeaturedModel.h" + +#include "discover_debug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +Q_GLOBAL_STATIC(QString, featuredCache) + +static QString featuredFileName() +{ + // kwriteconfig5 --file discoverrc --group Software --key FeaturedListingFileName featured-5.9.json + KConfigGroup grp(KSharedConfig::openConfig(), "Software"); + if (grp.hasKey("FeaturedListingFileName")) { + return grp.readEntry("FeaturedListingFileName", QString()); + } + static const bool isMobile = QByteArrayList{"1", "true"}.contains(qgetenv("QT_QUICK_CONTROLS_MOBILE")); + return isMobile ? QLatin1String("featured-mobile-5.9.json") : QLatin1String("featured-5.9.json"); +} + +FeaturedModel::FeaturedModel() +{ + const QString dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + QDir().mkpath(dir); + + static const QString fileName = featuredFileName(); + *featuredCache = dir + '/' + fileName; + const QUrl featuredUrl(QStringLiteral("https://autoconfig.kde.org/discover/") + fileName); + auto *fetchJob = KIO::storedGet(featuredUrl, KIO::NoReload, KIO::HideProgressInfo); + acquireFetching(true); + connect(fetchJob, &KIO::StoredTransferJob::result, this, [this, fetchJob]() { + const auto dest = qScopeGuard([this] { + acquireFetching(false); + refresh(); + }); + if (fetchJob->error() != 0) + return; + + QFile f(*featuredCache); + if (!f.open(QIODevice::WriteOnly)) + qCWarning(DISCOVER_LOG) << "could not open" << *featuredCache << f.errorString(); + f.write(fetchJob->data()); + f.close(); + }); +} + +void FeaturedModel::refresh() +{ + // usually only useful if launching just fwupd or kns backends + if (!currentApplicationBackend()) + return; + + acquireFetching(true); + const auto dest = qScopeGuard([this] { + acquireFetching(false); + }); + QFile f(*featuredCache); + if (!f.open(QIODevice::ReadOnly)) { + qCWarning(DISCOVER_LOG) << "couldn't open file" << *featuredCache << f.errorString(); + return; + } + QJsonParseError error; + const auto array = QJsonDocument::fromJson(f.readAll(), &error).array(); + if (error.error) { + qCWarning(DISCOVER_LOG) << "couldn't parse" << *featuredCache << ". error:" << error.errorString(); + return; + } + + const auto uris = kTransform>(array, [](const QJsonValue &uri) { + return QUrl(uri.toString()); + }); + setUris(uris); +} diff --git a/discover/FeaturedModel.h b/discover/FeaturedModel.h new file mode 100644 index 0000000..c615adc --- /dev/null +++ b/discover/FeaturedModel.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "AbstractAppsModel.h" +#include + +namespace KIO +{ +class StoredTransferJob; +} + +class FeaturedModel : public AbstractAppsModel +{ + Q_OBJECT +public: + FeaturedModel(); + ~FeaturedModel() override + { + } + + void refresh() override; +}; diff --git a/discover/OdrsAppsModel.cpp b/discover/OdrsAppsModel.cpp new file mode 100644 index 0000000..46b0b0e --- /dev/null +++ b/discover/OdrsAppsModel.cpp @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "OdrsAppsModel.h" +#include "appstream/AppStreamIntegration.h" +#include +#include + +OdrsAppsModel::OdrsAppsModel() +{ + auto x = AppStreamIntegration::global()->reviews(); + connect(x.get(), &OdrsReviewsBackend::ratingsReady, this, &OdrsAppsModel::refresh); + if (!x->top().isEmpty()) { + refresh(); + } +} + +void OdrsAppsModel::refresh() +{ + const auto top = AppStreamIntegration::global()->reviews()->top(); + setUris(kTransform>(top, [](auto r) { + return QUrl("appstream://" + r->packageName()); + })); +} diff --git a/discover/OdrsAppsModel.h b/discover/OdrsAppsModel.h new file mode 100644 index 0000000..5b388dc --- /dev/null +++ b/discover/OdrsAppsModel.h @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "AbstractAppsModel.h" + +class OdrsAppsModel : public AbstractAppsModel +{ + Q_OBJECT +public: + OdrsAppsModel(); + + void refresh() override; +}; diff --git a/discover/PaginateModel.cpp b/discover/PaginateModel.cpp new file mode 100644 index 0000000..79c5764 --- /dev/null +++ b/discover/PaginateModel.cpp @@ -0,0 +1,388 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "PaginateModel.h" +#include "discover_debug.h" +#include + +class PaginateModel::PaginateModelPrivate +{ +public: + int m_firstItem = 0; + int m_pageSize = 0; + QAbstractItemModel *m_sourceModel = nullptr; + bool m_hasStaticRowCount = false; +}; + +PaginateModel::PaginateModel(QObject *object) + : QAbstractListModel(object) + , d(new PaginateModelPrivate) +{ +} + +PaginateModel::~PaginateModel() = default; + +int PaginateModel::firstItem() const +{ + return d->m_firstItem; +} + +void PaginateModel::setFirstItem(int row) +{ + Q_ASSERT(row >= 0 && row < d->m_sourceModel->rowCount()); + if (row != d->m_firstItem) { + beginResetModel(); + d->m_firstItem = row; + endResetModel(); + Q_EMIT firstItemChanged(); + } +} + +int PaginateModel::pageSize() const +{ + return d->m_pageSize; +} + +void PaginateModel::setPageSize(int count) +{ + if (count != d->m_pageSize) { + const int oldSize = rowsByPageSize(d->m_pageSize); + const int newSize = rowsByPageSize(count); + const int difference = newSize - oldSize; + if (difference == 0) { + d->m_pageSize = count; + } else if (difference > 0) { + beginInsertRows(QModelIndex(), d->m_pageSize, d->m_pageSize + difference - 1); + d->m_pageSize = count; + endInsertRows(); + } else { + beginRemoveRows(QModelIndex(), d->m_pageSize + difference, d->m_pageSize - 1); + d->m_pageSize = count; + endRemoveRows(); + } + Q_EMIT pageSizeChanged(); + } +} + +QAbstractItemModel *PaginateModel::sourceModel() const +{ + return d->m_sourceModel; +} + +void PaginateModel::setSourceModel(QAbstractItemModel *model) +{ + if (d->m_sourceModel) { + disconnect(d->m_sourceModel, nullptr, this, nullptr); + } + + if (model != d->m_sourceModel) { + beginResetModel(); + d->m_sourceModel = model; + if (model) { + connect(d->m_sourceModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &PaginateModel::_k_sourceRowsAboutToBeInserted); + connect(d->m_sourceModel, &QAbstractItemModel::rowsInserted, this, &PaginateModel::_k_sourceRowsInserted); + connect(d->m_sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PaginateModel::_k_sourceRowsAboutToBeRemoved); + connect(d->m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &PaginateModel::_k_sourceRowsRemoved); + connect(d->m_sourceModel, &QAbstractItemModel::rowsAboutToBeMoved, this, &PaginateModel::_k_sourceRowsAboutToBeMoved); + connect(d->m_sourceModel, &QAbstractItemModel::rowsMoved, this, &PaginateModel::_k_sourceRowsMoved); + + connect(d->m_sourceModel, &QAbstractItemModel::columnsAboutToBeInserted, this, &PaginateModel::_k_sourceColumnsAboutToBeInserted); + connect(d->m_sourceModel, &QAbstractItemModel::columnsInserted, this, &PaginateModel::_k_sourceColumnsInserted); + connect(d->m_sourceModel, &QAbstractItemModel::columnsAboutToBeRemoved, this, &PaginateModel::_k_sourceColumnsAboutToBeRemoved); + connect(d->m_sourceModel, &QAbstractItemModel::columnsRemoved, this, &PaginateModel::_k_sourceColumnsRemoved); + connect(d->m_sourceModel, &QAbstractItemModel::columnsAboutToBeMoved, this, &PaginateModel::_k_sourceColumnsAboutToBeMoved); + connect(d->m_sourceModel, &QAbstractItemModel::columnsMoved, this, &PaginateModel::_k_sourceColumnsMoved); + + connect(d->m_sourceModel, &QAbstractItemModel::dataChanged, this, &PaginateModel::_k_sourceDataChanged); + connect(d->m_sourceModel, &QAbstractItemModel::headerDataChanged, this, &PaginateModel::_k_sourceHeaderDataChanged); + + connect(d->m_sourceModel, &QAbstractItemModel::modelAboutToBeReset, this, &PaginateModel::_k_sourceModelAboutToBeReset); + connect(d->m_sourceModel, &QAbstractItemModel::modelReset, this, &PaginateModel::_k_sourceModelReset); + + connect(d->m_sourceModel, &QAbstractItemModel::rowsInserted, this, &PaginateModel::pageCountChanged); + connect(d->m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &PaginateModel::pageCountChanged); + connect(d->m_sourceModel, &QAbstractItemModel::modelReset, this, &PaginateModel::pageCountChanged); + } + endResetModel(); + Q_EMIT sourceModelChanged(); + } +} + +QHash PaginateModel::roleNames() const +{ + return d->m_sourceModel ? d->m_sourceModel->roleNames() : QAbstractItemModel::roleNames(); +} + +int PaginateModel::rowsByPageSize(int size) const +{ + return d->m_hasStaticRowCount ? size : !d->m_sourceModel ? 0 : qMin(d->m_sourceModel->rowCount() - d->m_firstItem, size); +} + +int PaginateModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : rowsByPageSize(d->m_pageSize); +} + +QModelIndex PaginateModel::mapToSource(const QModelIndex &idx) const +{ + if (!d->m_sourceModel) + return QModelIndex(); + return d->m_sourceModel->index(idx.row() + d->m_firstItem, idx.column()); +} + +QModelIndex PaginateModel::mapFromSource(const QModelIndex &idx) const +{ + Q_ASSERT(idx.model() == d->m_sourceModel); + if (!d->m_sourceModel) + return QModelIndex(); + return index(idx.row() - d->m_firstItem, idx.column()); +} + +QVariant PaginateModel::data(const QModelIndex &index, int role) const +{ + if (!d->m_sourceModel) + return QVariant(); + QModelIndex idx = mapToSource(index); + return idx.data(role); +} + +void PaginateModel::firstPage() +{ + setFirstItem(0); +} + +void PaginateModel::lastPage() +{ + setFirstItem((pageCount() - 1) * d->m_pageSize); +} + +void PaginateModel::nextPage() +{ + setFirstItem(d->m_firstItem + d->m_pageSize); +} + +void PaginateModel::previousPage() +{ + setFirstItem(d->m_firstItem - d->m_pageSize); +} + +int PaginateModel::currentPage() const +{ + if (d->m_pageSize == 0) + return 0; + + return d->m_firstItem / d->m_pageSize; +} + +int PaginateModel::pageCount() const +{ + if (!d->m_sourceModel || d->m_pageSize == 0) + return 0; + const int rc = d->m_sourceModel->rowCount(); + const int r = (rc % d->m_pageSize == 0) ? 1 : 0; + return qMax(qCeil(float(rc) / d->m_pageSize) - r, 1); +} + +bool PaginateModel::hasStaticRowCount() const +{ + return d->m_hasStaticRowCount; +} + +void PaginateModel::setStaticRowCount(bool src) +{ + if (src == d->m_hasStaticRowCount) { + return; + } + + beginResetModel(); + d->m_hasStaticRowCount = src; + endResetModel(); + + Q_EMIT staticRowCountChanged(); +} + +////////////////////////////// + +void PaginateModel::_k_sourceColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(end) + if (parent.isValid() || start != 0) { + return; + } + beginResetModel(); +} + +void PaginateModel::_k_sourceColumnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest) +{ + Q_UNUSED(sourceParent) + Q_UNUSED(sourceStart) + Q_UNUSED(sourceEnd) + Q_UNUSED(destParent) + Q_UNUSED(dest) + beginResetModel(); +} + +void PaginateModel::_k_sourceColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(end) + if (parent.isValid() || start != 0) { + return; + } + beginResetModel(); +} + +void PaginateModel::_k_sourceColumnsInserted(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(end) + if (parent.isValid() || start != 0) { + return; + } + endResetModel(); +} + +void PaginateModel::_k_sourceColumnsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest) +{ + Q_UNUSED(sourceParent) + Q_UNUSED(sourceStart) + Q_UNUSED(sourceEnd) + Q_UNUSED(destParent) + Q_UNUSED(dest) + endResetModel(); +} + +void PaginateModel::_k_sourceColumnsRemoved(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(end) + if (parent.isValid() || start != 0) { + return; + } + endResetModel(); +} + +void PaginateModel::_k_sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + if (topLeft.parent().isValid() || bottomRight.row() < d->m_firstItem || topLeft.row() > lastItem()) { + return; + } + + QModelIndex idxTop = mapFromSource(topLeft); + QModelIndex idxBottom = mapFromSource(bottomRight); + if (!idxTop.isValid()) + idxTop = index(0); + if (!idxBottom.isValid()) + idxBottom = index(rowCount() - 1); + + Q_EMIT dataChanged(idxTop, idxBottom, roles); +} + +void PaginateModel::_k_sourceHeaderDataChanged(Qt::Orientation orientation, int first, int last) +{ + Q_UNUSED(last) + if (first == 0) + Q_EMIT headerDataChanged(orientation, 0, 0); +} + +void PaginateModel::_k_sourceModelAboutToBeReset() +{ + beginResetModel(); +} + +void PaginateModel::_k_sourceModelReset() +{ + endResetModel(); +} + +bool PaginateModel::isIntervalValid(const QModelIndex &parent, int start, int /*end*/) const +{ + return !parent.isValid() && start <= lastItem(); +} + +bool PaginateModel::canSizeChange() const +{ + return !d->m_hasStaticRowCount && currentPage() == pageCount() - 1; +} + +void PaginateModel::_k_sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) +{ + if (!isIntervalValid(parent, start, end)) { + return; + } + + if (canSizeChange()) { + const int newStart = qMax(start - d->m_firstItem, 0); + const int insertedCount = qMin(end - start, pageSize() - newStart - 1); + beginInsertRows(QModelIndex(), newStart, newStart + insertedCount); + } else { + beginResetModel(); + } +} + +void PaginateModel::_k_sourceRowsInserted(const QModelIndex &parent, int start, int end) +{ + if (!isIntervalValid(parent, start, end)) { + return; + } + + if (canSizeChange()) { + endInsertRows(); + } else { + endResetModel(); + } +} + +void PaginateModel::_k_sourceRowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest) +{ + Q_UNUSED(sourceParent) + Q_UNUSED(sourceStart) + Q_UNUSED(sourceEnd) + Q_UNUSED(destParent) + Q_UNUSED(dest) + // NOTE could optimize, unsure if it makes sense + beginResetModel(); +} + +void PaginateModel::_k_sourceRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest) +{ + Q_UNUSED(sourceParent) + Q_UNUSED(sourceStart) + Q_UNUSED(sourceEnd) + Q_UNUSED(destParent) + Q_UNUSED(dest) + endResetModel(); +} + +void PaginateModel::_k_sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + if (!isIntervalValid(parent, start, end)) { + return; + } + + if (canSizeChange()) { + const int removedCount = end - start; + const int newStart = qMax(start - d->m_firstItem, 0); + beginRemoveRows(QModelIndex(), newStart, newStart + removedCount); + } else { + beginResetModel(); + } +} + +void PaginateModel::_k_sourceRowsRemoved(const QModelIndex &parent, int start, int end) +{ + if (!isIntervalValid(parent, start, end)) { + return; + } + + if (canSizeChange()) { + endRemoveRows(); + } else { + beginResetModel(); + } +} + +int PaginateModel::lastItem() const +{ + return d->m_firstItem + rowCount(); +} diff --git a/discover/PaginateModel.h b/discover/PaginateModel.h new file mode 100644 index 0000000..0602e44 --- /dev/null +++ b/discover/PaginateModel.h @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include + +/** + * @class PaginateModel + * + * This class can be used to create representations of only a chunk of a model. + * + * With this component it will be possible to create views that only show a page + * of a model, instead of drawing all the elements in the model. + */ +class PaginateModel : public QAbstractListModel +{ + Q_OBJECT + /** Holds the number of elements that will fit in a page */ + Q_PROPERTY(int pageSize READ pageSize WRITE setPageSize NOTIFY pageSizeChanged) + + /** Tells what is the first row shown in the model */ + Q_PROPERTY(int firstItem READ firstItem WRITE setFirstItem NOTIFY firstItemChanged) + + /** The model we will be proxying */ + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) + + /** Among the totality of elements, indicates the one we're currently offering */ + Q_PROPERTY(int currentPage READ currentPage NOTIFY firstItemChanged) + + /** Provides the number of pages available, given the sourceModel size */ + Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged) + + /** If enabled, ensures that pageCount and pageSize are the same. */ + Q_PROPERTY(bool staticRowCount READ hasStaticRowCount WRITE setStaticRowCount NOTIFY staticRowCountChanged) + +public: + explicit PaginateModel(QObject *object = nullptr); + ~PaginateModel() override; + + int pageSize() const; + void setPageSize(int count); + + int firstItem() const; + void setFirstItem(int row); + + /** + * @returns Last visible item. + * + * Convenience function + */ + int lastItem() const; + + QAbstractItemModel *sourceModel() const; + void setSourceModel(QAbstractItemModel *model); + + QModelIndex mapToSource(const QModelIndex &idx) const; + QModelIndex mapFromSource(const QModelIndex &idx) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int currentPage() const; + int pageCount() const; + QHash roleNames() const override; + + void setStaticRowCount(bool src); + bool hasStaticRowCount() const; + + /** Display the first rows of the model */ + Q_SCRIPTABLE void firstPage(); + + /** Display the rows right after the ones that are currently being served */ + Q_SCRIPTABLE void nextPage(); + + /** Display the rows right before the ones that are currently being served */ + Q_SCRIPTABLE void previousPage(); + + /** Display the last set of rows of the source model */ + Q_SCRIPTABLE void lastPage(); + +private Q_SLOTS: + void _k_sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void _k_sourceRowsInserted(const QModelIndex &parent, int start, int end); + void _k_sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void _k_sourceRowsRemoved(const QModelIndex &parent, int start, int end); + void _k_sourceRowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest); + void _k_sourceRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest); + + void _k_sourceColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void _k_sourceColumnsInserted(const QModelIndex &parent, int start, int end); + void _k_sourceColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void _k_sourceColumnsRemoved(const QModelIndex &parent, int start, int end); + void _k_sourceColumnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest); + void _k_sourceColumnsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destParent, int dest); + + void _k_sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void _k_sourceHeaderDataChanged(Qt::Orientation orientation, int first, int last); + + void _k_sourceModelAboutToBeReset(); + void _k_sourceModelReset(); + +Q_SIGNALS: + void pageSizeChanged(); + void firstItemChanged(); + void sourceModelChanged(); + void pageCountChanged(); + void staticRowCountChanged(); + +private: + bool canSizeChange() const; + bool isIntervalValid(const QModelIndex &parent, int start, int end) const; + int rowsByPageSize(int size) const; + + class PaginateModelPrivate; + QScopedPointer d; +}; diff --git a/discover/PowerManagementInterface.cpp b/discover/PowerManagementInterface.cpp new file mode 100644 index 0000000..b88d6ca --- /dev/null +++ b/discover/PowerManagementInterface.cpp @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2019 (c) Matthieu Gallien + + SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#include "PowerManagementInterface.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +class PowerManagementInterfacePrivate +{ +public: + bool mPreventSleep = false; + + bool mInhibitedSleep = false; + + uint mInhibitSleepCookie = 0; + + QString m_reason; +}; + +PowerManagementInterface::PowerManagementInterface(QObject *parent) + : QObject(parent) + , d(std::make_unique()) +{ + auto sessionBus = QDBusConnection::sessionBus(); + + sessionBus.connect(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("HasInhibitChanged"), + this, + SLOT(hostSleepInhibitChanged())); +} + +PowerManagementInterface::~PowerManagementInterface() = default; + +bool PowerManagementInterface::preventSleep() const +{ + return d->mPreventSleep; +} + +bool PowerManagementInterface::sleepInhibited() const +{ + return d->mInhibitedSleep; +} + +void PowerManagementInterface::setPreventSleep(bool value) +{ + if (d->mPreventSleep == value) { + return; + } + + if (value) { + inhibitSleep(); + d->mPreventSleep = true; + } else { + uninhibitSleep(); + d->mPreventSleep = false; + } + + Q_EMIT preventSleepChanged(); +} + +void PowerManagementInterface::retryInhibitingSleep() +{ + if (d->mPreventSleep && !d->mInhibitedSleep) { + inhibitSleep(); + } +} + +void PowerManagementInterface::hostSleepInhibitChanged() +{ +} + +void PowerManagementInterface::inhibitDBusCallFinished(QDBusPendingCallWatcher *aWatcher) +{ + QDBusPendingReply reply = *aWatcher; + if (reply.isError()) { + } else { + d->mInhibitSleepCookie = reply.argumentAt<0>(); + d->mInhibitedSleep = true; + + Q_EMIT sleepInhibitedChanged(); + } + aWatcher->deleteLater(); +} + +void PowerManagementInterface::uninhibitDBusCallFinished(QDBusPendingCallWatcher *aWatcher) +{ + QDBusPendingReply<> reply = *aWatcher; + if (reply.isError()) { + qDebug() << "PowerManagementInterface::uninhibitDBusCallFinished" << reply.error(); + } else { + d->mInhibitedSleep = false; + + Q_EMIT sleepInhibitedChanged(); + } + aWatcher->deleteLater(); +} + +void PowerManagementInterface::inhibitSleep() +{ + auto sessionBus = QDBusConnection::sessionBus(); + + auto inhibitCall = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("Inhibit")); + + inhibitCall.setArguments({{QGuiApplication::desktopFileName()}, {d->m_reason}}); + + auto asyncReply = sessionBus.asyncCall(inhibitCall); + + auto replyWatcher = new QDBusPendingCallWatcher(asyncReply, this); + + QObject::connect(replyWatcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInterface::inhibitDBusCallFinished); +} + +void PowerManagementInterface::uninhibitSleep() +{ + auto sessionBus = QDBusConnection::sessionBus(); + + auto uninhibitCall = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("UnInhibit")); + + uninhibitCall.setArguments({{d->mInhibitSleepCookie}}); + + auto asyncReply = sessionBus.asyncCall(uninhibitCall); + + auto replyWatcher = new QDBusPendingCallWatcher(asyncReply, this); + + QObject::connect(replyWatcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInterface::uninhibitDBusCallFinished); +} + +QString PowerManagementInterface::reason() const +{ + return d->m_reason; +} + +void PowerManagementInterface::setReason(const QString &reason) +{ + d->m_reason = reason; +} diff --git a/discover/PowerManagementInterface.h b/discover/PowerManagementInterface.h new file mode 100644 index 0000000..c9353f9 --- /dev/null +++ b/discover/PowerManagementInterface.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2019 (c) Matthieu Gallien + + SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#pragma once + +#include + +#include + +class QDBusPendingCallWatcher; +class PowerManagementInterfacePrivate; + +class PowerManagementInterface : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString reason READ reason WRITE setReason) + + Q_PROPERTY(bool preventSleep READ preventSleep WRITE setPreventSleep NOTIFY preventSleepChanged) + + Q_PROPERTY(bool sleepInhibited READ sleepInhibited NOTIFY sleepInhibitedChanged) + +public: + explicit PowerManagementInterface(QObject *parent = nullptr); + + ~PowerManagementInterface() override; + + [[nodiscard]] bool preventSleep() const; + + [[nodiscard]] bool sleepInhibited() const; + + QString reason() const; + void setReason(const QString &reason); + +Q_SIGNALS: + + void preventSleepChanged(); + + void sleepInhibitedChanged(); + +public Q_SLOTS: + + void setPreventSleep(bool value); + + void retryInhibitingSleep(); + +private Q_SLOTS: + + void hostSleepInhibitChanged(); + + void inhibitDBusCallFinished(QDBusPendingCallWatcher *aWatcher); + + void uninhibitDBusCallFinished(QDBusPendingCallWatcher *aWatcher); + +private: + void inhibitSleep(); + + void uninhibitSleep(); + + std::unique_ptr d; +}; diff --git a/discover/ReadFile.cpp b/discover/ReadFile.cpp new file mode 100644 index 0000000..0ee89a4 --- /dev/null +++ b/discover/ReadFile.cpp @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "ReadFile.h" +#include "discover_debug.h" + +ReadFile::ReadFile() +{ + connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &ReadFile::openNow); + connect(&m_file, &QFile::readyRead, this, &ReadFile::process); +} + +void ReadFile::componentComplete() +{ + completed = true; + openNow(); +} + +void ReadFile::setPath(QString path) +{ + processPath(path); + if (path == m_file.fileName()) + return; + + if (path.isEmpty()) + return; + + if (m_file.isOpen()) + m_watcher.removePath(m_file.fileName()); + + m_file.setFileName(path); + m_sizeOnSet = m_file.size() + 1; + openNow(); + + m_watcher.addPath(m_file.fileName()); +} + +void ReadFile::openNow() +{ + if (!completed) + return; + + if (!m_contents.isEmpty()) { + m_contents.clear(); + Q_EMIT contentsChanged(m_contents); + } + m_file.close(); + const auto open = m_file.open(QIODevice::ReadOnly | QIODevice::Text); + Q_EMIT pathChanged(path()); + if (!open) + return; + + m_stream.reset(new QTextStream(&m_file)); + m_stream->seek(m_sizeOnSet); + process(); +} + +void ReadFile::processPath(QString &path) +{ + const QRegularExpression envRx(QStringLiteral("\\$([A-Z_]+)")); + auto matchIt = envRx.globalMatch(path); + while (matchIt.hasNext()) { + auto match = matchIt.next(); + path.replace(match.capturedStart(), match.capturedLength(), QString::fromUtf8(qgetenv(match.capturedView(1).toUtf8().constData()))); + } +} + +void ReadFile::process() +{ + const QString read = m_stream->readAll(); + + if (m_filter.isValid() && !m_filter.pattern().isEmpty()) { + auto it = m_filter.globalMatch(read); + while (it.hasNext()) { + const auto match = it.next(); + m_contents.append(match.capturedView(match.lastCapturedIndex())); + m_contents.append(QLatin1Char('\n')); + } + } else + m_contents += read; + Q_EMIT contentsChanged(m_contents); +} + +void ReadFile::setFilter(const QString &filter) +{ + m_filter = QRegularExpression(filter); + if (!m_filter.isValid()) + qCDebug(DISCOVER_LOG) << "error" << m_filter.errorString(); + Q_ASSERT(filter.isEmpty() || m_filter.isValid()); +} + +QString ReadFile::filter() const +{ + return m_filter.pattern(); +} diff --git a/discover/ReadFile.h b/discover/ReadFile.h new file mode 100644 index 0000000..a066afc --- /dev/null +++ b/discover/ReadFile.h @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class ReadFile : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(QString contents READ contents NOTIFY contentsChanged) + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + Q_PROPERTY(QString filter READ filter WRITE setFilter FINAL) +public: + ReadFile(); + + QString contents() const + { + return m_contents; + } + QString path() const + { + return m_file.fileName(); + } + void setPath(QString path); + + QString filter() const; + void setFilter(const QString &filter); + + void classBegin() override + { + } + void componentComplete() override; + +Q_SIGNALS: + void pathChanged(const QString &path); + void contentsChanged(const QString &contents); + +private: + void process(); + void openNow(); + void processPath(QString &path); + + bool completed = false; + QFile m_file; + QString m_contents; + QSharedPointer m_stream; + QFileSystemWatcher m_watcher; + QRegularExpression m_filter; + qint64 m_sizeOnSet = 0; +}; diff --git a/discover/UnityLauncher.cpp b/discover/UnityLauncher.cpp new file mode 100644 index 0000000..d96e738 --- /dev/null +++ b/discover/UnityLauncher.cpp @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + * + */ + +#include "UnityLauncher.h" + +#include +#include +#include + +UnityLauncher::UnityLauncher(QObject *parent) + : QObject(parent) +{ +} + +UnityLauncher::~UnityLauncher() = default; + +QString UnityLauncher::launcherId() const +{ + return m_launcherId; +} + +void UnityLauncher::setLauncherId(const QString &launcherId) +{ + m_launcherId = launcherId; +} + +bool UnityLauncher::progressVisible() const +{ + return m_progressVisible; +} + +void UnityLauncher::setProgressVisible(bool progressVisible) +{ + if (m_progressVisible != progressVisible) { + m_progressVisible = progressVisible; + + update({{QStringLiteral("progress-visible"), progressVisible}}); + } +} + +int UnityLauncher::progress() const +{ + return m_progress; +} + +void UnityLauncher::setProgress(int progress) +{ + if (m_progress != progress) { + m_progress = progress; + + update({{QStringLiteral("progress"), progress / 100.0}}); + } +} + +void UnityLauncher::update(const QVariantMap &properties) +{ + if (m_launcherId.isEmpty()) { + return; + } + + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/org/discover/UnityLauncher"), + QStringLiteral("com.canonical.Unity.LauncherEntry"), + QStringLiteral("Update")); + message.setArguments({m_launcherId, properties}); + QDBusConnection::sessionBus().send(message); +} diff --git a/discover/UnityLauncher.h b/discover/UnityLauncher.h new file mode 100644 index 0000000..dfb3e73 --- /dev/null +++ b/discover/UnityLauncher.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + * + * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + * + */ + +#pragma once + +#include + +class UnityLauncher : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString launcherId READ launcherId WRITE setLauncherId) + Q_PROPERTY(bool progressVisible READ progressVisible WRITE setProgressVisible) + Q_PROPERTY(int progress READ progress WRITE setProgress) + +public: + explicit UnityLauncher(QObject *parent = nullptr); + ~UnityLauncher() override; + + QString launcherId() const; + void setLauncherId(const QString &launcherId); + + bool progressVisible() const; + void setProgressVisible(bool progressVisible); + + int progress() const; + void setProgress(int progress); + +private: + void update(const QVariantMap &properties); + + QString m_launcherId; + bool m_progressVisible = false; + int m_progress = 0; +}; diff --git a/discover/autotests/CMakeLists.txt b/discover/autotests/CMakeLists.txt new file mode 100644 index 0000000..ca77aef --- /dev/null +++ b/discover/autotests/CMakeLists.txt @@ -0,0 +1,22 @@ +set(plasma_discover_autotest_SRCS) +ecm_qt_declare_logging_category(plasma_discover_autotest_SRCS HEADER discover_debug.h IDENTIFIER DISCOVER_LOG CATEGORY_NAME org.kde.plasma.discover) +ecm_add_test(PaginateModelTest.cpp ../PaginateModel.cpp ${plasma_discover_autotest_SRCS} TEST_NAME PaginateModelTest LINK_LIBRARIES Qt::Test) +target_include_directories(PaginateModelTest PUBLIC ${CMAKE_SOURCE_DIR}/libdiscover/) + +if(BUILD_DummyBackend) + add_test(NAME toplevels COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/toplevels.qml") + add_test(NAME install COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/install.qml") + + add_test(NAME appstreamUrl COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/appstreamUrl.qml" "dummy://techie1") + add_test(NAME missingResource COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/missingResource.qml" "dummy://caca") + add_test(NAME apparg COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/appstreamUrl.qml" --application "dummy://techie1") + add_test(NAME categoryarg COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/categoryArg.qml" --category "dummy 2.1") + add_test(NAME wrongInput COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/wrongInput.qml" "CMakeLists.txt") + add_test(NAME packageArgument COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/packageArgument.qml" + --local-filename "${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt") + add_test(NAME updateandinstall COMMAND Plasma::Discover --test "${CMAKE_CURRENT_SOURCE_DIR}/updateandinstall.qml") + +# Just make sure they exit successfully + add_test(NAME listbackends COMMAND Plasma::Discover --listbackends) + add_test(NAME listmodes COMMAND Plasma::Discover --listmodes) +endif() diff --git a/discover/autotests/DiscoverTest.qml b/discover/autotests/DiscoverTest.qml new file mode 100644 index 0000000..0e55992 --- /dev/null +++ b/discover/autotests/DiscoverTest.qml @@ -0,0 +1,125 @@ +import QtQuick 2.1 +import QtTest 1.1 +import org.kde.discover.app 1.0 + +Item +{ + id: testRoot + + signal reset() + property QtObject appRoot + + 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 chooseChildren(objects, validator) { + for (var v in objects) { + var obj = objects[v]; + if (validator(obj)) + return true; + } + return false; + } + + function chooseChild(obj, validator) { + verify(obj, "can't find a null's child") + if (validator(obj)) + return true; + var children = obj.data ? obj.data : obj.contentData + for(var v in children) { + var stop = chooseChild(children[v], validator) + if (stop) + return true + } + return false + } + + function findChild(obj, typename) { + var ret = null; + chooseChild(obj, function(o) { + var found = isType(o, typename); + if (found) { + ret = o; + } + return found + }) + return ret; + } + + SignalSpy { + id: spy + } + + function waitForSignal(object, name, timeout) { + if (!timeout) timeout = 5000; + + spy.clear(); + spy.signalName = "" + spy.target = object; + spy.signalName = name; + verify(spy); + verify(spy.valid); + verify(spy.count == 0); + + try { + spy.wait(timeout); + } catch (e) { + console.warn("wait for signal '"+name+"' failed") + return false; + } + return spy.count>0; + } + + function waitForRendering() { + return waitForSignal(appRoot, "frameSwapped") + } + + property string currentTest: "" + onCurrentTestChanged: console.log("changed to test", currentTest) + + Connections { + target: ResourcesModel + property bool done: false + function onIsFetchingChanged() { + if (ResourcesModel.isFetching || done) + return; + + done = true; + for(var v in testRoot) { + if (v.indexOf("test_") === 0) { + console.log("doing", v) + testRoot.currentTest = v; + testRoot.reset(); + testRoot[v](); + } + } + console.log("done") + appRoot.close() + } + } +} diff --git a/discover/autotests/PaginateModelTest.cpp b/discover/autotests/PaginateModelTest.cpp new file mode 100644 index 0000000..96f5ced --- /dev/null +++ b/discover/autotests/PaginateModelTest.cpp @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "../PaginateModel.h" +#include +#include +#include + +void insertRow(QStringListModel *model, int row, const QString &appendString) +{ + model->insertRow(row); + model->setData(model->index(row, 0), appendString); +} +void appendRow(QStringListModel *model, const QString &appendString) +{ + int count = model->rowCount(); + insertRow(model, count, appendString); +} + +class PaginateModelTest : public QObject +{ + Q_OBJECT +public: + PaginateModelTest() + : m_testModel(new QStringListModel) + { + for (int i = 0; i < 13; ++i) { + appendRow(m_testModel, QStringLiteral("figui%1").arg(i)); + } + } + +private Q_SLOTS: + void testPages() + { + PaginateModel pm; + new QAbstractItemModelTester(&pm, &pm); + pm.setSourceModel(m_testModel); + pm.setPageSize(5); + QCOMPARE(pm.pageCount(), 3); + QCOMPARE(pm.rowCount(), 5); + QCOMPARE(pm.firstItem(), 0); + QCOMPARE(pm.currentPage(), 0); + pm.nextPage(); + QCOMPARE(pm.rowCount(), 5); + QCOMPARE(pm.currentPage(), 1); + pm.nextPage(); + QCOMPARE(pm.rowCount(), 3); + QCOMPARE(pm.currentPage(), 2); + + pm.firstPage(); + QCOMPARE(pm.firstItem(), 0); + pm.setFirstItem(0); + QCOMPARE(pm.firstItem(), 0); + QCOMPARE(pm.currentPage(), 0); + pm.lastPage(); + QCOMPARE(pm.firstItem(), 10); + QCOMPARE(pm.currentPage(), 2); + } + + void testPageSize() + { + PaginateModel pm; + new QAbstractItemModelTester(&pm, &pm); + pm.setSourceModel(m_testModel); + pm.setPageSize(5); + QCOMPARE(pm.pageCount(), 3); + pm.setPageSize(10); + QCOMPARE(pm.pageCount(), 2); + pm.setPageSize(5); + QCOMPARE(pm.pageCount(), 3); + } + + void testItemAdded() + { + PaginateModel pm; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + new QAbstractItemModelTester(&pm, &pm); +#endif + pm.setSourceModel(m_testModel); + pm.setPageSize(5); + QCOMPARE(pm.pageCount(), 3); + QSignalSpy spy(&pm, &QAbstractItemModel::rowsAboutToBeInserted); + insertRow(m_testModel, 3, QStringLiteral("mwahahaha")); + insertRow(m_testModel, 3, QStringLiteral("mwahahaha")); + QCOMPARE(spy.count(), 0); + appendRow(m_testModel, QStringLiteral("mwahahaha")); + + pm.lastPage(); + for (int i = 0; i < 7; ++i) + appendRow(m_testModel, QStringLiteral("mwahahaha%1").arg(i)); + QCOMPARE(spy.count(), 4); + pm.firstPage(); + + for (int i = 0; i < 7; ++i) + appendRow(m_testModel, QStringLiteral("faraway%1").arg(i)); + QCOMPARE(spy.count(), 4); + } + + void testItemAddBeginning() + { + QStringListModel smallerModel; + + PaginateModel pm; + new QAbstractItemModelTester(&pm, &pm); + pm.setSourceModel(&smallerModel); + pm.setPageSize(5); + QCOMPARE(pm.pageCount(), 1); + QCOMPARE(pm.rowCount(), 0); + insertRow(&smallerModel, 0, QStringLiteral("just one")); + QCOMPARE(pm.pageCount(), 1); + QCOMPARE(pm.rowCount(), 1); + smallerModel.removeRow(0); + QCOMPARE(pm.pageCount(), 1); + QCOMPARE(pm.rowCount(), 0); + } + + void testItemRemoved() + { + PaginateModel pm; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + new QAbstractItemModelTester(&pm, &pm); +#endif + pm.setSourceModel(m_testModel); + pm.setPageSize(5); + QCOMPARE(pm.pageCount(), 5); + QSignalSpy spy(&pm, &QAbstractItemModel::rowsAboutToBeRemoved); + m_testModel->removeRow(3); + QCOMPARE(spy.count(), 0); + spy.clear(); + + pm.lastPage(); + m_testModel->removeRow(m_testModel->rowCount() - 1); + QCOMPARE(spy.count(), 1); + } + + void testMove() + { + PaginateModel pm; + new QAbstractItemModelTester(&pm, &pm); + pm.setSourceModel(m_testModel); + pm.setPageSize(5); + m_testModel->moveRow({}, 0, {}, 3); + } + +private: + QStringListModel *const m_testModel; +}; + +QTEST_GUILESS_MAIN(PaginateModelTest) + +#include "PaginateModelTest.moc" diff --git a/discover/autotests/appstreamUrl.qml b/discover/autotests/appstreamUrl.qml new file mode 100644 index 0000000..51900bf --- /dev/null +++ b/discover/autotests/appstreamUrl.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_open() { + verify(appRoot.stack.currentItem, "has a loading page"); + compare(appRoot.stack.currentItem.title, "techie1", "same title"); + compare(appRoot.stack.currentItem.application.packageName, "techie1", "pkgname"); + } +} diff --git a/discover/autotests/categoryArg.qml b/discover/autotests/categoryArg.qml new file mode 100644 index 0000000..7a4a5a9 --- /dev/null +++ b/discover/autotests/categoryArg.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_openCategory() { + verify(appRoot.stack.currentItem, "has a page"); + while (appRoot.stack.currentItem.title === "") + verify(waitForRendering()); + compare(appRoot.stack.currentItem.title, "dummy 2.1", "same title"); + } +} diff --git a/discover/autotests/install.qml b/discover/autotests/install.qml new file mode 100644 index 0000000..5994b47 --- /dev/null +++ b/discover/autotests/install.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import org.kde.discover.app 1.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_openResource() { + app.openApplication("dummy://dummy.1"); + verify(waitForSignal(appRoot.stack, "currentItemChanged")) + verify(appRoot.stack.currentItem, "has a page"); + + var button = findChild(appRoot.stack.currentItem, "InstallApplicationButton") + verify(!button.isActive) + button.click() + verify(button.isActive) + verify(waitForSignal(button, "isActiveChanged")) + verify(!button.isActive) + } + + SignalSpy { + id: cancelSpy + target: TransactionModel + signalName: "transactionRemoved" + } + function test_cancel() { + app.openApplication("dummy://dummy.2"); + verify(waitForSignal(appRoot.stack, "currentItemChanged")) + var button = findChild(appRoot.stack.currentItem, "InstallApplicationButton") + verify(!button.isActive) + + cancelSpy.clear() + var state = button.application.state; + + button.click() + verify(button.isActive) + button.listener.cancel() + verify(!button.isActive) + compare(cancelSpy.count, 1) + verify(state === button.application.state) + } +} diff --git a/discover/autotests/missingResource.qml b/discover/autotests/missingResource.qml new file mode 100644 index 0000000..3a2a5de --- /dev/null +++ b/discover/autotests/missingResource.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_open() { + compare(appRoot.stack.currentItem.title, "Error") + } +} diff --git a/discover/autotests/packageArgument.qml b/discover/autotests/packageArgument.qml new file mode 100644 index 0000000..8f33bfa --- /dev/null +++ b/discover/autotests/packageArgument.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_open() { + verify(appRoot.stack.currentItem, "has a page"); + while (appRoot.stack.currentItem.title === "Loading…") + waitForRendering(); + compare(appRoot.stack.currentItem.application.packageName, "CMakeLists.txt", "pkgname"); + } +} diff --git a/discover/autotests/toplevels.qml b/discover/autotests/toplevels.qml new file mode 100644 index 0000000..452fb9b --- /dev/null +++ b/discover/autotests/toplevels.qml @@ -0,0 +1,110 @@ +import QtQuick 2.0 +import org.kde.discover.app 1.0 +import QtTest 1.1 + +DiscoverTest +{ + onReset: { + appRoot.currentTopLevel = appRoot.topBrowsingComp + } + + function test_openCategory() { + var categoryName = "dummy 3"; + app.openCategory(categoryName); + verify(appRoot.stack.currentItem, "has a page"); + compare(appRoot.stack.currentItem.title, categoryName, "same title"); + verify(waitForRendering()) + + categoryName = "dummy 4"; + app.openCategory(categoryName); + verify(appRoot.stack.currentItem, "has a page"); + compare(appRoot.stack.currentItem.title, categoryName, "same title"); + verify(waitForRendering()) + } + + function test_openHome() { + var drawer = appRoot.globalDrawer; + drawer.actions[0].children[2].trigger() + compare(appRoot.stack.currentItem.title, "dummy 3", "same title"); + + app.openMode("Browsing"); + + compare(appRoot.stack.currentItem.title, "Featured", "same title"); + compare(drawer.currentSubMenu, null) + } + + function test_navigateThenUpdate() { + var drawer = appRoot.globalDrawer; + var firstitem = drawer.actions[0].children[2] + var updateButton; + chooseChild(drawer, function(object) { + if (object.objectName === "updateButton") { + updateButton = object; + return true + } + return false; + }); + + firstitem.trigger() + verify(updateButton.enabled) + updateButton.clicked() + + compare(appRoot.currentTopLevel, appRoot.topUpdateComp, "correct component, updates"); + } + + function test_update() { + app.openMode("Update"); + + var updatePage = appRoot.stack.currentItem; + compare(typeName(updatePage), "UpdatesPage") + compare(updatePage.state, "has-updates", "to update") + var action = updatePage.actions.main + verify(action); + action.triggered(updatePage); + compare(updatePage.state, "progressing", "updating") + + //make sure the window doesn't close while updating + verify(appRoot.visible); + verify(waitForRendering()) + appRoot.close() + verify(appRoot.visible); + + while(updatePage.state !== "now-uptodate") + waitForSignal(updatePage, "stateChanged") + compare(ResourcesModel.updatesCount, 0, "should be up to date") + } + + function test_search() { + app.openMode("Browsing"); + app.openSearch("cocacola") + while(!isType(appRoot.stack.currentItem, "ApplicationsListPage")) + verify(waitForSignal(appRoot.stack, "currentItemChanged")) + var listPage = appRoot.stack.currentItem + while(listPage.count>0) + verify(waitForSignal(listPage, "countChanged")) + compare(listPage.count, 0) + compare(listPage.search, "cocacola") + app.openSearch("dummy") + listPage = appRoot.stack.currentItem + compare(listPage.search, "dummy") +// compare(listPage.count, ResourcesModel.rowCount()/2) + } + + function test_modes() { + app.openMode("Browsing"); + compare(appRoot.currentTopLevel, appRoot.topBrowsingComp, "correct component, browsing"); + verify(waitForRendering()) + + app.openMode("Installed"); + compare(appRoot.currentTopLevel, appRoot.topInstalledComp, "correct component, installed"); + verify(waitForRendering()) + + app.openMode("Update"); + compare(appRoot.currentTopLevel, appRoot.topUpdateComp, "correct component, updates"); + verify(waitForRendering()) + + app.openMode("Sources"); + compare(appRoot.currentTopLevel, appRoot.topSourcesComp, "correct component, sources"); + verify(waitForRendering()) + } +} diff --git a/discover/autotests/updateandinstall.qml b/discover/autotests/updateandinstall.qml new file mode 100644 index 0000000..976b8ed --- /dev/null +++ b/discover/autotests/updateandinstall.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import org.kde.discover.app 1.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_openResource() { + app.openMode("Update"); + + {// we start an update + var updatePage = appRoot.stack.currentItem; + compare(typeName(updatePage), "UpdatesPage") + compare(updatePage.state, "has-updates", "to update") + var action = updatePage.actions.main + verify(action); + action.triggered(null); + compare(updatePage.state, "progressing", "updating") + } + + {//we start installing a resource + app.openApplication("dummy://dummy.1"); + verify(waitForSignal(appRoot.stack, "currentItemChanged")) + + var button = findChild(appRoot.stack.currentItem, "InstallApplicationButton") + console.log("button", appRoot.stack.currentItem, button) + verify(button) + verify(!button.isActive) + button.click() + } + + app.openMode("Update"); + { + var updatePage = appRoot.stack.currentItem; + compare(typeName(updatePage), "UpdatesPage") + while(updatePage.state === "fetching" || updatePage.state === "progressing") { + waitForSignal(updatePage, "stateChanged") + } + compare(updatePage.state, "now-uptodate", "to update") + } + } +} diff --git a/discover/autotests/wrongInput.qml b/discover/autotests/wrongInput.qml new file mode 100644 index 0000000..2b5c55a --- /dev/null +++ b/discover/autotests/wrongInput.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import QtTest 1.1 + +DiscoverTest +{ + function test_open() { + } +} diff --git a/discover/discover.schema b/discover/discover.schema new file mode 100644 index 0000000..6e64f97 --- /dev/null +++ b/discover/discover.schema @@ -0,0 +1,329 @@ +{ + "aggregation": [ + { + "elements": [ + { + "schemaEntry": "", + "schemaEntryElement": "", + "type": "value" + } + ], + "name": "CPU Architecture Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "", + "schemaEntryElement": "", + "type": "value" + } + ], + "name": "CPU Count Distribution", + "type": "numeric" + }, + { + "elements": [ + { + "schemaEntry": "", + "schemaEntryElement": "", + "type": "value" + } + ], + "name": "OS Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "", + "schemaEntryElement": "", + "type": "value" + }, + { + "schemaEntry": "", + "schemaEntryElement": "", + "type": "value" + } + ], + "name": "Platform Details", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "style", + "schemaEntryElement": "style", + "type": "value" + } + ], + "name": "Widget Style Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "style", + "schemaEntryElement": "dark", + "type": "value" + } + ], + "name": "Palette Color Scheme", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "usageTime", + "schemaEntryElement": "value", + "type": "value" + } + ], + "name": "Usage Time Distribution", + "type": "numeric" + }, + { + "elements": [ + { + "schemaEntry": "screens", + "type": "size" + } + ], + "name": "Amount of Screens", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "screens", + "schemaEntryElement": "dpi", + "type": "value" + } + ], + "name": "DPI Distribution", + "type": "numeric" + }, + { + "elements": [ + { + "schemaEntry": "opengl", + "schemaEntryElement": "type", + "type": "value" + } + ], + "name": "OpenGL Stack Type", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "opengl", + "schemaEntryElement": "vendor", + "type": "value" + } + ], + "name": "OpenGL Vendor Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "opengl", + "schemaEntryElement": "renderer", + "type": "value" + } + ], + "name": "OpenGL Renderer Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "opengl", + "schemaEntryElement": "type", + "type": "value" + }, + { + "schemaEntry": "opengl", + "schemaEntryElement": "version", + "type": "value" + } + ], + "name": "OpenGL Version Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "opengl", + "schemaEntryElement": "type", + "type": "value" + }, + { + "schemaEntry": "opengl", + "schemaEntryElement": "glslVersion", + "type": "value" + } + ], + "name": "OpenGL GLSL Version Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "opengl", + "schemaEntryElement": "profile", + "type": "value" + } + ], + "name": "OpenGL Profile Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "locale", + "schemaEntryElement": "language", + "type": "value" + } + ], + "name": "Language Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "locale", + "schemaEntryElement": "region", + "type": "value" + } + ], + "name": "Region Distribution", + "type": "category" + }, + { + "elements": [ + { + "schemaEntry": "qtVersion", + "schemaEntryElement": "value", + "type": "value" + } + ], + "name": "Qt Version Distribution", + "type": "category" + } + ], + "name": "org.kde.discover", + "schema": [ + { + "elements": [ + { + "name": "style", + "type": "string" + }, + { + "name": "dark", + "type": "bool" + } + ], + "name": "style", + "type": "scalar" + }, + { + "elements": [ + { + "name": "value", + "type": "int" + } + ], + "name": "usageTime", + "type": "scalar" + }, + { + "elements": [ + { + "name": "width", + "type": "int" + }, + { + "name": "height", + "type": "int" + }, + { + "name": "dpi", + "type": "int" + } + ], + "name": "screens", + "type": "list" + }, + { + "elements": [ + { + "name": "type", + "type": "string" + }, + { + "name": "vendor", + "type": "string" + }, + { + "name": "renderer", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "vendorVersion", + "type": "string" + }, + { + "name": "glslVersion", + "type": "string" + }, + { + "name": "profile", + "type": "string" + } + ], + "name": "opengl", + "type": "scalar" + }, + { + "elements": [ + { + "name": "language", + "type": "string" + }, + { + "name": "region", + "type": "string" + } + ], + "name": "locale", + "type": "scalar" + }, + { + "elements": [ + { + "name": "value", + "type": "string" + } + ], + "name": "qtVersion", + "type": "scalar" + }, + { + "elements": [ + { + "name": "value", + "type": "string" + } + ], + "name": "applicationSourceName", + "type": "scalar" + } + ] +} diff --git a/discover/discoversettings.kcfg b/discover/discoversettings.kcfg new file mode 100644 index 0000000..668a3cf --- /dev/null +++ b/discover/discoversettings.kcfg @@ -0,0 +1,8 @@ + + + + + ResourcesProxyModel::SortableRatingRole + ResourcesProxyModel::NameRole + + diff --git a/discover/discoversettings.kcfgc b/discover/discoversettings.kcfgc new file mode 100644 index 0000000..9afaaf7 --- /dev/null +++ b/discover/discoversettings.kcfgc @@ -0,0 +1,5 @@ +File=discoversettings.kcfg +ClassName=DiscoverSettings +GenerateProperties=true +Mutators=true +IncludeFiles=resources/ResourcesProxyModel.h diff --git a/discover/icons/128-apps-plasmadiscover.png b/discover/icons/128-apps-plasmadiscover.png new file mode 100644 index 0000000000000000000000000000000000000000..d248cc671e19fb02b646c93e1d01db4a15a073b1 GIT binary patch literal 4687 zcmZu#XHXLikPRi$rAY^ah*U$7-g`%iD7{M+B7vaP(4>Q)h)4}cN2K>60clc$^d6c7 zP#_QlLd*Hh{k+?mw{K==-pua4A3L)NCPq3mlxRtDC$>gohp} zJ5zYGQOj01nneo@vO`}R=bn#oE4eEv5q@? z*3meOlEaY@PioREn(GENkHUMo?zwnsc-RniZUY&Q6B zW2)&Oc1dYx+__e#M^5Q&zUO;eFsb&K_9jgus>nNshpDmQGS+{4{l4NMNp)N0wI+}wA7IlnRM@+!rARbZ zJV-Ls3D=VeSZ?iy$r1dZub*&4u{FlmhMLOB^kU8lBX6$H4BWpB*YJS_0ZZd>>RvWh z6YYbAGsG7BZ?h92pYQOQc2_hJwby>&K}PVB35|6%mE|RN{)_C9lB&!PK}OiP5)_Ua z(w_G_Eh-Xj_!kT^0fV&1+7tbBB!Dr%a>* z5_MvmD{PA(^3ZR?Jc@|`b%GT~!zY==QvGrpYUp@M2l@qN7Mk`$T=1B}sy2E+7%(~k zL!0P0uzyMPz4!iRjN{~@a+S9G2O@vC`cR3HPX$sIR(}XzTjTr-EO%TUlWrJ!4K%}Xs+FdF#` z>QDJz8-8maFK@l<`W>E_+-~a3Z&Y8;TuI+llQRhWVqJKoC!6y4m|C* z@X>tCNt5GhOBtgO8Q1G336f%oI7>-ASukD=(KJRV-hZlGF%yQP?aNgZ?`Y#&?aN|_ zFsd35GK?NZ`B%$6X$`gkIpAHGzUJgCwg3_|E!OvF-(B_0rIyq2hzSufY*}Th8oZB2f?_=~f7;5$ zf@X+OdG*E>CZ`qZA`rqsNoL4TuS%EY{ZaF^1z=a*@|v9Sju`H^(xB^V94tJ_*bO<&6(fM?yru6FTy&WZ|c0x98G|*gkwL4ypr#vQ>SwD zn~HUJ-mFyMn&*(N!9#gh-Z%lcL&mqAENGHv>pS&5Bj7&19UTawRZhMS6I?SF?vxDD zsr`=H)CczABrm-+I9KLu!Ky`nID&JyhU3z*3BVKcKIpor8F&*O>dV&=$rtigj0HHA^Q`O&W522j?78M{VaqZa{lTxiUTJMEVa-* z29dNn*Cspmr4K(ESYvtVVk;?fWQreA^C}*64WNC$ zDX*^uVV$LCu%fLV=-B8%yYNur2$}?91Q~KzaNL#NB zLl0#g`j)HErZ2Q~^@dZf;JEK+lWDX2pw*-A0;xk)9AIzr*@UrOi08JsKLFSz+r+p5 zbJ#=^OKZJyE|V}+10^m^cKR*d_z3F(|HIM6NB_?&AoklL6}R)tSTwy6;NLWw(;2#e zkz66t$oIt7S>-^>ZB63;h2*uhswTv<8X`siihqwdL+(A`fANe?wK&4ObobL6;d@*b zodIvQN7FZ02BZ0DgFmyECxu+I;=_%khH`XYs4n>vWyFfw-gST8f|Na-ZK|CeK5kks z0T0Ve5@mt98|5Z{2TPIUQKC%r=OFq$=SPXS_HrFu*2o6$9bU=M z?Rr_{TqQ?XYD4L}#0spF&McN5?9Y@UztN4<+)rEfg= z?9DV}Gbtp9uzL}IdgtVqqo!tm`ZCd=m6Du6BHPLRscRUoDeH$)(z0 zS(MtZfUGF>Txu7ivEbeNA+}v|I|ym^%q(wk=-w;waRZq-gGy$D%Vt1-@xvW^#?IQ| zaC76~cG4ktZVCC{P3XnVqaQD6pJ&QEkX$}pWW~Iybe$)bTA#SGtS3YisCO ze!jKM3itJ7BWfITo+vb7_ODidj8IkN&*Ue6@%4%BJ(5C`p##rPc{%2`&H9_*4f}jf zA?We6tkBQhRPF5C)7!-E8a5Mc3`|toWMWdLyp*{Q(>O%)y6S9>j>g2jtC+osx+F3& zbryqwP;s`JgBAf(dx zNMfjYzPD9Qg?cw=eJ5_{hAY?cqc%GR$R8ciGS|}`dhBHsl(a`giu8kRoc#w%V{OA* z&2jSwoC>t&^O=HFX>eUP-tr1H)$%J5Cyn9S&5ep|6%ZPogGorkQ5>7ZlMfH6eGSr@ zc%)WvOBt$`d)we~QrFw=CJqHlANPzqtY2L6L9ERD@uah7eDg;aALk{8v%yOLnqaI+ z!Gq)1F!bbVOmW#UorKwV0diaU-&2WQq_Ek65p%)VTZ0An?W_idqg+IVV3gt_zb_e< zJ6}EUH4wGLU|RhkcX5SLq4J9S`Xe@{?z$u^kG5d%e(u{^_(U@9wGViKloc|5XM*DRUCV56h* zAQeOaz6QRT?so3C8TN&n@Us(JM6wzMBN*=wVf}-;b+T|JvNOqqI77Jkh0UD}`5R6@ z#y$Y}K7h2VZRA%;QJbgQJmGO`C?2E3hT;%Yqq!6LM|O)(XqXUpNv&`;rnG-~azL%Z znOnS^1(VNT$aAnxg}yzJcefQQJ1Xyr|t@_{N4<$r1D#FUvli%D#4FB_9nA6Xe6%rl)*#j?htXrHp!l{phRBN^7#|%=!T6F6zc-%6+^mv0Tzh;q_wusdI(YZ{KD?5M^D9$ z?)Os^q=~xD;tUwI^nsv`h#4{Z#-~(bF|pG^nTkX&oxW_Na+gG) zV~apHn2d?SZbGGyTP{rg&w4PK2jX)?e9xYCn) z8nU*XHT#Jr&tch;he=jNw$#lZSc2B{Ru!Ju@FD;C&N|Y?C<;TcH5kH|a2x77bZtq5 z)w-22J4DY!g4$S0Ia$){o?=D}F)~;S6Ps3`K;gYMhDZE8vQv4fx%N29o8Ptx7CceW zXqS!{LGfpm+xf2{aL<&5_*wyA1=GzcGHd+ISU_r_;GH^iuA!@|rF7pF+2b;~@I@CB zZ1}5-y5r+A<(we zT>yZR;hO;4-?(`Fy7sB};krbkG^)Pbx`3U}IX}b~a4Lhaz(-%c;X}iL!Q3ne~2N6PJ8; zdRT#SLs;a6&}f8^MoL(|f9h>jgt436-LGxk`nsX3de3Mz-?$v$+7G{{YCsy=@H5s9 z;JtS=xJU&VT5go@p(4A=4^k6^av~F&g#xRR6M@e|!fmXCB`<$9mH?g{#he}CF;*qY zwoG|j@;7yfnvVD$-pME9anekxPVYPFsH%~H#cr~CwkoY=Oo;$-vui&pS&(&@P~hv^ zDHIsg4bMAIPWHb!p~%Tw)vcB2&@uNc*NBOKEFNBQ*V7R@0^vSOIM8Vq{GPUAl3Z;qD<_eR`&GA!in7(3* z%nd|Hkl=~|@nGL6MPUP_z=MT-$ zw;2FTO)&SF3ApA-t4Vy@M(xzNXLZtx-*Hf!5ov_5h;h->@yaetzMsl<2m|59_Bj~+ zkM>OiG7mrC)NhgFmY)*q6c95U!r}F7yuEcErD`!8Go*hQN@3?YDY-{1*Ubk z`^Ms+VP1yE4`WYveh6vdc{00Rd2Me!_Nv+gfBbhJ@0beYVFPY69XheiT=78Km}>*m zkth`D)VC<4f=5@?5@ajX1D8A^SLL9$2E6cZWq&P6^hA2d zY!-=1CRt3LAf6y26tTb7l#Kr+M;R|b-wwnEk6)I(U}d2Hf5KGw24t#UMQdoF4gdEc O0_bTOX;!N{M*R<9DGA8{ literal 0 HcmV?d00001 diff --git a/discover/icons/16-apps-plasmadiscover.png b/discover/icons/16-apps-plasmadiscover.png new file mode 100644 index 0000000000000000000000000000000000000000..35a9bb331d5139c28d122e9b54cf2bb35859d616 GIT binary patch literal 464 zcmV;>0WbcEP)TlRZmgK@^6cncO6rtC%205E2WU1*4T9%eDcr^Jm!D zs9&E{T%g_6(F(VuuWct?r%nVOhfZjD_s^_WH zB@5S`)&P^KOBSESilcF11AgBdK4HbN76IT?1ID-eyW@s4C2*r)40u6vkgNfe?Sbc` zudevjA(`!WXbtUr0|4LDygLH_@1qD>*gU*NY8Yw{LFszklQosl<38|EzM~?O45VT8 z&|~wj!bGA7CuT{g1L{j)=QM)!W26%v+b0RoAx2AtmYC3ftELk04XK37&rBP56$l)J zjzS>ckf3bxg)$T_d@vwF4MGBqo2~&U9c;Z!zWUR?(;fjtmalG;c<)yL0000rlf6$=K@`P*Gw&_C%OWUWt3VJVg&2kS9c+xH7MA)C z7-M0Bt95wWqbu(W}~PHjjmiD*pli$V<~!UBSuz_Kj+KIU2=yYKBj0&3w+CV6k> z+>>+eoPqyc?PJo^J|<1O#jBkFhh7$Yjn6gFY6d_V>CzS>)0g7!|61kYXSvqS(ZW4M z$^t~x!I8q<{iC^-KT>?)*6>Z`=1&6Xu8YUb?D!Pkh8F_a30rscA2BnpJ_1NA^=|&r z?Z?{^cMOa+rq+K<>U_2Xz-d2A&D=z!qTrcHHO}U1Tl;1oUd>h!cfZITwLeTfp-@O| z?dfD&+7vR2kyev!X$hXilX8joU zv4>Qo;?qwHoETDEL-~qnua@CAMSNF4DbN;p0>7QAfV!|^iHMe}<$PC?#sob7wt=zY zD5*h=L5T*VD8(+IE0AN*rkWQZ?bfX~Dl}*VaMJtGy>PFotNJNyz6vwI5Vohd}%;)`jUCc~c zbMM@liKH8S;4JQY-t#=qdC$ka7yj#zoesSFOU+hX?^uh}Ikd2ZRW8rQ8So2B*p5AG*2`Nl9k#9j zhawGc56qtYEYqz0`G(zCeOlZ`x)XOt4+D@WU5?X_%+thB%^TYHekwiw>JO(LuErWT z{ND06_GTp$BG%y%fP`$U#~O&q^K#khjo8W^&SVY*cmo-h?;$6|L8xw1E!a&i*}y*N?YoUir@B=W?*S75Jl~ zynD$)JrCBe^H;@Z?iU~R;k6?+)7c)C=xv~nMiSEF;CQfE;@q;!g@T74C@I?75xktA9d?cT}RS;!6dj?1Uw&9 z2$Ytf5d_oXi}~>Dy8>HC43o7j?!6<)ucane>a=H>LNV=gY>>cel|N?o(FP(m%6805 zdb$bt0-?AP@h;&G*Y;`O7rEVp^RH|$x(jn_{Wce4>H~SB%o0Z&5 zJkWng-N5(01gwd4M`=?>xe;IA$CVyyWDkPm3+{a`Xl1k9)6afR=x9HzX+! zP}OW-fLONPVIs2ecozAbi`4)we)zh^7ZKbpweqqk6E0xr?;iciPRo2 zL9|l?d9_vL0R|uI7}P*sC5(2YmEV#Fx}SXcfE^lqxY3w%&x=Gq?19lgXC`@mH|F*y zu>-F(95y%fescQVmmZmJnOrNf+@4kaPGlR7Rq|aF3v-Z#%UfA$66)ls<`yz9VP4d; zs=t+WrVXK$yY_D{3=c2MOMymYZH|JKhGWgBUYPr%U15%zkNFW5J#DVd-Q4Qj1SA@uR;Q(n#SvaO z?6R>q(h`3?Juo@Ok)09AN!*5vXTA$z^ekcn^!#7Y zxfqN?5JEMGc^>VF-}jx&#)yn_z(D7X=!L%^@L_BS<}G61aV3gi>rJs2HupG05$rz| zwyE}IT!kv+Kyfi#97cl>#?B#~i`ai6B#e|zwNQF!xgm@kJ}#y6#8oPz4$NHyT!FbKi-A=mz2mL`p?wGdj@ZP?K5aJXFM>ofJ0$H3g{(TNc>3fZ_c zgr-H87`(`}@0C-&@2B z!3*Kj6CMYDy8swO1@!|o2--SMrw*aRr7CN?^Bg%fidH8O73f${$6*MKGr98vr3nT~ zm3Zf^pa3FqeN}Ts|CkP`tI*LihNCBkQGWsv0dE4ef^=Lr$um$^!YD*TpKk{zCc{QW zi4VjvX%xlbB(4AL$)*puU0?+bDySIntFaMtIwx7aDcg9LtpSmmgC$nh7*IjPpqFe) zYru@Fkh=i22BK=LOz1;(916wDJjMkTGzu;&F9vL^e5|>F8ML5mj}q>6^mG(hx1xvw z-y9#Kd~pKubL*H2am;HJE1N=jXj+A|ExNOJ9*z-mMp)C=8CMs-GKEJZhmZqHob(E) zG+#RbnK!b~#uPsbq6WeUwH`91VL1pj<=fc#OUV32cC_rXW_b<}zAKHRDyYUGqZ%Mi zzK**V04c5>nO|s5pN9|5RglO71Vw69MZj9nEAI+vkZeGTGnSAC5j6DUgq2#wxnzvh z_*(|Jig)51Vg$7Tag5hj8%Y&5Q>7%J)x%tK7?6BiGDYGhg;0CQ1c?SXzwXUU$Sh_t ci>rr!0Q4}{^_*A>S<=gp6uo!xa^H?CPxXZ_Jgw5<_rAdt|e zAuSX`{|T{_{@aS$f` zgM}?C4!XL$+uc?R-D-}^f&9u9`}2j&{`~H_%2;tA|LltW`9jxg`K}06yA4jQ1gp<; z1YGFtNbQ}gjK$g1UJ2;2mgl3?Z6azyK-VkTg=%yQhCMtg!Cd8>+A6+U_NdXPRvqYX zjZ()kRJ;phcK)_A_;1f4wYF(Sp`9CBopnzvwI_ddR=vEbO2K4f%z5~yYwejMm-Rn? z8=A47awSst)%NB4UCp?!(|3y-2U7VWtZx3-`)>jYPi(CYGY|1wUtfya7%%w9}9P@eK!_xVyK}q0n^Ca z`ORDMJ{Yb}-eiWGF|cH=x&g4gy`PV+hHrlRl72oIytgWO zlTnfYt#AQ&zK1CWNZ<1+9zR8%cT1iZfxO9xd;(+_~F97 z;&t`%CZl8!%)D8MnDhFg_lbsTgWz3le@R5X1nxFO9t7_;%zHq|9`ha$4Fl^JI-DP| z_*{H!2Qu!!- z9-!4XV-(+j2`pr;xyA{*exc)y;xuLXFq72+wbf^USG$+?v z1^oV;!8eSzRm~31Zb;DRR59-thJY~=()U2T5w(H_78$+?K}f=Z&jS8%F2FN7-nyEF zpMNz$queP1q6kE{1to!WJH*pPf)z%yU;&>mhJRjv*cz;&Vdo@q&V;NDi2wj}T-7=Ns%z}ue> z6Q0=!qJp9z4p>D!4~Zu$S8ft&QVgBAY%uO9ooV6Jl@V@^2H{H_%|dz+II-LMe{@}G zP6j--EWH1x3pw8H$pZp1$#^8(O;9xOk!jNEN>Gzx$O0$(8X%uV3;E)5b-)DZNI#Np zn?l07y#{MQ1CQf9IYQ$>l%ny)HLr{x5;k0owjt~O{Q7P6FO!zHh?h>e*2Q(*zU{Xq)P87E_tKb)= z&+sIUU~dJwh~E4d>T)0gaDCX~8HZ%-mVp-se6WHR(_E~JOq=F9#rmE|jW0?rfrv_B z*Mnnc3k>EBR+Zj9J)HVL4pJ5{QoOzEOwnPh1Gx@qIq5 zkeyozjw))Z&+y7c-9L@ z@XEy<{4^FvL{RJ3be}J+MG0t4KaV>g3DgE*tv_Y*8e2~p1UQ%9uelN-CX{(miY$SW p1W4dsib9up%wrz&n8z0o{{vkGdqzK+R=5BF002ovPDHLkV1i#0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/discover/main.cpp b/discover/main.cpp new file mode 100644 index 0000000..c4dda07 --- /dev/null +++ b/discover/main.cpp @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// #define QT_QML_DEBUG + +#include "DiscoverObject.h" +#include "DiscoverVersion.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if WITH_QTWEBVIEW +#include +#endif + +#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() +{ + // clang-format off + QCommandLineParser *parser = new QCommandLineParser; + parser->addOption(QCommandLineOption(QStringLiteral("application"), i18n("Directly open the specified application by its appstream:// URI."), QStringLiteral("name"))); + parser->addOption(QCommandLineOption(QStringLiteral("mime"), i18n("Open with a search for programs 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("feedback"), i18n("Lists the available options for user feedback"))); + parser->addOption(QCommandLineOption(QStringLiteral("test"), QStringLiteral("Test file"), QStringLiteral("file.qml"))); + parser->addPositionalArgument(QStringLiteral("urls"), i18n("Supports appstream: url scheme")); + // clang-format on + DiscoverBackendsFactory::setupCommandLine(parser); + KAboutData::applicationData().setupCommandLine(parser); + 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"))); + else 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)); + + const auto positionalArguments = parser->positionalArguments(); + for (const QString &arg : 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); + } + + if (mainWindow->rootObject()->property("pageStack").value()->property("depth").toInt() == 0) { + mainWindow->openMode(QStringLiteral("Browsing")); + } +} + +static void raiseWindow(QWindow *window) +{ + KWindowSystem::updateStartupId(window); + KWindowSystem::activateWindow(window); +} + +int main(int argc, char **argv) +{ + // needs to be set before we create the QGuiApplication + QCoreApplication::setAttribute(Qt::AA_DisableSessionManager, true); +#if WITH_QTWEBVIEW + QtWebView::initialize(); +#endif + + auto format = QSurfaceFormat::defaultFormat(); + format.setOption(QSurfaceFormat::ResetNotification); + QSurfaceFormat::setDefaultFormat(format); + + QApplication app(argc, argv); + app.setWindowIcon(QIcon::fromTheme(QStringLiteral("plasmadiscover"))); + app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); +#endif + KCrash::initialize(); + KLocalizedString::setApplicationDomain("plasma-discover"); + KAboutData about(QStringLiteral("discover"), + i18n("Discover"), + version, + i18n("An application explorer"), + KAboutLicense::GPL, + i18n("© 2010-2022 Plasma Development Team")); + about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org"), QStringLiteral("https://proli.net"), QStringLiteral("apol")); + about.addAuthor(i18n("Nate Graham"), + i18n("Quality Assurance, Design and Usability"), + QStringLiteral("nate@kde.org"), + QStringLiteral("https://pointieststick.com/"), + QStringLiteral("ngraham")); + about.addAuthor(i18n("Dan Leinir Turthra Jensen"), + i18n("KNewStuff"), + QStringLiteral("admin@leinir.dk"), + QStringLiteral("https://leinir.dk/"), + QStringLiteral("leinir")); + about.setProductName("discover/discover"); + about.setProgramLogo(app.windowIcon()); + + about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("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"))); + const bool feedback = parser->isSet(QStringLiteral("feedback")); + + if (parser->isSet(QStringLiteral("listbackends"))) { + QTextStream(stdout) << i18n("Available backends:\n"); + DiscoverBackendsFactory f; + const auto backendNames = f.allBackendNames(false, true); + for (const QString &name : backendNames) + QTextStream(stdout) << " * " << name << '\n'; + return 0; + } + + if (parser->isSet(QStringLiteral("test"))) { + QStandardPaths::setTestModeEnabled(true); + } + + KDBusService *service = !feedback ? new KDBusService(KDBusService::Unique, &app) : nullptr; + + { + auto options = parser->optionNames(); + options.removeAll(QStringLiteral("backends")); + options.removeAll(QStringLiteral("test")); + QVariantMap initialProperties; + if (!options.isEmpty() || !parser->positionalArguments().isEmpty()) + initialProperties = {{QStringLiteral("currentTopLevel"), QStringLiteral("qrc:/qml/LoadingPage.qml")}}; + if (feedback) { + initialProperties.insert("visible", false); + } + mainWindow = new DiscoverObject(s_decodeCompactMode->value(parser->value(QStringLiteral("compact")), DiscoverObject::Full), initialProperties); + } + if (feedback) { + QTextStream(stdout) << mainWindow->describeSources() << '\n'; + delete mainWindow; + return 0; + } else { + auto onActivateRequested = [mainWindow](const QStringList &arguments, const QString & /*workingDirectory*/) { + mainWindow->restore(); + auto window = qobject_cast(mainWindow->rootObject()); + if (!window) { + // Should never happen anyway + QCoreApplication::instance()->quit(); + return; + } + + raiseWindow(window); + + if (arguments.isEmpty()) + return; + QScopedPointer parser(createParser()); + parser->parse(arguments); + processArgs(parser.data(), mainWindow); + }; + QObject::connect(service, &KDBusService::activateRequested, mainWindow, onActivateRequested); + } + + QObject::connect(&app, &QCoreApplication::aboutToQuit, mainWindow, &DiscoverObject::deleteLater); + + processArgs(parser.data(), mainWindow); + + if (parser->isSet(QStringLiteral("listmodes"))) { + QTextStream(stdout) << i18n("Available modes:\n"); + const auto modes = mainWindow->modes(); + for (const QString &mode : 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/org.kde.discover.appdata.xml b/discover/org.kde.discover.appdata.xml new file mode 100644 index 0000000..9165f82 --- /dev/null +++ b/discover/org.kde.discover.appdata.xml @@ -0,0 +1,294 @@ + + + org.kde.discover.desktop + CC0-1.0 + GPL-2.0+ + Discover + المستكشف + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + डिस्कवर + Discover + Discoperi + Discover + Discover + Discover + Discover + Discover + ഡിസ്കവർ + ဒစ်(စ)ကာဗာ + Discover + Ontdekken + Discover + ਡਿਸਕਵਰ + Odkrywca + Discover + Discover + Discover + Центр приложений Discover + Discover + Discover + Oткривач + Otkrivač + Oткривач + Otkrivač + Discover + டிஸ்கவர் + Кашфиёт + Keşfet + Discover + xxDiscoverxx + Discover 软件管理中心 + Discover + Discover + المستكشف + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + Discover + डिस्कवर + Discover + Discoperi + Discover + Discover + Discover + Discover + Discover + ഡിസ്കവർ + ဒစ်(စ)ကာဗာ + Discover + Ontdekken + Discover + ਡਿਸਕਵਰ + Odkrywca + Discover + Discover + Discover + Центр приложений Discover + Discover + Discover + Oткривач + Otkrivač + Oткривач + Otkrivač + Discover + டிஸ்கவர் + Кашфиёт + Keşfet + Discover + xxDiscoverxx + Discover 软件管理中心 + Discover + +

Discover helps you find and install applications, games, and tools. You can search or browse by category, and look at screenshots and read reviews to help you pick the perfect app.

+

يساعدك المستكشف في العثور على التطبيقات والألعاب والأدوات وتثبيتها. يمكنك البحث أو التصفح حسب الفئة، وإلقاء نظرة على لقطات الشاشة وقراءة المراجعات لمساعدتك في اختيار التطبيق المثالي.

+

Discover sizə tətbiqləri, oyunları və digər alət və vasitələri tapmağa və quraşdırmağa kömək edir. Siz kateqoriyalar üzrə axtararaq, ekran şəkillərinə baxmaqla və rəyləri oxumaqla istəyinizə uyğun ən mükəmməl tətbiqi və ya əlavələri seçə bilərsiniz.

+

Discover ви помага да намерите и инсталирате приложения, игри и инструменти. Можете да търсите или разглеждате по категории и да погледнете снимки на екрана и да прочетете ревютата, за да ви помогнат да изберете перфектното приложение.

+

El Discover ajuda a instal·lar aplicacions, jocs i eines. Podeu cercar o explorar per categoria, i veure les captures de pantalla i llegir els comentaris com a ajuda per a triar l'aplicació perfecta.

+

Discover ajuda a instal·lar aplicacions, jocs i eines. Podeu buscar o explorar per categoria, i veure les captures de pantalla i llegir els comentaris com a ajuda per a triar l'aplicació perfecta.

+

Discover hjælper dig med at finde og installere programmer, spil og værktøjer. Du kan søge eller gennemse efter kategori og kigge på skærmbilleder og læse anmeldelser for at hjælpe dig med vælge det perfekte program.

+

Discover hilft Ihnen dabei, Anwendungen, Spiele und Werkzeuge zu finden und zu installieren. Sie können suchen oder durch Kategorien blättern sowie Bildschirmfotos ansehen und Bewertung lesen, um die für Sie beste Anwendung zu finden.

+

Discover helps you find and install applications, games, and tools. You can search or browse by category, and look at screenshots and read reviews to help you pick the perfect app.

+

Discover le ayuda a encontrar e instalar aplicaciones, juegos y herramientas. Puede buscar o explorar por categorías, ver capturas de pantalla y leer reseñas que le ayudarán a escoger la aplicación perfecta.

+

Discover aitab leida ja paigaldada rakendusi, mänge ja tööriistu. Otsida või sirvida saab kategooriaid pidi, ekraanipiltide uurimine ja ülevaadete lugemine lubab leida just selle õige rakenduse.

+

Discover-rek aplikazioak, jokoak eta tresnak bilatzen eta instalatzen laguntzen dizu. Kategoria bidez bilatu eta arakatu dezakezu, eta pantaila-argazkiak ikusi eta iritziak irakur ditzakezu aplikazio egokia aukeratzen laguntzako.

+

Discover auttaa sinua etsimään ja asentamaan sovelluksia, pelejä ja apuohjelmia. Voit etsiä tai selata luokittain, katsella ruutukaappauskuvia ja lukea arvosteluja.

+

Discover vous aide à trouver et à installer des applications, des jeux et des outils. Vous pouvez chercher ou naviguer par catégorie, ou consulter les captures d'écran et les avis d'utilisateurs pour vous aider à trouver l'appli parfaite.

+

Discover axúdalle a atopar e instalar aplicacións, xogos e ferramentas. Pode buscar ou explorar por categoría, e ollar capturas de pantalla e ler recensións como axuda para escoller a aplicación perfecta.

+

डिस्कवर आपको अनुप्रयोग, खेल और उपकरणों को ढूंढने और संस्थापित करने में मदद करता है। आप श्रेणी के आधार पर खोज या ब्राउज़ कर सकते हैं, और स्क्रीनशॉट देख सकते हैं और समीक्षाएँ पढ़ सकते हैं ताकि आपको सही ऐप चुनने में मदद मिल सके।

+

A Discover segít az alkalmazások, játékok és eszközök keresésében és telepítésében. Kereshet vagy böngészhet kategóriák szerint, nézhet képernyőképeket és olvashat értékeléseket, hogy könnyen megtalálja a tökéletes alkalmazást.

+

Discover adjuta te a trovar e installar aplicationes, jocos, e instrumentos. Tu pote cercar e navigar per categoria, e reguardar a instantanee de schermo e lege reviews per adjutar te a colliger le perfect app.

+

Discover membantu kamu untuk menemukan dan menginstal aplikasi, permainan, dan peralatan. Kamu bisa mencari atau menelusuri berdasarkan kategori, dan melihat screenshot dan membaca ulasan untuk membantu kamu memilih aplikasi yang sempurna.

+

Discover ti aiuta a trovare e installare applicazioni, giochi e strumenti. Puoi cercare o sfogliare per categoria, vedere schermate e leggere recensioni per aiutarti a scegliere l'applicazione perfetta.

+

Discover გეხმარებად იპოვოთ და დააყენოთ აპლიკაციები, თამაშები და საჭირო პგორამები. შეგიძლიათ მოძებნოთ კატეგორიის მიხედვით, დაათვალიეროთ ეკრანის ანაბეჭდები და წაიკითხოთ კომენტარები, რათა თქვენთვის იდეალური აპი აარჩიოთ.

+

Discover를 사용하여 앱, 게임, 도구를 찾고 설치할 수 있습니다. 분류별로 검색 및 탐색하고, 스크린샷과 리뷰를 참조하여 꼭 필요한 앱을 찾으십시오.

+

Discover padeda jums rasti ir įdiegti programas, žaidimus bei įrankius. Galite ieškoti ar naršyti pagal kategoriją ir žiūrėti ekrano kopijas bei skaityti apžvalgas, kurios padeda jums išsirinkti tobulą programą.

+

ആപ്ലിക്കേഷൻ, ഗെയിമുകൾ, ഉപകരണങ്ങൾ എന്നിവ കണ്ടെത്താനും ഇൻസ്റ്റാൾ ചെയ്യാനും ഡിസ്കവർ നിങ്ങളെ സഹായിക്കുന്നു. നിങ്ങൾക്ക് വിഭാഗം അനുസരിച്ച് തിരയാനോ ബ്രൗസുചെയ്യാനോ കഴിയും, കൂടാതെ മികച്ച ആപ്ലിക്കേഷൻ തിരഞ്ഞെടുക്കാൻ സഹായിക്കുന്നതിന് സ്‌ക്രീൻഷോട്ടുകൾ കാണാനും അവലോകനങ്ങൾ വായിക്കാനും കഴിയും.

+

ဒစ်(စ)ကာဗာက သင်ကို အပ္ပလီကေးရှင်း၊ ဂိမ်းနှင့် ကိရိယာများ ရှာဖွေတပ်ဆင်ရန် ကူညီပေးသည်။ သင်အနေဖြင့် ရှာဖွေနိုင်သည် သို့မဟုတ် အမျိုးအစားအလိုက် ရှောက်ကြည့်နိုင်သည်။ ထို့ပြင် စခရင်ရှော့များနှင့် သုံးသပ်ချက်များကို ကြည့်၍ ကြိုက်နှစ်သက်ရာ အပ်များ ရွေးနိုင်သည်။

+

Discover hjelper deg å finne og installere programmer, spill og verktøy. Du kan søke etter programmer eller bla gjennom programkategorierer, se skjermbilder og lese brukeromtaler for å finne det perfekte programmet.

+

Ontdekken helpt om toepassingen, spellen en hulpmiddelen te zoeken en te installeren. U kunt zoeken of bladeren op categorie, schermafdrukken bekijken en reviews lezen om u te helpen de perfecte toepassing te kiezen.

+

Discover hjelper deg å finna og installera program, spel og verktøy. Du kan søkja etter program eller bla gjennom program­kategoriar, sjå skjermbilete og lesa brukaromtalar for å finna det perfekte programmet.

+

Odkrywca pomaga znaleźć i wgrać aplikacje, gry oraz narzędzia. Można wyszukiwać lub przeglądać po kategorii. Można oglądać zrzuty ekranu i czytać oceny, aby wybrać najlepszą aplikację.

+

O Discover ajuda-o a instalar aplicações, jogos e ferramentas. Poderá pesquisar ou navegar por categoria, assim como ver as imagens e ler as revisões e comentários que o possam ajudar a escolher a aplicação perfeita.

+

O Discover ajuda-o a encontrar e instalar aplicativos, jogos e ferramentas. Você pode pesquisar ou navegar por categoria, assim como ver as capturas de tela e ler as avaliações e comentários que o possam ajudá-lo a escolher o aplicativo perfeito.

+

Discover vă permite să găsiți și să instalați aplicații, jocuri și unelte. Puteți căuta sau răsfoi după categorie, precum și să vedeți capturi de ecran și să citiți recenzii ce vă ajută la alegerea aplicației perfecte.

+

Центр программ Discover предназначен для поиска, в числе по категориям и установки приложений и игр. Описания приложений содержат снимки экрана и отзывы других пользователей.

+

Aplikácia Discover vám pomôže nájsť a nainštalovať aplikácie, hry a nástroje. Môžete vyhľadávať alebo prehliadať podľa kategórií, prezerať si snímky obrazoviek a čítať recenzie, ktoré vám pomôžu vybrať si perfektnú aplikáciu.

+

Discover vam pomaga najti in namestiti programe, igre in orodja. Lahko iščete ali brskate po kategorijah in si ogledate posnetke zaslona in preberete ocene, ki vam pomagajo izbrati popoln program.

+

Discover hjälper till att hitta och installera program, spel och verktyg. Man kan söka eller bläddra enligt kategori, och titta på skärmbilder och läsa recensioner för att hjälpa till att välja det perfekta programmet.

+

Кашфиёт ба шумо барои ёфтан ва насб кардани барномаҳо, бозиҳо ва абзорҳо кумак мерасонад. Шумо метавонед барномаҳоро аз рӯи навъ ҷустуҷӯ карда, аз назар гузаронед, аксҳои барномаҳоро бинед ва барои кумак ҳангоми интихоб намудани барномаҳои беҳтарин тақризҳоро хонед.

+

Keşfet; uygulamaları, oyunları ve araçları bulmanıza ve kurmanıza yardımcı olur. Kategoriye göre arama yapabilir veya göz atabilirsiniz. Mükemmel uygulamayı seçmenize yardımcı olacak ekran görüntülerine de bakabilir ve yorumları okuyabilirsiniz.

+

Discover допоможе вам знайти і встановити програми, ігри та інструменти. Ви можете виконувати пошук або навігацію за категоріями, переглядати знімки вікон і читати рецензії, щоб вибрати ту програму, яка пасує вам найкраще.

+

xxDiscover helps you find and install applications, games, and tools. You can search or browse by category, and look at screenshots and read reviews to help you pick the perfect app.xx

+

Discover 是 KDE 的软件管理中心,它能帮助您查找并安装所需的应用程序、游戏和工具。您可以通过关键词查找程序,也可以按分类浏览程序。您还可以查看屏幕截图,阅读用户评价,选出最适合自己的应用程序。

+

利用 Discover 來尋找及安裝應用程式、遊戲及工具。您可以直接搜尋,或者從分類目錄開始瀏覽,預覽螢幕截圖和閱讀評論,以發現最佳的 App。

+

With Discover, you can manage software from multiple sources, including your operating system's software repository, Flatpak repos, the Snap store, or even AppImages from store.kde.org.

+

باستخدام المستكشف ، يمكنك إدارة البرامج من مصادر متعددة ، بما في ذلك مستودع برامج نظام التشغيل الخاص بك ، ومستودعات Flatpak ، ومتجر Snap ، أو حتى AppImages من store.kde.org.

+

Discover ilə proqram təminatınızı bir çox mənbədən, o cümlədən əməliyyat sisteminizin proqram təminatı bazasından, Flatpak depolarından, Snap mağazasından və hətta store.kde.org saytından AppImages daxil olmaqla idarə edə bilərsiniz.

+

С Discover можете да управлявате софтуер от множество източници, включващи хранилището на вашата дистрибуция, Flatpak, Snap, дори и от AppImages на store.kde.org.

+

Amb el Discover podeu gestionar programari de diverses fonts, incloent-hi el repositori de programari del sistema operatiu, repositoris del Flatpak, la botiga de l'Snap, o també AppImages des de «store.kde.org».

+

Amb Discover podeu gestionar programari de diverses fonts, incloent-hi el repositori de programari del sistema operatiu, repositoris de Flatpak, la botiga de Snap, o també les AppImage des de «store.kde.org».

+

Med Discover kan du administrere software fra flere forskellige kilder, inklusiv dit styresystems softwarekilder, Flatpak-softwarekilder, Snap-butikken eller sågar AppImages fra store.kde.org.

+

Mit Discover können Sie Software von mehreren Quellen verwalten, einschließlich der Ihres Betriebssystems, Flatpak-Quellen, dem Snap-Store und sogar AppImages von store.kde.org.

+

With Discover, you can manage software from multiple sources, including your operating system's software repository, Flatpak repos, the Snap store, or even AppImages from store.kde.org.

+

Con Discover puede gestionar software de múltiples fuentes, incluyendo los repositorios de software de su sistema operativo, repositorios de Flatpak, la tienda de Snap, e incluso AppImages de store.kde.org.

+

Discoveriga saab hallata tarkvara, mis on pärit mitmest allikast, kaasa arvatud su enda operatsioonisüsteemi tarkvarahoidla, Flatpaki hoidlad, Snapi hoidla või isegi hoidlas store.kde.org pakutavad AppImage'id.

+

Discover-rekin, sorburu askotako softwarea kudeatu dezakezu, tartean zure sistema eragilearen software gordetegia, Flatpak gordetegiak, Snap biltegia, edo baita store.kde.org-eko AppImages-etik ere.

+

Discoverilla voit hallita ohjelmia eri lähteistä: käyttöjärjestelmän tai Flatpakin asennuslähteistä, Snap-kaupasta tai jopa store.kde.orgin AppImageista.

+

Grâce à Discover, vous pouvez gérer des logiciels provenant de plusieurs sources, dont le dépôt de logiciels de votre système d'exploitation, les dépôts « Flatpak », la boutique « Snap » ou même des modules « AppImage  »de la boutique « store.kde.org ».

+

Con Discover pode xestionar software de varias fontes, entre elas o repositorio de software do seu sistema operativo, os repositorios de Flatpak, a tenda de Snap, ou mesmo as AppImage de store.kde.org.

+

डिस्कवर के साथ, आप अपने ऑपरेटिंग तंत्र के सॉफ़्टवेयर भंडार, फ़्लैटपैक भंडार, स्नैप स्टोर, या यहां तक कि store.kde.org से एपइमेजस सहित कई स्रोतों से सॉफ़्टवेयर का प्रबंधन कर सकते हैं।

+

A Discoverrel egy helyen kezelheti több forrásból származó szoftvereit, legyen szó akár az operációs rendszer tárolóiról, a Flatpak tárolókról a Snap boltról vagy akár a store.kde.org-ról származó AppImage-ekről.

+

Con Discover, tu pote gerer software ab multiple fontes, includente le repositorio de software de tusystema operative. repos de Flatpak, le magazin de Snap, o etiam Appimages ex store.kde.org.

+

Dengan Discover, kamu bisa mengelola perangkat lunak dari banyak sumber, termasuk repositori perangkat lunak operasi sistemmu, repo Flatpak, Snap store, atau bahkan AppImages dari store.kde.org.

+

Con Discover, puoi gestire i programmi da diverse fonti, inclusi i depositi del software del tuo sistema operativo, depositi Flatpak, lo Snap Store o anche AppImages da store.kde.org.

+

Discover -თან ერთად შეგიძლიათ მართოთ პროგრამები მრავალი წყაროდან, თქვენი ოპერაციული სისტემის პროგრამების რეპოზიტორიის, Flatpak-ის რეპოზიტორიის, Snap Store-ის და store.kde.org-დან გადმოწერილი AppImage-ების ჩათვლით.

+

Discover를 사용하면 여러 원본에서 제공되는 소프트웨어를 관리할 수 있습니다. 운영 체제 소프트웨어 저장소, Flatpak 저장소, Snap 스토어, store.kde.org의 AppImage를 한 곳에서 관리할 수 있습니다.

+

Naudodami Discover, galite tvarkyti programinę įrangą iš daugelio šaltinių, įskaitant jūsų operacinės sistemos programinės įrangos saugyklas, Flatpak saugyklas, Snap parduotuvę ar netgi AppImage paketus iš store.kde.org.

+

ഡിസ്കവർ ഉപയോഗിച്ച്, നിങ്ങളുടെ പ്രവര്‍ത്തകസംവിധാനത്തിന്റെ സോഫ്റ്റ്വെയർ ശേഖരം, ഫ്ലാറ്റ്പ്പാക്ക് ശേഖരണങ്ങൾ, സ്നാപ്പ് സ്റ്റോർ അല്ലെങ്കിൽ store.kde.org- ൽ നിന്നുള്ള AppImages എന്നിവ ഉൾപ്പെടെ ഒന്നിലധികം ഉറവിടങ്ങളിൽ നിന്നുള്ള സോഫ്റ്റ്വെയർ നിങ്ങൾക്ക് കൈകാര്യം ചെയ്യാൻ കഴിയും.

+

ဒစ်(စ)ကာဗာကို သုံး၍ သင်အနေဖြင့် ဇစ်မြစ်ပေါင်းစုံမှ ဆော့ဖ်ဝဲလ်များကို စီမံနိုင်သည်။ သာဓက။ ။ သင့်စီမံမောင်းနှင်စနစ်၏ဆော့ဖ်ဝဲလ်ရီပိုစစ်တိုရီ၊ ဖလက်ပတ်ခ် ရီပိုများ၊ စနပ်စတိုးနှင် store.kde.orgမှ appimagesများ။

+

Med Discover kan du installere programmer fra ulike kilder, blant annet standard pakkebrønner for operativsystemet, Flatpak-depot, Snap-butikken og AppImage fra store.kde.org.

+

Met Ontdekken kunt u software uit meerdere bronnen beheren, inclusief de repositories van uw besturingssysteem, Flatpak-repo's, de Snap store of zelfs AppImages uit store.kde.org.

+

Men Discover kan du installera program frå ulike kjelder, blant anna standard pakkebrønnar for operativsystemet, Flatpak-depot, Snap-butikken og AppImages frå store.kde.org.

+

Dzięki Odkrywcy, można zarządzać oprogramowaniem z wielu źródeł, uwzględniając w tym źródło oprogramowania systemu operacyjnego, repozytoria Flatpak, sklep Snap lub nawet AppImage z store.kde.org.

+

Com o Discover, poderá gerir aplicações de várias fontes, incluindo os repositórios de aplicações do seu sistema operativo, os repositórios do Flatpak, a loja do Snap ou mesmo AppImages do store.kde.org.

+

Com o Discover você poderá gerenciar aplicativos de várias origens, incluindo os repositórios de aplicativos do seu sistema operacional, os repositórios do Flatpak, a loja do Snap ou mesmo AppImages do store.kde.org.

+

Cu Discover puteți gestiona programe din surse diferite, inclusiv depozitul software al sistemului de operare, depozite Flatpak, magazinul Snap, sau chiar programe AppImage de la store.kde.org.

+

Discover позволяет управлять приложениями, получаемыми из различных источников: репозиториев установленной системы, репозиториев Flatpak, магазина приложений Snap, и даже приложений AppImages, расположенных по адресу store.kde.org.

+

S Discover môžete spravovať softvér z viacerých zdrojov vrátane úložiska softvéru vášho operačného systému, úložiska Flatpak, úložiska Snap alebo dokonca AppImages z obchodu store.kde.org.

+

S programom Discover lahko upravljate programsko opremo iz več virov, vključno s svojo shrambo programske opreme operacijskega sistema, programske pakete Flatpak, trgovina Snap ali celo programe AppImages iz store.kde.org.

+

Man kan hantera programvara från flera olika källor med Discover, inklusive operativsystemets programvaruarkiv, Flatpak-arkiv, Snap-butiken eller till och med programavbilder från store.kde.org.

+

Ба воситаи Кашфиёт шумо метавонед нармафзорро аз якчанд манбаъ идора намоед, аз он ҷумла тавассути анбори додаҳои нармафзори низоми шумо, анборҳои додаҳои Flatpak, дукони Snap ё ҳатто файлҳои AppImages аз сомонаи store.kde.org.

+

Keşfet ile işletim sisteminizin yazılım deposu, Flatpak depoları, Snap mağazası ve hatta store.kde.org'dan AppImages dahil olmak üzere birden çok kaynaktan gelen yazılımı yönetebilirsiniz.

+

За допомогою Discover ви можете керувати програмним забезпеченням із багатьох джерел, зокрема зі сховищ програмного забезпечення вашої операційної системи, зі сховищ Flatpak, із крамниці Snap або навіть із образів AppImage з store.kde.org.

+

xxWith Discover, you can manage software from multiple sources, including your operating system's software repository, Flatpak repos, the Snap store, or even AppImages from store.kde.org.xx

+

您可以在 Discover 中管理多个软件来源,包括本机操作系统的软件仓库、Flatpak 软件仓库、Snap 软件商店、store.kde.org 提供的 AppImage 软件包等。

+

Discover 能幫助您管理各色來源的軟體,無論是系統內建的軟體庫、Flatpak 軟體庫、Snap 商店,甚或是來自 store.kde.org 的 AppImage。

+

Finally, Discover also allows you to find, install, and manage add-ons for Plasma and all your favorite KDE apps!

+

أخيرًا ، يتيح لك المستكشف أيضًا العثور على الوظائف الإضافية وتثبيتها وإدارتها لبلازما وجميع تطبيقات كِيدِي المفضلة لديك!

+

Nəhayət, Discover həmçinin Plazma və bütün sevdiyiniz KDE tətbiqlərini tapmaq, quraşdırmaq və idarə etməyə imkan verir!

+

Накрая, Discover ви позволява също да намирате, инсталирате и управлявате разширения за Plasma и всички ваши любими приложения на KDE.

+

Finalment, el Discover també permet cercar, instal·lar i gestionar complements per al Plasma i totes les aplicacions preferides del KDE!

+

Finalment, Discover també permet buscar, instal·lar i gestionar complements per a Plasma i totes les aplicacions preferides de KDE!

+

Endelig lader Discover dig også finde, installere og administrere tilføjelser til Plasma og alle dine yndlings KDE-programmer!

+

Außerdem können Sie mit Discover Erweiterungen für Plasma und Ihre Lieblings-KDE-Anwendungen suchen, installieren und verwalten.

+

Finally, Discover also allows you to find, install, and manage add-ons for Plasma and all your favourite KDE apps!

+

Por último, Discover también le permite encontrar, instalar y gestionar complementos para Plasma y para todas sus aplicaciones favoritas de KDE.

+

Ja lõpuks lubab Discover leida, paigaldada ja hallata lisasid nii Plasmale kui ka kõigile KDE parimatele rakendustele!

+

Azkenik, Discover-rek uzten dizu Plasma eta zure KDEko aplikazio gogoko guztietarako gehigarriak bilatu, instalatu eta kudeatzen.

+

Discoverilla voit myös etsiä, asentaa ja hallita Plasman laajennusosia ja kaikkia suosikki-KDE-sovelluksiasi!

+

Enfin, Discover vous permet de trouver, d'installer et de gérer des modules complémentaires pour Plasma et toutes vos applis KDE préférées !

+

Por último, Discover tamén lle permite atopar, instalar e xestionar complementos de Plasma e das súas aplicacións favoritas de KDE!

+

अंत में, डिस्कवर आपको प्लाज़्मा और आपके सभी पसंदीदा केडीई ऐप्स के लिए ऐड-ऑन ढूंढने, संस्थापित करने और प्रबंधित करने मैं मदद करता है!

+

Végül, de nem utolsósorban a Discoverrel kereshet, telepíthet és kezelhet kiegészítőket a Plasmához és a kedvenc KDE alkalmazásaihoz!

+

Al fin, Discover anque te permitte trovar, installar, e gerer add-ons per Plasma e omne tu favorite apps de KDE!

+

Terakhir, Discover juga memungkinkan kamu untuk menemukan, menginstal, dan mengelola pernik untuk Plasma dan semua aplikasi KDE favoritmu.

+

Infine, Discover ti consente di trovare, installare e gestire componenti aggiuntivi di Plasma e tutte le tue applicazioni preferite di KDE!

+

და ბოლოს, Discover -ი ასევე საშუალებას გაძლევთ მოძებნოთ, დააყენოთ და მართოთ Plasma-ის და ყველა თქვენი საყვარელი KDE-ის აპების დამატებები!

+

또한 Discover를 사용하면 Plasma와 KDE 앱의 추가 기능을 찾고, 설치하고, 관리할 수 있습니다!

+

Pagaliau, Discover taip pat leidžia jums rasti, įdiegti ir tvarkyti Plasma papildinius ir visas jūsų mėgstamas KDE programas!

+

അവസാനമായി, പ്ലാസ്മയ്ക്കും നിങ്ങളുടെ പ്രിയപ്പെട്ട എല്ലാ കെ‌ഡി‌ഇ അപ്ലിക്കേഷനുകൾക്കുമായുള്ള ആഡ്-ഓണുകൾ കണ്ടെത്താനും ഇൻസ്റ്റാൾ ചെയ്യാനും നിയന്ത്രിക്കാനും ഡിസ്കവർ നിങ്ങളെ അനുവദിക്കുന്നു!

+

နောက်ဆုံးအားဖြင့် ဒစ်(စ)ကာဗာသည် သင့်အား ပလက်စမာနှင့် ကေဒီအီးအပ်များ၏ အက်အွန်များကို ရှာဖွေ၊ တပ်ဆင်၊ စီမံခြင်း လုပ်ငန်းများကို လုပ်ဆောင်စေနိုင်သည်။

+

Discover lar deg også finne, installere og håndtere programtillegg for Plasma og andre KDE-programmer.

+

Met Ontdekken vindt, installeert en beheert u ook add-ons voor Plasma en alle KDE toepassingen!

+

Discover lèt deg òg finna, installera og handsama programtillegg for Plasma og andre KDE-program.

+

Odkrywca umożliwia także znalezienie, wgranie i zrządzanie dodatkami do Plazmy i innych ulubionych aplikacji KDE!

+

Finalmente, o Discover também lhe permite procurar, instalar e gerir as extensões do Plasma e de todas as suas aplicações favoritas do KDE!

+

Finalmente, o Discover também lhe permite procurar, instalar e gerenciar as extensões do Plasma e de todos os seus aplicativos favoritos do KDE!

+

De asemenea, Discover vă ajută să găsiți, instalați și gestionați suplimente pentru Plasma și toate aplicațiile KDE favorite!

+

Кроме того, при помощи Discover возможно искать, устанавливать и управлять дополнениями рабочей среды Plasma и приложений KDE.

+

Konečne aplikácia Discover tiež umožňuje nájsť, nainštalovať a spravovať doplnky pre Plasmu a všetky vaše obľúbené aplikácie prostredia KDE!

+

Končno Discover vam omogoča iskanje, namestitev in upravljanje dodatkov za Plazma in vse vaše najljubše programe za KDE!

+

Slutligen låter Discovery dig söka efter, installera och hantera tilläggsprogram för Plasma och alla favoritprogram i KDE.

+

Дар охир, Кашфиёт ба шумо барои ёфтан, насб ва идора кардани барномаҳои иловагӣ барои Плазма ва ҳамаи барномаҳои пазируфтаи шумо аз KDE имкон медиҳад!

+

Son olarak Keşfet; Plasma ve tüm favori KDE uygulamalarınız için eklentiler bulmanıza, kurmanıza ve yönetmenize de olanak tanır!

+

Нарешті, Discover надає вам змогу шукати, встановлювати додатки до Плазми та усі ваші улюблені програми KDE та керувати ними!

+

xxFinally, Discover also allows you to find, install, and manage add-ons for Plasma and all your favorite KDE apps!xx

+

不仅如此,Discover 还能查找、安装和管理 Plasma 附加组件以及您喜爱的 KDE 应用程序!

+

最後,Discover 也能協助您搜尋、安裝與管理 Plasma 的外掛程式及你所有喜歡的 KDE 應用程式!

+
+ https://bugs.kde.org/enter_bug.cgi?format=guided&product=Discover + + + Plasma Discover + مستكشف بلازما + Plasma Discover + Plasma Discover + Discover del Plasma + Discover de Plasma + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover. + प्लाज़्मा डिस्कवर + Plasma Discover + Plasma Discover (Discoperi de Plasma) + Discover Plasma + Plasma Discover + Plasma Discover + Plasma Discover + Plasma Discover + പ്ലാസ്മ ഡിസ്കവർ + ပလက်စမာ ဒစ်(စ)ကာဗာ + Plasma Discover + Plasma Ontdekken + Plasma Discover + ਪਲਾਜ਼ਮਾ ਡਿਸਕਵਰ + Odkrywca Plazmy + Plasma Discover + Plasma Discover + Plasma Discover + Центр приложений Discover + Plasma Discover + Plasma Discover + Plasma Discover + பிளாஸ்மா டிஸ்கவர் + Кашфиёти Плазма + Plasma Keşfet + Discover у Плазмі + xxPlasma Discoverxx + Plasma Discover + Plasma Discover + https://cdn.kde.org/screenshots/plasma-discover/plasma-discover.png + + + KDE + + plasma-discover + + KDE + + + + + + +
diff --git a/discover/org.kde.discover.desktop.cmake b/discover/org.kde.discover.desktop.cmake new file mode 100644 index 0000000..91fb70a --- /dev/null +++ b/discover/org.kde.discover.desktop.cmake @@ -0,0 +1,207 @@ +[Desktop Entry] +Name=Discover +Name[ar]=المستكشف +Name[az]=Discover +Name[bg]=Discover +Name[ca]=Discover +Name[ca@valencia]=Discover +Name[cs]=Discover +Name[da]=Discover +Name[de]=Discover +Name[el]=Discover +Name[en_GB]=Discover +Name[es]=Discover +Name[et]=Discover +Name[eu]=Discover +Name[fi]=Discover +Name[fr]=Discover +Name[gl]=Descubrir +Name[he]=Discover +Name[hi]=डिस्कवर +Name[hu]=Discover +Name[ia]=Discover (Discoperi) +Name[id]=Discover +Name[ie]=Discover +Name[it]=Discover +Name[ja]=Discover +Name[ka]=Discover +Name[ko]=Discover +Name[lt]=Discover +Name[ml]=കണ്ടെത്തുക +Name[my]=ဒစ်(စ)ကာဗာ +Name[nb]=Discover +Name[nl]=Ontdekken +Name[nn]=Discover +Name[pa]=ਡਿਸਕਵਰ +Name[pl]=Odkrywca +Name[pt]=Discover +Name[pt_BR]=Discover +Name[ro]=Discover +Name[ru]=Discover +Name[sk]=Discover +Name[sl]=Discover +Name[sr]=Oткривач +Name[sr@ijekavian]=Oткривач +Name[sr@ijekavianlatin]=Otkrivač +Name[sr@latin]=Otkrivač +Name[sv]=Upptäck +Name[ta]=டிஸ்கவர் +Name[tg]=Кашфиёт +Name[tr]=Keşfet +Name[uk]=Discover +Name[x-test]=xxDiscoverxx +Name[zh_CN]=Discover 软件管理中心 +Name[zh_TW]=Discover +MimeType=@DesktopMimeType@ +Exec=@DesktopExec@ +Icon=plasmadiscover +Type=Application +X-DocPath=plasma-discover/index.html +InitialPreference=5 +NoDisplay=@DesktopNoDisplay@ +Actions=Updates; +SingleMainWindow=true +GenericName=Software Center +GenericName[ar]=مركز البرمجيّات +GenericName[az]=Proqram Təminatı Mərkəzi +GenericName[bg]=Софтуерен център +GenericName[ca]=Centre de programari +GenericName[ca@valencia]=Centre de programari +GenericName[cs]=Centrum softwaru +GenericName[da]=Softwarecenter +GenericName[de]=Programmverwaltung +GenericName[el]=Κέντρο λογισμικού +GenericName[en_GB]=Software Centre +GenericName[es]=Centro de software +GenericName[et]=Tarkvarakeskus +GenericName[eu]=Software gunea +GenericName[fi]=Sovellusvalikoima +GenericName[fr]=Logithèque +GenericName[gl]=Centro de Software +GenericName[he]=מרכז התכנה +GenericName[hi]=सॉफ्टवेयर केन्द्र +GenericName[hsb]=Srjedźišćo za software +GenericName[hu]=Szoftverközpont +GenericName[ia]=Centro de Software +GenericName[id]=Pusat Perangkat Lunak +GenericName[ie]=Programmarium +GenericName[it]=Software Center +GenericName[ja]=ソフトウェアセンター +GenericName[ka]=პროგრამების ცენტრი +GenericName[ko]=소프트웨어 센터 +GenericName[lt]=Programinės įrangos centras +GenericName[ml]=സോഫ്റ്റ്വെയർ സെന്റർ +GenericName[my]=ဆော့ဖ်ဝဲလ်စင်တာ +GenericName[nb]=Programvaresenter +GenericName[nl]=Softwarecentrum +GenericName[nn]=Programvaresenter +GenericName[pa]=ਸਾਫਟਵੇਅਰ ਸੈਂਟਰ +GenericName[pl]=Ośrodek programów +GenericName[pt]=Centro de Aplicações +GenericName[pt_BR]=Central de aplicativos +GenericName[ro]=Centru de aplicații +GenericName[ru]=Центр программ +GenericName[sk]=Centrum softvéru +GenericName[sl]=Programsko središče +GenericName[sr]=Софтверски центар +GenericName[sr@ijekavian]=Софтверски центар +GenericName[sr@ijekavianlatin]=Softverski centar +GenericName[sr@latin]=Softverski centar +GenericName[sv]=Programvarucentral +GenericName[ta]=மென்பொருள் மையம் +GenericName[tg]=Маркази нармафзор +GenericName[tr]=Yazılım Merkezi +GenericName[uk]=Центр програм +GenericName[x-test]=xxSoftware Centerxx +GenericName[zh_CN]=软件管理中心程序 +GenericName[zh_TW]=軟體中心 +Categories=Qt;KDE;System; +Keywords=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware; +Keywords[ar]=برنامج;برمجيّة;برمجية;مستودع;حزمة;تثبيت;إزالة;أرشيف;تحديث;تطبيقات;تطبيق; +Keywords[az]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;proqram;proqram təminatı;anbar;paket;əlavə edin;quraşdırın;silin;ləğv edin;yeniləyin;tətbiqlər; tətbiqetmələr;oyunlar;əlavələr +Keywords[bg]=програма;софтуер;хранилище;пакет;инсталиране;премахване;актуализация;програми;приложения; +Keywords[ca]=programa;programari;repositori;paquet;afegeix;instal·la;desinstal·la;elimina;actualitza;apps;aplicacions;jocs;flatpak;snap;complements;microprogramari;firmware; +Keywords[ca@valencia]=programa;programari;repositori;paquet;afig;instal·la;desinstal·la;elimina;actualitza;apps;aplicacions;jocs;flatpak;snap;complements;microprogramari;firmware; +Keywords[cs]=program;software;repozitář;balíček;přidat;instalovat;odinstalovat;odstranit;aktualizovat;programy;aplikace;hry;flatpak;snap;doplňky;addony;firmware; +Keywords[de]=Programm;Software;Archiv;Paket;Installieren;Deinstallieren;Entfernen;Aktualisieren;Programme;Anwendungen;Spiele; +Keywords[en_GB]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware; +Keywords[es]=programa;software;repositorio;paquete;añadir;agregar;instalar;desinstalar;eliminar;borrar;actualizar;apps;aplicaciones;juegos;flatpak;snap;complementos;añadidos;firmware; +Keywords[eu]=programa;softwarea;gordetegia;paketea;gehitu;instalatu;desinstalatu;kendu;eguneratu;appak;aplikazioak;jokoak;flatpak;snap;gehigarriak;firmwarea; +Keywords[fi]=ohjelma;ohjelmisto;asennuslähde;repositorio;paketti;lisää;asenna;asennus;poista asennus;poista;päivitä;päivitys;sovellus;sovellukset;peli;pelit;flatpak;snap;lisäosa;lisäosat;laiteohjelma;varusohjelma; +Keywords[fr]=programme ; logiciel ; dépôt ; paquet ; ajout ; installation ; désinstallation ; suppression ; mise à jour ; applications ; applications ; jeux ; flatpak ; snap ; modules externes ; micrologiciel ; +Keywords[gl]=aplicación;software;repositorio;paquete;empaquetar;engadir;instalar;desinstalar;retirar;actualizar;aplicativos;aplicacións;xogos;videoxogos;flatpak;snap;complementos;engadidos;extensións;firmware; +Keywords[hu]=program;szoftver;tároló;csomag;hozzáadás;telepítés;eltávolítás;törlés;frissítés;alkalmazások;alkalmazások;játékok;flatpak;snap;bővítmények;kiterjesztések;firmware; +Keywords[ia]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware; +Keywords[id]=program;perangkat lunak;repositori;paket;tambah;instal;uninstal;hapus;hilangkan;app;aplikasi;game;permainan;snap;addon;pernik;pernak-pernik;firmware; +Keywords[ie]=programma;repositoria;paccage;adjunter;installar;deinstallar;actualisar;app;applicationes;ludes;flatpak;snap;addons;add-ons;firmware; +Keywords[it]=programma;software;deposito;pacchetto;installa;disinstalla;rimuovi;aggiorna;app;applicazioni;giochi;flatpak;snap;componenti aggiuntivi;firmware; +Keywords[ja]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;インストール;アンインストール;アプリケーション;ソフトウェア;更新;削除;リポジトリ;アドオン;ゲーム;パッケージ;ファームウェア; +Keywords[ka]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware; +Keywords[ko]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;앱;소프트웨어;저장소;리포지토리;패키지;꾸러미;추가;설치;삭제;업데이트;앱;게임;추가 기능;부가 기능;펌웨어; +Keywords[nb]=program;programvare;pakkebrønn;pakke;legg til;installer;avinstaller;fjern;oppdater;apper;applikasjoner;spill;flatpak;snap;tillegg;programtillegg;fastvare; +Keywords[nl]=programma;software;repository;pakket;toevoegen;installeren;installatie ongedaan maken;verwijderen;bijwerken;apps;toepassingen;applicaties;spellen;flatpak;snap;add-ons;firmware; +Keywords[nn]=program;programvare;pakkebrønn;pakke;pakkar;legg til;leggja til;installera;installering;avinstallera;avinstallering;fjerna;fjerning;oppdatera;oppdatering;app;appar;applikasjonar;spel;dataspel;flatpak;snap;tillegg;programtillegg;fastvare; +Keywords[pa]=ਪਰੋਗਰਾਮ;ਸਾਫਟਵੇਅਰ;ਰਿਪੋਜ਼ਟਰੀ;ਪੈਕੇਜ;ਇੰਸਟਾਲ;ਪ੍ਰੋਗਰਾਮ;ਇੰਸਟਾਲ;ਹਟਾਓ;ਮਿਟਾਓ;ਅੱਪਡੇਟ;ਐਪ;ਐਪਲੀਕੇਸ਼ਨਾਂ;ਅਣ-ਇੰਸਟਾਲ;ਖੇਡਾਂ;ਗੇਮਾਂ;ਫਲੈਟਪੈਕ;ਸਨੈਪ;ਐਡ-ਆਨ;ਐਡਆਨ;ਫਿਰਮਵੇਅਰ;ਫਰਮਵੇਅਰ;ਜੋੜੋ; +Keywords[pl]=program;oprogramowanie;repozytorium;archiwum;pakiet;paczka;dodaj;instaluj;zainstaluj;usuń;odinstaluj;uaktualnij;aktualizuj;programy;aplikacje;deb;gry;flatpak;snap;dodatki;oprogramowanie układowe; +Keywords[pt]=programa;software;repositório;pacote;instalar;remover;actualizar;aplicações;jogos;flatpak;snap;extensões;firmware; +Keywords[pt_BR]=programa;software;repositório;pacote;adicionar;instalar;desinstalar;remover;atualizar;aplicativos;apps;aplicações;jogos;flatpak;snap;complemento;extensão;firmware; +Keywords[ro]=program;software;depozit;pachet;instalare;eliminare;actualizare;aplicații;instalează;elimină;actualizează;jocuri;flatpak;snap;suplimente;firmware;microcod; +Keywords[ru]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;программа;приложение;репозиторий;пакет;добавить;установка;удаление;deb;игры;расширения;прошивка +Keywords[sk]=program;softvér;repozitár;balík;inštalácia;inľtalovať;odstránenie;odstrániť;aktualizovať;aktualizácia;appky;aplikácie; +Keywords[sl]=program;programska oprema;skladišče;paket;dodaj;namesti;odstrani;posodobi;programi;aplikacije;igre;flatpak;posnetek;dodatki;vgrajeno programje; +Keywords[sv]=program;programvara;arkiv;paket;lägg till;installera;avinstallera;ta bort;uppdatera;appar;program;spel;flatpak;snap;tillägg;fast programvara +Keywords[ta]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;மென்பொருள்;பயன்பாடு;செயலி;ஆப்ஸ்;ஆப்சு;நிரல்;கிடங்கு;களஞ்சியம்;தொகுப்பு;சேர்;நிறுவு;நிறுவல்;நீக்கு;புதுப்பி;புதுப்பித்தல்;ஃபலாட்பாக்;ஸ்னாப்;சுனாப்;செருகுநிரல்;துணை நிரல்கள்;துணைநிரல்;சாதனநிரல்; +Keywords[tr]=program;yazılım;depo;paket;ekle;kur;kaldır;güncelle;uygulama;uygulamalar;oyunlar;flatpak;snap;eklentiler;gömülü yazılım +Keywords[uk]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;програма;програмне забезпечення;сховище;архів;пакунок;додати;встановити;встановлення;вилучити;вилучення;оновлення;оновити;ігри;флетпак;снеп;снап;додатки;мікропрограма; +Keywords[x-test]=xxprogramxx;xxsoftwarexx;xxrepositoryxx;xxpackagexx;xxaddxx;xxinstallxx;xxuninstallxx;xxremovexx;xxupdatexx;xxappsxx;xxapplicationsxx;xxgamesxx;xxflatpakxx;xxsnapxx;xxaddonsxx;xxadd-onsxx;xxfirmwarexx; +Keywords[zh_CN]=program;software;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware;程序;软件;仓库;软件包;添加;安装;卸载;移除;删除;升级;应用;应用程序;游戏;扩展程序;附加程序;固件;chengxu;ruanjian;cangku;ruanjiancangku;ruanjianbao;tianjia;anzhuang;xiezai;yichu;shanchu;shengji;yingyong;yingyongchengxu;youxi;kuozhanchengxu;fujiachengxu;gujian; +Keywords[zh_TW]=program;software;repository;package;install;remove;update;apps;applications;軟體;程式;軟體包;軟體庫;安裝;移除;更新; + +[Desktop Action Updates] +Name=See Available Updates +Name[ar]=اعرض التحديثات المتوفرة +Name[az]=Əlçatan yenilənmələrə baxın +Name[bg]=Преглед на наличие актуализации +Name[ca]=Mostra les actualitzacions disponibles +Name[ca@valencia]=Mostra les actualitzacions disponibles +Name[cs]=Zobrazit dostupné aktualizace +Name[de]=Zeigt verfügbare Aktualisierungen +Name[en_GB]=See Available Updates +Name[es]=Ver las actualizaciones disponibles +Name[eu]=Ikusi eguneratze erabilgarriak +Name[fi]=Näytä saatavilla olevat päivitykset +Name[fr]=Consulter les mises à jour disponibles +Name[gl]=Ver as actualizacións dispoñíbeis +Name[hi]=उपलब्ध अद्यतन देखें +Name[hsb]=Pokaž móžne aktualizowanja +Name[hu]=Elérhető frissítések megtekintése +Name[ia]=Vide actualisationes dispoibile +Name[id]=Lihat Pembaruan Tersedia +Name[ie]=Vider li disponibil actualisamentes +Name[it]=Vedi gli aggiornamenti disponibili +Name[ja]=利用可能な更新を見る +Name[ka]=ხელმისაწვდომი განახლებების ნახვა +Name[ko]=사용 가능한 업데이트 보기 +Name[lt]=Rodyti prieinamus atnaujinimus +Name[ml]=ലഭ്യമായ അപ്ഡേറ്റുകൾ കാണുക +Name[my]=ရရှိနိုင်သော အပ်ဒိတ်များ ကြည့်မည် +Name[nb]=Se tilgjengelige oppdateringer +Name[nl]=Bekijk beschikbare updates +Name[nn]=Sjå tilgjengelege oppdateringar +Name[pa]=ਮੌਜੂਦ ਅੱਪਡੇਟ ਵੇਖੋ +Name[pl]=Obejrzyj dostępne uaktualnienia +Name[pt]=Ver as Actualizações Disponíveis +Name[pt_BR]=Ver as atualizações disponíveis +Name[ro]=Vezi actualizările disponibile +Name[ru]=Просмотреть доступные обновления +Name[sk]=Zobraziť dostupné aktualizácie +Name[sl]=Poglej razpoložljive posodobitve +Name[sv]=Se Tillgängliga uppdateringar +Name[ta]=கிடைக்கும் புதுப்பிப்புகளை காட்டும் +Name[tr]=Kullanılabilir Güncellemeleri Gör +Name[uk]=Перегляд доступних оновлень +Name[x-test]=xxSee Available Updatesxx +Name[zh_CN]=查看可用更新 +Name[zh_TW]=檢視可用的更新 +Icon=update-low +Exec=plasma-discover --mode update diff --git a/discover/plasmadiscoverui.rc b/discover/plasmadiscoverui.rc new file mode 100644 index 0000000..cfd3837 --- /dev/null +++ b/discover/plasmadiscoverui.rc @@ -0,0 +1,10 @@ + + + + + diff --git a/discover/plasmauserfeedback.kcfg b/discover/plasmauserfeedback.kcfg new file mode 100644 index 0000000..f44e538 --- /dev/null +++ b/discover/plasmauserfeedback.kcfg @@ -0,0 +1,7 @@ + + + + + int(KUserFeedback::Provider::NoTelemetry) + + diff --git a/discover/plasmauserfeedback.kcfgc b/discover/plasmauserfeedback.kcfgc new file mode 100644 index 0000000..26a1e9b --- /dev/null +++ b/discover/plasmauserfeedback.kcfgc @@ -0,0 +1,5 @@ +File=plasmauserfeedback.kcfg +ClassName=PlasmaUserFeedback +GenerateProperties=true +Mutators=true +IncludeFiles=KUserFeedback/Provider diff --git a/discover/qml/AboutPage.qml b/discover/qml/AboutPage.qml new file mode 100644 index 0000000..8036df8 --- /dev/null +++ b/discover/qml/AboutPage.qml @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml 2.1 +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.AboutPage +{ + readonly property bool isHome: true + aboutData: discoverAboutData +} diff --git a/discover/qml/ActionListItem.qml b/discover/qml/ActionListItem.qml new file mode 100644 index 0000000..d676eb8 --- /dev/null +++ b/discover/qml/ActionListItem.qml @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QQC2 + +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.BasicListItem +{ + id: item + property QtObject action: null + checked: action.checked + icon: action.iconName + separatorVisible: false + visible: action.enabled + + onClicked: trigger() + Keys.onEnterPressed: trigger() + Keys.onReturnPressed: trigger() + + function trigger() { + drawer.resetMenu() + action.trigger() + } + + Kirigami.MnemonicData.enabled: item.enabled && item.visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: action.text + label: Kirigami.MnemonicData.richTextLabel + + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: hovered && p0.nativeText.length > 0 + QQC2.ToolTip.text: p0.nativeText + + readonly property var p0: Shortcut { + sequence: item.Kirigami.MnemonicData.sequence + onActivated: item.clicked() + } + + // Using the generic onPressed so individual instances can override + // behaviour using Keys.on{Up,Down}Pressed + Keys.onPressed: { + if (event.accepted) { + return + } + + // Using forceActiveFocus here since the item may be in a focus scope + // and just setting focus won't focus the scope. + if (event.key === Qt.Key_Up) { + nextItemInFocusChain(false).forceActiveFocus() + event.accepted = true + } else if (event.key === Qt.Key_Down) { + nextItemInFocusChain(true).forceActiveFocus() + event.accepted = true + } + } +} diff --git a/discover/qml/AddSourceDialog.qml b/discover/qml/AddSourceDialog.qml new file mode 100644 index 0000000..6cd8f9d --- /dev/null +++ b/discover/qml/AddSourceDialog.qml @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.PromptDialog +{ + id: newSourceDialog + preferredWidth: Kirigami.Units.gridUnit * 20 + + property string displayName + property QtObject source + + title: i18n("Add New %1 Repository", displayName) + + onVisibleChanged: { + if (visible) { + repository.forceActiveFocus(); + } + } + + standardButtons: Kirigami.Dialog.NoButton + + onAccepted: { + if (source.addSource(repository.text)) { + newSourceDialog.close() + } else { + repository.color = Kirigami.Theme.negativeTextColor + } + } + + onRejected: { + newSourceDialog.close() + } + + customFooterActions: [ + Kirigami.Action { + text: i18n("Add") + icon.name: "list-add" + onTriggered: newSourceDialog.accept(); + }, + Kirigami.Action { + text: i18n("Cancel") + icon.name: "dialog-cancel" + onTriggered: newSourceDialog.reject(); + } + ] + + ColumnLayout { + Label { + Layout.fillWidth: true + wrapMode: Text.Wrap + textFormat: Text.PlainText + text: source.idDescription + } + + TextField { + id: repository + Layout.fillWidth: true + onAccepted: newSourceDialog.accept() + focus: true + onTextChanged: color = Kirigami.Theme.textColor + } + } +} diff --git a/discover/qml/AddonsView.qml b/discover/qml/AddonsView.qml new file mode 100644 index 0000000..ea158b8 --- /dev/null +++ b/discover/qml/AddonsView.qml @@ -0,0 +1,72 @@ +import QtQuick 2.1 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.1 +import org.kde.discover 2.0 +import "navigation.js" as Navigation +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.OverlaySheet { + id: addonsView + parent: applicationWindow().overlay + + property alias application: addonsModel.application + property bool isInstalling: false + readonly property alias addonsCount: listview.count + readonly property bool containsAddons: listview.count > 0 || isExtended + readonly property bool isExtended: ResourcesModel.isExtended(application.appstreamId) + + title: i18n("Addons for %1", application.name) + + ListView { + id: listview + + implicitWidth: Kirigami.Units.gridUnit * 25 + + visible: addonsView.containsAddons + enabled: !addonsView.isInstalling + + model: ApplicationAddonsModel { id: addonsModel } + + delegate: Kirigami.CheckableListItem { + id: listItem + + enabled: !addonsView.isInstalling + + icon: undefined + label: model.display + subtitle: model.toolTip + + checked: model.checked + + onCheckedChanged: addonsModel.changeState(packageName, listItem.checked) + } + } + + footer: RowLayout { + + readonly property bool active: addonsModel.hasChanges && !addonsView.isInstalling + + Button { + text: i18n("More…") + visible: application.appstreamId.length>0 && addonsView.isExtended + onClicked: Navigation.openExtends(application.appstreamId, application.name) + } + + Item { Layout.fillWidth: true } + + Button { + icon.name: "dialog-ok" + text: i18n("Apply Changes") + onClicked: addonsModel.applyChanges() + + enabled: parent.active + } + Button { + icon.name: "document-revert" + text: i18n("Reset") + onClicked: addonsModel.discardChanges() + + enabled: parent.active + } + } +} diff --git a/discover/qml/ApplicationDelegate.qml b/discover/qml/ApplicationDelegate.qml new file mode 100644 index 0000000..050d9fc --- /dev/null +++ b/discover/qml/ApplicationDelegate.qml @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018-2021 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 +import "navigation.js" as Navigation +import org.kde.discover 2.0 +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.AbstractCard +{ + id: delegateArea + property alias application: installButton.application + property bool compact: false + property bool showRating: true + property bool showSize: false + + readonly property bool appIsFromNonDefaultBackend: ResourcesModel.currentApplicationBackend !== application.backend && application.backend.hasApplications + showClickFeedback: true + + function trigger() { + const view = typeof delegateRecycler !== 'undefined' ? delegateRecycler.ListView.view : ListView.view + if (view) + view.currentIndex = index + Navigation.openApplication(application) + } + highlighted: ListView.isCurrentItem || (typeof delegateRecycler !== 'undefined' && delegateRecycler.ListView.isCurrentItem) + Keys.onReturnPressed: trigger() + onClicked: trigger() + + contentItem: Item { + implicitHeight: delegateArea.compact ? Kirigami.Units.gridUnit * 2 : Math.round(Kirigami.Units.gridUnit * 3.5) + + // App icon + Kirigami.Icon { + id: resourceIcon + readonly property real contHeight: delegateArea.compact ? Kirigami.Units.iconSizes.large : Kirigami.Units.iconSizes.huge + source: application.icon + height: contHeight + width: contHeight + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: delegateArea.compact ? Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing * 2 + } + } + + // Container for everything but the app icon + ColumnLayout { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + left: resourceIcon.right + leftMargin: Kirigami.Units.largeSpacing * 2 + } + spacing: 0 + + // Container for app name and backend name labels + RowLayout { + + // App name label + Kirigami.Heading { + id: head + Layout.fillWidth: true + level: delegateArea.compact ? 2 : 1 + type: Kirigami.Heading.Type.Primary + text: delegateArea.application.name + elide: Text.ElideRight + maximumLineCount: 1 + } + + // Backend name label (shown if app is from a non-default backend and + // we're not using the compact view, where there's no space for it) + RowLayout { + Layout.alignment: Qt.AlignRight + visible: delegateArea.appIsFromNonDefaultBackend && !delegateArea.compact + spacing: 0 + + Kirigami.Icon { + source: application.sourceIcon + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + } + Label { + text: application.backend.displayName + font: Kirigami.Theme.smallFont + } + } + } + + // Description/"Comment" label + Label { + id: description + Layout.fillWidth: true + text: delegateArea.application.comment + elide: Text.ElideRight + maximumLineCount: 1 + textFormat: Text.PlainText + } + + // Container for rating, size, and install button + RowLayout { + Layout.fillWidth: true + Layout.topMargin: delegateArea.compact ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing + + // Container for rating and size labels + ColumnLayout { + Layout.fillWidth: true + // Include height of sizeInfo for full-sized view even when + // the actual sizeInfo layout isn't visible. This tightens up + // the layout and prevents the install button from appearing + // at a different position based on whether or not the + // sizeInfo text is visible, because the base layout is + // vertically centered rather than filling a distinct space. + Layout.preferredHeight: delegateArea.compact ? -1 : rating.implicitHeight + sizeInfo.implicitHeight + spacing: 0 + + // Rating stars + label + RowLayout { + id: rating + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + visible: showRating + opacity: 0.6 + spacing: Kirigami.Units.largeSpacing + + Rating { + rating: delegateArea.application.rating ? delegateArea.application.rating.sortableRating : 0 + starSize: delegateArea.compact ? description.font.pointSize : head.font.pointSize + } + Label { + Layout.fillWidth: true + visible: delegateArea.application.rating || (delegateArea.application.backend.reviewsBackend && delegateArea.application.backend.reviewsBackend.isResourceSupported(delegateArea.application)) + text: delegateArea.application.rating ? i18np("%1 rating", "%1 ratings", delegateArea.application.rating.ratingCount) : i18n("No ratings yet") + font: Kirigami.Theme.smallFont + elide: Text.ElideRight + } + } + + // Size label + Label { + id: sizeInfo + Layout.fillWidth: true + visible: !delegateArea.compact && showSize + text: visible ? delegateArea.application.sizeDescription : "" + horizontalAlignment: Text.AlignRight + opacity: 0.6; + font: Kirigami.Theme.smallFont + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + // Install button + InstallApplicationButton { + id: installButton + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + visible: !delegateArea.compact + } + } + } + } +} diff --git a/discover/qml/ApplicationPage.qml b/discover/qml/ApplicationPage.qml new file mode 100644 index 0000000..12116fe --- /dev/null +++ b/discover/qml/ApplicationPage.qml @@ -0,0 +1,947 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2022 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 +import QtQuick.Layouts 1.15 +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import org.kde.kirigami 2.20 as Kirigami +import org.kde.purpose 1.0 as Purpose +import "navigation.js" as Navigation + +DiscoverPage { + id: appInfo + + title: appInfo.application.name + clip: true + + property QtObject application: null + readonly property int visibleReviews: 3 + readonly property int internalSpacings: Kirigami.Units.largeSpacing + readonly property int pageContentMargins: Kirigami.Units.gridUnit + + // 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 + + readonly property bool isOfflineUpgrade: application.packageName === "discover-offline-upgrade" + + function searchFor(text) { + if (text.length === 0) + return; + Navigation.openCategory(null, "") + } + + 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: 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) { + appInfo.application = model.application + } + } + } + } + + 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.isActive ? appbutton.cancelAction : appbutton.action + right: invokeAction + } + + InstallApplicationButton { + id: appbutton + Layout.rightMargin: Kirigami.Units.smallSpacing + application: appInfo.application + visible: false + } + + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + + // Page content + ColumnLayout { + id: pageLayout + + spacing: appInfo.internalSpacings + + // Header and its background rectangle + Rectangle { + Layout.fillWidth: true + + implicitHeight: headerLayout.height + (headerLayout.anchors.topMargin * 2) + color: Qt.darker(Kirigami.Theme.backgroundColor, 1.05) + + // Header layout with App icon, name, author, rating, + // screenshots, and metadata + ColumnLayout { + id: headerLayout + + anchors { + topMargin: appInfo.internalSpacings + top: parent.top + left: parent.left + right: parent.right + } + + spacing: appInfo.internalSpacings + + // App icon, name, author or update info, rating + RowLayout { + Layout.leftMargin: appInfo.internalSpacings + Layout.rightMargin: appInfo.internalSpacings + + spacing: 0 // Children bring their own + + // App icon + Kirigami.Icon { + readonly property int size : applicationWindow().wideScreen ? Kirigami.Units.iconSizes.large * 2 : Kirigami.Units.iconSizes.huge + + implicitWidth: size + implicitHeight: size + source: appInfo.application.icon + Layout.rightMargin: appInfo.internalSpacings + } + + // App name, author, and rating + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + // App name + Kirigami.Heading { + Layout.fillWidth: true + text: appInfo.application.name + type: Kirigami.Heading.Type.Primary + wrapMode: Text.Wrap + maximumLineCount: 5 + elide: Text.ElideRight + } + + // Author (for apps) or upgrade info (for offline upgrades) + Label { + id: author + + Layout.fillWidth: true + Layout.bottomMargin: appInfo.internalSpacings + + visible: text.length > 0 + + opacity: 0.8 + text: { + if (appInfo.isOfflineUpgrade) { + return appInfo.application.upgradeText.length > 0 ? appInfo.application.upgradeText : ""; + } else if (appInfo.application.author.length > 0) { + return appInfo.application.author; + } else { + return i18n("Unknown author"); + } + } + wrapMode: Text.Wrap + maximumLineCount: 5 + elide: Text.ElideRight + } + + // Rating + RowLayout { + Layout.fillWidth: true + + // Not relevant to the offline upgrade use case + visible: !appInfo.isOfflineUpgrade + + Rating { + opacity: 0.8 + rating: appInfo.application.rating ? appInfo.application.rating.sortableRating : 0 + starSize: author.font.pointSize + } + + Label { + opacity: 0.8 + text: appInfo.application.rating ? i18np("%1 rating", "%1 ratings", appInfo.application.rating.ratingCount) : i18n("No ratings yet") + } + } + } + } + + // Screenshots + Kirigami.InlineMessage { + type: Kirigami.MessageType.Warning + Layout.fillWidth: true + Layout.margins: Kirigami.Units.smallSpacing + visible: screenshots.hasFailed + text: i18n("Could not access the screenshots") + } + + ScrollView { + id: screenshotsScroll + visible: screenshots.count > 0 && !screenshots.hasFailed + Layout.maximumWidth: headerLayout.width + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: Math.min(Kirigami.Units.gridUnit * 20, Window.height * 0.25) + + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + + ApplicationScreenshots { + id: screenshots + resource: appInfo.application + delegateHeight: parent.Layout.preferredHeight * 0.8 + showNavigationArrows: screenshotsScroll.width === headerLayout.width + } + } + + // Metadata + Flow { + id: metadataLayout + readonly property int itemWidth: Kirigami.Units.gridUnit * 7 + readonly property int visibleChildren: countVisibleChildren(children) + function countVisibleChildren(items) { + let ret = 0; + for (const itemPos in items) { + const item = items[itemPos]; + ret += item.visible; + } + return ret; + } + + // This centers the Flow in the page, no matter how many items have + // flowed onto other rows + Layout.maximumWidth: ((itemWidth + spacing) * Math.min(visibleChildren, Math.floor(headerLayout.width / (itemWidth + spacing)))) - spacing + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: appInfo.internalSpacings + Layout.rightMargin: appInfo.internalSpacings + + spacing: Kirigami.Units.smallSpacing + + // Not relevant to the offline upgrade use case + visible: !appInfo.isOfflineUpgrade + + onImplicitWidthChanged: visibleChildrenChanged() + + // Version + ColumnLayout { + id: versionColumn + width: metadataLayout.itemWidth + + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + opacity: 0.7 + text: i18n("Version") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + maximumLineCount: 2 + } + Label { + Layout.fillWidth: true + text: appInfo.application.versionString + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + } + } + + // Size + ColumnLayout { + id: sizeColumn + width: metadataLayout.itemWidth + + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + opacity: 0.7 + text: i18n("Size") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + maximumLineCount: 2 + } + Label { + Layout.fillWidth: true + text: appInfo.application.sizeDescription + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + } + } + + // Distributor + ColumnLayout { + id: distributorColumn + width: metadataLayout.itemWidth + + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + opacity: 0.7 + text: i18n("Distributed by") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + maximumLineCount: 2 + } + Label { + Layout.fillWidth: true + text: appInfo.application.displayOrigin + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + } + } + + // License(s) + ColumnLayout { + id: licenseColumn + width: metadataLayout.itemWidth + + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + opacity: 0.7 + text: i18np("License", "Licenses", appInfo.application.licenses.length) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + maximumLineCount: 2 + } + Label { + Layout.fillWidth: true + visible : appInfo.application.licenses.length === 0 + text: i18nc("The app does not provide any licenses", "Unknown") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + } + ColumnLayout { + Layout.fillWidth: true + visible : appInfo.application.licenses.length > 0 + spacing: 0 + + Repeater { + model: appInfo.application.licenses.slice(0, 2) + delegate: RowLayout { + Layout.fillWidth: true + spacing: 0 + + Kirigami.UrlButton { + Layout.fillWidth: true + enabled: url !== "" + text: modelData.name + url: modelData.url + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + color: !modelData.hasFreedom ? Kirigami.Theme.neutralTextColor: enabled ? Kirigami.Theme.linkColor : Kirigami.Theme.textColor + } + + // Button to open "What's the risk of proprietary software?" sheet + ToolButton { + visible: !modelData.hasFreedom + icon.name: "help-contextual" + onClicked: properietarySoftwareRiskExplanationDialog.open(); + + ToolTip { + text: i18n("What does this mean?") + } + } + } + } + + // "See More licenses" link, in case there are a lot of them + Kirigami.LinkButton { + Layout.fillWidth: true + visible: application.licenses.length > 3 + text: i18np("See more…", "See more…", appInfo.application.licenses.length) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + elide: Text.ElideRight + onClicked: allLicensesSheet.open(); + } + + } + } + // Content Rating + ColumnLayout { + width: metadataLayout.itemWidth + visible: appInfo.application.contentRatingText.length > 0 + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + opacity: 0.7 + text: i18n("Content Rating") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + maximumLineCount: 2 + } + + Label { + Layout.fillWidth: true + visible: text.length > 0 + text: appInfo.application.contentRatingMinimumAge === 0 ? "" : i18n("Age: %1+", appInfo.application.contentRatingMinimumAge) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + } + + Label { + Layout.fillWidth: true + text: appInfo.application.contentRatingText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + maximumLineCount: 3 + elide: Text.ElideRight + + readonly property var colors: [ Kirigami.Theme.textColor, Kirigami.Theme.neutralTextColor ] + color: colors[appInfo.application.contentRatingIntensity] + } + + Kirigami.LinkButton { + Layout.fillWidth: true + visible: appInfo.application.contentRatingDescription.length > 0 + text: i18nc("@action", "See details…") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + elide: Text.ElideRight + onClicked: contentRatingDialog.open(); + } + } + } + } + + Kirigami.Separator { + anchors { + left: parent.left + right: parent.right + top: parent.bottom + } + } + } + + + Repeater { + Layout.bottomMargin: appInfo.internalSpacings * 2 + + model: application.topObjects + delegate: Loader { + property QtObject resource: appInfo.application + + Layout.fillWidth: item.Layout.fillWidth + Layout.leftMargin: appInfo.pageContentMargins + Layout.rightMargin: appInfo.pageContentMargins + + source: modelData + } + } + + // Layout for textual content; this isn't in the main ColumnLayout + // because we want it to be bounded to a maximum width + ColumnLayout { + id: textualContentLayout + + Layout.fillWidth: true + Layout.maximumWidth: Math.max(Math.round(appInfo.width * 0.75), Kirigami.Units.gridUnit * 35) + Layout.margins: appInfo.pageContentMargins + Layout.alignment: Qt.AlignHCenter + + spacing: appInfo.internalSpacings + + // Short description + // Not using Kirigami.Heading here because that component doesn't + // support selectable text, and we want this to be selectable because + // it's also used to show the path for local packages, and that makes + // sense to be selectable + Kirigami.SelectableLabel { + Layout.fillWidth: true + // Not relevant to the offline upgrade use case because we + // display the info in the header instead + visible: !appInfo.isOfflineUpgrade + text: appInfo.application.comment + wrapMode: Text.Wrap + + // Match `level: 2` in Kirigami.Heading + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.2 + // Match `type: Kirigami.Heading.Type.Primary` in Kirigami.Heading + font.weight: Font.DemiBold + + Accessible.role: Accessible.Heading + } + + // Long app description + Kirigami.SelectableLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: appInfo.application.longDescription + textFormat: TextEdit.RichText + onLinkActivated: Qt.openUrlExternally(link); + } + + // External resources + GridLayout { + id: externalResourcesLayout + readonly property int visibleButtons: (helpButton.visible ? 1 : 0) + + (homepageButton.visible ? 1: 0) + + (addonsButton.visible ? 1 : 0) + + (shareButton.visible ? 1 : 0) + readonly property int buttonWidth: Math.round(textualContentLayout.width / columns) + readonly property int tallestButtonHeight: Math.max(helpButton.implicitHeight, + homepageButton.implicitHeight, + shareButton.implicitHeight, + addonsButton.implicitHeight) + readonly property int minWidth: Math.max(helpButton.visible ? helpButton.implicitMinWidth : 0, + homepageButton.visible ? homepageButton.implicitMinWidth: 0, + addonsButton.visible ? addonsButton.implicitMinWidth : 0, + shareButton.visible ? shareButton.implicitMinWidth : 0) + readonly property bool stackedlayout: minWidth > Math.round(textualContentLayout.width / visibleButtons) - + (columnSpacing * (visibleButtons + 1)) + + Layout.fillWidth: true + Layout.bottomMargin: appInfo.internalSpacings * 2 + + + rows: stackedlayout ? visibleButtons : 1 + columns: stackedlayout ? 1: visibleButtons + rowSpacing: Kirigami.Units.smallSpacing + columnSpacing: Kirigami.Units.smallSpacing + + visible: visibleButtons > 0 + + ApplicationResourceButton { + id: helpButton + + Layout.fillWidth: true + Layout.maximumWidth: externalResourcesLayout.buttonWidth + Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight + + visible: application.helpURL != "" + + buttonIcon: "documentation" + title: i18n("Documentation") + subtitle: i18n("Read the project's official documentation") + tooltipText: application.helpURL + + onClicked: Qt.openUrlExternally(application.helpURL); + } + + ApplicationResourceButton { + id: homepageButton + + Layout.fillWidth: true + Layout.maximumWidth: externalResourcesLayout.buttonWidth + Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight + + visible: application.homepage != "" + + buttonIcon: "internet-services" + title: i18n("Website") + subtitle: i18n("Visit the project's website") + tooltipText: application.homepage + + onClicked: Qt.openUrlExternally(application.homepage); + } + + ApplicationResourceButton { + id: addonsButton + + Layout.fillWidth: true + Layout.maximumWidth: externalResourcesLayout.buttonWidth + Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight + + visible: addonsView.containsAddons + + buttonIcon: "extension-symbolic" + title: i18n("Addons") + subtitle: i18n("Install or remove additional functionality") + + onClicked: { + if (addonsView.addonsCount === 0) { + Navigation.openExtends(application.appstreamId, appInfo.application.name) + } else { + addonsView.sheetOpen = true + } + } + } + + ApplicationResourceButton { + id: shareButton + + Layout.fillWidth: true + Layout.maximumWidth: externalResourcesLayout.buttonWidth + Layout.minimumHeight: externalResourcesLayout.tallestButtonHeight + + buttonIcon: "document-share" + title: i18nc("Exports the application's URL to an external service", "Share") + subtitle: i18n("Send a link for this application") + tooltipText: application.url.toString() + visible: tooltipText.length > 0 && !appInfo.isOfflineUpgrade + + Kirigami.PromptDialog { + id: shareSheet + parent: applicationWindow().overlay + title: shareButton.title + standardButtons: Dialog.NoButton + + Purpose.AlternativesView { + id: alts + implicitWidth: Kirigami.Units.gridUnit + pluginType: "ShareUrl" + inputData: { + "urls": [ application.url.toString() ], + "title": i18nc("The subject line for an email. %1 is the name of an application", "Check out the %1 app!", application.name) + } + onFinished: { + shareSheet.close() + if (error !== 0) { + console.error("job finished with error", error, message) + } + alts.reset() + } + } + } + + onClicked: { + shareSheet.open(); + } + } + } + + Kirigami.Heading { + visible: changelogLabel.visible + text: i18n("What's New") + level: 2 + type: Kirigami.Heading.Type.Primary + wrapMode: Text.Wrap + } + + // Changelog text + Label { + id: changelogLabel + + Layout.fillWidth: true + Layout.bottomMargin: appInfo.internalSpacings * 2 + + // Some backends are known to produce empty line break as a text + visible: text !== "" && text !== "
" + wrapMode: Text.WordWrap + + Component.onCompleted: appInfo.application.fetchChangelog() + Connections { + target: appInfo.application + function onChangelogFetched(changelog) { + changelogLabel.text = changelog + } + } + } + + Kirigami.LoadingPlaceholder { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: Kirigami.Units.gridUnit * 15 + Layout.bottomMargin: appInfo.internalSpacings * 2 + visible: reviewsModel.fetching && !reviewsError.visible + text: i18n("Loading reviews for %1", appInfo.application.name) + } + + Kirigami.Heading { + Layout.fillWidth: true + visible: rep.count > 0 || reviewsError.visible + text: i18n("Reviews") + level: 2 + type: Kirigami.Heading.Type.Primary + wrapMode: Text.Wrap + } + + Kirigami.InlineMessage { + id: reviewsError + type: Kirigami.MessageType.Warning + Layout.fillWidth: true + visible: reviewsModel.backend && text.length > 0 + text: reviewsModel.backend ? reviewsModel.backend.errorMessage : "" + } + + // Top three reviews + Repeater { + id: rep + model: PaginateModel { + sourceModel: reviewsSheet.model + pageSize: visibleReviews + } + delegate: ReviewDelegate { + Layout.fillWidth: true + separator: false + compact: true + } + } + + // Review-related buttons + Flow { + Layout.fillWidth: true + Layout.bottomMargin: appInfo.internalSpacings *2 + + spacing: appInfo.internalSpacings + + Button { + visible: reviewsModel.count > visibleReviews + + text: i18np("Show all %1 Reviews", "Show all %1 Reviews", reviewsModel.count) + icon.name: "view-visible" + + onClicked: { + reviewsSheet.open() + } + } + + Button { + visible: appbutton.isStateAvailable && reviewsModel.backend && !reviewsError.visible && reviewsModel.backend.isResourceSupported(appInfo.application) + enabled: appInfo.application.isInstalled + + text: appInfo.application.isInstalled ? i18n("Write a Review") : i18n("Install to Write a Review") + icon.name: "document-edit" + + onClicked: { + reviewsSheet.openReviewDialog() + } + } + } + + // "Get Involved" section + Kirigami.Heading { + visible: getInvolvedLayout.visible + text: i18n("Get Involved") + level: 2 + type: Kirigami.Heading.Type.Primary + wrapMode: Text.Wrap + } + + GridLayout { + id: getInvolvedLayout + + readonly property int visibleButtons: (donateButton.visible ? 1 : 0) + + (bugButton.visible ? 1 : 0) + + (contributeButton.visible ? 1 : 0) + readonly property int buttonWidth: Math.round(textualContentLayout.width / columns) + readonly property int tallestButtonHeight: Math.max(donateButton.implicitHeight, + bugButton.implicitHeight, + contributeButton.implicitHeight) + readonly property int minWidth: Math.max(donateButton.visible ? donateButton.implicitMinWidth : 0, + bugButton.visible ? bugButton.implicitMinWidth: 0, + contributeButton.visible ? contributeButton.implicitMinWidth : 0) + readonly property bool stackedlayout: minWidth > Math.round(textualContentLayout.width / visibleButtons) - + (columnSpacing * (visibleButtons + 1)) + + Layout.fillWidth: true + Layout.bottomMargin: appInfo.internalSpacings * 2 + + rows: stackedlayout ? visibleButtons : 1 + columns: stackedlayout ? 1: visibleButtons + rowSpacing: Kirigami.Units.smallSpacing + columnSpacing: Kirigami.Units.smallSpacing + + visible: visibleButtons > 0 + + ApplicationResourceButton { + id: donateButton + + Layout.fillWidth: true + Layout.maximumWidth: getInvolvedLayout.buttonWidth + Layout.minimumHeight: getInvolvedLayout.tallestButtonHeight + + visible: application.donationURL != "" + + buttonIcon: "help-donate" + title: i18n("Donate") + subtitle: i18n("Support and thank the developers by donating to their project") + tooltipText: application.donationURL + + onClicked: Qt.openUrlExternally(application.donationURL); + } + + ApplicationResourceButton { + id: bugButton + + Layout.fillWidth: true + Layout.maximumWidth: getInvolvedLayout.buttonWidth + Layout.minimumHeight: getInvolvedLayout.tallestButtonHeight + + visible: application.bugURL != "" + + buttonIcon: "tools-report-bug" + title: i18n("Report Bug") + subtitle: i18n("Log an issue you found to help get it fixed") + tooltipText: application.bugURL + + onClicked: Qt.openUrlExternally(application.bugURL); + } + + ApplicationResourceButton { + id: contributeButton + + Layout.fillWidth: true + Layout.maximumWidth: getInvolvedLayout.buttonWidth + Layout.minimumHeight: getInvolvedLayout.tallestButtonHeight + + visible: application.contributeURL != "" + title: i18n("Contribute") + subtitle: i18n("Help the developers by coding, designing, testing, or translating") + tooltipText: application.contributeURL + buttonIcon: "project-development" + onClicked: Qt.openUrlExternally(application.contributeURL); + } + } + + Repeater { + model: application.objects + delegate: Loader { + property QtObject resource: appInfo.application + source: modelData + Layout.fillWidth: true + } + } + } + } + + readonly property var addons: AddonsView { + id: addonsView + application: appInfo.application + } + + Kirigami.Dialog { + id: allLicensesSheet + title: i18n("All Licenses") + standardButtons: Kirigami.Dialog.NoButton + preferredWidth: Kirigami.Units.gridUnit * 16 + maximumHeight: Kirigami.Units.gridUnit * 20 + + ColumnLayout { + spacing: 0 + + Repeater { + id: listview + + model: appInfo.application.licenses + + delegate: Kirigami.BasicListItem { + activeBackgroundColor: "transparent" + activeTextColor: Kirigami.Theme.textColor + separatorVisible: false + contentItem: Kirigami.UrlButton { + enabled: url !== "" + text: modelData.name + url: modelData.url + horizontalAlignment: Text.AlignLeft + color: !modelData.hasFreedom ? Kirigami.Theme.neutralTextColor: enabled ? Kirigami.Theme.linkColor : Kirigami.Theme.textColor + } + } + } + } + } + + Kirigami.PromptDialog { + id: contentRatingDialog + title: i18n("Content Rating") + preferredWidth: Kirigami.Units.gridUnit * 25 + standardButtons: Kirigami.Dialog.NoButton + + Label { + text: appInfo.application.contentRatingDescription + textFormat: Text.MarkdownText + wrapMode: Text.Wrap + } + } + + Kirigami.PromptDialog { + id: properietarySoftwareRiskExplanationDialog + preferredWidth: Kirigami.Units.gridUnit * 25 + standardButtons: Kirigami.Dialog.NoButton + + title: i18n("Risks of proprietary software") + + TextEdit { + readonly property string proprietarySoftwareExplanationPage: "https://www.gnu.org/proprietary" + + text: homepageButton.visible ? + xi18nc("@info", "This application's source code is partially or entirely closed to public inspection and improvement. That means third parties and users like you cannot verify its operation, security, and trustworthiness, or modify and redistribute it without the authors' express permission.The application may be perfectly safe to use, or it may be acting against you in various ways—such as harvesting your personal information, tracking your location, or transmitting the contents of your files to someone else. There is no easy way to be sure, so you should only install this application if you fully trust its authors (%2).You can learn more at %3.", application.homepage, author.text, proprietarySoftwareExplanationPage) : + xi18nc("@info", "This application's source code is partially or entirely closed to public inspection and improvement. That means third parties and users like you cannot verify its operation, security, and trustworthiness, or modify and redistribute it without the authors' express permission.The application may be perfectly safe to use, or it may be acting against you in various ways—such as harvesting your personal information, tracking your location, or transmitting the contents of your files to someone else. There is no easy way to be sure, so you should only install this application if you fully trust its authors (%1).You can learn more at %2.", author.text, proprietarySoftwareExplanationPage) + wrapMode: Text.Wrap + textFormat: TextEdit.RichText + readOnly: true + + color: Kirigami.Theme.textColor + selectedTextColor: Kirigami.Theme.highlightedTextColor + selectionColor: Kirigami.Theme.highlightColor + + onLinkActivated: (url) => Qt.openUrlExternally(url) + + HoverHandler { + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } +} diff --git a/discover/qml/ApplicationResourceButton.qml b/discover/qml/ApplicationResourceButton.qml new file mode 100644 index 0000000..e124237 --- /dev/null +++ b/discover/qml/ApplicationResourceButton.qml @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2022 Nate Graham + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.19 as Kirigami + +QQC2.Button { + id: root + + required property string buttonIcon + required property string title + required property string subtitle + property string tooltipText + readonly property int implicitMinWidth: leftPadding + + layout.spacing + + text.width + + layout.spacing + + icon.Layout.preferredWidth + + layout.spacing + + rightPadding + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + + TextMetrics { + id: text + text: root.title + } + + contentItem: ColumnLayout { + spacing: 0 + RowLayout { + id: layout + spacing: Kirigami.Units.smallSpacing + Layout.minimumHeight: Kirigami.Units.gridUnit * 2 + spacing + + // Icon + Kirigami.Icon { + id: icon + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium + Layout.alignment: Qt.AlignVCenter + source: root.buttonIcon + } + + // Title + Kirigami.Heading { + Layout.fillWidth: true + level: 4 + text: root.title + verticalAlignment: Text.AlignVCenter + maximumLineCount: 2 + elide: Text.ElideRight + wrapMode: Text.Wrap + } + } + + // Subtitle + QQC2.Label { + Layout.fillWidth: true + Layout.fillHeight: true + visible: root.subtitle + text: root.subtitle + elide: Text.ElideRight + wrapMode: Text.Wrap + opacity: 0.6 + verticalAlignment: Text.AlignTop + } + } + QQC2.ToolTip { + text: root.tooltipText ? root.tooltipText : "" + } +} diff --git a/discover/qml/ApplicationScreenshots.qml b/discover/qml/ApplicationScreenshots.qml new file mode 100644 index 0000000..9f73154 --- /dev/null +++ b/discover/qml/ApplicationScreenshots.qml @@ -0,0 +1,234 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2020 Carl Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import org.kde.discover 2.0 +import org.kde.kirigami 2.19 as Kirigami + +ListView { + id: root + readonly property alias count: screenshotsModel.count + property bool showNavigationArrows: true + property alias resource: screenshotsModel.application + property var resource + property int failedCount: 0 + readonly property bool hasFailed: count !== 0 && failedCount === count + + spacing: Kirigami.Units.largeSpacing + focus: overlay.visible + orientation: Qt.Horizontal + cacheBuffer: 10 // keep some screenshots in memory + + Keys.onLeftPressed: if (leftAction.visible) leftAction.trigger() + Keys.onRightPressed: if (rightAction.visible) rightAction.trigger() + + model: ScreenshotsModel { + id: screenshotsModel + } + + property real delegateHeight: Kirigami.Units.gridUnit * 4 + + delegate: AbstractButton { + readonly property bool animated: isAnimated + readonly property url imageSource: large_image_url + readonly property real proportion: thumbnail.status === Image.Ready && thumbnail.sourceSize.width>1 ? thumbnail.sourceSize.height/thumbnail.sourceSize.width : 1 + + implicitWidth: root.delegateHeight / proportion + implicitHeight: root.delegateHeight + opacity: hovered ? 0.7 : 1 + + hoverEnabled: true + onClicked: { + root.currentIndex = model.row + overlay.open() + } + + HoverHandler { + cursorShape: Qt.PointingHandCursor + } + + background: Item { + BusyIndicator { + visible: running + running: thumbnail.status === Image.Loading + anchors.centerIn: parent + } + Kirigami.Icon { + anchors.fill: parent + anchors.margins: Kirigami.Units.gridUnit + visible: thumbnail.status === Image.Error + source: "emblem-error" + } + ConditionalLoader { + id: thumbnail + anchors.fill: parent + readonly property var status: item.status + readonly property var sourceSize: item.sourceSize + condition: isAnimated + + componentFalse: Component { + Image { + source: small_image_url + } + } + componentTrue: Component { + AnimatedImage { + source: small_image_url + } + } + + onStatusChanged: { + if (status === Image.Error) { + root.failedCount += 1; + } + } + } + } + } + + Popup { + id: overlay + parent: applicationWindow().overlay + z: applicationWindow().globalDrawer.z + 10 + modal: true + clip: false + + x: (parent.width - width)/2 + y: (parent.height - height)/2 + readonly property real proportion: overlayImage.sourceSize.width>1 ? overlayImage.sourceSize.height/overlayImage.sourceSize.width : 1 + height: overlayImage.status >= Image.Loading ? Kirigami.Units.gridUnit * 5 : Math.min(parent.height * 0.9, (parent.width * 0.9) * proportion, overlayImage.sourceSize.height) + width: (height - 2 * padding)/proportion + + BusyIndicator { + id: indicator + visible: running + running: overlayImage.status === Image.Loading + anchors.centerIn: parent + } + + Kirigami.Icon { + anchors.fill: parent + visible: overlayImage.status === Image.Error + source: "emblem-error" + } + + ConditionalLoader { + id: overlayImage + anchors.fill: parent + readonly property var status: item.status + readonly property var sourceSize: item.sourceSize + condition: root.currentItem.animated + + componentFalse: Component { + Image { + source: root.currentItem ? root.currentItem.imageSource : "" + fillMode: Image.PreserveAspectFit + smooth: true + } + } + componentTrue: Component { + AnimatedImage { + source: root.currentItem ? root.currentItem.imageSource : "" + fillMode: Image.PreserveAspectFit + smooth: true + } + } + + onStatusChanged: { + if (status === Image.Error) { + root.failedCount += 1; + } + } + } + + Button { + anchors { + left: parent.right + bottom: parent.top + } + icon.name: "window-close" + onClicked: overlay.close() + } + + + RoundButton { + anchors { + right: parent.left + verticalCenter: parent.verticalCenter + } + visible: leftAction.visible + icon.name: leftAction.iconName + onClicked: leftAction.triggered(null) + } + + RoundButton { + anchors { + left: parent.right + verticalCenter: parent.verticalCenter + } + visible: rightAction.visible + icon.name: rightAction.iconName + onClicked: rightAction.triggered(null) + } + + Kirigami.Action { + id: leftAction + icon.name: root.LayoutMirroring.enabled ? "arrow-right" : "arrow-left" + enabled: overlay.visible && visible + visible: root.currentIndex >= 1 && !indicator.running + onTriggered: root.currentIndex = (root.currentIndex - 1) % screenshotsModel.count + } + + Kirigami.Action { + id: rightAction + icon.name: root.LayoutMirroring.enabled ? "arrow-left" : "arrow-right" + enabled: overlay.visible && visible + visible: root.currentIndex < (root.count - 1) && !indicator.running + onTriggered: root.currentIndex = (root.currentIndex + 1) % screenshotsModel.count + } + } + + + clip: true + + RoundButton { + anchors { + left: parent.left + leftMargin: Kirigami.Units.largeSpacing + verticalCenter: parent.verticalCenter + } + width: Kirigami.Units.gridUnit * 2 + height: width + icon.name: root.LayoutMirroring.enabled ? "arrow-right" : "arrow-left" + visible: !Kirigami.Settings.isMobile + && root.count > 1 + && root.currentIndex > 0 + && root.showNavigationArrows + Keys.forwardTo: [root] + onClicked: root.currentIndex -= 1 + } + + RoundButton { + anchors { + right: parent.right + rightMargin: Kirigami.Units.largeSpacing + verticalCenter: parent.verticalCenter + } + width: Kirigami.Units.gridUnit * 2 + height: width + icon.name: root.LayoutMirroring.enabled ? "arrow-left" : "arrow-right" + visible: !Kirigami.Settings.isMobile + && root.count > 1 + && root.currentIndex < root.count - 1 + && root.showNavigationArrows + Keys.forwardTo: [root] + onClicked: root.currentIndex += 1 + } +} diff --git a/discover/qml/ApplicationsListPage.qml b/discover/qml/ApplicationsListPage.qml new file mode 100644 index 0000000..e8b7d68 --- /dev/null +++ b/discover/qml/ApplicationsListPage.qml @@ -0,0 +1,252 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.5 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import "navigation.js" as Navigation +import org.kde.discover.app 1.0 +import org.kde.discover 2.0 +import org.kde.kirigami 2.14 as Kirigami + +DiscoverPage { + id: page + readonly property var model: appsModel + property alias category: appsModel.filteredCategory + property alias sortRole: appsModel.sortRole + property alias sortOrder: appsModel.sortOrder + property alias originFilter: appsModel.originFilter + property alias mimeTypeFilter: appsModel.mimeTypeFilter + property alias stateFilter: appsModel.stateFilter + property alias extending: appsModel.extending + property alias search: appsModel.search + property alias resourcesUrl: appsModel.resourcesUrl + property alias isBusy: appsModel.isBusy + property alias allBackends: appsModel.allBackends + property alias count: apps.count + property alias listHeader: apps.header + property alias listHeaderPositioning: apps.headerPositioning + property string sortProperty: "appsListPageSorting" + property bool compact: page.width < Kirigami.Units.gridUnit * 28 || !applicationWindow().wideScreen + property bool showRating: true + property bool showSize: false + property bool searchPage: false + + property bool canNavigate: true + readonly property alias subcategories: appsModel.subcategories + + function stripHtml(input) { + var regex = /(<([^>]+)>)/ig + return input.replace(regex, ""); + } + + property string name: category ? category.name : "" + title: { + const count = appsModel.count; + if (search.length > 0) { + if (count.valid) { + return i18np("Search: %2 - %3 item", "Search: %2 - %3 items", count.number, stripHtml(search), count.string) + } else { + return i18n("Search: %1", stripHtml(search)) + } + } else if (name.length > 0) { + if (count.valid) { + return i18np("%2 - %1 item", "%2 - %1 items", count.number, name) + } else { + return name + } + } else { + if (count.valid) { + return i18np("Search - %1 item", "Search - %1 items", count.number) + } else { + return i18n("Search") + } + } + } + + signal clearSearch() + + supportsRefreshing: true + onRefreshingChanged: if (refreshing) { + appsModel.invalidateFilter() + refreshing = false + } + + ActionGroup { + id: sortGroup + exclusive: true + } + + contextualActions: [ + Kirigami.Action { + visible: !appsModel.sortByRelevancy + text: i18n("Sort: %1", sortGroup.checkedAction.text) + Action { + ActionGroup.group: sortGroup + text: i18n("Name") + onTriggered: { + DiscoverSettings[page.sortProperty] = ResourcesProxyModel.NameRole + } + checkable: true + checked: appsModel.sortRole === ResourcesProxyModel.NameRole + } + Action { + ActionGroup.group: sortGroup + text: i18n("Rating") + onTriggered: { + DiscoverSettings[page.sortProperty] = ResourcesProxyModel.SortableRatingRole + } + checkable: true + checked: appsModel.sortRole === ResourcesProxyModel.SortableRatingRole + } + Action { + ActionGroup.group: sortGroup + text: i18n("Size") + onTriggered: { + DiscoverSettings[page.sortProperty] = ResourcesProxyModel.SizeRole + } + checkable: true + checked: appsModel.sortRole === ResourcesProxyModel.SizeRole + } + Action { + ActionGroup.group: sortGroup + text: i18n("Release Date") + onTriggered: { + DiscoverSettings[page.sortProperty] = ResourcesProxyModel.ReleaseDateRole + } + checkable: true + checked: appsModel.sortRole === ResourcesProxyModel.ReleaseDateRole + } + } + ] + + Kirigami.CardsListView { + id: apps + activeFocusOnTab: true + currentIndex: -1 + onActiveFocusChanged: if (activeFocus && currentIndex === -1) { + currentIndex = 0; + } + + section.delegate: Label { + text: section + anchors { + right: parent.right + } + } + + model: ResourcesProxyModel { + id: appsModel + sortRole: DiscoverSettings.appsListPageSorting + sortOrder: sortRole === ResourcesProxyModel.NameRole ? Qt.AscendingOrder : Qt.DescendingOrder + + onBusyChanged: if (isBusy) { + apps.currentIndex = -1 + } + } + delegate: ApplicationDelegate { + application: model.application + compact: !applicationWindow().wideScreen + showRating: page.showRating + showSize: page.showSize + } + + Item { + readonly property bool nothingFound: apps.count == 0 && !appsModel.isBusy && !ResourcesModel.isInitializing && (!page.searchPage || appsModel.search.length > 0) + + anchors.fill: parent + opacity: nothingFound ? 1 : 0 + visible: opacity > 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } } + + Kirigami.PlaceholderMessage { + visible: !searchedForThingNotFound.visible + anchors.centerIn: visible ? parent : undefined + width: parent.width - (Kirigami.Units.largeSpacing * 8) + + icon.name: "edit-none" + text: i18n("Nothing found") + } + + Kirigami.PlaceholderMessage { + id: searchedForThingNotFound + + property var searchAllCategoriesAction: Kirigami.Action { + text: i18nc("@action:button", "Search in All Categories") + icon.name: "search" + onTriggered: { + window.globalDrawer.resetMenu(); + Navigation.clearStack() + Navigation.openApplicationList( { search: page.search } ); + } + } + property var searchTheWebAction: Kirigami.Action { + text: i18nc("@action:button %1 is the name of an application", "Search the Web for \"%1\"", appsModel.search) + icon.name: "internet-web-browser" + onTriggered: { + const searchTerm = encodeURIComponent("Linux " + appsModel.search); + Qt.openUrlExternally(i18nc("If appropriate, localize this URL to be something more relevant to the language. %1 is the text that will be searched for.", "https://duckduckgo.com/?q=%1", searchTerm)); + } + } + + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 8) + + visible: appsModel.search.length > 0 && stateFilter !== AbstractResource.Installed + + icon.name: "edit-none" + text: page.category ? i18nc("@info:placeholder %1 is the name of an application; %2 is the name of a category of apps or add-ons", + "\"%1\" was not found in the \"%2\" category", appsModel.search, page.category.name) + : i18nc("@info:placeholder %1 is the name of an application", + "\"%1\" was not found in the available sources", appsModel.search) + explanation: page.category ? "" : i18nc("@info:placeholder%1 is the name of an application", "\"%1\" may be available on the web. Software acquired from the web has not been reviewed by your distributor for functionality or stability. Use with caution.", appsModel.search) + + // If we're in a category, first direct the user to search globally, + // because they might not have realized they were in a category and + // therefore the results were limited to just what was in the category + helpfulAction: page.category ? searchAllCategoriesAction : searchTheWebAction + } + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + + visible: opacity !== 0 + opacity: apps.count == 0 && page.searchPage && appsModel.search.length == 0 ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad } } + + icon.name: "search" + text: i18n("Search") + } + + footer: ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + visible: appsModel.isBusy && apps.atYEnd + opacity: visible ? 0.5 : 0 + height: visible ? Layout.preferredHeight : 0 + + Item { + Layout.preferredHeight: Kirigami.Units.gridUnit + } + Kirigami.Heading { + level: 2 + Layout.alignment: Qt.AlignCenter + text: i18n("Still looking…") + } + BusyIndicator { + running: parent.visible + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: Kirigami.Units.gridUnit * 4 + Layout.preferredHeight: Kirigami.Units.gridUnit * 4 + } + Behavior on opacity { + PropertyAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } + } + } + } +} diff --git a/discover/qml/BrowsingPage.qml b/discover/qml/BrowsingPage.qml new file mode 100644 index 0000000..6c6a14f --- /dev/null +++ b/discover/qml/BrowsingPage.qml @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2021 Carl Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.1 +import QtQml.Models 2.15 +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import "navigation.js" as Navigation +import org.kde.kirigami 2.19 as Kirigami + +DiscoverPage +{ + id: page + title: i18n("Discover") + objectName: "featured" + + actions.main: window.wideScreen ? searchAction : null + + header: Item { + height: message.height + message.anchors.margins + + DiscoverInlineMessage { + id: message + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: Kirigami.Units.smallSpacing + } + + inlineMessage: ResourcesModel.inlineMessage + } + } + + readonly property bool isHome: true + + function searchFor(text) { + if (text.length === 0) + return; + Navigation.openCategory(null, "") + } + + Kirigami.LoadingPlaceholder { + visible: featuredModel.isFetching + anchors.centerIn: parent + } + + Loader { + active: featuredModel.count === 0 && !featuredModel.isFetching + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + sourceComponent: Kirigami.PlaceholderMessage { + readonly property var helpfulError: featuredModel.currentApplicationBackend.explainDysfunction() + icon.name: helpfulError.iconName + text: i18n("Unable to load applications") + explanation: helpfulError.message + + Repeater { + model: helpfulError.actions + delegate: Button { + Layout.alignment: Qt.AlignHCenter + action: ConvertDiscoverAction { + action: modelData + } + } + } + } + } + + signal clearSearch() + + readonly property bool compact: page.width < 550 || !applicationWindow().wideScreen + + footer: ColumnLayout { + spacing: 0 + + Kirigami.Separator { + Layout.fillWidth: true + visible: Kirigami.Settings.isMobile && inlineMessage.visible + } + } + + Kirigami.CardsLayout { + id: apps + maximumColumns: 4 + rowSpacing: Kirigami.Units.largeSpacing + columnSpacing: Kirigami.Units.largeSpacing + + maximumColumnWidth: Kirigami.Units.gridUnit * 6 + Layout.preferredWidth: Math.max(maximumColumnWidth, Math.min((width / columns) - columnSpacing)) + + Kirigami.Heading { + Layout.columnSpan: apps.columns + text: i18nc("@title:group", "Most Popular") + visible: popRep.count > 0 && !featuredModel.isFetching + } + + Repeater { + id: popRep + model: PaginateModel { + pageSize: apps.maximumColumns * 2 + sourceModel: OdrsAppsModel { + // filter: FOSS + } + } + delegate: GridApplicationDelegate { visible: !featuredModel.isFetching } + } + + Kirigami.Heading { + Layout.topMargin: Kirigami.Units.largeSpacing * 5 + Layout.columnSpan: apps.columns + text: i18nc("@title:group", "Editor's Choice") + visible: !featuredModel.isFetching + } + + Repeater { + model: FeaturedModel { + id: featuredModel + } + delegate: GridApplicationDelegate { visible: !featuredModel.isFetching } + } + + Kirigami.Heading { + Layout.topMargin: Kirigami.Units.largeSpacing * 5 + Layout.columnSpan: apps.columns + text: i18nc("@title:group", "Highest-Rated Games") + visible: gamesRep.count > 0 && !featuredModel.isFetching + } + + Repeater { + id: gamesRep + model: PaginateModel { + pageSize: apps.maximumColumns + sourceModel: ResourcesProxyModel { + filteredCategoryName: "Games" + backendFilter: ResourcesModel.currentApplicationBackend + sortRole: ResourcesProxyModel.SortableRatingRole + sortOrder: Qt.DescendingOrder + } + } + delegate: GridApplicationDelegate { visible: !featuredModel.isFetching } + } + + Button { + text: i18nc("@action:button", "See More") + icon.name: "go-next-view" + Layout.columnSpan: apps.columns + onClicked: Navigation.openCategory(CategoryModel.findCategoryByName("Games")) + visible: gamesRep.count > 0 && !featuredModel.isFetching + } + + Kirigami.Heading { + Layout.topMargin: Kirigami.Units.largeSpacing * 5 + Layout.columnSpan: apps.columns + text: i18nc("@title:group", "Highest-Rated Developer Tools") + visible: devRep.count > 0 && !featuredModel.isFetching + } + + Repeater { + id: devRep + model: PaginateModel { + pageSize: apps.maximumColumns + sourceModel: ResourcesProxyModel { + filteredCategoryName: "Developer Tools" + backendFilter: ResourcesModel.currentApplicationBackend + sortRole: ResourcesProxyModel.SortableRatingRole + sortOrder: Qt.DescendingOrder + } + } + delegate: GridApplicationDelegate { visible: !featuredModel.isFetching } + } + + Button { + text: i18nc("@action:button", "See More") + icon.name: "go-next-view" + Layout.columnSpan: apps.columns + onClicked: Navigation.openCategory(CategoryModel.findCategoryByName("Developer Tools")) + visible: devRep.count > 0 && !featuredModel.isFetching + } + } +} diff --git a/discover/qml/ConditionalLoader.qml b/discover/qml/ConditionalLoader.qml new file mode 100644 index 0000000..9460968 --- /dev/null +++ b/discover/qml/ConditionalLoader.qml @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +Loader +{ + id: root + + property Component componentTrue + property Component componentFalse + property bool condition + + Layout.minimumHeight: item && item.Layout ? item.Layout.minimumHeight : 0 + Layout.minimumWidth: item && item.Layout ? item.Layout.minimumWidth : 0 + sourceComponent: condition ? componentTrue : componentFalse +} diff --git a/discover/qml/ConditionalObject.qml b/discover/qml/ConditionalObject.qml new file mode 100644 index 0000000..5aba630 --- /dev/null +++ b/discover/qml/ConditionalObject.qml @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.1 + +QtObject +{ + id: root + + property Component componentTrue + property Component componentFalse + property bool condition + + onConditionChanged: { + if (object) + object.destroy(100) + + var component = (condition ? componentTrue : componentFalse) + object = component ? component.createObject(root) : null + } + + property QtObject object +} diff --git a/discover/qml/ConvertDiscoverAction.qml b/discover/qml/ConvertDiscoverAction.qml new file mode 100644 index 0000000..af5c090 --- /dev/null +++ b/discover/qml/ConvertDiscoverAction.qml @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQml 2.15 +import org.kde.kirigami 2.15 as Kirigami + +/* + * Converts a DiscoverAction into a Kirigami.Action so we can use DiscoverActions + * with QQC2 components + */ +Kirigami.Action { + property QtObject action + icon.name: action.iconName + text: action.text + tooltip: action.toolTip + visible: action.visible + onTriggered: action.trigger() +} diff --git a/discover/qml/DiscoverDrawer.qml b/discover/qml/DiscoverDrawer.qml new file mode 100644 index 0000000..11de537 --- /dev/null +++ b/discover/qml/DiscoverDrawer.qml @@ -0,0 +1,201 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.5 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.1 +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import org.kde.kirigami 2.19 as Kirigami +import "navigation.js" as Navigation + +Kirigami.GlobalDrawer { + id: drawer + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + // FIXME: Dirty workaround for 385992 + width: Kirigami.Units.gridUnit * 14 + + property bool wideScreen: false + readonly property real minimumHeight: header.implicitHeight + content.height + footer.implicitHeight + + resetMenuOnTriggered: false + + property string currentSearchText + + onCurrentSubMenuChanged: { + if (currentSubMenu) + currentSubMenu.trigger() + else if (currentSearchText.length > 0) + window.leftPage.category = null + else + Navigation.openHome() + } + + function suggestSearchText(text) { + if (searchField.visible) { + searchField.text = text + forceSearchFieldFocus() + } + } + + function forceSearchFieldFocus() { + if (searchField.visible && wideScreen) { + searchField.forceActiveFocus(); + } + } + + header: Kirigami.AbstractApplicationHeader { + visible: drawer.wideScreen + + contentItem: SearchField { + id: searchField + + anchors { + left: parent.left + leftMargin: Kirigami.Units.smallSpacing + right: parent.right + rightMargin: Kirigami.Units.smallSpacing + } + + // Give the search field keyboard focus by default, unless it would + // make the virtual keyboard appear, because we don't want that + focus: !Kirigami.InputMethod.willShowOnActive + + visible: window.leftPage && (window.leftPage.searchFor !== null || window.leftPage.hasOwnProperty("search")) + + page: window.leftPage + + onCurrentSearchTextChanged: { + var curr = window.leftPage; + + if (pageStack.depth>1) + pageStack.pop() + + if (currentSearchText === "" && window.currentTopLevel === "" && !window.leftPage.category) { + Navigation.openHome() + } else if (!curr.hasOwnProperty("search")) { + if (currentSearchText) { + Navigation.clearStack() + Navigation.openApplicationList( { search: currentSearchText }) + } + } else { + curr.search = currentSearchText; + curr.forceActiveFocus() + } + } + } + } + + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + + Kirigami.Separator { + Layout.fillWidth: true + } + + ProgressView { + separatorVisible: false + } + + ActionListItem { + action: featuredAction + } + ActionListItem { + action: searchAction + } + ActionListItem { + action: installedAction + visible: drawer.wideScreen + } + ActionListItem { + action: sourcesAction + } + + ActionListItem { + action: aboutAction + } + + ActionListItem { + objectName: "updateButton" + action: updateAction + visible: drawer.wideScreen + + trailing: Kirigami.Icon { + visible: ResourcesModel.updatesCount > 0 + width: Kirigami.Units.iconSizes.sizeForLabels + height: Kirigami.Units.iconSizes.sizeForLabels + source: "emblem-important" + } + + // Disable down navigation on the last item so we don't escape the + // actual list. + Keys.onDownPressed: event.accepted = true + } + + states: [ + State { + name: "full" + when: drawer.wideScreen + PropertyChanges { target: drawer; drawerOpen: true } + }, + State { + name: "compact" + when: !drawer.wideScreen + PropertyChanges { target: drawer; drawerOpen: false } + } + ] + } + + Component { + id: categoryActionComponent + Kirigami.Action { + property QtObject category + readonly property bool itsMe: window.leftPage && window.leftPage.hasOwnProperty("category") && (window.leftPage.category === category) + text: category ? category.name : "" + iconName: category ? category.icon : "" + checked: itsMe + visible: (!window.leftPage + || !window.leftPage.subcategories + || window.leftPage.subcategories === undefined + || currentSearchText.length === 0 + || (category && category.contains(window.leftPage.subcategories)) + ) + onTriggered: { + if (!window.leftPage.canNavigate) + Navigation.openCategory(category, currentSearchText) + else { + if (pageStack.depth>1) + pageStack.pop() + pageStack.currentIndex = 0 + window.leftPage.category = category + } + + if (!drawer.wideScreen && category.subcategories.length === 0) { + drawer.close(); + } + } + } + } + + function createCategoryActions(categories) { + return categories.map((cat) => { + return categoryActionComponent.createObject(drawer, { + category: cat, + children: createCategoryActions(cat.subcategories) + }); + }); + } + + actions: createCategoryActions(CategoryModel.rootCategories) + + modal: !drawer.wideScreen +} diff --git a/discover/qml/DiscoverInlineMessage.qml b/discover/qml/DiscoverInlineMessage.qml new file mode 100644 index 0000000..abb35fc --- /dev/null +++ b/discover/qml/DiscoverInlineMessage.qml @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import org.kde.kirigami 2.19 as Kirigami + +Loader +{ + id: root + property QtObject inlineMessage + + active: inlineMessage + sourceComponent: Kirigami.InlineMessage { + text: root.inlineMessage.message + type: root.inlineMessage.type + icon.name: root.inlineMessage.iconName + + Component { + id: comp + ConvertDiscoverAction {} + } + actions: root.inlineMessage.actions.map((discoverAction) => comp.createObject(this, {action: discoverAction}) ) + visible: true + } +} diff --git a/discover/qml/DiscoverPage.qml b/discover/qml/DiscoverPage.qml new file mode 100644 index 0000000..ddaff6f --- /dev/null +++ b/discover/qml/DiscoverPage.qml @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.ScrollablePage +{ + id: root + + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + + readonly property var s1: Shortcut { + sequences: [ StandardKey.MoveToNextPage ] + enabled: root.isCurrentPage + onActivated: { + root.flickable.contentY = Math.min(root.flickable.contentHeight - root.flickable.height, + root.flickable.contentY + root.flickable.height); + } + } + + readonly property var s2: Shortcut { + sequences: [ StandardKey.MoveToPreviousPage ] + enabled: root.isCurrentPage + onActivated: { + root.flickable.contentY = Math.max(0, root.flickable.contentY - root.flickable.height); + } + } + + readonly property var sClose: Shortcut { + sequences: [ StandardKey.Cancel ] + enabled: root.isCurrentPage && applicationWindow().pageStack.depth>1 + onActivated: { + applicationWindow().pageStack.pop() + } + } + + readonly property var sRefresh: Shortcut { + sequences: [ StandardKey.Refresh ] + enabled: root.isCurrentPage && root.supportsRefreshing + onActivated: { + if (root.supportsRefreshing) + root.refreshing = true + } + } + + readonly property var readableCharacters: /\w+/ + Keys.onPressed: { + if(event.text.length > 0 && event.modifiers === Qt.NoModifier && event.text.match(readableCharacters)) { + window.globalDrawer.suggestSearchText(event.text) + } + } +} diff --git a/discover/qml/DiscoverWindow.qml b/discover/qml/DiscoverWindow.qml new file mode 100644 index 0000000..8c4aadc --- /dev/null +++ b/discover/qml/DiscoverWindow.qml @@ -0,0 +1,473 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.14 +import QtQml.Models 2.15 +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.kirigami 2.19 as Kirigami +import "navigation.js" as Navigation + +Kirigami.ApplicationWindow +{ + id: window + readonly property string topBrowsingComp: ("qrc:/qml/BrowsingPage.qml") + readonly property string topInstalledComp: ("qrc:/qml/InstalledPage.qml") + readonly property string topSearchComp: ("qrc:/qml/SearchPage.qml") + readonly property string topUpdateComp: ("qrc:/qml/UpdatesPage.qml") + readonly property string topSourcesComp: ("qrc:/qml/SourcesPage.qml") + readonly property string topAboutComp: ("qrc:/qml/AboutPage.qml") + readonly property QtObject stack: window.pageStack + property string currentTopLevel + + objectName: "DiscoverMainWindow" + title: leftPage ? leftPage.title : "" + + width: app.initialGeometry.width>=10 ? app.initialGeometry.width : Kirigami.Units.gridUnit * 52 + height: app.initialGeometry.height>=10 ? app.initialGeometry.height : Math.max(Kirigami.Units.gridUnit * 38, window.globalDrawer.contentHeight) + + visible: true + + minimumWidth: Kirigami.Units.gridUnit * 17 + minimumHeight: Kirigami.Units.gridUnit * 17 + + pageStack.defaultColumnWidth: Math.max(Kirigami.Units.gridUnit * 25, pageStack.width / 4) + pageStack.globalToolBar.style: Kirigami.Settings.isMobile ? Kirigami.ApplicationHeaderStyle.Titles : Kirigami.ApplicationHeaderStyle.Auto + pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex == 0 ? Kirigami.ApplicationHeaderStyle.None : Kirigami.ApplicationHeaderStyle.ShowBackButton + pageStack.globalToolBar.canContainHandles: true // mobile handles in header + + readonly property var leftPage: window.stack.depth>0 ? window.stack.get(0) : null + + Component.onCompleted: { + if (app.isRoot) { + messagesSheet.addMessage(i18n("Running as root is discouraged and unnecessary.")); + } + } + + readonly property string describeSources: feedbackLoader.item ? feedbackLoader.item.describeDataSources : "" + Loader { + id: feedbackLoader + source: "Feedback.qml" + } + + TopLevelPageData { + id: featuredAction + iconName: "go-home" + text: i18n("&Home") + component: topBrowsingComp + objectName: "discover" + } + + TopLevelPageData { + id: searchAction + visible: enabled + enabled: !window.wideScreen + iconName: "search" + text: i18n("&Search") + component: topSearchComp + objectName: "search" + shortcut: StandardKey.Find + } + TopLevelPageData { + id: installedAction + iconName: "view-list-details" + text: i18n("&Installed") + component: topInstalledComp + objectName: "installed" + } + TopLevelPageData { + id: updateAction + iconName: ResourcesModel.updatesCount>0 ? ResourcesModel.hasSecurityUpdates ? "update-high" : "update-low" : "update-none" + text: ResourcesModel.updatesCount<=0 ? (ResourcesModel.isFetching ? i18n("Fetching &updates…") : i18n("&Up to date") ) : i18nc("Update section name", "&Update (%1)", ResourcesModel.updatesCount) + component: topUpdateComp + objectName: "update" + } + TopLevelPageData { + id: aboutAction + iconName: "help-feedback" + text: i18n("&About") + component: topAboutComp + objectName: "about" + shortcut: StandardKey.HelpContents + } + TopLevelPageData { + id: sourcesAction + iconName: "configure" + text: i18n("S&ettings") + component: topSourcesComp + objectName: "sources" + shortcut: StandardKey.Preferences + } + + Kirigami.Action { + id: refreshAction + readonly property QtObject action: ResourcesModel.updateAction + text: action.text + icon.name: "view-refresh" + onTriggered: action.trigger() + enabled: action.enabled + // Don't need to show this action in mobile view since you can pull down + // on the view to refresh, and this is the common and expected behavior + //on that platform + visible: window.wideScreen + tooltip: shortcut.nativeText + + // Need to define an explicit Shortcut object so we can get its text + // using shortcut.nativeText + shortcut: Shortcut { + sequences: [ StandardKey.Refresh ] + onActivated: refreshAction.trigger() + } + } + + Connections { + target: app + function onOpenApplicationInternal(app) { + Navigation.clearStack() + Navigation.openApplication(app) + } + function onListMimeInternal(mime) { + currentTopLevel = topBrowsingComp; + Navigation.openApplicationMime(mime) + } + function onListCategoryInternal(cat) { + currentTopLevel = topBrowsingComp; + Navigation.openCategory(cat, "") + } + + function onOpenSearch(search) { + Navigation.clearStack() + Navigation.openApplicationList({search: search}) + } + + function onOpenErrorPage(errorMessage, errorExplanation, buttonText, buttonIcon, buttonUrl) { + Navigation.clearStack() + console.warn("Error: " + errorMessage + "\n" + errorExplanation + "\n" + "Please visit " + buttonUrl) + window.stack.push(errorPageComponent, { title: i18n("Error"), errorMessage: errorMessage, errorExplanation: errorExplanation, buttonText: buttonText, buttonIcon: buttonIcon, buttonUrl: buttonUrl }) + } + + function onUnableToFind(resid) { + messagesSheet.addMessage(i18n("Unable to find resource: %1", resid)); + Navigation.openHome() + } + } + + Connections { + target: ResourcesModel + + function onPassiveMessage(message) { + messagesSheet.addMessage(message); + } + } + + footer: Loader { + active: !window.wideScreen + visible: active // ensure that no height is used when not loaded + height: item ? item.implicitHeight : 0 + sourceComponent: Kirigami.NavigationTabBar { + actions: [ + featuredAction, + searchAction, + installedAction, + updateAction + ] + Component.onCompleted: { + // Exclusivity is already handled by the actions. This prevents BUG:448460 + tabGroup.exclusive = false + } + } + } + + Component { + id: errorPageComponent + Kirigami.Page { + id: page + property string errorMessage: "" + property string errorExplanation: "" + property string buttonText: "" + property string buttonIcon: "" + property string buttonUrl: "" + readonly property bool isHome: true + function searchFor(text) { + if (text.length === 0) + return; + Navigation.openCategory(null, "") + } + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 8) + visible: page.errorMessage !== "" + icon.name: "emblem-warning" + text: page.errorMessage + explanation: page.errorExplanation + helpfulAction: Kirigami.Action { + icon.name: page.buttonIcon + text: page.buttonText + onTriggered: { + Qt.openUrlExternally(page.buttonUrl) + } + } + } + } + } + + Component { + id: proceedDialog + Kirigami.OverlaySheet { + id: sheet + showCloseButton: false + property QtObject transaction + property alias description: desc.text + property bool acted: false + + // No need to add our own ScrollView since OverlaySheet includes + // one automatically. + // But we do need to put the label into a Layout of some sort so we + // can limit the width of the sheet. + contentItem: ColumnLayout { + Label { + id: desc + + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 20 + + textFormat: Text.StyledText + wrapMode: Text.WordWrap + } + } + + footer: RowLayout { + + Item { Layout.fillWidth : true } + + Button { + text: i18n("Proceed") + icon.name: "dialog-ok" + onClicked: { + transaction.proceed() + sheet.acted = true + sheet.close() + } + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } + + Button { + Layout.alignment: Qt.AlignRight + text: i18n("Cancel") + icon.name: "dialog-cancel" + onClicked: { + transaction.cancel() + sheet.acted = true + sheet.close() + } + Keys.onEscapePressed: clicked() + } + } + + onSheetOpenChanged: if(!sheetOpen) { + sheet.destroy(1000) + if (!sheet.acted) + transaction.cancel() + } + } + } + + Component { + id: distroErrorMessageDialog + Kirigami.OverlaySheet { + id: sheet + property alias message: desc.text + + // No need to add our own ScrollView since OverlaySheet includes + // one automatically. + // But we do need to put the label into a Layout of some sort so we + // can limit the width of the sheet. + contentItem: ColumnLayout { + Label { + id: desc + + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 20 + + textFormat: Text.StyledText + wrapMode: Text.WordWrap + } + + RowLayout { + Item { Layout.fillWidth : true } + Button { + icon.name: "tools-report-bug" + text: i18n("Report this issue") + onClicked: { + Qt.openUrlExternally(ResourcesModel.distroBugReportUrl()) + } + } + } + } + + onSheetOpenChanged: if(!sheetOpen) { + sheet.destroy(1000) + } + } + } + + Kirigami.OverlaySheet { + id: messagesSheet + + property bool copyButtonEnabled: true + + function addMessage(message: string) { + messages.append({message: message}); + app.restore() + } + + title: messages.count > 1 ? i18n("Error %1 of %2", messagesSheetView.currentIndex + 1, messages.count) : i18n("Error") + + // No need to add our own ScrollView since OverlaySheet includes + // one automatically. + // But we do need to put the label into a Layout of some sort so we + // can limit the width of the sheet. + contentItem: ColumnLayout { + Item { + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 20 + } + + StackLayout { + id: messagesSheetView + + Layout.fillWidth: true + Layout.bottomMargin: Kirigami.Units.gridUnit + + Repeater { + model: ListModel { + id: messages + + onCountChanged: { + messagesSheet.sheetOpen = (count > 0); + + if (count > 0 && messagesSheetView.currentIndex === -1) { + messagesSheetView.currentIndex = 0; + } + } + } + + delegate: Label { + Layout.fillWidth: true + + text: model.message + textFormat: Text.StyledText + wrapMode: Text.WordWrap + } + } + } + + RowLayout { + Layout.fillWidth: true + + Button { + text: i18nc("@action:button", "Show Previous") + icon.name: "go-previous" + visible: messages.count > 1 + enabled: visible && messagesSheetView.currentIndex > 0 + + onClicked: { + if (messagesSheetView.currentIndex > 0) { + messagesSheetView.currentIndex--; + } + } + } + + Button { + text: i18nc("@action:button", "Show Next") + icon.name: "go-next" + visible: messages.count > 1 + enabled: visible && messagesSheetView.currentIndex < messages.count - 1 + + onClicked: { + if (messagesSheetView.currentIndex < messages.count) { + messagesSheetView.currentIndex++; + } + } + } + + Item { Layout.fillWidth: true } + + Button { + Layout.alignment: Qt.AlignRight + text: i18n("Copy to Clipboard") + icon.name: "edit-copy" + + onClicked: { + app.copyTextToClipboard(messages.get(messagesSheetView.currentIndex).message); + } + } + } + } + + onSheetOpenChanged: if (!sheetOpen) { + messagesSheetView.currentIndex = -1; + messages.clear(); + } + } + + Instantiator { + model: TransactionModel + + delegate: Connections { + target: model.transaction ? model.transaction : null + + function onProceedRequest(title, description) { + var dialog = proceedDialog.createObject(window, {transaction: transaction, title: title, description: description}) + dialog.open() + app.restore() + } + + function onPassiveMessage(message) { + messagesSheet.addMessage(message); + } + + function onDistroErrorMessage(message, actions) { + var dialog = distroErrorMessageDialog.createObject(window, {transaction: transaction, title: i18n("Error"), message: message}) + dialog.open() + app.restore() + } + function onWebflowStarted(url) { + var component = Qt.createComponent("WebflowDialog.qml"); + if (component.status == Component.Error) { + Qt.openUrlExternally(url); + console.error("Webflow Error", component.errorString()) + } else if (component.status == Component.Ready) { + const sheet = component.createObject(window, {transaction: transaction, url: url }); + sheet.open() + } + component.destroy(); + } + } + } + + PowerManagementInterface { + reason: TransactionModel.mainTransactionText + preventSleep: TransactionModel.count > 0 + } + + contextDrawer: Kirigami.ContextDrawer {} + + globalDrawer: DiscoverDrawer { + wideScreen: window.wideScreen + } + + onCurrentTopLevelChanged: { + window.pageStack.clear() + if (currentTopLevel) + window.pageStack.push(currentTopLevel, {}, window.status!==Component.Ready) + globalDrawer.forceSearchFieldFocus(); + } + + UnityLauncher { + launcherId: "org.kde.discover.desktop" + progressVisible: TransactionModel.count > 0 + progress: TransactionModel.progress + } +} diff --git a/discover/qml/Feedback.qml b/discover/qml/Feedback.qml new file mode 100644 index 0000000..68424cc --- /dev/null +++ b/discover/qml/Feedback.qml @@ -0,0 +1,81 @@ +import org.kde.kirigami 2.14 as Kirigami +import org.kde.userfeedback 1.0 as UserFeedback +import org.kde.kquickcontrolsaddons 2.0 as KQCA +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import QtQml 2.0 + +UserFeedback.Provider +{ + readonly property list actions: [ + Kirigami.Action { + text: i18n("Submit usage information") + tooltip: i18n("Sends anonymized usage information to KDE so we can better understand our users. For more information see https://kde.org/privacypolicy-apps.php.") + displayHint: Kirigami.DisplayHint.AlwaysHide + onTriggered: { + provider.submit() + showPassiveNotification(i18n("Submitting usage information…"), "short", i18n("Configure"), provider.encouraged) + } + }, + Kirigami.Action { + text: i18n("Configure feedback…") + displayHint: Kirigami.DisplayHint.AlwaysHide + onTriggered: { + provider.encouraged() + } + }, + Kirigami.Action { + text: i18n("Configure Updates…") + displayHint: Kirigami.DisplayHint.AlwaysHide + onTriggered: { + KQCA.KCMShell.openSystemSettings("kcm_updates"); + } + } + ] + + id: provider + + submissionInterval: 7 + surveyInterval: -1 + feedbackServer: "https://telemetry.kde.org/" + encouragementInterval: 30 + applicationStartsUntilEncouragement: 1 + applicationUsageTimeUntilEncouragement: 1 + telemetryMode: UserFeedbackSettings.feedbackLevel + + function encouraged() { + KQCA.KCMShell.openSystemSettings("kcm_feedback"); + } + + property var lastSurvey: null + + function openSurvey() { + Qt.openUrlExternally(lastSurvey.url); + surveyCompleted(lastSurvey); + } + + onShowEncouragementMessage: { + showPassiveNotification(i18n("You can help us improving this application by sharing statistics and participate in surveys."), 5000, i18n("Contribute…"), encouraged) + } + + onSurveyAvailable: { + lastSurvey = survey + showPassiveNotification(i18n("We are looking for your feedback!"), 5000, i18n("Participate…"), openSurvey) + } + + UserFeedback.ApplicationVersionSource { mode: UserFeedback.Provider.BasicSystemInformation } + UserFeedback.PlatformInfoSource { mode: UserFeedback.Provider.BasicSystemInformation } + UserFeedback.QtVersionSource { mode: UserFeedback.Provider.BasicSystemInformation } + UserFeedback.StartCountSource { mode: UserFeedback.Provider.BasicUsageStatistics } + UserFeedback.UsageTimeSource { mode: UserFeedback.Provider.BasicUsageStatistics } + UserFeedback.LocaleInfoSource { mode: UserFeedback.Provider.DetailedSystemInformation } + UserFeedback.OpenGLInfoSource{ mode: UserFeedback.Provider.DetailedSystemInformation } + UserFeedback.ScreenInfoSource { mode: UserFeedback.Provider.DetailedSystemInformation } + UserFeedback.PropertySource { + mode: UserFeedback.Provider.DetailedUsageStatistics + name: "Application Source Name" + sourceId: "applicationSourceName" + data: { "value": ResourcesModel.applicationSourceName } + description: "The source for applications" + } +} diff --git a/discover/qml/GridApplicationDelegate.qml b/discover/qml/GridApplicationDelegate.qml new file mode 100644 index 0000000..b85542c --- /dev/null +++ b/discover/qml/GridApplicationDelegate.qml @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2021 Carl Schwan + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +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 + showClickFeedback: true + + topPadding: 0 + bottomPadding: 0 + + contentItem: Item { + implicitHeight: Kirigami.Units.gridUnit * 5 + + RowLayout { + anchors.fill: parent + anchors.margins: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.huge + implicitHeight: Kirigami.Units.iconSizes.huge + source: model.application.icon + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 0 + + Kirigami.Heading { + id: head + level: delegateArea.compact ? 3 : 2 + type: Kirigami.Heading.Type.Primary + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + wrapMode: Text.WordWrap + maximumLineCount: 2 + + text: model.application.name + } + + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + maximumLineCount: head.lineCount === 1 ? 3 : 2 + opacity: 0.6 + wrapMode: Text.WordWrap + + text: model.application.comment + } + } + } + + } + + onClicked: Navigation.openApplication(model.application) +} diff --git a/discover/qml/InstallApplicationButton.qml b/discover/qml/InstallApplicationButton.qml new file mode 100644 index 0000000..48e09de --- /dev/null +++ b/discover/qml/InstallApplicationButton.qml @@ -0,0 +1,89 @@ +import QtQuick 2.1 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import org.kde.discover 2.0 +import org.kde.kirigami 2.14 as Kirigami + +ConditionalLoader +{ + id: root + property Component additionalItem: null + property alias application: listener.resource + + readonly property alias isActive: listener.isActive + readonly property alias progress: listener.progress + readonly property bool isStateAvailable: application.state !== AbstractResource.Broken + readonly property alias listener: listener + + TransactionListener { + id: listener + } + + readonly property Kirigami.Action action: Kirigami.Action { + text: { + if (!root.isStateAvailable) { + return i18nc("State being fetched", "Loading…") + } + if (!application.isInstalled) { + return i18n("Install"); + } + return i18n("Remove"); + } + icon { + name: application.isInstalled ? "edit-delete" : "download" + color: !enabled ? Kirigami.Theme.backgroundColor : !listener.isActive ? (application.isInstalled ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.positiveTextColor) : Kirigami.Theme.backgroundColor + } + visible: !listener.isActive && (!application.isInstalled || application.isRemovable) + enabled: !listener.isActive && root.isStateAvailable + onTriggered: root.click() + } + readonly property Kirigami.Action cancelAction: Kirigami.Action { + text: i18n("Cancel") + icon.name: "dialog-cancel" + enabled: listener.isCancellable + tooltip: listener.statusText + onTriggered: { + listener.cancel() + enabled = false + } + visible: listener.isActive + onVisibleChanged: enabled = true + } + + function click() { + if (!isActive) { + if(application.isInstalled) + ResourcesModel.removeApplication(application); + else + ResourcesModel.installApplication(application); + } else { + console.warn("trying to un/install but resource still active", application.name) + } + } + + condition: listener.isActive + componentTrue: RowLayout { + ToolButton { + Layout.fillHeight: true + action: root.cancelAction + text: "" + ToolTip.visible: hovered + ToolTip.text: root.cancelAction.text + } + + LabelBackground { + Layout.fillWidth: true + text: listener.statusText + progress: listener.progress/100 + } + } + + componentFalse: Button { + visible: !application.isInstalled || application.isRemovable + enabled: application.state !== AbstractResource.Broken + text: root.action.text + + activeFocusOnTab: false + onClicked: root.click() + } +} diff --git a/discover/qml/InstalledPage.qml b/discover/qml/InstalledPage.qml new file mode 100644 index 0000000..76ab434 --- /dev/null +++ b/discover/qml/InstalledPage.qml @@ -0,0 +1,22 @@ +import QtQuick 2.1 +import QtQuick.Controls 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.14 as Kirigami + +ApplicationsListPage { + id: page + stateFilter: AbstractResource.Installed + allBackends: true + sortProperty: "installedPageSorting" + sortRole: DiscoverSettings.installedPageSorting + + name: i18n("Installed") + compact: true + showRating: false + showSize: true + canNavigate: false + + listHeader: null +} diff --git a/discover/qml/LabelBackground.qml b/discover/qml/LabelBackground.qml new file mode 100644 index 0000000..c731e94 --- /dev/null +++ b/discover/qml/LabelBackground.qml @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import org.kde.discover.app 1.0 +import org.kde.kirigami 2.14 as Kirigami + +Control +{ + id: root + property alias text: theLabel.text + property real progress: 1.0 + readonly property bool inProgress: progress > 0 + padding: Kirigami.Units.smallSpacing * 1.5 + + background: Item { + visible: root.inProgress + Rectangle { + color: Kirigami.Theme.disabledTextColor + border.width: 1 + border.color: Qt.darker(Kirigami.Theme.disabledTextColor) + anchors.fill: parent + radius: root.padding + } + + Rectangle { + anchors { + fill: parent + leftMargin: 1 + rightMargin: ((1-root.progress) * parent.width) + 1 + topMargin: 1 + bottomMargin: 1 + } + color: Kirigami.Theme.highlightColor + radius: root.padding-2 + } + } + + contentItem: Label { + id: theLabel + horizontalAlignment: Text.AlignHCenter + color: root.inProgress ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + } +} diff --git a/discover/qml/LoadingPage.qml b/discover/qml/LoadingPage.qml new file mode 100644 index 0000000..3cf4c7c --- /dev/null +++ b/discover/qml/LoadingPage.qml @@ -0,0 +1,11 @@ +import org.kde.kirigami 2.19 as Kirigami + +Kirigami.Page { + title: placeholder.text + readonly property bool isHome: true + + Kirigami.LoadingPlaceholder { + id: placeholder + anchors.centerIn: parent + } +} diff --git a/discover/qml/ProgressView.qml b/discover/qml/ProgressView.qml new file mode 100644 index 0000000..c7f0c7b --- /dev/null +++ b/discover/qml/ProgressView.qml @@ -0,0 +1,127 @@ +import QtQuick 2.1 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import org.kde.discover 2.0 +import org.kde.kirigami 2.14 as Kirigami +import "navigation.js" as Navigation + +Kirigami.AbstractListItem { + id: listItem + + contentItem: ColumnLayout { + Label { + id: label + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.iconSizes.smallMedium + (LayoutMirroring.enabled ? listItem.rightPadding : listItem.leftPadding) + Layout.rightMargin: Layout.leftMargin + text: TransactionModel.count ? i18n("Tasks (%1%)", TransactionModel.progress) : i18n("Tasks") + } + ProgressBar { + Layout.fillWidth: true + value: TransactionModel.progress/100 + } + } + visible: TransactionModel.count > 0 + + property Kirigami.OverlaySheet sheetObject: null + onClicked: { + if (!sheetObject) + sheetObject = sheet.createObject() + + if (!sheetObject.sheetOpen) + sheetObject.open() + } + + readonly property var v3: Component { + id: sheet + Kirigami.OverlaySheet { + parent: applicationWindow().overlay + + title: i18n("Tasks") + + onSheetOpenChanged: { + if (!sheetOpen) { + sheetObject.destroy(100) + } + } + + contentItem: ListView { + id: tasksView + spacing: 0 + implicitWidth: Kirigami.Units.gridUnit * 30 + + Component { + id: listenerComp + TransactionListener {} + } + model: TransactionModel + + Connections { + target: TransactionModel + function onRowsRemoved() { + if (TransactionModel.count === 0) { + sheetObject.close(); + } + } + } + + delegate: Kirigami.AbstractListItem { + id: del + width: tasksView.width + + // Don't need a highlight or hover effects as it can make the + // progress bar a bit hard to see + highlighted: false + activeBackgroundColor: "transparent" + activeTextColor: Kirigami.Theme.textColor + separatorVisible: false + hoverEnabled: false + + readonly property QtObject listener: listenerComp.createObject(del, (model.transaction.resource ? {resource: model.transaction.resource} : {transaction: model.transaction})) + + contentItem: ColumnLayout { + + RowLayout { + Layout.fillWidth: true + + Kirigami.Icon { + Layout.fillHeight: true + Layout.minimumWidth: height + source: model.transaction.icon + } + + Label { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + elide: Text.ElideRight + text: listener.isActive && model.transaction.remainingTime>0 ? i18nc("TransactioName - TransactionStatus: speed, remaining time", "%1 - %2: %3, %4 remaining", model.transaction.name, listener.statusText, model.transaction.downloadSpeedString, model.transaction.remainingTime) : + listener.isActive && model.transaction.downloadSpeed>0 ? i18nc("TransactioName - TransactionStatus: speed", "%1 - %2: %3", model.transaction.name, listener.statusText, model.transaction.downloadSpeedString) : + listener.isActive ? i18nc("TransactioName - TransactionStatus", "%1 - %2", model.transaction.name, listener.statusText) + : model.transaction.name + } + ToolButton { + icon.name: "dialog-cancel" + text: i18n("Cancel") + visible: listener.isCancellable + onClicked: listener.cancel() + } + ToolButton { + icon.name: "system-run" + visible: model.application !== undefined && model.application.isInstalled && !listener.isActive && model.application.canExecute + onClicked: { + model.application.invokeApplication() + model.remove(index) + } + } + } + ProgressBar { + Layout.fillWidth: true + visible: listener.isActive + value: listener.progress / 100 + } + } + } + } + } + } +} diff --git a/discover/qml/Rating.qml b/discover/qml/Rating.qml new file mode 100644 index 0000000..13fb2a5 --- /dev/null +++ b/discover/qml/Rating.qml @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.14 as Kirigami + +RowLayout +{ + id: view + property bool editable: false + property int max: 10 + property int rating: 0 + property real starSize: Kirigami.Units.gridUnit + + clip: true + spacing: 0 + + readonly property var ratingIndex: (theRepeater.count/view.max)*view.rating + + Repeater { + id: theRepeater + model: 5 + delegate: Kirigami.Icon { + Layout.minimumWidth: view.starSize + Layout.minimumHeight: view.starSize + Layout.preferredWidth: view.starSize + Layout.preferredHeight: view.starSize + + width: height + source: "rating" + opacity: (view.editable && mouse.item.containsMouse ? 0.7 + : index>=view.ratingIndex ? 0.2 + : 1) + + ConditionalLoader { + id: mouse + + anchors.fill: parent + condition: view.editable + componentTrue: MouseArea { + hoverEnabled: true + onClicked: rating = (max/theRepeater.model*(index+1)) + } + componentFalse: null + } + } + } +} diff --git a/discover/qml/ReviewDelegate.qml b/discover/qml/ReviewDelegate.qml new file mode 100644 index 0000000..0e04f18 --- /dev/null +++ b/discover/qml/ReviewDelegate.qml @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +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.14 as Kirigami + +Kirigami.AbstractCard { + id: reviewDelegateItem + visible: model.shouldShow + property bool compact: false + property bool separator: true + signal markUseful(bool useful) + + // Spacers to indent nested comments/replies + Layout.leftMargin: depth * Kirigami.Units.largeSpacing + + contentItem: Item { + implicitHeight: mainContent.childrenRect.height + implicitWidth: mainContent.childrenRect.width + ColumnLayout { + id: mainContent + anchors { + left: parent.left + top: parent.top + right: parent.right + } + // Header with stars and date of review + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Rating { + id: rating + Layout.fillWidth: true + Layout.bottomMargin: Kirigami.Units.largeSpacing + rating: model.rating + starSize: Kirigami.Units.gridUnit + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: date.toLocaleDateString(Qt.locale(), "MMMM yyyy") + opacity: 0.6 + } + } + + // Review title and author + Label { + id: content + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + + 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) + } + + // Review text + Label { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + + text: display + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + text: packageVersion ? i18n("Version: %1", packageVersion) : i18n("Version: unknown") + elide: Text.ElideRight + opacity: 0.6 + } + } + } + + footer: Loader { + active: !reviewDelegateItem.compact + sourceComponent: RowLayout { + id: rateTheReviewLayout + visible: !reviewDelegateItem.compact + Label { + Layout.leftMargin: Kirigami.Units.largeSpacing + visible: usefulnessTotal !== 0 + text: i18n("Votes: %1 out of %2", usefulnessFavorable, usefulnessTotal) + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: usefulnessTotal === 0 ? Kirigami.Units.largeSpacing : 0 + horizontalAlignment: Text.AlignRight + text: i18n("Was this review useful?") + elide: Text.ElideLeft + } + + // Useful/Not Useful buttons + Button { + id: yesButton + Layout.maximumWidth: Kirigami.Units.gridUnit * 3 + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + Layout.alignment: Qt.AlignVCenter + + text: i18nc("Keep this string as short as humanly possible", "Yes") + + checkable: true + checked: usefulChoice === ReviewsModel.Yes + onClicked: { + noButton.checked = false + reviewDelegateItem.markUseful(true) + } + } + Button { + id: noButton + Layout.maximumWidth: Kirigami.Units.gridUnit * 3 + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.alignment: Qt.AlignVCenter + + text: i18nc("Keep this string as short as humanly possible", "No") + + checkable: true + checked: usefulChoice === ReviewsModel.No + onClicked: { + yesButton.checked = false + reviewDelegateItem.markUseful(false) + } + } + } + } +} diff --git a/discover/qml/ReviewDialog.qml b/discover/qml/ReviewDialog.qml new file mode 100644 index 0000000..8bd84d9 --- /dev/null +++ b/discover/qml/ReviewDialog.qml @@ -0,0 +1,80 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.20 as Kirigami + +Kirigami.PromptDialog +{ + id: reviewDialog + preferredWidth: Kirigami.Units.gridUnit * 30 + + property QtObject application + readonly property alias rating: ratingInput.rating + readonly property alias name: nameInput.text + readonly property alias summary: titleInput.text + readonly property alias review: reviewInput.text + property QtObject backend: null + + title: i18n("Reviewing %1", application.name) + + standardButtons: Kirigami.Dialog.NoButton + + customFooterActions: [ + Kirigami.Action { + text: i18n("Submit review") + icon.name: "document-send" + enabled: !instructionalLabel.visible + onTriggered: reviewDialog.accept(); + } + ] + + ColumnLayout { + Kirigami.FormLayout { + Layout.fillWidth: true + + Rating { + id: ratingInput + Kirigami.FormData.label: i18n("Rating:") + editable: true + } + QQC2.TextField { + id: nameInput + Kirigami.FormData.label: i18n("Name:") + visible: page.reviewsBackend !== null && reviewDialog.backend.preferredUserName.length > 0 + Layout.fillWidth: true + readOnly: !reviewDialog.backend.supportsNameChange + text: visible ? reviewDialog.backend.preferredUserName : "" + } + QQC2.TextField { + id: titleInput + Kirigami.FormData.label: i18n("Title:") + Layout.fillWidth: true + validator: RegularExpressionValidator { regularExpression: /.{3,70}/ } + } + } + + QQC2.TextArea { + id: reviewInput + readonly property bool acceptableInput: length > 15 && length < 3000 + Layout.fillWidth: true + Layout.minimumHeight: Kirigami.Units.gridUnit * 8 + } + + QQC2.Label { + id: instructionalLabel + Layout.fillWidth: true + text: { + if (rating < 2) return i18n("Enter a rating"); + if (! titleInput.acceptableInput) return i18n("Write the title"); + if (reviewInput.length === 0) return i18n("Write the review"); + if (reviewInput.length < 15) return i18n("Keep writing…"); + if (reviewInput.length > 3000) return i18n("Too long!"); + if (nameInput.visible && nameInput.length < 1) return i18nc("@info:usagetip", "Insert a name"); + return ""; + } + wrapMode: Text.WordWrap + opacity: 0.6 + visible: text.length > 0 + } + } +} diff --git a/discover/qml/ReviewsPage.qml b/discover/qml/ReviewsPage.qml new file mode 100644 index 0000000..89477de --- /dev/null +++ b/discover/qml/ReviewsPage.qml @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Controls 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.14 as Kirigami + +Kirigami.OverlaySheet { + id: page + parent: applicationWindow().overlay + + property alias model: reviewsView.model + readonly property QtObject reviewsBackend: resource.backend.reviewsBackend + readonly property var resource: model.resource + + readonly property var rd: ReviewDialog { + id: reviewDialog + + application: page.resource + backend: page.reviewsBackend + onAccepted: backend.submitReview(resource, summary, review, rating, name) + } + + function openReviewDialog() { + page.sheetOpen = false + reviewDialog.open() + } + + header: ColumnLayout { + width: parent.width + spacing: Kirigami.Units.largeSpacing + + Kirigami.Heading { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: i18n("Reviews for %1", page.resource.name) + } + + RowLayout { + Layout.bottomMargin: Kirigami.Units.largeSpacing + + Button { + id: reviewButton + + visible: page.reviewsBackend != null + enabled: page.resource.isInstalled + text: i18n("Write a Review…") + onClicked: page.openReviewDialog() + } + Label { + Layout.fillWidth: true + text: i18n("Install this app to write a review") + wrapMode: Text.WordWrap + visible: !reviewButton.enabled + opacity: 0.6 + } + + } + } + + ListView { + id: reviewsView + + clip: true + topMargin: Kirigami.Units.largeSpacing + leftMargin: Kirigami.Units.largeSpacing + rightMargin: Kirigami.Units.largeSpacing + bottomMargin: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.smallSpacing + implicitWidth: Kirigami.Units.gridUnit * 25 + // Still preload some items to make the scrollbar behave better, but can't preload all the comments as some apps like Firefox have thousands of them which will freeze Discover for minutes + cacheBuffer: height * 2 + reuseItems: true + + delegate: ReviewDelegate { + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + separator: index !== ListView.view.count - 1 + onMarkUseful: page.model.markUseful(index, useful) + } + } +} diff --git a/discover/qml/SearchField.qml b/discover/qml/SearchField.qml new file mode 100644 index 0000000..f13b139 --- /dev/null +++ b/discover/qml/SearchField.qml @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2019 Carl Schwan + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.SearchField +{ + id: searchField + + // Search operations are network-intensive, so we can't have search-as-you-type. + // This means we should turn off auto-accept entirely, rather than having it on + // with a delay. The result just isn't good. See Bug 445142. + autoAccept: false + + property QtObject page + property string currentSearchText + + placeholderText: (!enabled || !page || page.hasOwnProperty("isHome") || window.leftPage.name.length === 0) ? i18n("Search…") : i18n("Search in '%1'…", window.leftPage.name) + + onAccepted: { + searchField.text = searchField.text.replace(/\n/g, ' '); + currentSearchText = searchField.text + } + + function clearText() { + searchField.text = "" + searchField.accepted() + } + + Connections { + ignoreUnknownSignals: true + target: page + function onClearSearch() { + clearText() + } + } + + Connections { + target: applicationWindow() + function onCurrentTopLevelChanged() { + if (applicationWindow().currentTopLevel.length > 0) + clearText() + } + } +} diff --git a/discover/qml/SearchPage.qml b/discover/qml/SearchPage.qml new file mode 100644 index 0000000..f5e34ff --- /dev/null +++ b/discover/qml/SearchPage.qml @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.5 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigami 2.14 as Kirigami +import QtGraphicalEffects 1.12 + +ApplicationsListPage { + id: searchPage + searchPage: true + + signal shown() + Timer { + interval: 0 + running: true + onTriggered: { + searchPage.shown() + } + } + + globalToolBarStyle: Kirigami.ApplicationHeaderStyle.ToolBar + + titleDelegate: Controls.Control { + Layout.fillWidth: true + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + z: 100 + + contentItem: SearchField { + id: searchField + focus: !window.wideScreen + visible: !window.wideScreen + z: 100 + Component.onCompleted: forceActiveFocus() + + Connections { + ignoreUnknownSignals: true + target: searchPage + function onShown() { + searchField.forceActiveFocus() + } + } + + onCurrentSearchTextChanged: { + searchPage.search = currentSearchText + } + } + } + + topPadding: 0 +} diff --git a/discover/qml/Shadow.qml b/discover/qml/Shadow.qml new file mode 100644 index 0000000..d6f5f73 --- /dev/null +++ b/discover/qml/Shadow.qml @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtGraphicalEffects 1.0 +import org.kde.kirigami 2.2 + +LinearGradient { + id: shadow + property int edge: Qt.LeftEdge + + width: Units.gridUnit/2 + height: Units.gridUnit/2 + + start: Qt.point((edge !== Qt.RightEdge ? 0 : width), (edge !== Qt.BottomEdge ? 0 : height)) + end: Qt.point((edge !== Qt.LeftEdge ? 0 : width), (edge !== Qt.TopEdge ? 0 : height)) + gradient: Gradient { + GradientStop { + position: 0.0 + color: Theme.backgroundColor + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.1) + } + GradientStop { + position: 1.0 + color: "transparent" + } + } +} + diff --git a/discover/qml/SourcesPage.qml b/discover/qml/SourcesPage.qml new file mode 100644 index 0000000..50f72e1 --- /dev/null +++ b/discover/qml/SourcesPage.qml @@ -0,0 +1,308 @@ +import QtQuick 2.15 +import QtQuick.Controls 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.14 as Kirigami +import "navigation.js" as Navigation +import org.kde.kquickcontrolsaddons 2.0 as KQCA + +DiscoverPage { + id: page + clip: true + title: i18n("Settings") + property string search: "" + readonly property string name: title + + Kirigami.Action { + id: configureUpdatesAction + text: i18n("Configure Updates…") + displayHint: Kirigami.DisplayHint.AlwaysHide + onTriggered: { + KQCA.KCMShell.openSystemSettings("kcm_updates"); + } + } + + contextualActions: feedbackLoader.item ? feedbackLoader.item.actions : [configureUpdatesAction] + + header: ColumnLayout { + Repeater { + id: rep + model: SourcesModel.sources + delegate: Kirigami.InlineMessage { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.smallSpacing + text: modelData.inlineAction ? modelData.inlineAction.toolTip : "" + visible: modelData.inlineAction && modelData.inlineAction.visible + actions: Kirigami.Action { + icon.name: modelData.inlineAction ? modelData.inlineAction.iconName : "" + text: modelData.inlineAction ? modelData.inlineAction.text : "" + onTriggered: modelData.inlineAction.trigger() + } + + } + } + } + + ListView { + id: sourcesView + model: SourcesModel + Component.onCompleted: Qt.callLater(SourcesModel.showingNow) + currentIndex: -1 + pixelAligned: true + section.property: "sourceName" + section.delegate: Kirigami.ListSectionHeader { + id: backendItem + height: Math.ceil(Math.max(Kirigami.Units.gridUnit * 2.5, contentItem.implicitHeight)) + + readonly property QtObject backend: SourcesModel.sourcesBackendByName(section) + readonly property QtObject resourcesBackend: backend.resourcesBackend + readonly property bool isDefault: ResourcesModel.currentApplicationBackend === resourcesBackend + + width: sourcesView.width + + readonly property var p0: Connections { + target: backendItem.backend + function onPassiveMessage(message) { + window.showPassiveNotification(message) + } + function onProceedRequest(title, description) { + var dialog = sourceProceedDialog.createObject(window, {sourcesBackend: backendItem.backend, title: title, description: description}) + dialog.open() + } + } + + contentItem: RowLayout { + Kirigami.Heading { + text: resourcesBackend.displayName + level: 3 + font.weight: backendItem.isDefault ? Font.Bold : Font.Normal + } + + Kirigami.ActionToolBar { + id: actionBar + + alignment: Qt.AlignRight + + Kirigami.Action { + id: isDefaultbackendLabelAction + + visible: backendItem.isDefault + displayHint: Kirigami.DisplayHint.KeepVisible + displayComponent: Kirigami.Heading { + text: i18n("Default source") + level: 3 + font.weight: Font.Bold + } + } + Kirigami.Action { + id: addSourceAction + text: i18n("Add Source…") + icon.name: "list-add" + visible: backendItem.backend && backendItem.backend.supportsAdding + + readonly property Component p0: Component { + id: dialogComponent + AddSourceDialog { + source: backendItem.backend + + onVisibleChanged: if(!visible) { + destroy(1000) + } + } + } + + onTriggered: { + var addSourceDialog = dialogComponent.createObject(window, {displayName: backendItem.backend.resourcesBackend.displayName }) + addSourceDialog.open() + } + } + Kirigami.Action { + id: makeDefaultAction + visible: resourcesBackend && resourcesBackend.hasApplications && !backendItem.isDefault + + text: i18n("Make default") + icon.name: "favorite" + onTriggered: ResourcesModel.currentApplicationBackend = backendItem.backend.resourcesBackend + } + + Component { + id: kirigamiAction + ConvertDiscoverAction {} + } + + function mergeActions(moreActions) { + var actions = [isDefaultbackendLabelAction, + makeDefaultAction, + addSourceAction] + for(var i in moreActions) { + actions.push(kirigamiAction.createObject(actionBar, {action: moreActions[i]})) + } + return actions; + } + actions: mergeActions(backendItem.backend.actions) + } + } + } + + Component { + id: sourceProceedDialog + Kirigami.OverlaySheet { + id: sheet + parent: applicationWindow().overlay + + showCloseButton: false + property QtObject sourcesBackend + property alias description: desc.text + property bool acted: false + + ColumnLayout { + Label { + id: desc + Layout.fillWidth: true + textFormat: Text.StyledText + wrapMode: Text.WordWrap + } + RowLayout { + Layout.alignment: Qt.AlignRight + Button { + text: i18n("Proceed") + icon.name: "dialog-ok" + onClicked: { + sourcesBackend.proceed() + sheet.acted = true + sheet.close() + } + } + Button { + Layout.alignment: Qt.AlignRight + text: i18n("Cancel") + icon.name: "dialog-cancel" + onClicked: { + sourcesBackend.cancel() + sheet.acted = true + sheet.close() + } + } + } + } + onSheetOpenChanged: if(!sheetOpen) { + sheet.destroy(1000) + if (!sheet.acted) + sourcesBackend.cancel() + } + } + } + + delegate: Kirigami.SwipeListItem { + id: delegate + enabled: model.display.length>0 && model.enabled + highlighted: ListView.isCurrentItem + supportsMouseEvents: false + visible: model.display.indexOf(page.search)>=0 + height: visible ? implicitHeight : 0 + + Keys.onReturnPressed: enabledBox.clicked() + Keys.onSpacePressed: enabledBox.clicked() + actions: [ + Kirigami.Action { + iconName: "go-up" + tooltip: i18n("Increase priority") + enabled: sourcesBackend.firstSourceId !== sourceId + visible: sourcesBackend.canMoveSources + onTriggered: { + var ret = sourcesBackend.moveSource(sourceId, -1) + if (!ret) + window.showPassiveNotification(i18n("Failed to increase '%1' preference", model.display)) + } + }, + Kirigami.Action { + iconName: "go-down" + tooltip: i18n("Decrease priority") + enabled: sourcesBackend.lastSourceId !== sourceId + visible: sourcesBackend.canMoveSources + onTriggered: { + var ret = sourcesBackend.moveSource(sourceId, +1) + if (!ret) + window.showPassiveNotification(i18n("Failed to decrease '%1' preference", model.display)) + } + }, + Kirigami.Action { + iconName: "edit-delete" + tooltip: i18n("Remove repository") + visible: sourcesBackend.supportsAdding + onTriggered: { + var backend = sourcesBackend + if (!backend.removeSource(sourceId)) { + console.warn("Failed to remove the source", model.display) + } + } + }, + Kirigami.Action { + iconName: delegate.LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic" + tooltip: i18n("Show contents") + visible: sourcesBackend.canFilterSources + onTriggered: { + Navigation.openApplicationListSource(sourceId) + } + } + ] + + RowLayout { + CheckBox { + id: enabledBox + + readonly property variant idx: sourcesView.model.index(index, 0) + readonly property variant modelChecked: model.checkState + checked: modelChecked !== Qt.Unchecked + enabled: sourcesView.model.flags(idx) & Qt.ItemIsUserCheckable + onClicked: { + sourcesView.model.setData(idx, checkState, Qt.CheckStateRole) + checked = Qt.binding(function() { return modelChecked !== Qt.Unchecked; }) + } + } + Label { + text: model.display + (model.toolTip ? " - " + model.toolTip + "" : "") + elide: Text.ElideRight + textFormat: Text.StyledText + Layout.fillWidth: true + } + } + } + + footer: ColumnLayout { + id: foot + anchors { + right: parent.right + left: parent.left + margins: Kirigami.Units.smallSpacing + } + Kirigami.ListSectionHeader { + contentItem: Kirigami.Heading { + Layout.fillWidth: true + text: i18n("Missing Backends") + visible: back.count > 0 + level: 3 + } + } + spacing: 0 + Repeater { + id: back + model: ResourcesProxyModel { + extending: "org.kde.discover.desktop" + filterMinimumState: false + stateFilter: AbstractResource.None + } + delegate: Kirigami.BasicListItem { + supportsMouseEvents: false + label: name + icon: model.icon + subtitle: model.comment + trailing: InstallApplicationButton { + application: model.application + } + } + } + } + } +} diff --git a/discover/qml/TopLevelPageData.qml b/discover/qml/TopLevelPageData.qml new file mode 100644 index 0000000..6cf6601 --- /dev/null +++ b/discover/qml/TopLevelPageData.qml @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import org.kde.kirigami 2.0 + +Action { + property string component + checked: window.currentTopLevel==component + + onTriggered: { + if(window.currentTopLevel!=component) + window.currentTopLevel=component + } +} diff --git a/discover/qml/UpdatesPage.qml b/discover/qml/UpdatesPage.qml new file mode 100644 index 0000000..49028a0 --- /dev/null +++ b/discover/qml/UpdatesPage.qml @@ -0,0 +1,546 @@ +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import QtQuick 2.15 +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import "navigation.js" as Navigation +import org.kde.kirigami 2.19 as Kirigami + +DiscoverPage +{ + id: page + title: i18n("Updates") + + property string footerLabel: "" + property int footerProgress: 0 + property bool isBusy: false + readonly property string name: title + + readonly property var resourcesUpdatesModel: ResourcesUpdatesModel { + id: resourcesUpdatesModel + onPassiveMessage: { + sheet.errorMessage = message; + sheet.sheetOpen = true; + } + onIsProgressingChanged: { + if (!isProgressing) { + resourcesUpdatesModel.prepare() + } + } + + Component.onCompleted: { + if (!isProgressing) { + resourcesUpdatesModel.prepare() + } + } + } + + readonly property var sheet: Kirigami.OverlaySheet { + id: sheet + + property string errorMessage: "" + + parent: applicationWindow().overlay + + title: contentLoader.sourceComponent === friendlyMessageComponent ? i18n("Update Issue") : i18n("Technical details") + + Loader { + id: contentLoader + active: true + sourceComponent: friendlyMessageComponent + + Component { + id: friendlyMessageComponent + + ColumnLayout { + Label { + id: friendlyMessage + Layout.fillWidth: true + Layout.maximumWidth: Math.round(page.width*0.75) + Layout.bottomMargin: Kirigami.Units.largeSpacing * 2 + text: i18n("There was an issue installing this update. Please try again later.") + wrapMode: Text.WordWrap + } + Button { + id: seeDetailsAndreportIssueButton + Layout.alignment: Qt.AlignRight + text: i18n("See Technical Details") + icon.name: "view-process-system" + onClicked: { + contentLoader.sourceComponent = nerdyDetailsComponent; + } + } + } + } + + Component { + id: nerdyDetailsComponent + + ColumnLayout { + Label { + Layout.fillWidth: true + Layout.maximumWidth: Math.round(page.width*0.75) + text: i18n("If you would like to report the update issue to your distribution's packagers, include this information:") + wrapMode: Text.WordWrap + } + + TextArea { + Layout.fillWidth: true + text: sheet.errorMessage + textFormat: Text.RichText + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.alignment: Qt.AlignRight + + Button { + text: i18n("Copy to Clipboard") + icon.name: "edit-copy" + onClicked: { + app.copyTextToClipboard(sheet.errorMessage); + window.showPassiveNotification(i18n("Error message copied to clipboard")); + } + } + + Button { + text: i18n("Report This Issue") + icon.name: "tools-report-bug" + onClicked: { + Qt.openUrlExternally(ResourcesModel.distroBugReportUrl()) + sheet.sheetOpen = false + } + } + } + } + } + } + + // Ensure that friendly message is shown if the user closes the sheet and + // then opens it again + onSheetOpenChanged: if (sheetOpen) { + contentLoader.sourceComponent = friendlyMessageComponent; + } + } + + readonly property var updateModel: UpdateModel { + id: updateModel + backend: resourcesUpdatesModel + } + + readonly property var updateAction: Kirigami.Action + { + id: updateAction + text: page.unselected>0 ? i18n("Update Selected") : i18n("Update All") + visible: updateModel.toUpdateCount + iconName: "update-none" + + function anyVisible(items) { + for (const itemPos in items) { + const item = items[itemPos]; + if (item.visible && item instanceof Kirigami.InlineMessage) { + return true + } + } + return false; + } + + enabled: !resourcesUpdatesModel.isProgressing && !ResourcesModel.isFetching && !anyVisible(page.header.children) + onTriggered: resourcesUpdatesModel.updateAll() + } + + header: ColumnLayout { + id: errorsColumn + + DiscoverInlineMessage { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.smallSpacing + inlineMessage: ResourcesModel.inlineMessage + } + + Repeater { + model: resourcesUpdatesModel.errorMessages + delegate: Kirigami.InlineMessage { + id: inline + Layout.fillWidth: true + Layout.margins: visible ? Kirigami.Units.smallSpacing : 0 + text: modelData + visible: true + type: Kirigami.MessageType.Error + onVisibleChanged: errorsColumn.childrenChanged() + + actions: [ + Kirigami.Action { + icon.name: "dialog-cancel" + text: i18n("Ignore") + onTriggered: { + inline.visible = false + } + } + ] + } + } + } + + footer: ColumnLayout { + width: parent.width + spacing: 0 + + ScrollView { + id: scv + Layout.fillWidth: true + Layout.preferredHeight: 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" + } + } + } + + ToolBar { + id: footerToolbar + Layout.fillWidth: true + visible: (updateModel.totalUpdatesCount > 0 && resourcesUpdatesModel.isProgressing) || updateModel.hasUpdates + + position: ToolBar.Footer + + contentItem: RowLayout { + ToolButton { + enabled: page.unselected > 0 && updateAction.enabled && !ResourcesModel.isFetching + visible: updateModel.totalUpdatesCount > 1 && !resourcesUpdatesModel.isProgressing + icon.name: "edit-select-all" + text: i18n("Select All") + onClicked: { updateModel.checkAll(); } + } + + ToolButton { + enabled: page.unselected !== updateModel.totalUpdatesCount && updateAction.enabled && !ResourcesModel.isFetching + visible: updateModel.totalUpdatesCount > 1 && !resourcesUpdatesModel.isProgressing + icon.name: "edit-select-none" + text: i18n("Select None") + onClicked: { updateModel.uncheckAll(); } + } + + CheckBox { + id: rebootAtEnd + visible: resourcesUpdatesModel.needsReboot && resourcesUpdatesModel.isProgressing + text: i18n("Restart automatically after update has completed"); + } + + Label { + Layout.fillWidth: true + Layout.rightMargin: Kirigami.Units.largeSpacing + horizontalAlignment: Text.AlignRight + text: i18n("Total size: %1", updateModel.updateSize) + elide: Text.ElideLeft + } + } + } + } + + 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) + + supportsRefreshing: true + onRefreshingChanged: { + ResourcesModel.updateAction.triggered() + refreshing = false + } + + readonly property Item report: ColumnLayout { + parent: page + anchors.fill: parent + anchors.margins: Kirigami.Units.largeSpacing * 2 + Item { + Layout.fillHeight: true + width: 1 + } + + Kirigami.Action { + id: restartAction + icon.name: "system-reboot" + text: i18n("Restart Now") + visible: false + onTriggered: app.reboot() + } + Kirigami.LoadingPlaceholder { + id: statusLabel + icon.name: { + if (page.footerProgress === 0 && page.footerLabel !== "" && !page.isBusy) { + return "update-none" + } else { + return "" + } + } + text: page.footerLabel + determinate: true + progressBar.value: page.footerProgress + } + + Item { + Layout.fillHeight: true + width: 1 + } + } + ListView { + id: updatesView + currentIndex: -1 + reuseItems: true + clip: true + + 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 + highlighted: ListView.isCurrentItem + hoverEnabled: !page.isBusy + onEnabledChanged: if (!enabled) { + model.extended = false; + } + + visible: resourceState < 3 //3=AbstractBackendUpdater.Done + + Keys.onReturnPressed: { + itemChecked.clicked() + } + Keys.onPressed: if (event.key===Qt.Key_Alt) model.extended = true + Keys.onReleased: if (event.key===Qt.Key_Alt) model.extended = false + + ColumnLayout { + id: layout + property bool extended: model.extended + onExtendedChanged: if (extended) { + updateModel.fetchUpdateDetails(index) + } + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + CheckBox { + id: itemChecked + 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 { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing + Layout.alignment: Qt.AlignVCenter + + spacing: 0 + + // 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: resource.upgradeText + 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 + 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 } } + } + + RowLayout { + Layout.fillWidth: true + visible: layout.extended + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + text: i18n("Update from:") + } + // Backend icon + Kirigami.Icon { + source: resource.sourceIcon + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + } + // Backend label and origin/remote + Label { + Layout.fillWidth: true + text: resource.origin.length === 0 ? resource.backend.displayName + : i18nc("%1 is the backend that provides this app, %2 is the specific repository or address within that backend","%1 (%2)", + resource.backend.displayName, resource.origin) + elide: Text.ElideRight + } + + Button { + Layout.alignment: Qt.AlignRight + text: i18n("More Information…") + enabled: !resourcesUpdatesModel.isProgressing + onClicked: Navigation.openApplication(resource) + } + } + } + + onClicked: { + model.extended = !model.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 } + PropertyChanges { target: statusLabel; visible: false } + }, + State { + name: "has-updates" + PropertyChanges { target: page; title: i18nc("@info", "Updates") } + // On mobile, we want "Update" to be the primary action so it's in + // the center, but on desktop this feels a bit awkward and it would + // be better to have "Update" be the right-most action + PropertyChanges { target: page.actions; main: applicationWindow().wideScreen ? refreshAction : updateAction} + PropertyChanges { target: page.actions; left: applicationWindow().wideScreen ? updateAction : refreshAction} + PropertyChanges { target: statusLabel; visible: false } + }, + State { + name: "reboot" + PropertyChanges { target: page; footerLabel: i18nc("@info", "Restart the system to complete the update process") } + PropertyChanges { target: statusLabel; helpfulAction: restartAction } + PropertyChanges { target: statusLabel; explanation: "" } + PropertyChanges { target: statusLabel.progressBar; visible: false } + StateChangeScript { + script: if (rebootAtEnd.checked && resourcesUpdatesModel.readyToReboot) { + app.rebootNow() + } + } + }, + State { + name: "now-uptodate" + PropertyChanges { target: page; footerLabel: i18nc("@info", "Up to date") } + PropertyChanges { target: page.actions; main: refreshAction } + PropertyChanges { target: statusLabel; explanation: "" } + PropertyChanges { target: statusLabel.progressBar; visible: false } + }, + State { + name: "uptodate" + PropertyChanges { target: page; footerLabel: i18nc("@info", "Up to date") } + PropertyChanges { target: page.actions; main: refreshAction } + PropertyChanges { target: statusLabel; explanation: "" } + PropertyChanges { target: statusLabel.progressBar; visible: false } + }, + State { + name: "medium" + PropertyChanges { target: page; title: i18nc("@info", "Up to date") } + PropertyChanges { target: page.actions; main: refreshAction } + PropertyChanges { target: statusLabel; explanation: "" } + PropertyChanges { target: statusLabel.progressBar; visible: false } + }, + State { + name: "low" + PropertyChanges { target: page; title: i18nc("@info", "Should check for updates") } + PropertyChanges { target: page.actions; main: refreshAction } + PropertyChanges { target: statusLabel; explanation: "" } + PropertyChanges { target: statusLabel.progressBar; visible: false } + }, + State { + name: "unknown" + PropertyChanges { target: page; title: i18nc("@info", "Time of last update unknown") } + PropertyChanges { target: page.actions; main: refreshAction } + PropertyChanges { target: statusLabel; explanation: "" } + PropertyChanges { target: statusLabel.progressBar; visible: false } + } + ] +} diff --git a/discover/qml/WebflowDialog.qml b/discover/qml/WebflowDialog.qml new file mode 100644 index 0000000..a3114ea --- /dev/null +++ b/discover/qml/WebflowDialog.qml @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import org.kde.kirigami 2.19 as Kirigami +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtWebView 1.15 + +Kirigami.OverlaySheet { + id: sheet + showCloseButton: false + property QtObject transaction + property bool acted: false + property alias url: view.url + + readonly property var p0: Connections { + target: transaction + function onWebflowDone() { + acted = true + sheet.close() + } + } + + contentItem: WebView { + id: view + Layout.preferredWidth: Math.round(window.width * 0.75) + height: Math.round(window.height * 0.75) + } + + footer: RowLayout { + Item { Layout.fillWidth : true } + + Button { + Layout.alignment: Qt.AlignRight + text: i18n("Cancel") + icon.name: "dialog-cancel" + onClicked: { + transaction.cancel() + sheet.acted = true + sheet.close() + } + Keys.onEscapePressed: clicked() + } + } + + onSheetOpenChanged: if(!sheetOpen) { + sheet.destroy(1000) + if (!sheet.acted) + transaction.cancel() + } +} diff --git a/discover/qml/navigation.js b/discover/qml/navigation.js new file mode 100644 index 0000000..28d8861 --- /dev/null +++ b/discover/qml/navigation.js @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +function clearStack() +{ + window.currentTopLevel = "" + window.stack.clear(); +} + +function openApplicationListSource(origin) { + openApplicationList({ originFilter: origin, title: origin, allBackends: true }) +} + +function openApplicationMime(mime) { + clearStack() + openApplicationList({ mimeTypeFilter: mime, title: i18n("Resources for '%1'", mime) }) +} + +function openApplicationList(props) { + var page = window.stack.push("qrc:/qml/ApplicationsListPage.qml", props) + if (props.search === "") + page.clearSearch(); +} + +function openCategory(cat, search) { + clearStack() + openApplicationList({ category: cat, search: search }) +} + +function openApplication(app) { + console.assert(app) + window.stack.push("qrc:/qml/ApplicationPage.qml", { application: app }) +} + +function openReviews(model) { + window.stack.push("qrc:/qml/ReviewsPage.qml", { model: model }) +} + +function openExtends(ext, appname) { + window.stack.push("qrc:/qml/ApplicationsListPage.qml", { extending: ext, title: i18n("Addons for %1", appname) }) +} + +function openHome() { + if (window.globalDrawer.currentSubMenu) + window.globalDrawer.resetMenu(); + clearStack() + var page = window.stack.push(topBrowsingComp) + page.clearSearch() +} diff --git a/discover/resources.qrc b/discover/resources.qrc new file mode 100644 index 0000000..0008b9e --- /dev/null +++ b/discover/resources.qrc @@ -0,0 +1,42 @@ + + + + qml/TopLevelPageData.qml + qml/ApplicationsListPage.qml + qml/ApplicationPage.qml + qml/ApplicationResourceButton.qml + qml/ReviewsPage.qml + qml/AddonsView.qml + qml/ApplicationDelegate.qml + qml/GridApplicationDelegate.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/DiscoverInlineMessage.qml + qml/DiscoverPage.qml + qml/DiscoverWindow.qml + qml/DiscoverDrawer.qml + qml/ActionListItem.qml + qml/LoadingPage.qml + qml/SearchField.qml + qml/Shadow.qml + qml/AboutPage.qml + qml/Feedback.qml + qml/ConvertDiscoverAction.qml + qml/WebflowDialog.qml + + qml/navigation.js + + diff --git a/exporter/CMakeLists.txt b/exporter/CMakeLists.txt new file mode 100644 index 0000000..ae68030 --- /dev/null +++ b/exporter/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(plasma-discover-exporter main.cpp DiscoverExporter.cpp DiscoverExporter.h) + +target_link_libraries(plasma-discover-exporter Discover::Common KF5::CoreAddons KF5::I18n) diff --git a/exporter/DiscoverExporter.cpp b/exporter/DiscoverExporter.cpp new file mode 100644 index 0000000..292a734 --- /dev/null +++ b/exporter/DiscoverExporter.cpp @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "DiscoverExporter.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +DiscoverExporter::DiscoverExporter() + : QObject(nullptr) + , m_exculdedProperties({"executables", "canExecute"}) +{ + connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, this, &DiscoverExporter::fetchResources); +} + +DiscoverExporter::~DiscoverExporter() = default; + +void DiscoverExporter::setExportPath(const QUrl &url) +{ + m_path = url; +} + +QJsonObject itemDataToMap(const AbstractResource *res, const QSet &excluded) +{ + QJsonObject ret; + int propsCount = res->metaObject()->propertyCount(); + for (int i = 0; i < propsCount; i++) { + QMetaProperty prop = res->metaObject()->property(i); + if (prop.type() == QVariant::UserType || excluded.contains(prop.name())) + continue; + + const QVariant val = prop.read(res); + if (val.isNull()) + continue; + + ret.insert(QLatin1String(prop.name()), QJsonValue::fromVariant(val)); + } + return ret; +} + +void DiscoverExporter::fetchResources() +{ + ResourcesModel *m = ResourcesModel::global(); + QSet streams; + const auto backends = m->backends(); + for (auto backend : backends) { + streams << backend->search({}); + } + auto stream = new StoredResultsStream(streams); + connect(stream, &StoredResultsStream::finishedResources, this, &DiscoverExporter::exportResources); + QTimer::singleShot(15s, stream, &AggregatedResultsStream::finished); +} + +void DiscoverExporter::exportResources(const QVector &resources) +{ + QJsonArray data; + for (auto res : resources) { + data += itemDataToMap(res, m_exculdedProperties); + } + + QJsonDocument doc = QJsonDocument(data); + if (doc.isNull()) { + qWarning() << "Could not completely export the data to " << m_path; + return; + } + + QFile f(m_path.toLocalFile()); + if (f.open(QIODevice::WriteOnly | QIODevice::Text)) { + int w = f.write(doc.toJson(QJsonDocument::Indented)); + if (w <= 0) + qWarning() << "Could not completely export the data to " << m_path; + } else { + qWarning() << "Could not write to " << m_path; + } + qDebug() << "exported items: " << data.count() << " to " << m_path; + Q_EMIT exportDone(); +} + +#include "moc_DiscoverExporter.cpp" diff --git a/exporter/DiscoverExporter.h b/exporter/DiscoverExporter.h new file mode 100644 index 0000000..3e3f725 --- /dev/null +++ b/exporter/DiscoverExporter.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +class AbstractResource; + +class DiscoverExporter : public QObject +{ + Q_OBJECT +public: + explicit DiscoverExporter(); + ~DiscoverExporter() override; + + void setExportPath(const QUrl &url); + +public Q_SLOTS: + void fetchResources(); + void exportResources(const QVector &resources); + +Q_SIGNALS: + void exportDone(); + +private: + QUrl m_path; + const QSet m_exculdedProperties; +}; diff --git a/exporter/main.cpp b/exporter/main.cpp new file mode 100644 index 0000000..6701398 --- /dev/null +++ b/exporter/main.cpp @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "../DiscoverVersion.h" +#include "DiscoverExporter.h" +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); + KLocalizedString::setApplicationDomain("plasma-discover-exporter"); + KAboutData about(QStringLiteral("discover-exporter"), + i18n("Discover Exporter"), + version, + QString(), + KAboutLicense::GPL, + i18n("©2013 Aleix Pol Gonzalez"), + QString()); + about.addAuthor(i18n("Jonathan Thomas"), QString(), QStringLiteral("echidnaman@kubuntu.org")); + about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@blue-systems.com")); + about.setProductName("discover/exporter"); + + DiscoverExporter exp; + { + QCommandLineParser parser; + parser.addPositionalArgument(QStringLiteral("file"), i18n("File to which we'll export")); + DiscoverBackendsFactory::setupCommandLine(&parser); + about.setupCommandLine(&parser); + parser.process(app); + about.processCommandLine(&parser); + DiscoverBackendsFactory::processCommandLine(&parser, false); + + if (parser.positionalArguments().count() != 1) { + parser.showHelp(1); + } + exp.setExportPath(QUrl::fromUserInput(parser.positionalArguments().at(0), QString(), QUrl::AssumeLocalFile)); + } + + QObject::connect(&exp, &DiscoverExporter::exportDone, &app, &QCoreApplication::quit); + + return app.exec(); +} diff --git a/kcm/CMakeLists.txt b/kcm/CMakeLists.txt new file mode 100644 index 0000000..7cc6605 --- /dev/null +++ b/kcm/CMakeLists.txt @@ -0,0 +1,33 @@ +#SPDX-FileCopyrightText: (C) 2020 Aleix Pol Gonzalzez +#SPDX-License-Identifier: BSD-3-Clause + +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_updates\") + +kcmutils_generate_module_data( + kcm_updates_PART_SRCS + MODULE_DATA_HEADER discoverdata.h + MODULE_DATA_CLASS_NAME DiscoverData + SETTINGS_HEADERS discoversettings.h + SETTINGS_CLASSES DiscoverSettings +) +kcmutils_generate_module_data( + kcm_updates_PART_SRCS + MODULE_DATA_HEADER updatesdata.h + MODULE_DATA_CLASS_NAME UpdatesData + SETTINGS_HEADERS updatessettings.h + SETTINGS_CLASSES UpdatesSettings +) + +kconfig_add_kcfg_files(kcm_updates_PART_SRCS updatessettings.kcfgc GENERATE_MOC) +kconfig_add_kcfg_files(kcm_updates_PART_SRCS discoversettings.kcfgc GENERATE_MOC) +add_library(kcm_updates MODULE updates.cpp updates.h ${kcm_updates_PART_SRCS}) + +kcmutils_generate_desktop_file(kcm_updates) +target_link_libraries(kcm_updates + KF5::I18n + KF5::KCMUtils + KF5::QuickAddons +) + +install(TARGETS kcm_updates DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/kcms/systemsettings) +kpackage_install_package(package kcm_updates kcms) diff --git a/kcm/discoversettings.kcfg b/kcm/discoversettings.kcfg new file mode 100644 index 0000000..5de87f6 --- /dev/null +++ b/kcm/discoversettings.kcfg @@ -0,0 +1,18 @@ + + + + + + + + false + + + diff --git a/kcm/discoversettings.kcfgc b/kcm/discoversettings.kcfgc new file mode 100644 index 0000000..dcc69df --- /dev/null +++ b/kcm/discoversettings.kcfgc @@ -0,0 +1,11 @@ +; SPDX-FileCopyrightText: (C) 2021 Aleix Pol Gonzalzez +; +; SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +File=discoversettings.kcfg +ClassName=DiscoverSettings +Notifiers=true +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/kcm/kcm_updates.json b/kcm/kcm_updates.json new file mode 100644 index 0000000..7f572e9 --- /dev/null +++ b/kcm/kcm_updates.json @@ -0,0 +1,115 @@ +{ + "Categories": "Qt;KDE;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_updates", + "Description": "Configure software update behavior", + "Description[ar]": "اضبط سلوك تحديث التطبيقات", + "Description[az]": "Proqram təminatı yenilənməsi davranışını tənzimləyin", + "Description[bg]": "Конфигуриране на актуализация на софтуера", + "Description[ca@valencia]": "Configura el comportament de l'actualització de programari", + "Description[ca]": "Configura el comportament de l'actualització de programari", + "Description[cs]": "Nastavit chování aktualizací software", + "Description[de]": "Verhalten der Softwareaktualisierungen einrichten", + "Description[en_GB]": "Configure software update behaviour", + "Description[es]": "Configurar el comportamiento de las actualizaciones de software", + "Description[et]": "Tarkvarauuenduste käitumise seadistamine", + "Description[eu]": "Konfiguratu software eguneratzearen jokabidea", + "Description[fi]": "Määritä ohjelmapäivitysten toiminta", + "Description[fr]": "Configurer le comportement pour les mises à jour des logiciels ", + "Description[gl]": "Configurar o comportamento das actualizacións de software.", + "Description[hu]": "A szoftverfrissítés működésének beállítása", + "Description[ia]": "Configura ambiente de actualisation de software", + "Description[id]": "Konfigurasikan perilaku pembaruan perangkat lunak", + "Description[ie]": "Configurar parametres del actualisation de programmas", + "Description[is]": "Stilla hegðun hugbúnaðaruppfærslna", + "Description[it]": "Configura il comportamento degli aggiornamenti software", + "Description[ja]": "ソフトウェア更新の挙動を設定します", + "Description[ka]": "პროგრამების განახლებების ქცევის მორგება", + "Description[ko]": "소프트웨어 업데이트 행동 설정", + "Description[nb]": "Sett opp håndtering av programvareoppdateringer", + "Description[nl]": "Kies update-methode", + "Description[nn]": "Set opp handtering av programoppdateringar", + "Description[pa]": "ਸਾਫਟਵੇਅਰ ਅੱਪਡੇਟ ਰਵੱਈਏ ਦੀ ਸੰਰਚਨਾ", + "Description[pl]": "Ustawienia zachowania uaktualnień oprogramowania", + "Description[pt]": "Configurar o comportamento da actualização das aplicações", + "Description[pt_BR]": "Configurar o comportamento da atualização de aplicativos", + "Description[ro]": "Configurează comportamentul actualizării de programe", + "Description[ru]": "Настройка службы обновления программ", + "Description[sk]": "Nakonfigurovať správanie softvérových aktualizácii", + "Description[sl]": "Nastavite obnašanje posodobitve programov", + "Description[sv]": "Anpassa beteende för uppdateringar av programvara", + "Description[ta]": "மென்பொருள் புதுப்பிப்புகளின் நடத்தையை அமையுங்கள்", + "Description[tr]": "Yazılım güncelleme davranışını yapılandır", + "Description[uk]": "Налаштовування поведінки при оновленні програм", + "Description[x-test]": "xxConfigure software update behaviorxx", + "Description[zh_CN]": "配置软件更新行为", + "Description[zh_TW]": "設定軟體更新行為", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "system-software-update", + "Name": "Software Update", + "Name[ar]": "تحديث البرمجيّات", + "Name[az]": "Proqram Təminatı Yenilənməsi", + "Name[bg]": "Актуализация на софтуера", + "Name[ca@valencia]": "Actualització de programari", + "Name[ca]": "Actualització de programari", + "Name[cs]": "Aktualizace softwaru", + "Name[de]": "Softwareaktualisierung", + "Name[en_GB]": "Software Update", + "Name[es]": "Actualización de software", + "Name[et]": "Tarkvarauuendus", + "Name[eu]": "Software eguneratzea", + "Name[fi]": "Ohjelmapäivitys", + "Name[fr]": "Mise à jour des logiciels ", + "Name[gl]": "Actualización de software", + "Name[hi]": "सॉफ्टवेयर अद्यतन", + "Name[hsb]": "Aktualizowanje", + "Name[hu]": "Szoftverfrissítés", + "Name[ia]": "Actualisationes de Software", + "Name[id]": "Pembaruan Perangkat Lunak", + "Name[ie]": "Actualisation de programmas", + "Name[is]": "Hugbúnaðaruppfærsla", + "Name[it]": "Aggiornamento software", + "Name[ja]": "ソフトウェア更新", + "Name[ka]": "პროგრამების განახლება", + "Name[ko]": "소프트웨어 업데이트", + "Name[lt]": "Programinės įrangos atnaujinimas", + "Name[my]": "ဆော့ဖ်ဝဲလ်အပ်ဒိတ်", + "Name[nb]": "Programvareoppdatering", + "Name[nl]": "Software update", + "Name[nn]": "Program­oppdatering", + "Name[pa]": "ਸਾਫਟਵੇਅਰ ਅੱਪਡੇਟ", + "Name[pl]": "Uaktualnienia oprogramowania", + "Name[pt]": "Actualização das Aplicações", + "Name[pt_BR]": "Atualização de aplicativos", + "Name[ro]": "Actualizare de programe", + "Name[ru]": "Обновление программ", + "Name[sk]": "Aktualizácia softvéru", + "Name[sl]": "Posodobi programe", + "Name[sv]": "Uppdatering av programvara", + "Name[ta]": "மென்பொருள் புதுப்பிப்புகள்", + "Name[tg]": "Навсозии нармафзор", + "Name[tr]": "Yazılım Güncelleme", + "Name[uk]": "Оновлення програм", + "Name[x-test]": "xxSoftware Updatexx", + "Name[zh_CN]": "软件更新", + "Name[zh_TW]": "軟體更新", + "ServiceTypes": [ + "KCModule" + ] + }, + "X-KDE-Keywords": "updates", + "X-KDE-Keywords[ca]": "actualitzacions", + "X-KDE-Keywords[es]": "actualizaciones", + "X-KDE-Keywords[fr]": "mises à jour", + "X-KDE-Keywords[it]": "aggiornamenti", + "X-KDE-Keywords[nl]": "elementen voor bijwerken", + "X-KDE-Keywords[sv]": "uppdateringar", + "X-KDE-Keywords[uk]": "оновлення", + "X-KDE-Keywords[x-test]": "xxupdatesxx", + "X-KDE-ParentApp": "kcontrol", + "X-KDE-System-Settings-Parent-Category": "system-administration" +} diff --git a/kcm/package/contents/ui/main.qml b/kcm/package/contents/ui/main.qml new file mode 100644 index 0000000..ba18fe8 --- /dev/null +++ b/kcm/package/contents/ui/main.qml @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QQC2 +import org.kde.kirigami 2.14 as Kirigami +import org.kde.kcm 1.6 + +SimpleKCM { + id: root + + ConfigModule.buttons: ConfigModule.Default | ConfigModule.Apply + QQC2.ButtonGroup { + id: autoUpdatesGroup + onCheckedButtonChanged: { + kcm.updatesSettings.useUnattendedUpdates = automaticallyRadio.checked + } + } + + implicitWidth: Kirigami.Units.gridUnit * 38 + implicitHeight: Kirigami.Units.gridUnit * 35 + + + Kirigami.FormLayout { + width: parent.width + + QQC2.RadioButton { + Kirigami.FormData.label: i18n("Update software:") + text: i18n("Manually") + + QQC2.ButtonGroup.group: autoUpdatesGroup + checked: !kcm.updatesSettings.useUnattendedUpdates + } + RowLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.RadioButton { + id: automaticallyRadio + text: i18n("Automatically") + + QQC2.ButtonGroup.group: autoUpdatesGroup + checked: kcm.updatesSettings.useUnattendedUpdates + } + + ContextualHelpButton { + toolTipText: xi18nc("@info", "Software updates will be downloaded automatically when they become available. Updates for applications will be installed immediately, while system updates will be installed the next time the computer is restarted.") + } + } + + SettingStateBinding { + configObject: kcm.updatesSettings + settingName: "useUnattendedUpdates" + target: automaticallyRadio + } + + QQC2.ComboBox { + Kirigami.FormData.label: kcm.updatesSettings.useUnattendedUpdates ? i18nc("@title:group", "Update frequency:") : i18nc("@title:group", "Notification frequency:") + + readonly property var updatesFrequencyModel: [ + i18nc("@item:inlistbox", "Daily"), + i18nc("@item:inlistbox", "Weekly"), + i18nc("@item:inlistbox", "Monthly"), + i18nc("@item:inlistbox", "Never") + ] + + // Same as updatesFrequencyModel but without "Never" + readonly property var unattendedUpdatesFrequencyModel: [ + updatesFrequencyModel[0], + updatesFrequencyModel[1], + updatesFrequencyModel[2], + ] + + model: kcm.updatesSettings.useUnattendedUpdates ? unattendedUpdatesFrequencyModel : updatesFrequencyModel + + readonly property var options: [ + 60 * 60 * 24, + 60 * 60 * 24 * 7, + 60 * 60 * 24 * 30, + -1 + ] + + currentIndex: { + let index = -1 + for (const i in options) { + if (options[i] === kcm.updatesSettings.requiredNotificationInterval) { + index = i + } + } + return index + } + onActivated: { + kcm.updatesSettings.requiredNotificationInterval = options[index] + } + SettingStateProxy { + id: settingState + configObject: kcm.updatesSettings + settingName: "requiredNotificationInterval" + } + } + + Item { + implicitHeight: Kirigami.Units.largeSpacing + } + + RowLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.FormData.label: i18n("Use offline updates:") + visible: !kcm.isRpmOstree + + QQC2.CheckBox { + id: offlineUpdatesBox + enabled: !kcm.discoverSettings.isUseOfflineUpdatesImmutable + checked: kcm.discoverSettings.useOfflineUpdates + onToggled: { + kcm.discoverSettings.useOfflineUpdates = checked + } + } + + ContextualHelpButton { + toolTipText: i18n("Offline updates maximize system stability by applying changes while restarting the system. Using this update mode is strongly recommended.") + } + } + + SettingStateBinding { + configObject: kcm.discoverSettings + settingName: "useOfflineUpdates" + target: offlineUpdatesBox + } + } +} diff --git a/kcm/updates.cpp b/kcm/updates.cpp new file mode 100644 index 0000000..c0d46dd --- /dev/null +++ b/kcm/updates.cpp @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "updates.h" + +#include +#include +#include +#include + +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(UpdatesFactory, "kcm_updates.json", registerPlugin(); registerPlugin();) + +Updates::Updates(QObject *parent, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent) + , m_data(new UpdatesData(this)) + , m_discoverData(new DiscoverData(this)) +{ + Q_UNUSED(args) + + qmlRegisterAnonymousType("org.kde.discover.updates", 1); + qmlRegisterAnonymousType("org.kde.discover.updates", 1); + + setAboutData(new KAboutData(QStringLiteral("kcm_updates"), + i18n("Software Update"), + QStringLiteral("1.0"), + i18n("Configure software update settings"), + KAboutLicense::LGPL)); +} + +Updates::~Updates() = default; + +UpdatesSettings *Updates::updatesSettings() const +{ + return m_data->settings(); +} + +DiscoverSettings *Updates::discoverSettings() const +{ + return m_discoverData->settings(); +} + +bool Updates::isRpmOstree() const +{ + return QFile::exists(QStringLiteral("/run/ostree-booted")); +} + +#include "updates.moc" diff --git a/kcm/updates.h b/kcm/updates.h new file mode 100644 index 0000000..bcb93e5 --- /dev/null +++ b/kcm/updates.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include +#include +#include +class UpdatesData; +class DiscoverData; + +class Updates : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(UpdatesSettings *updatesSettings READ updatesSettings CONSTANT) + Q_PROPERTY(DiscoverSettings *discoverSettings READ discoverSettings CONSTANT) + Q_PROPERTY(bool isRpmOstree READ isRpmOstree CONSTANT) + +public: + explicit Updates(QObject *parent = nullptr, const QVariantList &list = QVariantList()); + ~Updates() override; + + UpdatesSettings *updatesSettings() const; + DiscoverSettings *discoverSettings() const; + + /* Returns true if we're running on an rpm-ostree managed systems. Used to + * show/hide PackageKit specific settings and show/hide rpm-ostree specific + * settings only on systems where those are relevant.*/ + bool isRpmOstree() const; + +private: + UpdatesData *const m_data; + DiscoverData *const m_discoverData; +}; diff --git a/kcm/updatessettings.kcfg b/kcm/updatessettings.kcfg new file mode 100644 index 0000000..15ce77c --- /dev/null +++ b/kcm/updatessettings.kcfg @@ -0,0 +1,23 @@ + + + + + + + + false + + + + 60 * 60 * 24 + + + + diff --git a/kcm/updatessettings.kcfgc b/kcm/updatessettings.kcfgc new file mode 100644 index 0000000..75202a3 --- /dev/null +++ b/kcm/updatessettings.kcfgc @@ -0,0 +1,11 @@ +; SPDX-FileCopyrightText: (C) 2020 Aleix Pol Gonzalzez +; +; SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +File=updatessettings.kcfg +ClassName=UpdatesSettings +Notifiers=true +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/libdiscover/ApplicationAddonsModel.cpp b/libdiscover/ApplicationAddonsModel.cpp new file mode 100644 index 0000000..445b0d8 --- /dev/null +++ b/libdiscover/ApplicationAddonsModel.cpp @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "ApplicationAddonsModel.h" +#include "libdiscover_debug.h" +#include "utils.h" +#include +#include +#include + +ApplicationAddonsModel::ApplicationAddonsModel(QObject *parent) + : QAbstractListModel(parent) + , m_app(nullptr) +{ + // new QAbstractItemModelTester(this, this); + + connect(TransactionModel::global(), &TransactionModel::transactionRemoved, this, &ApplicationAddonsModel::transactionOver); + connect(ResourcesModel::global(), &ResourcesModel::resourceDataChanged, this, [this](AbstractResource *resource, const QVector &properties) { + if (!properties.contains("state")) + return; + + const QString appstreamId = resource->appstreamId(); + if (kContains(m_initial, [&appstreamId](const PackageState &state) { + return appstreamId == state.packageName(); + })) { + resetState(); + } + }); +} + +QHash ApplicationAddonsModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles.insert(Qt::CheckStateRole, "checked"); + roles.insert(PackageNameRole, "packageName"); + return roles; +} + +void ApplicationAddonsModel::setApplication(AbstractResource *app) +{ + if (app == m_app) + return; + + if (m_app) + disconnect(m_app, nullptr, this, nullptr); + + m_app = app; + resetState(); + if (m_app) { + connect(m_app, &QObject::destroyed, this, [this]() { + setApplication(nullptr); + }); + } + Q_EMIT applicationChanged(); +} + +void ApplicationAddonsModel::resetState() +{ + beginResetModel(); + m_state.clear(); + m_initial = m_app ? m_app->addonsInformation() : QList(); + endResetModel(); + + Q_EMIT stateChanged(); +} + +AbstractResource *ApplicationAddonsModel::application() const +{ + return m_app; +} + +int ApplicationAddonsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_initial.size(); +} + +QVariant ApplicationAddonsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_initial.size()) + return QVariant(); + + switch (role) { + case Qt::DisplayRole: + return m_initial[index.row()].name(); + case Qt::ToolTipRole: + return m_initial[index.row()].description(); + case PackageNameRole: + return m_initial[index.row()].packageName(); + case Qt::CheckStateRole: { + const PackageState init = m_initial[index.row()]; + const AddonList::State state = m_state.addonState(init.name()); + if (state == AddonList::None) { + return init.isInstalled() ? Qt::Checked : Qt::Unchecked; + } else { + return state == AddonList::ToInstall ? Qt::Checked : Qt::Unchecked; + } + } + } + + return QVariant(); +} + +void ApplicationAddonsModel::discardChanges() +{ + // dataChanged should suffice, but it doesn't + beginResetModel(); + m_state.clear(); + Q_EMIT stateChanged(); + endResetModel(); +} + +void ApplicationAddonsModel::applyChanges() +{ + ResourcesModel::global()->installApplication(m_app, m_state); +} + +void ApplicationAddonsModel::changeState(const QString &packageName, bool installed) +{ + auto it = m_initial.constBegin(); + for (; it != m_initial.constEnd(); ++it) { + if (it->packageName() == packageName) + break; + } + Q_ASSERT(it != m_initial.constEnd()); + + const bool restored = it->isInstalled() == installed; + + if (restored) + m_state.resetAddon(packageName); + else + m_state.addAddon(packageName, installed); + + Q_EMIT stateChanged(); +} + +bool ApplicationAddonsModel::hasChanges() const +{ + return !m_state.isEmpty(); +} + +bool ApplicationAddonsModel::isEmpty() const +{ + return m_initial.isEmpty(); +} + +void ApplicationAddonsModel::transactionOver(Transaction *t) +{ + if (t->resource() != m_app) + return; + + resetState(); +} + +#include "moc_ApplicationAddonsModel.cpp" diff --git a/libdiscover/ApplicationAddonsModel.h b/libdiscover/ApplicationAddonsModel.h new file mode 100644 index 0000000..383deaf --- /dev/null +++ b/libdiscover/ApplicationAddonsModel.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "Transaction/AddonList.h" +#include +#include + +#include "discovercommon_export.h" + +class Transaction; +class AbstractResource; + +class DISCOVERCOMMON_EXPORT ApplicationAddonsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(AbstractResource *application READ application WRITE setApplication NOTIFY applicationChanged) + Q_PROPERTY(bool hasChanges READ hasChanges NOTIFY stateChanged) + Q_PROPERTY(bool isEmpty READ isEmpty NOTIFY applicationChanged) +public: + enum Roles { + PackageNameRole = Qt::UserRole, + }; + + explicit ApplicationAddonsModel(QObject *parent = nullptr); + + AbstractResource *application() const; + void setApplication(AbstractResource *app); + bool hasChanges() const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + bool isEmpty() const; + +public Q_SLOTS: + void discardChanges(); + void applyChanges(); + void changeState(const QString &packageName, bool installed); + +Q_SIGNALS: + void stateChanged(); + void applicationChanged(); + +private: + void transactionOver(Transaction *t); + void resetState(); + + AbstractResource *m_app; + QList m_initial; + AddonList m_state; +}; diff --git a/libdiscover/CMakeLists.txt b/libdiscover/CMakeLists.txt new file mode 100644 index 0000000..32c49f3 --- /dev/null +++ b/libdiscover/CMakeLists.txt @@ -0,0 +1,72 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"libdiscover\") + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-paths.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-paths.h) + +add_subdirectory(backends) +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/ReviewsModel.cpp + Transaction/AddonList.cpp + Transaction/Transaction.cpp + Transaction/TransactionListener.cpp + Transaction/TransactionModel.cpp + UpdateModel/UpdateItem.cpp + UpdateModel/UpdateModel.cpp + resources/DiscoverAction.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 + resources/StoredResultsStream.cpp + DiscoverBackendsFactory.cpp + ScreenshotsModel.cpp + ApplicationAddonsModel.cpp + CachedNetworkAccessManager.cpp +) + +ecm_qt_declare_logging_category(discovercommon_SRCS HEADER libdiscover_debug.h IDENTIFIER LIBDISCOVER_LOG CATEGORY_NAME org.kde.plasma.libdiscover DESCRIPTION "libdiscover" EXPORT DISCOVER) + + +add_library(DiscoverCommon ${discovercommon_SRCS}) +if(TARGET AppStreamQt) + target_sources(DiscoverCommon PRIVATE + appstream/OdrsReviewsBackend.cpp + appstream/AppStreamIntegration.cpp + appstream/AppStreamUtils.cpp + ) + target_link_libraries(DiscoverCommon PRIVATE Qt::Concurrent AppStreamQt) +endif() + +target_link_libraries(DiscoverCommon +PUBLIC + Qt::Core + Qt::Qml + Qt::Gui + KF5::I18n +PRIVATE + KF5::CoreAddons + KF5::ConfigCore + KF5::KIOCore +) +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 ${KDE_INSTALL_LIBDIR}/plasma-discover) +install(FILES resources/discoverabstractnotifier.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR}) diff --git a/libdiscover/CachedNetworkAccessManager.cpp b/libdiscover/CachedNetworkAccessManager.cpp new file mode 100644 index 0000000..e818f5f --- /dev/null +++ b/libdiscover/CachedNetworkAccessManager.cpp @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "CachedNetworkAccessManager.h" + +#include +#include +#include +#include + +CachedNetworkAccessManager::CachedNetworkAccessManager(const QString &path, QObject *parent) + : QNetworkAccessManager(parent) +{ + const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + path; + QNetworkDiskCache *cache = new QNetworkDiskCache(this); + QStorageInfo storageInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + cache->setCacheDirectory(cacheDir); + cache->setMaximumCacheSize(storageInfo.bytesTotal() / 1000); + setCache(cache); + + setTransferTimeout(); +} + +QNetworkReply *CachedNetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) +{ + QNetworkRequest req(request); + req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + return QNetworkAccessManager::createRequest(op, request, outgoingData); +} diff --git a/libdiscover/CachedNetworkAccessManager.h b/libdiscover/CachedNetworkAccessManager.h new file mode 100644 index 0000000..7b8cd6f --- /dev/null +++ b/libdiscover/CachedNetworkAccessManager.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +class Q_DECL_EXPORT CachedNetworkAccessManager : public QNetworkAccessManager +{ + Q_OBJECT +public: + explicit CachedNetworkAccessManager(const QString &path, QObject *parent = nullptr); + + virtual QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr) override; +}; diff --git a/libdiscover/Category/CategoriesReader.cpp b/libdiscover/Category/CategoriesReader.cpp new file mode 100644 index 0000000..6d68f9c --- /dev/null +++ b/libdiscover/Category/CategoriesReader.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "CategoriesReader.h" +#include "Category.h" +#include "libdiscover_debug.h" +#include +#include +#include +#include + +#include +#include + +QVector CategoriesReader::loadCategoriesFile(AbstractResourcesBackend *backend) +{ + QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("libdiscover/categories/") + backend->name() + QStringLiteral("-categories.xml")); + if (path.isEmpty()) { + auto cat = backend->category(); + if (cat.isEmpty()) + qCDebug(LIBDISCOVER_LOG) << "Couldn't find a category for " << backend->name(); + + Category::sortCategories(cat); + return cat; + } + return loadCategoriesPath(path); +} + +QVector CategoriesReader::loadCategoriesPath(const QString &path) +{ + QVector ret; + QFile menuFile(path); + if (!menuFile.open(QIODevice::ReadOnly)) { + qCWarning(LIBDISCOVER_LOG) << "couldn't open" << path; + return ret; + } + + QXmlStreamReader xml(&menuFile); + xml.readNextStartElement(); // We want to skip the first overall + + while (!xml.atEnd() && !xml.hasError()) { + xml.readNext(); + + if (xml.isStartElement() && xml.name() == QLatin1String("Menu")) { + ret << new Category({path}, qApp); + ret.last()->parseData(path, &xml); + } + } + + if (xml.hasError()) { + qCWarning(LIBDISCOVER_LOG) << "error while parsing the categories file:" << path << ':' << xml.lineNumber() << xml.errorString(); + } + + Category::sortCategories(ret); + return ret; +} diff --git a/libdiscover/Category/CategoriesReader.h b/libdiscover/Category/CategoriesReader.h new file mode 100644 index 0000000..449e241 --- /dev/null +++ b/libdiscover/Category/CategoriesReader.h @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include + +class Category; +class AbstractResourcesBackend; +class DISCOVERCOMMON_EXPORT CategoriesReader +{ +public: + QVector loadCategoriesPath(const QString &path); + QVector loadCategoriesFile(AbstractResourcesBackend *backend); +}; diff --git a/libdiscover/Category/Category.cpp b/libdiscover/Category/Category.cpp new file mode 100644 index 0000000..f16e1d7 --- /dev/null +++ b/libdiscover/Category/Category.cpp @@ -0,0 +1,401 @@ +/* + * SPDX-FileCopyrightText: 2010 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "Category.h" + +#include + +#include "libdiscover_debug.h" +#include +#include +#include +#include +#include + +Category::Category(QSet pluginName, QObject *parent) + : QObject(parent) + , m_iconString(QStringLiteral("applications-other")) + , m_plugins(std::move(pluginName)) +{ + // Use a timer so to compress the rootCategoriesChanged signal + // It generally triggers when KNS is unavailable at large (as explained in bug 454442) + m_subCategoriesChanged = new QTimer(this); + m_subCategoriesChanged->setInterval(0); + m_subCategoriesChanged->setSingleShot(true); + connect(m_subCategoriesChanged, &QTimer::timeout, this, &Category::subCategoriesChanged); +} + +Category::Category(const QString &name, + const QString &iconName, + const CategoryFilter &filter, + const QSet &pluginName, + const QVector &subCategories, + bool isAddons) + : QObject(nullptr) + , m_name(name) + , m_iconString(iconName) + , m_filter(filter) + , m_subCategories(subCategories) + , m_plugins(pluginName) + , m_isAddons(isAddons) + , m_priority(isAddons ? 5 : 0) +{ + setObjectName(m_name); + + // Use a timer so to compress the rootCategoriesChanged signal + // It generally triggers when KNS is unavailable at large (as explained in bug 454442) + m_subCategoriesChanged = new QTimer(this); + m_subCategoriesChanged->setInterval(0); + m_subCategoriesChanged->setSingleShot(true); + connect(m_subCategoriesChanged, &QTimer::timeout, this, &Category::subCategoriesChanged); +} + +Category::~Category() = default; + +void Category::parseData(const QString &path, QXmlStreamReader *xml) +{ + Q_ASSERT(xml->name() == QLatin1String("Menu")); + while (!xml->atEnd() && !xml->hasError()) { + xml->readNext(); + + if (xml->isEndElement() && xml->name() == QLatin1String("Menu")) { + break; + } else if (!xml->isStartElement()) { + if (xml->isCharacters() && xml->text().trimmed().isEmpty()) + ; + else if (!xml->isComment()) + qDebug() << "skipping" << xml->tokenString() << xml->name(); + continue; + } + + if (xml->name() == QLatin1String("Name")) { + m_untranslatedName = xml->readElementText(); + m_name = i18nc("Category", m_untranslatedName.toUtf8().constData()); + setObjectName(m_untranslatedName); + } else if (xml->name() == QLatin1String("Menu")) { + m_subCategories << new Category(m_plugins, this); + m_subCategories.last()->parseData(path, xml); + } else if (xml->name() == QLatin1String("Addons")) { + m_isAddons = true; + m_priority = 5; + xml->readNext(); + } else if (xml->name() == QLatin1String("Icon")) { + m_iconString = xml->readElementText(); + } else if (xml->name() == QLatin1String("Include") + || xml->name() == QLatin1String("Categories")) { + const QString opening = xml->name().toString(); + while (!xml->atEnd() && !xml->hasError()) { + xml->readNext(); + + if (xml->isEndElement() && xml->name() == opening) { + qDebug() << "weird, let's go" << opening << xml->lineNumber(); + break; + } else if (!xml->isStartElement()) { + if (xml->isCharacters() && xml->text().trimmed().isEmpty()) + ; + else if (!xml->isComment()) + qDebug() << "include skipping" << xml->tokenString() << xml->text() << xml->name() << opening << xml->lineNumber(); + continue; + } + break; + } + m_filter = parseIncludes(xml); + + // Here we are at the end of the last item in the group, we need to finish what we started + while (!xml->atEnd() && !xml->hasError()) { + xml->readNext(); + + if (xml->isEndElement() && xml->name() == opening) { + break; + } else { + if (xml->isCharacters() && xml->text().trimmed().isEmpty()) + ; + else if (!xml->isComment()) + qDebug() << "include2 skipping" << xml->tokenString() << xml->text() << xml->name() << opening << xml->lineNumber(); + continue; + } + break; + } + } else if (xml->name() == QLatin1String("Top")) { + xml->skipCurrentElement(); + m_priority = -5; + } else { + qDebug() << "unknown element" << xml->name(); + xml->skipCurrentElement(); + } + Q_ASSERT(xml->isEndElement()); + } + Q_ASSERT(xml->isEndElement() && xml->name() == QLatin1String("Menu")); +} + +CategoryFilter Category::parseIncludes(QXmlStreamReader *xml) +{ + const QString opening = xml->name().toString(); + Q_ASSERT(xml->isStartElement()); + + auto subIncludes = [&]() { + QVector filters; + + Q_ASSERT(xml->isStartElement()); + const QString opening = xml->name().toString(); + + while (!xml->atEnd() && !xml->hasError()) { + xml->readNext(); + + if (xml->isEndElement()) { + break; + } else if (xml->isStartElement()) { + filters.append(parseIncludes(xml)); + } + } + Q_ASSERT(xml->isEndElement()); + Q_ASSERT(xml->name() == opening); + return filters; + }; + + CategoryFilter filter; + if (xml->name() == QLatin1String("And")) { + filter = {CategoryFilter::AndFilter, subIncludes()}; + } else if (xml->name() == QLatin1String("Or")) { + filter = {CategoryFilter::OrFilter, subIncludes()}; + } else if (xml->name() == QLatin1String("Not")) { + filter = {CategoryFilter::NotFilter, subIncludes()}; + } else if (xml->name() == QLatin1String("PkgSection")) { + filter = {CategoryFilter::PkgSectionFilter, xml->readElementText()}; + } else if (xml->name() == QLatin1String("Category")) { + filter = {CategoryFilter::CategoryNameFilter, xml->readElementText()}; + Q_ASSERT(xml->isEndElement() && xml->name() == QLatin1String("Category")); + } else if (xml->name() == QLatin1String("PkgWildcard")) { + filter = {CategoryFilter::PkgWildcardFilter, xml->readElementText()}; + } else if (xml->name() == QLatin1String("AppstreamIdWildcard")) { + filter = {CategoryFilter::AppstreamIdWildcardFilter, xml->readElementText()}; + } else if (xml->name() == QLatin1String("PkgName")) { + filter = {CategoryFilter::PkgNameFilter, xml->readElementText()}; + } else { + qCWarning(LIBDISCOVER_LOG) << "unknown" << xml->name() << xml->lineNumber(); + } + + Q_ASSERT(xml->isEndElement()); + Q_ASSERT(xml->name() == opening); + + return filter; +} + +QString Category::name() const +{ + return m_name; +} + +void Category::setName(const QString &name) +{ + m_name = name; + Q_EMIT nameChanged(); +} + +QString Category::icon() const +{ + return m_iconString; +} + +CategoryFilter Category::filter() const +{ + return m_filter; +} + +void Category::setFilter(const CategoryFilter &filter) +{ + m_filter = filter; +} + +QVector Category::subCategories() const +{ + return m_subCategories; +} + +bool Category::categoryLessThan(Category *c1, const Category *c2) +{ + return (c1->priority() < c2->priority()) || (c1->priority() == c2->priority() && QString::localeAwareCompare(c1->name(), c2->name()) < 0); +} + +static bool isSorted(const QVector &vector) +{ + Category *last = nullptr; + for (auto a : vector) { + if (last && !Category::categoryLessThan(last, a)) + return false; + last = a; + } + return true; +} + +void Category::sortCategories(QVector &cats) +{ + std::sort(cats.begin(), cats.end(), &categoryLessThan); + for (auto cat : cats) { + sortCategories(cat->m_subCategories); + } + Q_ASSERT(isSorted(cats)); +} + +QDebug operator<<(QDebug debug, const CategoryFilter &filter) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "Filter("; + debug << filter.type << ", "; + + if (auto x = std::get_if(&filter.value)) { + debug << std::get(filter.value); + } else { + debug << std::get>(filter.value); + } + debug.nospace() << ')'; + return debug; +} + +void Category::addSubcategory(QVector &list, Category *newcat) +{ + Q_ASSERT(isSorted(list)); + + auto it = std::lower_bound(list.begin(), list.end(), newcat, &categoryLessThan); + if (it == list.end()) { + list << newcat; + return; + } + + auto c = *it; + if (c->name() == newcat->name()) { + if (c->icon() != newcat->icon() || c->m_priority != newcat->m_priority) { + qCWarning(LIBDISCOVER_LOG) << "the following categories seem to be the same but they're not entirely" << c->icon() << newcat->icon() << "--" + << c->name() << newcat->name() << "--" << c->isAddons() << newcat->isAddons(); + } else { + CategoryFilter newFilter = {CategoryFilter::OrFilter, QVector{c->m_filter, newcat->m_filter}}; + c->m_filter = newFilter; + c->m_plugins.unite(newcat->m_plugins); + const auto subCategories = newcat->subCategories(); + for (Category *nc : subCategories) { + addSubcategory(c->m_subCategories, nc); + } + return; + } + } + + list.insert(it, newcat); + Q_ASSERT(isSorted(list)); +} + +void Category::addSubcategory(Category *cat) +{ + int i = 0; + for (Category *subCat : qAsConst(m_subCategories)) { + if (!categoryLessThan(subCat, cat)) { + break; + } + ++i; + } + m_subCategories.insert(i, cat); + Q_ASSERT(isSorted(m_subCategories)); +} + +bool Category::blacklistPluginsInVector(const QSet &pluginNames, QVector &subCategories) +{ + bool ret = false; + for (QVector::iterator it = subCategories.begin(); it != subCategories.end();) { + if ((*it)->blacklistPlugins(pluginNames)) { + delete *it; + it = subCategories.erase(it); + ret = true; + } else + ++it; + } + return ret; +} + +bool Category::blacklistPlugins(const QSet &pluginNames) +{ + if (m_plugins.subtract(pluginNames).isEmpty()) { + return true; + } + + if (blacklistPluginsInVector(pluginNames, m_subCategories)) { + m_subCategoriesChanged->start(); + } + return false; +} + +QVariantList Category::subCategoriesVariant() const +{ + return kTransform(m_subCategories, [](Category *cat) { + return QVariant::fromValue(cat); + }); +} + +bool Category::matchesCategoryName(const QString &name) const +{ + return involvedCategories().contains(name); +} + +bool Category::contains(Category *cat) const +{ + const bool ret = cat == this || (cat && contains(qobject_cast(cat->parent()))); + return ret; +} + +bool Category::contains(const QVariantList &cats) const +{ + bool ret = false; + for (const auto &itCat : cats) { + if (contains(qobject_cast(itCat.value()))) { + ret = true; + break; + } + } + return ret; +} + +static QStringList involvedCategories(const CategoryFilter &f) +{ + switch (f.type) { + case CategoryFilter::CategoryNameFilter: + return {std::get(f.value)}; + case CategoryFilter::OrFilter: + case CategoryFilter::AndFilter: { + const auto filters = std::get>(f.value); + QStringList ret; + ret.reserve(filters.size()); + for (const auto &subFilters : filters) { + ret << involvedCategories(subFilters); + } + ret.removeDuplicates(); + return ret; + } break; + case CategoryFilter::AppstreamIdWildcardFilter: + case CategoryFilter::NotFilter: + case CategoryFilter::PkgSectionFilter: + case CategoryFilter::PkgWildcardFilter: + case CategoryFilter::PkgNameFilter: + break; + } + qCWarning(LIBDISCOVER_LOG) << "cannot infer categories from" << f.type; + return {}; +} + +QStringList Category::involvedCategories() const +{ + return ::involvedCategories(m_filter); +} + +bool CategoryFilter::operator==(const CategoryFilter &other) const +{ + if (other.type != type) { + return false; + } + + if (auto x = std::get_if(&value)) { + return *x == std::get(other.value); + } else { + return std::get>(value) == std::get>(other.value); + } +} diff --git a/libdiscover/Category/Category.h b/libdiscover/Category/Category.h new file mode 100644 index 0000000..7d4c99e --- /dev/null +++ b/libdiscover/Category/Category.h @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2010 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "discovercommon_export.h" + +class QXmlStreamReader; +class QTimer; + +class CategoryFilter +{ + Q_GADGET +public: + enum FilterType { + CategoryNameFilter, + PkgSectionFilter, + PkgWildcardFilter, + PkgNameFilter, + AppstreamIdWildcardFilter, + OrFilter, + AndFilter, + NotFilter, + }; + Q_ENUM(FilterType) + + FilterType type; + std::variant> value; + + bool operator==(const CategoryFilter &other) const; + bool operator!=(const CategoryFilter &other) const + { + return !operator==(other); + } +}; + +class DISCOVERCOMMON_EXPORT Category : public QObject +{ + Q_OBJECT +public: + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString icon READ icon CONSTANT) + Q_PROPERTY(QObject *parent READ parent CONSTANT) + Q_PROPERTY(QVariantList subcategories READ subCategoriesVariant NOTIFY subCategoriesChanged) + explicit Category(QSet pluginNames, QObject *parent = nullptr); + + Category(const QString &name, + const QString &iconName, + const CategoryFilter &filters, + const QSet &pluginName, + const QVector &subCategories, + bool isAddons); + ~Category() override; + + QString name() const; + // You should never attempt to change the name of anything that is not a leaf category + // as the results could be potentially detremental to the function of the category filters + void setName(const QString &name); + QString icon() const; + void setFilter(const CategoryFilter &filter); + CategoryFilter filter() const; + QVector subCategories() const; + QVariantList subCategoriesVariant() const; + + static void sortCategories(QVector &cats); + static void addSubcategory(QVector &cats, Category *cat); + /** + * Add a subcategory to this category. This function should only + * be used during the initialisation stage, before adding the local + * root category to the global root category model. + */ + void addSubcategory(Category *cat); + void parseData(const QString &path, QXmlStreamReader *xml); + bool blacklistPlugins(const QSet &pluginName); + bool isAddons() const + { + return m_isAddons; + } + qint8 priority() const + { + return m_priority; + } + bool matchesCategoryName(const QString &name) const; + + Q_SCRIPTABLE bool contains(Category *cat) const; + Q_SCRIPTABLE bool contains(const QVariantList &cats) const; + + static bool categoryLessThan(Category *c1, const Category *c2); + static bool blacklistPluginsInVector(const QSet &pluginNames, QVector &subCategories); + + QStringList involvedCategories() const; + QString untranslatedName() const + { + return m_untranslatedName; + } + +Q_SIGNALS: + void subCategoriesChanged(); + void nameChanged(); + +private: + QString m_name; + QString m_untranslatedName; + QString m_iconString; + CategoryFilter m_filter; + QVector m_subCategories; + + CategoryFilter parseIncludes(QXmlStreamReader *xml); + QSet m_plugins; + bool m_isAddons = false; + qint8 m_priority = 0; + QTimer *m_subCategoriesChanged; +}; diff --git a/libdiscover/Category/CategoryModel.cpp b/libdiscover/Category/CategoryModel.cpp new file mode 100644 index 0000000..8410a0e --- /dev/null +++ b/libdiscover/Category/CategoryModel.cpp @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +// Own includes +#include "CategoryModel.h" +#include "CategoriesReader.h" +#include "libdiscover_debug.h" +#include +#include +#include + +CategoryModel::CategoryModel(QObject *parent) + : QObject(parent) +{ + QTimer *t = new QTimer(this); + t->setInterval(0); + t->setSingleShot(true); + connect(t, &QTimer::timeout, this, &CategoryModel::populateCategories); + connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, t, QOverload<>::of(&QTimer::start)); + + // Use a timer so to compress the rootCategoriesChanged signal + // It generally triggers when KNS is unavailable at large (as explained in bug 454442) + m_rootCategoriesChanged = new QTimer(this); + m_rootCategoriesChanged->setInterval(0); + m_rootCategoriesChanged->setSingleShot(true); + connect(m_rootCategoriesChanged, &QTimer::timeout, this, &CategoryModel::rootCategoriesChanged); + if (!ResourcesModel::global()->backends().isEmpty()) { + populateCategories(); + } +} + +CategoryModel *CategoryModel::global() +{ + static CategoryModel *instance = nullptr; + if (!instance) { + instance = new CategoryModel; + } + return instance; +} + +void CategoryModel::populateCategories() +{ + const auto backends = ResourcesModel::global()->backends(); + + QVector ret; + CategoriesReader cr; + for (const auto backend : backends) { + if (!backend->isValid()) + continue; + + const QVector cats = cr.loadCategoriesFile(backend); + + if (ret.isEmpty()) { + ret = cats; + } else { + for (Category *c : cats) + Category::addSubcategory(ret, c); + } + } + if (m_rootCategories != ret) { + m_rootCategories = ret; + m_rootCategoriesChanged->start(); + } +} + +QVariantList CategoryModel::rootCategoriesVL() const +{ + return kTransform(m_rootCategories, [](Category *cat) { + return QVariant::fromValue(cat); + }); +} + +void CategoryModel::blacklistPlugin(const QString &name) +{ + const bool ret = Category::blacklistPluginsInVector({name}, m_rootCategories); + if (ret) { + m_rootCategoriesChanged->start(); + } +} + +static Category *recFindCategory(Category *root, const QString &name) +{ + if (root->untranslatedName() == name) + return root; + else { + const auto subs = root->subCategories(); + for (Category *c : subs) { + Category *ret = recFindCategory(c, name); + if (ret) + return ret; + } + } + return nullptr; +} + +Category *CategoryModel::findCategoryByName(const QString &name) const +{ + for (Category *cat : m_rootCategories) { + Category *ret = recFindCategory(cat, name); + if (ret) + return ret; + } + if (!m_rootCategories.isEmpty()) { + qDebug() << "could not find category" << name << m_rootCategories; + } + return nullptr; +} diff --git a/libdiscover/Category/CategoryModel.h b/libdiscover/Category/CategoryModel.h new file mode 100644 index 0000000..87f8998 --- /dev/null +++ b/libdiscover/Category/CategoryModel.h @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "Category.h" +#include +#include + +#include "discovercommon_export.h" + +class QTimer; + +class DISCOVERCOMMON_EXPORT CategoryModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariantList rootCategories READ rootCategoriesVL NOTIFY rootCategoriesChanged) +public: + explicit CategoryModel(QObject *parent = nullptr); + + static CategoryModel *global(); + + Q_SCRIPTABLE Category *findCategoryByName(const QString &name) const; + void blacklistPlugin(const QString &name); + QVector rootCategories() const + { + return m_rootCategories; + } + QVariantList rootCategoriesVL() const; + void populateCategories(); + +Q_SIGNALS: + void rootCategoriesChanged(); + +private: + QTimer *m_rootCategoriesChanged; + QVector m_rootCategories; +}; diff --git a/libdiscover/DiscoverBackendsFactory.cpp b/libdiscover/DiscoverBackendsFactory.cpp new file mode 100644 index 0000000..3576104 --- /dev/null +++ b/libdiscover/DiscoverBackendsFactory.cpp @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DiscoverBackendsFactory.h" +#include "libdiscover_debug.h" +#include "resources/AbstractResourcesBackend.h" +#include "resources/ResourcesModel.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC(QStringList, s_requestedBackends) +static bool s_isFeedback = false; + +void DiscoverBackendsFactory::setRequestedBackends(const QStringList &backends) +{ + *s_requestedBackends = backends; +} + +bool DiscoverBackendsFactory::hasRequestedBackends() +{ + return !s_requestedBackends->isEmpty(); +} + +DiscoverBackendsFactory::DiscoverBackendsFactory() +{ +} + +QVector DiscoverBackendsFactory::backend(const QString &name) const +{ + if (QDir::isAbsolutePath(name) && QStandardPaths::isTestModeEnabled()) { + return backendForFile(name, QFileInfo(name).fileName()); + } else { + return backendForFile(name, name); + } +} + +QVector DiscoverBackendsFactory::backendForFile(const QString &libname, const QString &name) const +{ + QPluginLoader *loader = new QPluginLoader(QLatin1String("discover/") + libname, QCoreApplication::instance()); + + // qCDebug(LIBDISCOVER_LOG) << "trying to load plugin:" << loader->fileName(); + AbstractResourcesBackendFactory *f = qobject_cast(loader->instance()); + if (!f) { + qCWarning(LIBDISCOVER_LOG) << "error loading" << libname << loader->errorString() << loader->metaData(); + return {}; + } + auto instances = f->newInstance(QCoreApplication::instance(), name); + if (instances.isEmpty()) { + qCWarning(LIBDISCOVER_LOG) << "Couldn't find the backend: " << libname << "among" << allBackendNames(false, true); + return instances; + } + + return instances; +} + +QStringList DiscoverBackendsFactory::allBackendNames(bool whitelist, bool allowDummy) const +{ + if (whitelist) { + QStringList whitelistNames = *s_requestedBackends; + if (s_isFeedback || !whitelistNames.isEmpty()) + return whitelistNames; + } + + QStringList pluginNames; + const auto libraryPaths = QCoreApplication::libraryPaths(); + for (const QString &dir : libraryPaths) { + QDirIterator it(dir + QStringLiteral("/discover"), QDir::Files); + while (it.hasNext()) { + it.next(); + if (QLibrary::isLibrary(it.fileName()) && (allowDummy || it.fileName() != QLatin1String("dummy-backend.so"))) { + pluginNames += it.fileInfo().baseName(); + } + } + } + + pluginNames.removeDuplicates(); // will happen when discover is installed twice on the system + return pluginNames; +} + +QVector DiscoverBackendsFactory::allBackends() const +{ + QStringList names = allBackendNames(); + auto ret = kTransform>(names, [this](const QString &name) { + return backend(name); + }); + ret.removeAll(nullptr); + + if (ret.isEmpty()) + qCWarning(LIBDISCOVER_LOG) << "Didn't find any Discover backend!"; + return ret; +} + +int DiscoverBackendsFactory::backendsCount() const +{ + return allBackendNames().count(); +} + +void DiscoverBackendsFactory::setupCommandLine(QCommandLineParser *parser) +{ + parser->addOption(QCommandLineOption(QStringLiteral("backends"), + i18n("List all the backends we'll want to have loaded, separated by comma ','."), + QStringLiteral("names"))); +} + +void DiscoverBackendsFactory::processCommandLine(QCommandLineParser *parser, bool test) +{ + if (parser->isSet(QStringLiteral("feedback"))) { + s_isFeedback = true; + s_requestedBackends->clear(); + return; + } + + QStringList backends = test // + ? QStringList{QStringLiteral("dummy-backend")} // + : parser->value(QStringLiteral("backends")).split(QLatin1Char(','), Qt::SkipEmptyParts); + for (auto &backend : backends) { + if (!backend.endsWith(QLatin1String("-backend"))) + backend.append(QLatin1String("-backend")); + } + *s_requestedBackends = backends; +} diff --git a/libdiscover/DiscoverBackendsFactory.h b/libdiscover/DiscoverBackendsFactory.h new file mode 100644 index 0000000..8b7b3c1 --- /dev/null +++ b/libdiscover/DiscoverBackendsFactory.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include +#include +class QCommandLineParser; +class AbstractResourcesBackend; + +class DISCOVERCOMMON_EXPORT DiscoverBackendsFactory +{ +public: + DiscoverBackendsFactory(); + + QVector backend(const QString &name) const; + QVector allBackends() const; + QStringList allBackendNames(bool whitelist = true, bool allowDummy = false) const; + int backendsCount() const; + + static void setupCommandLine(QCommandLineParser *parser); + static void processCommandLine(QCommandLineParser *parser, bool test); + static void setRequestedBackends(const QStringList &backends); + static bool hasRequestedBackends(); + +private: + QVector backendForFile(const QString &path, const QString &name) const; +}; diff --git a/libdiscover/ReviewsBackend/AbstractReviewsBackend.cpp b/libdiscover/ReviewsBackend/AbstractReviewsBackend.cpp new file mode 100644 index 0000000..ec959c0 --- /dev/null +++ b/libdiscover/ReviewsBackend/AbstractReviewsBackend.cpp @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez +#include + +AbstractReviewsBackend::AbstractReviewsBackend(QObject *parent) + : QObject(parent) +{ +} + +bool AbstractReviewsBackend::isReviewable() const +{ + return true; +} + +bool AbstractReviewsBackend::supportsNameChange() const +{ + return false; +} + +QString AbstractReviewsBackend::preferredUserName() const +{ + if (!supportsNameChange()) { + return userName(); + } else { + auto config = KSharedConfig::openConfig(); + auto configGroup = KConfigGroup(config, "Identity"); + const QString configName = configGroup.readEntry("Name", QString()); + return configName.isEmpty() ? userName() : configName; + } +} + +void AbstractReviewsBackend::submitReview(AbstractResource *app, + const QString &summary, + const QString &review_text, + const QString &rating, + const QString &userName) +{ + if (supportsNameChange() && !userName.isEmpty()) { + auto config = KSharedConfig::openConfig(); + auto configGroup = KConfigGroup(config, "Identity"); + configGroup.writeEntry("Name", userName); + configGroup.config()->sync(); + + Q_EMIT preferredUserNameChanged(); + } + sendReview(app, summary, review_text, rating, userName); +} + +QString AbstractReviewsBackend::errorMessage() const +{ + return QString(); +} diff --git a/libdiscover/ReviewsBackend/AbstractReviewsBackend.h b/libdiscover/ReviewsBackend/AbstractReviewsBackend.h new file mode 100644 index 0000000..439179f --- /dev/null +++ b/libdiscover/ReviewsBackend/AbstractReviewsBackend.h @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + +#include "Review.h" +#include "ReviewsModel.h" +class Rating; +class AbstractResource; + +class DISCOVERCOMMON_EXPORT AbstractReviewsBackend : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isReviewable READ isReviewable CONSTANT) + Q_PROPERTY(bool supportsNameChange READ supportsNameChange CONSTANT) + Q_PROPERTY(bool hasCredentials READ hasCredentials) + Q_PROPERTY(QString preferredUserName READ preferredUserName NOTIFY preferredUserNameChanged) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) +public: + explicit AbstractReviewsBackend(QObject *parent = nullptr); + + virtual bool hasCredentials() const = 0; + + Q_SCRIPTABLE virtual Rating *ratingForApplication(AbstractResource *app) const = 0; + Q_INVOKABLE virtual QString errorMessage() const; + Q_INVOKABLE virtual bool isResourceSupported(AbstractResource *res) const = 0; + virtual bool isFetching() const = 0; + virtual bool isReviewable() const; + virtual bool supportsNameChange() const; + +public Q_SLOTS: + virtual void login() = 0; + virtual void registerAndLogin() = 0; + virtual void logout() = 0; + virtual void submitUsefulness(Review *r, bool useful) = 0; + // About all the different "user_names": the user_name that is taken as input here + // is the user_name the user typed in the review dialog, which defaults to what + // the backend returns in userName() or the last username used + // if the backend supports changing it (that is what the preferredUserName is). + // If the backend returns true for "supportsNameChange()", then the review dialog won't let them change it, + // therefore making the user_name here the same as "userName()". + void submitReview(AbstractResource *app, const QString &summary, const QString &review_text, const QString &rating, const QString &userName); + QString preferredUserName() const; + virtual void deleteReview(Review *r) = 0; + virtual void flagReview(Review *r, const QString &reason, const QString &text) = 0; + virtual void fetchReviews(AbstractResource *app, int page = 1) = 0; + +Q_SIGNALS: + void reviewsReady(AbstractResource *app, const QVector &reviews, bool canFetchMore); + void error(const QString &message); + void fetchingChanged(bool fetching); + void preferredUserNameChanged(); + void errorMessageChanged(); + +protected: + virtual void sendReview(AbstractResource *app, const QString &summary, const QString &review_text, const QString &rating, const QString &userName) = 0; + virtual QString userName() const = 0; +}; diff --git a/libdiscover/ReviewsBackend/Rating.cpp b/libdiscover/ReviewsBackend/Rating.cpp new file mode 100644 index 0000000..0bcbe3a --- /dev/null +++ b/libdiscover/ReviewsBackend/Rating.cpp @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "Rating.h" +#include "libdiscover_debug.h" +#include +#include + +inline double fastPow(double a, double b) +{ + union { + double d; + int x[2]; + } u = {a}; + +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + u.x[1] = (int)(b * (u.x[1] - 1072632447) + 1072632447); + u.x[0] = 0; +#else + u.x[1] = 0; + u.x[0] = (int)(b * (u.x[1] - 1072632447) + 1072632447); +#endif + + return u.d; +} + +// Converted from a Ruby example, returns an inverse normal distribution +double pnormaldist(double qn) +{ + double b[] = {1.570796288, + 0.03706987906, + -0.8364353589e-3, + -0.2250947176e-3, + 0.6841218299e-5, + 0.5824238515e-5, + -0.104527497e-5, + 0.8360937017e-7, + -0.3231081277e-8, + 0.3657763036e-10, + 0.6936233982e-12}; + + if (qn < 0.0 || 1.0 < qn) + return 0.0; + + if (qn == 0.5) + return 0.0; + + double w1 = qn; + if (qn > 0.5) + w1 = 1.0 - w1; + double w3 = -qLn(4.0 * w1 * (1.0 - w1)); + w1 = b[0]; + + for (int i = 1; i < 11; i++) + w1 += b[i] * fastPow(w3, i); + + if (qn > 0.5) + return qSqrt(w1 * w3); + return -qSqrt(w1 * w3); +} + +double wilson_score(int pos, int n, double power = 0.2) +{ + if (n == 0) + return 0; + + double z = pnormaldist(1 - power / 2); + double phat = 1.0 * pos / n; + return (phat + z * z / (2 * n) - z * qSqrt((phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n); +} + +double dampenedRating(int ratings[6], double power = 0.1) +{ + int tot_ratings = 0; + for (int i = 0; i < 6; ++i) + tot_ratings = ratings[i] + tot_ratings; + + double sum_scores = 0.0; + for (int i = 0; i < 6; i++) { + const int rating = ratings[i]; + double ws = wilson_score(rating, tot_ratings, power); + sum_scores = sum_scores + float((i + 1) - 3) * ws; + } + + return sum_scores + 3; +} + +Rating::Rating(const QString &packageName, quint64 ratingCount, int data[6]) + : m_packageName(packageName) + , m_ratingCount(ratingCount) + // TODO consider storing data[] and present in UI + , m_rating(((data[1] // + + (data[2] * 2) // + + (data[3] * 3) // + + (data[4] * 4) // + + (data[5] * 5)) // + * 2) + / qMax(1, ratingCount)) + , m_ratingPoints(0) + , m_sortableRating(0) +{ + int spread[6]; + for (int i = 0; i < 6; ++i) { + int points = data[i]; + m_ratingPoints += (i + 1) * points; + spread[i] = points; + } + + m_sortableRating = dampenedRating(spread) * 2; +} + +Rating::Rating(const QString &packageName, quint64 ratingCount, int rating) + : m_packageName(packageName) + , m_ratingCount(ratingCount) + , m_rating(rating) + , m_ratingPoints(rating) + , m_sortableRating(rating) +{ +} + +Rating::~Rating() = default; + +QString Rating::packageName() const +{ + return m_packageName; +} + +quint64 Rating::ratingCount() const +{ + return m_ratingCount; +} + +float Rating::rating() const +{ + return m_rating; +} + +int Rating::ratingPoints() const +{ + return m_ratingPoints; +} + +double Rating::sortableRating() const +{ + return m_sortableRating; +} diff --git a/libdiscover/ReviewsBackend/Rating.h b/libdiscover/ReviewsBackend/Rating.h new file mode 100644 index 0000000..2f75ca7 --- /dev/null +++ b/libdiscover/ReviewsBackend/Rating.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +#include "discovercommon_export.h" + +class DISCOVERCOMMON_EXPORT Rating +{ + Q_GADGET + Q_PROPERTY(double sortableRating READ sortableRating CONSTANT) + Q_PROPERTY(float rating READ rating CONSTANT) + Q_PROPERTY(int ratingPoints READ ratingPoints CONSTANT) + Q_PROPERTY(quint64 ratingCount READ ratingCount CONSTANT) +public: + Rating() + { + } + explicit Rating(const QString &packageName, quint64 ratingCount, int rating); + explicit Rating(const QString &packageName, quint64 ratingCount, int data[6]); + ~Rating(); + + QString packageName() const; + quint64 ratingCount() const; + // 0.0 - 10.0 ranged rating + float rating() const; + int ratingPoints() const; + // Returns a dampened rating calculated with the Wilson Score Interval algorithm + double sortableRating() const; + +private: + QString m_packageName; + quint64 m_ratingCount = 0; + float m_rating = 0; + int m_ratingPoints = 0; + double m_sortableRating = 0; +}; + +Q_DECLARE_METATYPE(Rating) diff --git a/libdiscover/ReviewsBackend/Review.cpp b/libdiscover/ReviewsBackend/Review.cpp new file mode 100644 index 0000000..72c54c0 --- /dev/null +++ b/libdiscover/ReviewsBackend/Review.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "Review.h" +#include + +Review::Review(QString name, + QString pkgName, + QString language, + QString summary, + QString reviewText, + QString userName, + const QDateTime &date, + bool show, + quint64 id, + int rating, + int usefulTotal, + int usefulFavorable, + QString packageVersion) + : m_appName(std::move(name)) + , m_creationDate(date) + , m_shouldShow(show) + , m_id(id) + , m_language(std::move(language)) + , m_packageName(std::move(pkgName)) + , m_rating(rating) + , m_reviewText(std::move(reviewText)) + , m_reviewer(std::move(userName)) + , m_usefulnessTotal(usefulTotal) + , m_usefulnessFavorable(usefulFavorable) + , m_usefulChoice(ReviewsModel::None) + , m_summary(std::move(summary)) + , m_packageVersion(std::move(packageVersion)) +{ +} + +Review::~Review() = default; + +bool Review::operator<(const Review &other) const +{ + return m_creationDate < other.m_creationDate; +} + +bool Review::operator>(const Review &other) const +{ + return m_creationDate > other.m_creationDate; +} + +QString Review::applicationName() const +{ + return m_appName; +} + +QString Review::packageName() const +{ + return m_packageName; +} + +QString Review::packageVersion() const +{ + return m_packageVersion; +} + +QString Review::language() const +{ + return m_language; +} + +QString Review::summary() const +{ + return m_summary; +} + +QString Review::reviewText() const +{ + return m_reviewText; +} + +QString Review::reviewer() const +{ + return m_reviewer; +} + +QDateTime Review::creationDate() const +{ + return m_creationDate; +} + +bool Review::shouldShow() const +{ + return m_shouldShow; +} + +quint64 Review::id() const +{ + return m_id; +} + +int Review::rating() const +{ + return m_rating; +} + +int Review::usefulnessTotal() const +{ + return m_usefulnessTotal; +} + +int Review::usefulnessFavorable() const +{ + return m_usefulnessFavorable; +} + +ReviewsModel::UserChoice Review::usefulChoice() const +{ + return m_usefulChoice; +} + +void Review::setUsefulChoice(ReviewsModel::UserChoice useful) +{ + m_usefulChoice = useful; +} + +void Review::addMetadata(const QString &key, const QVariant &value) +{ + m_metadata.insert(key, value); +} + +QVariant Review::getMetadata(const QString &key) +{ + return m_metadata.value(key); +} diff --git a/libdiscover/ReviewsBackend/Review.h b/libdiscover/ReviewsBackend/Review.h new file mode 100644 index 0000000..037132e --- /dev/null +++ b/libdiscover/ReviewsBackend/Review.h @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +#include "ReviewsModel.h" +#include "discovercommon_export.h" + +class DISCOVERCOMMON_EXPORT Review +{ +public: + Review(QString name, + QString pkgName, + QString language, + QString summary, + QString reviewText, + QString userName, + const QDateTime &date, + bool show, + quint64 id, + int rating, + int usefulTotal, + int usefulFavorable, + QString packageVersion); + ~Review(); + + // Creation date determines greater than/less than + bool operator<(const Review &rhs) const; + bool operator>(const Review &rhs) const; + + QString applicationName() const; + QString packageName() const; + QString packageVersion() const; + QString language() const; + QString summary() const; + QString reviewText() const; + QString reviewer() const; + QDateTime creationDate() const; + bool shouldShow() const; + quint64 id() const; + int rating() const; + int usefulnessTotal() const; + int usefulnessFavorable() const; + ReviewsModel::UserChoice usefulChoice() const; + void setUsefulChoice(ReviewsModel::UserChoice useful); + void addMetadata(const QString &key, const QVariant &value); + QVariant getMetadata(const QString &key); + +private: + QString m_appName; + QDateTime m_creationDate; + bool m_shouldShow; + quint64 m_id; + QString m_language; + QString m_packageName; + int m_rating; + QString m_reviewText; + QString m_reviewer; + int m_usefulnessTotal; + int m_usefulnessFavorable; + ReviewsModel::UserChoice m_usefulChoice; + QString m_summary; + QString m_packageVersion; + QVariantMap m_metadata; +}; diff --git a/libdiscover/ReviewsBackend/ReviewsModel.cpp b/libdiscover/ReviewsBackend/ReviewsModel.cpp new file mode 100644 index 0000000..9750618 --- /dev/null +++ b/libdiscover/ReviewsBackend/ReviewsModel.cpp @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "libdiscover_debug.h" +#include +#include +#include +#include +#include + +ReviewsModel::ReviewsModel(QObject *parent) + : QAbstractListModel(parent) + , m_lastPage(0) +{ +} + +ReviewsModel::~ReviewsModel() = default; + +QHash ReviewsModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles.insert(ShouldShow, "shouldShow"); + roles.insert(Reviewer, "reviewer"); + roles.insert(CreationDate, "date"); + roles.insert(UsefulnessTotal, "usefulnessTotal"); + roles.insert(UsefulnessFavorable, "usefulnessFavorable"); + roles.insert(UsefulChoice, "usefulChoice"); + roles.insert(Rating, "rating"); + roles.insert(Summary, "summary"); + roles.insert(Depth, "depth"); + roles.insert(PackageVersion, "packageVersion"); + return roles; +} + +QVariant ReviewsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + switch (role) { + case Qt::DisplayRole: + return m_reviews.at(index.row())->reviewText(); + case ShouldShow: + return m_reviews.at(index.row())->shouldShow(); + case Reviewer: + return m_reviews.at(index.row())->reviewer(); + case CreationDate: + return m_reviews.at(index.row())->creationDate(); + case UsefulnessTotal: + return m_reviews.at(index.row())->usefulnessTotal(); + case UsefulnessFavorable: + return m_reviews.at(index.row())->usefulnessFavorable(); + case UsefulChoice: + return m_reviews.at(index.row())->usefulChoice(); + case Rating: + return m_reviews.at(index.row())->rating(); + case Summary: + return m_reviews.at(index.row())->summary(); + case PackageVersion: + return m_reviews.at(index.row())->packageVersion(); + case Depth: + return m_reviews.at(index.row())->getMetadata(QStringLiteral("NumberOfParents")).toInt(); + } + return QVariant(); +} + +int ReviewsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_reviews.count(); +} + +AbstractResource *ReviewsModel::resource() const +{ + return m_app; +} + +AbstractReviewsBackend *ReviewsModel::backend() const +{ + return m_backend; +} + +void ReviewsModel::setResource(AbstractResource *app) +{ + if (m_app != app) { + beginResetModel(); + m_reviews.clear(); + m_lastPage = 0; + + if (m_backend) { + disconnect(m_backend, &AbstractReviewsBackend::errorMessageChanged, this, &ReviewsModel::restartFetching); + disconnect(m_backend, &AbstractReviewsBackend::reviewsReady, this, &ReviewsModel::addReviews); + disconnect(m_backend, &AbstractReviewsBackend::fetchingChanged, this, &ReviewsModel::fetchingChanged); + disconnect(m_app, &AbstractResource::versionsChanged, this, &ReviewsModel::restartFetching); + } + m_app = app; + m_backend = app ? app->backend()->reviewsBackend() : nullptr; + if (m_backend) { + connect(m_backend, &AbstractReviewsBackend::errorMessageChanged, this, &ReviewsModel::restartFetching); + connect(m_backend, &AbstractReviewsBackend::reviewsReady, this, &ReviewsModel::addReviews); + connect(m_backend, &AbstractReviewsBackend::fetchingChanged, this, &ReviewsModel::fetchingChanged); + connect(m_app, &AbstractResource::versionsChanged, this, &ReviewsModel::restartFetching); + + QMetaObject::invokeMethod(this, &ReviewsModel::restartFetching, Qt::QueuedConnection); + } + endResetModel(); + Q_EMIT rowsChanged(); + Q_EMIT resourceChanged(); + } +} + +void ReviewsModel::restartFetching() +{ + if (!m_app || !m_backend) + return; + + m_canFetchMore = true; + m_lastPage = 0; + fetchMore(); + Q_EMIT rowsChanged(); +} + +void ReviewsModel::fetchMore(const QModelIndex &parent) +{ + if (!m_backend || !m_app || parent.isValid() || m_backend->isFetching() || !m_canFetchMore) + return; + + m_lastPage++; + m_backend->fetchReviews(m_app, m_lastPage); + // qCDebug(LIBDISCOVER_LOG) << "fetching reviews... " << m_lastPage; +} + +void ReviewsModel::addReviews(AbstractResource *app, const QVector &reviews, bool canFetchMore) +{ + if (app != m_app) + return; + + m_canFetchMore = canFetchMore; + qCDebug(LIBDISCOVER_LOG) << "reviews arrived..." << m_lastPage << reviews.size(); + + if (!reviews.isEmpty()) { + beginInsertRows(QModelIndex(), rowCount(), rowCount() + reviews.size() - 1); + m_reviews += reviews; + endInsertRows(); + Q_EMIT rowsChanged(); + } +} + +bool ReviewsModel::canFetchMore(const QModelIndex & /*parent*/) const +{ + return m_canFetchMore; +} + +void ReviewsModel::markUseful(int row, bool useful) +{ + Review *r = m_reviews[row].data(); + r->setUsefulChoice(useful ? Yes : No); + // qCDebug(LIBDISCOVER_LOG) << "submitting usefulness" << r->applicationName() << r->id() << useful; + m_backend->submitUsefulness(r, useful); + const QModelIndex ind = index(row, 0, QModelIndex()); + Q_EMIT dataChanged(ind, ind, {UsefulnessTotal, UsefulnessFavorable, UsefulChoice}); +} + +void ReviewsModel::deleteReview(int row) +{ + Review *r = m_reviews[row].data(); + m_backend->deleteReview(r); +} + +void ReviewsModel::flagReview(int row, const QString &reason, const QString &text) +{ + Review *r = m_reviews[row].data(); + m_backend->flagReview(r, reason, text); +} + +bool ReviewsModel::isFetching() const +{ + return m_backend && m_backend->isFetching(); +} + +#include "moc_ReviewsModel.cpp" diff --git a/libdiscover/ReviewsBackend/ReviewsModel.h b/libdiscover/ReviewsBackend/ReviewsModel.h new file mode 100644 index 0000000..07d70ae --- /dev/null +++ b/libdiscover/ReviewsBackend/ReviewsModel.h @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include +#include + +class Review; +typedef QSharedPointer ReviewPtr; + +class AbstractResource; +class AbstractReviewsBackend; +class DISCOVERCOMMON_EXPORT ReviewsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(AbstractReviewsBackend *backend READ backend NOTIFY resourceChanged) + Q_PROPERTY(AbstractResource *resource READ resource WRITE setResource NOTIFY resourceChanged) + Q_PROPERTY(int count READ rowCount NOTIFY rowsChanged) + Q_PROPERTY(bool fetching READ isFetching NOTIFY fetchingChanged) +public: + enum Roles { + ShouldShow = Qt::UserRole + 1, + Reviewer, + CreationDate, + UsefulnessTotal, + UsefulnessFavorable, + UsefulChoice, + Rating, + Summary, + Depth, + PackageVersion, + }; + enum UserChoice { + None, + Yes, + No, + }; + Q_ENUM(UserChoice) + + explicit ReviewsModel(QObject *parent = nullptr); + ~ReviewsModel() override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + AbstractReviewsBackend *backend() const; + void setResource(AbstractResource *app); + AbstractResource *resource() const; + void fetchMore(const QModelIndex &parent = QModelIndex()) override; + bool canFetchMore(const QModelIndex & /*parent*/) const override; + QHash roleNames() const override; + bool isFetching() const; + +public Q_SLOTS: + void deleteReview(int row); + void flagReview(int row, const QString &reason, const QString &text); + void markUseful(int row, bool useful); + +private Q_SLOTS: + void addReviews(AbstractResource *app, const QVector &reviews, bool canFetchMore); + void restartFetching(); + +Q_SIGNALS: + void rowsChanged(); + void resourceChanged(); + void fetchingChanged(bool fetching); + +private: + AbstractResource *m_app = nullptr; + AbstractReviewsBackend *m_backend = nullptr; + QVector m_reviews; + int m_lastPage; + bool m_canFetchMore = true; +}; diff --git a/libdiscover/ScreenshotsModel.cpp b/libdiscover/ScreenshotsModel.cpp new file mode 100644 index 0000000..c9d9c98 --- /dev/null +++ b/libdiscover/ScreenshotsModel.cpp @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "ScreenshotsModel.h" +#include "libdiscover_debug.h" +#include "utils.h" +#include +// #include + +ScreenshotsModel::ScreenshotsModel(QObject *parent) + : QAbstractListModel(parent) + , m_resource(nullptr) +{ +} + +QHash ScreenshotsModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles.insert(ThumbnailUrl, "small_image_url"); + roles.insert(ScreenshotUrl, "large_image_url"); + roles.insert(IsAnimatedRole, "isAnimated"); + return roles; +} + +void ScreenshotsModel::setResource(AbstractResource *res) +{ + if (res == m_resource) + return; + + if (m_resource) { + disconnect(m_resource, &AbstractResource::screenshotsFetched, this, &ScreenshotsModel::screenshotsFetched); + } + m_resource = res; + Q_EMIT resourceChanged(res); + + beginResetModel(); + m_screenshots.clear(); + endResetModel(); + + if (res) { + connect(m_resource, &AbstractResource::screenshotsFetched, this, &ScreenshotsModel::screenshotsFetched); + res->fetchScreenshots(); + } else + qCWarning(LIBDISCOVER_LOG) << "empty resource!"; +} + +AbstractResource *ScreenshotsModel::resource() const +{ + return m_resource; +} + +void ScreenshotsModel::screenshotsFetched(const Screenshots &screenshots) +{ + if (screenshots.isEmpty()) + return; + + beginInsertRows(QModelIndex(), m_screenshots.size(), m_screenshots.size() + screenshots.size() - 1); + m_screenshots += screenshots; + endInsertRows(); + Q_EMIT countChanged(); +} + +QVariant ScreenshotsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.parent().isValid()) + return QVariant(); + + switch (role) { + case ThumbnailUrl: + return m_screenshots[index.row()].thumbnail; + case ScreenshotUrl: + return m_screenshots[index.row()].screenshot; + case IsAnimatedRole: + return m_screenshots[index.row()].isAnimated; + } + + return QVariant(); +} + +int ScreenshotsModel::rowCount(const QModelIndex &parent) const +{ + return !parent.isValid() ? m_screenshots.count() : 0; +} + +QUrl ScreenshotsModel::screenshotAt(int row) const +{ + return m_screenshots[row].screenshot; +} + +int ScreenshotsModel::count() const +{ + return m_screenshots.count(); +} + +void ScreenshotsModel::remove(const QUrl &url) +{ + int idxRemove = kIndexOf(m_screenshots, [url](const Screenshot &s) { + return s.thumbnail == url || s.screenshot == url; + }); + if (idxRemove >= 0) { + beginRemoveRows({}, idxRemove, idxRemove); + m_screenshots.removeAt(idxRemove); + endRemoveRows(); + Q_EMIT countChanged(); + + qDebug() << "screenshot removed" << url; + } +} + +#include "moc_ScreenshotsModel.cpp" diff --git a/libdiscover/ScreenshotsModel.h b/libdiscover/ScreenshotsModel.h new file mode 100644 index 0000000..edd1dd1 --- /dev/null +++ b/libdiscover/ScreenshotsModel.h @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include "resources/AbstractResource.h" +#include +#include + +class AbstractResource; + +class DISCOVERCOMMON_EXPORT ScreenshotsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(AbstractResource *application READ resource WRITE setResource NOTIFY resourceChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) +public: + enum Roles { + ThumbnailUrl = Qt::UserRole + 1, + ScreenshotUrl, + IsAnimatedRole, + }; + + explicit ScreenshotsModel(QObject *parent = nullptr); + QHash roleNames() const override; + + AbstractResource *resource() const; + void setResource(AbstractResource *res); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + Q_SCRIPTABLE QUrl screenshotAt(int row) const; + int count() const; + + Q_INVOKABLE void remove(const QUrl &url); + +private Q_SLOTS: + void screenshotsFetched(const Screenshots &screenshots); + +Q_SIGNALS: + void countChanged(); + void resourceChanged(const AbstractResource *resource); + +private: + AbstractResource *m_resource; + Screenshots m_screenshots; +}; diff --git a/libdiscover/Transaction/AddonList.cpp b/libdiscover/Transaction/AddonList.cpp new file mode 100644 index 0000000..648857b --- /dev/null +++ b/libdiscover/Transaction/AddonList.cpp @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2012 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AddonList.h" +#include "libdiscover_debug.h" + +AddonList::AddonList() +{ +} + +bool AddonList::isEmpty() const +{ + return m_toInstall.isEmpty() && m_toRemove.isEmpty(); +} + +QStringList AddonList::addonsToInstall() const +{ + return m_toInstall; +} + +QStringList AddonList::addonsToRemove() const +{ + return m_toRemove; +} + +void AddonList::addAddon(const QString &addon, bool toInstall) +{ + if (toInstall) { + m_toInstall.append(addon); + m_toRemove.removeAll(addon); + } else { + m_toInstall.removeAll(addon); + m_toRemove.append(addon); + } +} + +void AddonList::resetAddon(const QString &addon) +{ + m_toInstall.removeAll(addon); + m_toRemove.removeAll(addon); +} + +void AddonList::clear() +{ + m_toInstall.clear(); + m_toRemove.clear(); +} + +AddonList::State AddonList::addonState(const QString &addonName) const +{ + if (m_toInstall.contains(addonName)) + return ToInstall; + else if (m_toRemove.contains(addonName)) + return ToRemove; + else + return None; +} + +QDebug operator<<(QDebug debug, const AddonList &addons) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "AddonsList("; + debug.nospace() << "install:" << addons.addonsToInstall() << ','; + debug.nospace() << "remove:" << addons.addonsToRemove() << ','; + debug.nospace() << ')'; + return debug; +} diff --git a/libdiscover/Transaction/AddonList.h b/libdiscover/Transaction/AddonList.h new file mode 100644 index 0000000..1ad3a5d --- /dev/null +++ b/libdiscover/Transaction/AddonList.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2012 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +#include "discovercommon_export.h" + +class DISCOVERCOMMON_EXPORT AddonList +{ +public: + enum State { + None, + ToInstall, + ToRemove, + }; + AddonList(); + AddonList(const AddonList &other) = default; + + bool isEmpty() const; + QStringList addonsToInstall() const; + QStringList addonsToRemove() const; + State addonState(const QString &addonName) const; + + void addAddon(const QString &addon, bool toInstall); + void resetAddon(const QString &addon); + void clear(); + +private: + QStringList m_toInstall; + QStringList m_toRemove; +}; + +DISCOVERCOMMON_EXPORT QDebug operator<<(QDebug dbg, const AddonList &addons); diff --git a/libdiscover/Transaction/Transaction.cpp b/libdiscover/Transaction/Transaction.cpp new file mode 100644 index 0000000..ec8c11f --- /dev/null +++ b/libdiscover/Transaction/Transaction.cpp @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2012 Jonathan Thomas +#include +#include + +Transaction::Transaction(QObject *parent, AbstractResource *resource, Role role, const AddonList &addons) + : QObject(parent) + , m_resource(resource) + , m_role(role) + , m_status(CommittingStatus) + , m_addons(addons) + , m_isCancellable(true) + , m_progress(0) +{ +} + +Transaction::~Transaction() +{ + if (status() < DoneStatus || TransactionModel::global()->contains(this)) { + qCWarning(LIBDISCOVER_LOG) << "destroying Transaction before it's over" << this; + TransactionModel::global()->removeTransaction(this); + } +} + +AbstractResource *Transaction::resource() const +{ + return m_resource; +} + +Transaction::Role Transaction::role() const +{ + return m_role; +} + +Transaction::Status Transaction::status() const +{ + return m_status; +} + +AddonList Transaction::addons() const +{ + return m_addons; +} + +bool Transaction::isCancellable() const +{ + return m_isCancellable; +} + +int Transaction::progress() const +{ + return m_progress; +} + +void Transaction::setStatus(Status status) +{ + if (m_status != status) { + m_status = status; + Q_EMIT statusChanged(m_status); + + if (m_status == DoneStatus || m_status == CancelledStatus || m_status == DoneWithErrorStatus) { + setCancellable(false); + + TransactionModel::global()->removeTransaction(this); + } + } +} + +void Transaction::setCancellable(bool isCancellable) +{ + if (m_isCancellable != isCancellable) { + m_isCancellable = isCancellable; + Q_EMIT cancellableChanged(m_isCancellable); + } +} + +void Transaction::setProgress(int progress) +{ + if (m_progress != progress) { + Q_ASSERT(qBound(0, progress, 100) == progress); + m_progress = qBound(0, progress, 100); + Q_EMIT progressChanged(m_progress); + } +} + +bool Transaction::isActive() const +{ + return m_status == DownloadingStatus || m_status == CommittingStatus; +} + +QString Transaction::name() const +{ + return m_resource->name(); +} + +QVariant Transaction::icon() const +{ + return m_resource->icon(); +} + +bool Transaction::isVisible() const +{ + return m_visible; +} + +void Transaction::setVisible(bool visible) +{ + if (m_visible != visible) { + m_visible = visible; + Q_EMIT visibleChanged(visible); + } +} + +void Transaction::setDownloadSpeed(quint64 downloadSpeed) +{ + if (downloadSpeed != m_downloadSpeed) { + m_downloadSpeed = downloadSpeed; + Q_EMIT downloadSpeedChanged(downloadSpeed); + } +} + +QString Transaction::downloadSpeedString() const +{ + return i18nc("@label Download rate", "%1/s", KFormat().formatByteSize(downloadSpeed())); +} + +void Transaction::setRemainingTime(uint remainingTime) +{ + if (remainingTime != m_remainingTime) { + m_remainingTime = remainingTime; + Q_EMIT remainingTimeChanged(remainingTime); + } +} + +QString Transaction::remainingTimeString() const +{ + return KFormat().formatSpelloutDuration(m_remainingTime * 1000); +} + +#include "moc_Transaction.cpp" diff --git a/libdiscover/Transaction/Transaction.h b/libdiscover/Transaction/Transaction.h new file mode 100644 index 0000000..5491515 --- /dev/null +++ b/libdiscover/Transaction/Transaction.h @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: 2012 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +// Qt includes +#include + +// Own includes +#include "AddonList.h" + +#include "discovercommon_export.h" + +class AbstractResource; + +/** + * \class Transaction Transaction.h "Transaction.h" + * + * \brief This is the base class of all transactions. + * + * When there are transactions running inside Discover, the backends should + * provide the corresponding Transaction objects with proper information. + */ +class DISCOVERCOMMON_EXPORT Transaction : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QVariant icon READ icon CONSTANT) + Q_PROPERTY(AbstractResource *resource READ resource CONSTANT) + Q_PROPERTY(Role role READ role CONSTANT) + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + Q_PROPERTY(bool isCancellable READ isCancellable NOTIFY cancellableChanged) + Q_PROPERTY(int progress READ progress NOTIFY progressChanged) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) + Q_PROPERTY(quint64 downloadSpeed READ downloadSpeed WRITE setDownloadSpeed NOTIFY downloadSpeedChanged) + Q_PROPERTY(QString downloadSpeedString READ downloadSpeedString NOTIFY downloadSpeedChanged) + Q_PROPERTY(QString remainingTimeString READ remainingTimeString NOTIFY remainingTimeChanged) + Q_PROPERTY(uint remainingTime READ remainingTime NOTIFY remainingTimeChanged) + +public: + enum Status { + /// Not queued, newly created + SetupStatus = 0, + /// Queued, but not yet run + QueuedStatus, + /// Transaction is in the downloading phase + DownloadingStatus, + /// Transaction is doing an installation/removal + CommittingStatus, + /// Transaction is done + DoneStatus, + /// Transaction is done, but there was an error during transaction + DoneWithErrorStatus, + /// Transaction was cancelled + CancelledStatus, + }; + Q_ENUM(Status) + + enum Role { + /// The transaction is going to install a resource + InstallRole = 0, + /// The transaction is going to remove a resource + RemoveRole, + /// The transaction is going to change the addons of a resource + ChangeAddonsRole, + }; + Q_ENUM(Role) + + Transaction(QObject *parent, AbstractResource *resource, Transaction::Role role, const AddonList &addons = {}); + + ~Transaction() override; + + /** + * @returns the AbstractResource which this transaction works with + */ + AbstractResource *resource() const; + /** + * @returns the role which this transaction executes + */ + Role role() const; + /** + * @returns the current status + */ + Status status() const; + /** + * @returns the addons which this transaction works on + */ + AddonList addons() const; + /** + * @returns true when the transaction can be canceled + */ + bool isCancellable() const; + /** + * @returns a percentage of how much the transaction is already done + */ + int progress() const; + + /** + * Sets the status of the transaction + * @param status the new status + */ + void setStatus(Status status); + /** + * Sets whether the transaction can be canceled or not + * @param isCancellable should be true if the transaction can be canceled + */ + void setCancellable(bool isCancellable); + /** + * Sets the progress of the transaction + * @param progress this should be a percentage of how much of the transaction is already done + */ + void setProgress(int progress); + + /** + * Cancels the transaction + */ + Q_SCRIPTABLE virtual void cancel() = 0; + + /** + * @returns if the transaction is either downloading or committing + */ + bool isActive() const; + + Q_SCRIPTABLE virtual void proceed() + { + } + + /** @returns a name that identifies the transaction */ + virtual QString name() const; + + /** @returns an icon that describes the transaction */ + virtual QVariant icon() const; + + bool isVisible() const; + void setVisible(bool v); + + quint64 downloadSpeed() const + { + return m_downloadSpeed; + } + void setDownloadSpeed(quint64 downloadSpeed); + + uint remainingTime() const + { + return m_remainingTime; + } + void setRemainingTime(uint seconds); + + QString downloadSpeedString() const; + QString remainingTimeString() const; + +private: + AbstractResource *const m_resource; + const Role m_role; + Status m_status; + const AddonList m_addons; + bool m_isCancellable; + int m_progress; + bool m_visible = true; + quint64 m_downloadSpeed = 0; + uint m_remainingTime = 0; + +Q_SIGNALS: + /** + * This gets emitted when the status of the transaction changed + */ + void statusChanged(Transaction::Status status); + /** + * This gets emitted when the ability to cancel the transaction or not changed + */ + void cancellableChanged(bool cancellable); + /** + * This gets emitted when the transaction changed the percentage of how much of it is already done + */ + void progressChanged(int progress); + + /** + * Provides a message to be shown to the user + * + * The user gets to acknowledge and proceed or cancel the transaction. + * + * @sa proceed(), cancel() + */ + void proceedRequest(const QString &title, const QString &description); + + void passiveMessage(const QString &message); + + /** + * A fatal error was found on distro packaging. Provide a @p message to show + * in a modal dialog that should lead the user towards reporting the problem.. + */ + void distroErrorMessage(const QString &message); + + void visibleChanged(bool visible); + + void downloadSpeedChanged(quint64 downloadSpeed); + + void remainingTimeChanged(uint remainingTime); + + void webflowStarted(const QUrl &url, int id); + void webflowDone(int id); +}; diff --git a/libdiscover/Transaction/TransactionListener.cpp b/libdiscover/Transaction/TransactionListener.cpp new file mode 100644 index 0000000..0da2e34 --- /dev/null +++ b/libdiscover/Transaction/TransactionListener.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: 2010 Jonathan Thomas + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "TransactionListener.h" + +#include "../resources/AbstractResource.h" +#include "TransactionModel.h" +#include "libdiscover_debug.h" +#include + +TransactionListener::TransactionListener(QObject *parent) + : QObject(parent) + , m_resource(nullptr) + , m_transaction(nullptr) +{ + connect(TransactionModel::global(), &TransactionModel::transactionAdded, this, &TransactionListener::transactionAdded); +} + +void TransactionListener::cancel() +{ + if (!isCancellable()) { + return; + } + m_transaction->cancel(); +} + +bool TransactionListener::isCancellable() const +{ + return m_transaction && m_transaction->isCancellable(); +} + +bool TransactionListener::isActive() const +{ + return m_transaction && m_transaction->status() != Transaction::SetupStatus; +} + +QString TransactionListener::statusText() const +{ + QModelIndex index = TransactionModel::global()->indexOf(m_resource); + + return index.data(TransactionModel::StatusTextRole).toString(); +} + +void TransactionListener::setResource(AbstractResource *resource) +{ + setResourceInternal(resource); + // Catch already-started transactions + setTransaction(TransactionModel::global()->transactionFromResource(resource)); +} + +void TransactionListener::setResourceInternal(AbstractResource *resource) +{ + if (m_resource == resource) + return; + + m_resource = resource; + Q_EMIT resourceChanged(); +} + +void TransactionListener::transactionAdded(Transaction *trans) +{ + if (trans->resource() != m_resource) + return; + + setTransaction(trans); +} + +class CheckChange +{ +public: + CheckChange(QObject *obj, const QByteArray &prop) + : m_object(obj) + , m_prop(obj->metaObject()->property(obj->metaObject()->indexOfProperty(prop.constData()))) + , m_oldValue(m_prop.read(obj)) + { + Q_ASSERT(obj->metaObject()->indexOfProperty(prop.constData()) >= 0); + } + + ~CheckChange() + { + const QVariant newValue = m_prop.read(m_object); + if (newValue != m_oldValue) { + QMetaMethod m = m_prop.notifySignal(); + m.invoke(m_object, Qt::DirectConnection); + } + } + +private: + QObject *m_object; + QMetaProperty m_prop; + QVariant m_oldValue; +}; + +void TransactionListener::setTransaction(Transaction *trans) +{ + if (m_transaction == trans) { + return; + } + + if (m_transaction) { + disconnect(m_transaction, nullptr, this, nullptr); + } + + CheckChange change1(this, "isCancellable"); + CheckChange change2(this, "isActive"); + CheckChange change3(this, "statusText"); + CheckChange change4(this, "progress"); + + m_transaction = trans; + if (m_transaction) { + connect(m_transaction, &Transaction::cancellableChanged, this, &TransactionListener::cancellableChanged); + connect(m_transaction, &Transaction::statusChanged, this, &TransactionListener::transactionStatusChanged); + connect(m_transaction, &Transaction::progressChanged, this, &TransactionListener::progressChanged); + connect(m_transaction, &QObject::destroyed, this, [this]() { + qCDebug(LIBDISCOVER_LOG) << "destroyed transaction before finishing"; + setTransaction(nullptr); + }); + setResourceInternal(trans->resource()); + } + Q_EMIT transactionChanged(trans); +} + +void TransactionListener::transactionStatusChanged(Transaction::Status status) +{ + switch (status) { + case Transaction::CancelledStatus: + setTransaction(nullptr); + Q_EMIT cancelled(); + break; + case Transaction::DoneWithErrorStatus: + case Transaction::DoneStatus: + setTransaction(nullptr); + break; + default: + break; + } + + Q_EMIT statusTextChanged(); +} + +int TransactionListener::progress() const +{ + return m_transaction ? m_transaction->progress() : 0; +} + +#include "moc_TransactionListener.cpp" diff --git a/libdiscover/Transaction/TransactionListener.h b/libdiscover/Transaction/TransactionListener.h new file mode 100644 index 0000000..b1ec348 --- /dev/null +++ b/libdiscover/Transaction/TransactionListener.h @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2010 Jonathan Thomas + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include "Transaction.h" +#include "discovercommon_export.h" + +class AbstractResource; + +class DISCOVERCOMMON_EXPORT TransactionListener : public QObject +{ + Q_OBJECT + Q_PROPERTY(AbstractResource *resource READ resource WRITE setResource NOTIFY resourceChanged) + Q_PROPERTY(Transaction *transaction READ transaction WRITE setTransaction NOTIFY transactionChanged) + Q_PROPERTY(bool isCancellable READ isCancellable NOTIFY cancellableChanged) + Q_PROPERTY(bool isActive READ isActive NOTIFY isActiveChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(int progress READ progress NOTIFY progressChanged) +public: + explicit TransactionListener(QObject *parent = nullptr); + + AbstractResource *resource() const + { + return m_resource; + } + Transaction *transaction() const + { + return m_transaction; + } + bool isCancellable() const; + bool isActive() const; + QString statusText() const; + int progress() const; + + Q_SCRIPTABLE void cancel(); + + void setResource(AbstractResource *resource); + void setTransaction(Transaction *trans); + +private: + void setResourceInternal(AbstractResource *resource); + + AbstractResource *m_resource = nullptr; + Transaction *m_transaction = nullptr; + +private Q_SLOTS: + void transactionAdded(Transaction *trans); + void transactionStatusChanged(Transaction::Status status); + +Q_SIGNALS: + void resourceChanged(); + void cancellableChanged(); + void isActiveChanged(); + void statusTextChanged(); + void cancelled(); + void progressChanged(); + void transactionChanged(Transaction *transaction); +}; diff --git a/libdiscover/Transaction/TransactionModel.cpp b/libdiscover/Transaction/TransactionModel.cpp new file mode 100644 index 0000000..e002fe8 --- /dev/null +++ b/libdiscover/Transaction/TransactionModel.cpp @@ -0,0 +1,217 @@ +/* + * SPDX-FileCopyrightText: 2012 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "TransactionModel.h" + +// Qt includes +#include +#include +#include + +// Own includes +#include "libdiscover_debug.h" +#include "resources/AbstractResource.h" + +Q_GLOBAL_STATIC(TransactionModel, globalTransactionModel) + +TransactionModel *TransactionModel::global() +{ + return globalTransactionModel; +} + +TransactionModel::TransactionModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(this, &QAbstractItemModel::rowsInserted, this, &TransactionModel::countChanged); + connect(this, &QAbstractItemModel::rowsRemoved, this, &TransactionModel::countChanged); + connect(this, &TransactionModel::countChanged, this, &TransactionModel::progressChanged); +} + +QHash TransactionModel::roleNames() const +{ + QHash roles; + roles[TransactionRoleRole] = "transactionRole"; + roles[TransactionStatusRole] = "status"; + roles[CancellableRole] = "cancellable"; + roles[ProgressRole] = "progress"; + roles[StatusTextRole] = "statusText"; + roles[ResourceRole] = "resource"; + roles[TransactionRole] = "transaction"; + return roles; +} + +int TransactionModel::rowCount(const QModelIndex &parent) const +{ + // Root element parents all children + if (!parent.isValid()) + return m_transactions.size(); + + // Child elements have no children themselves + return 0; +} + +QVariant TransactionModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + Transaction *trans = m_transactions[index.row()]; + switch (role) { + case TransactionRoleRole: + return trans->role(); + case TransactionStatusRole: + return trans->status(); + case CancellableRole: + return trans->isCancellable(); + case ProgressRole: + return trans->progress(); + case StatusTextRole: + switch (trans->status()) { + case Transaction::SetupStatus: + return i18nc("@info:status", "Starting"); + case Transaction::QueuedStatus: + return i18nc("@info:status", "Waiting"); + case Transaction::DownloadingStatus: + return i18nc("@info:status", "Downloading"); + case Transaction::CommittingStatus: + switch (trans->role()) { + case Transaction::InstallRole: + return i18nc("@info:status", "Installing"); + case Transaction::RemoveRole: + return i18nc("@info:status", "Removing"); + case Transaction::ChangeAddonsRole: + return i18nc("@info:status", "Changing Addons"); + } + break; + case Transaction::DoneStatus: + return i18nc("@info:status", "Done"); + case Transaction::DoneWithErrorStatus: + return i18nc("@info:status", "Failed"); + case Transaction::CancelledStatus: + return i18nc("@info:status", "Cancelled"); + } + break; + case TransactionRole: + return QVariant::fromValue(trans); + case ResourceRole: + return QVariant::fromValue(trans->resource()); + } + + return QVariant(); +} + +Transaction *TransactionModel::transactionFromResource(AbstractResource *resource) const +{ + Transaction *ret = nullptr; + + for (Transaction *trans : qAsConst(m_transactions)) { + if (trans->resource() == resource) { + ret = trans; + break; + } + } + + return ret; +} + +QModelIndex TransactionModel::indexOf(Transaction *trans) const +{ + int row = m_transactions.indexOf(trans); + QModelIndex ret = index(row); + Q_ASSERT(!trans || ret.isValid()); + return ret; +} + +QModelIndex TransactionModel::indexOf(AbstractResource *res) const +{ + Transaction *trans = transactionFromResource(res); + + return indexOf(trans); +} + +void TransactionModel::addTransaction(Transaction *trans) +{ + if (!trans) + return; + + if (m_transactions.contains(trans)) + return; + + if (m_transactions.isEmpty()) + Q_EMIT startingFirstTransaction(); + + int before = m_transactions.size(); + beginInsertRows(QModelIndex(), before, before + 1); + m_transactions.append(trans); + + if (before == 0) { // Should emit before count changes + Q_EMIT mainTransactionTextChanged(); + } + endInsertRows(); + + connect(trans, &Transaction::statusChanged, this, [this]() { + transactionChanged(StatusTextRole); + }); + connect(trans, &Transaction::cancellableChanged, this, [this]() { + transactionChanged(CancellableRole); + }); + connect(trans, &Transaction::progressChanged, this, [this]() { + transactionChanged(ProgressRole); + Q_EMIT progressChanged(); + }); + + Q_EMIT transactionAdded(trans); +} + +void TransactionModel::removeTransaction(Transaction *trans) +{ + Q_ASSERT(trans); + trans->deleteLater(); + int r = m_transactions.indexOf(trans); + if (r < 0) { + qCWarning(LIBDISCOVER_LOG) << "transaction not part of the model" << trans; + return; + } + + disconnect(trans, nullptr, this, nullptr); + + beginRemoveRows(QModelIndex(), r, r); + m_transactions.removeAt(r); + endRemoveRows(); + + Q_EMIT transactionRemoved(trans); + if (m_transactions.isEmpty()) + Q_EMIT lastTransactionFinished(); + + if (r == 0) { + Q_EMIT mainTransactionTextChanged(); + } +} + +void TransactionModel::transactionChanged(int role) +{ + Transaction *trans = qobject_cast(sender()); + QModelIndex transIdx = indexOf(trans); + Q_EMIT dataChanged(transIdx, transIdx, {role}); +} + +int TransactionModel::progress() const +{ + int sum = 0; + int count = 0; + for (Transaction *t : qAsConst(m_transactions)) { + if (t->isActive() && t->isVisible()) { + sum += t->progress(); + ++count; + } + } + return count == 0 ? 0 : sum / count; +} + +QString TransactionModel::mainTransactionText() const +{ + return m_transactions.isEmpty() ? QString() : m_transactions.constFirst()->name(); +} diff --git a/libdiscover/Transaction/TransactionModel.h b/libdiscover/Transaction/TransactionModel.h new file mode 100644 index 0000000..46066eb --- /dev/null +++ b/libdiscover/Transaction/TransactionModel.h @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2012 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include "Transaction.h" + +#include "discovercommon_export.h" + +class DISCOVERCOMMON_EXPORT TransactionModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int progress READ progress NOTIFY progressChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(QString mainTransactionText READ mainTransactionText NOTIFY mainTransactionTextChanged) +public: + enum Roles { + TransactionRoleRole = Qt::UserRole, + TransactionStatusRole, + CancellableRole, + ProgressRole, + StatusTextRole, + ResourceRole, + TransactionRole, + }; + + explicit TransactionModel(QObject *parent = nullptr); + static TransactionModel *global(); + + // Reimplemented from QAbstractListModel + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_SCRIPTABLE Transaction *transactionFromResource(AbstractResource *resource) const; + QModelIndex indexOf(Transaction *trans) const; + QModelIndex indexOf(AbstractResource *res) const; + + void addTransaction(Transaction *trans); + void removeTransaction(Transaction *trans); + + bool contains(Transaction *transaction) const + { + return m_transactions.contains(transaction); + } + int progress() const; + QVector transactions() const + { + return m_transactions; + } + + QString mainTransactionText() const; + +private: + QVector m_transactions; + +Q_SIGNALS: + void startingFirstTransaction(); + void lastTransactionFinished(); + void transactionAdded(Transaction *trans); + void transactionRemoved(Transaction *trans); + void countChanged(); + void progressChanged(); + void proceedRequest(Transaction *transaction, const QString &title, const QString &description); + void mainTransactionTextChanged(); + +private Q_SLOTS: + void transactionChanged(int role); +}; diff --git a/libdiscover/UpdateModel/UpdateItem.cpp b/libdiscover/UpdateModel/UpdateItem.cpp new file mode 100644 index 0000000..db70b70 --- /dev/null +++ b/libdiscover/UpdateModel/UpdateItem.cpp @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "UpdateItem.h" +#include +#include +#include + +#include "libdiscover_debug.h" +#include +#include + +UpdateItem::UpdateItem(AbstractResource *app) + : m_app(app) +{ +} + +UpdateItem::~UpdateItem() +{ +} + +AbstractResource *UpdateItem::app() const +{ + return m_app; +} + +QString UpdateItem::name() const +{ + return m_app->name(); +} + +QVariant UpdateItem::icon() const +{ + return m_app->icon(); +} + +qint64 UpdateItem::size() const +{ + return m_app->size(); +} + +static bool isMarked(AbstractResource *res) +{ + return res->backend()->backendUpdater()->isMarked(res); +} + +Qt::CheckState UpdateItem::checked() const +{ + return isMarked(app()) ? Qt::Checked : Qt::Unchecked; +} + +qreal UpdateItem::progress() const +{ + return m_progress; +} + +void UpdateItem::setProgress(qreal progress) +{ + m_progress = progress; +} + +QString UpdateItem::changelog() const +{ + return m_changelog; +} + +void UpdateItem::setChangelog(const QString &changelog) +{ + m_changelog = changelog; +} diff --git a/libdiscover/UpdateModel/UpdateItem.h b/libdiscover/UpdateModel/UpdateItem.h new file mode 100644 index 0000000..e38ba85 --- /dev/null +++ b/libdiscover/UpdateModel/UpdateItem.h @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +// Qt includes +#include "discovercommon_export.h" +#include "resources/AbstractBackendUpdater.h" +#include +#include + +#include + +class AbstractResource; +class DISCOVERCOMMON_EXPORT UpdateItem +{ +public: + explicit UpdateItem(AbstractResource *app); + + ~UpdateItem(); + + void setProgress(qreal progress); + qreal progress() const; + + AbstractBackendUpdater::State state() const + { + return m_state; + } + void setState(AbstractBackendUpdater::State state) + { + m_state = state; + } + + QString changelog() const; + void setChangelog(const QString &changelog); + + AbstractResource *app() const; + QString name() const; + QVariant icon() const; + qint64 size() const; + Qt::CheckState checked() const; + + void setExtended(bool extended) + { + m_isExtended = extended; + } + AbstractResource *resource() const + { + return m_app; + } + bool isVisible() const + { + return m_visible; + } + bool isExtended() const + { + return m_isExtended; + } + void setVisible(bool visible) + { + m_visible = visible; + } + +private: + AbstractResource *const m_app; + + const QString m_categoryName; + const QIcon m_categoryIcon; + qreal m_progress = 0.; + bool m_visible = true; + AbstractBackendUpdater::State m_state = AbstractBackendUpdater::None; + QString m_changelog; + bool m_isExtended = false; +}; diff --git a/libdiscover/UpdateModel/UpdateModel.cpp b/libdiscover/UpdateModel/UpdateModel.cpp new file mode 100644 index 0000000..383b005 --- /dev/null +++ b/libdiscover/UpdateModel/UpdateModel.cpp @@ -0,0 +1,385 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "UpdateModel.h" + +// Qt includes +#include "libdiscover_debug.h" +#include +#include + +// KDE includes +#include +#include + +// Own includes +#include "UpdateItem.h" +#include +#include +#include + +UpdateModel::UpdateModel(QObject *parent) + : QAbstractListModel(parent) + , m_updateSizeTimer(new QTimer(this)) + , m_updates(nullptr) +{ + connect(ResourcesModel::global(), &ResourcesModel::fetchingChanged, this, &UpdateModel::activityChanged); + connect(ResourcesModel::global(), &ResourcesModel::updatesCountChanged, this, &UpdateModel::activityChanged); + connect(ResourcesModel::global(), &ResourcesModel::resourceDataChanged, this, &UpdateModel::resourceDataChanged); + connect(this, &UpdateModel::toUpdateChanged, this, &UpdateModel::updateSizeChanged); + + m_updateSizeTimer->setInterval(100); + m_updateSizeTimer->setSingleShot(true); + connect(m_updateSizeTimer, &QTimer::timeout, this, &UpdateModel::updateSizeChanged); +} + +UpdateModel::~UpdateModel() +{ + qDeleteAll(m_updateItems); + m_updateItems.clear(); +} + +QHash UpdateModel::roleNames() const +{ + auto ret = QAbstractItemModel::roleNames(); + ret.insert(Qt::CheckStateRole, "checked"); + ret.insert(ResourceProgressRole, "resourceProgress"); + ret.insert(ResourceStateRole, "resourceState"); + ret.insert(ResourceRole, "resource"); + ret.insert(SizeRole, "size"); + ret.insert(SectionRole, "section"); + ret.insert(ChangelogRole, "changelog"); + ret.insert(UpgradeTextRole, "upgradeText"); + ret.insert(ExtendedRole, "extended"); + return ret; +} + +void UpdateModel::setBackend(ResourcesUpdatesModel *updates) +{ + if (m_updates) { + disconnect(m_updates, nullptr, this, nullptr); + } + + m_updates = updates; + + connect(m_updates, &ResourcesUpdatesModel::progressingChanged, this, &UpdateModel::activityChanged); + connect(m_updates, &ResourcesUpdatesModel::resourceProgressed, this, &UpdateModel::resourceHasProgressed); + + activityChanged(); +} + +void UpdateModel::resourceHasProgressed(AbstractResource *res, qreal progress, AbstractBackendUpdater::State state) +{ + UpdateItem *item = itemFromResource(res); + if (!item) + return; + item->setProgress(progress); + item->setState(state); + + const QModelIndex idx = indexFromItem(item); + Q_EMIT dataChanged(idx, idx, {ResourceProgressRole, ResourceStateRole, SectionResourceProgressRole}); +} + +void UpdateModel::activityChanged() +{ + if (m_updates) { + if (!m_updates->isProgressing()) { + m_updates->prepare(); + setResources(m_updates->toUpdate()); + + for (auto item : qAsConst(m_updateItems)) { + item->setProgress(0); + } + } else + setResources(m_updates->toUpdate()); + } +} + +QVariant UpdateModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + UpdateItem *item = itemFromIndex(index); + + switch (role) { + case Qt::DisplayRole: + return item->name(); + case Qt::DecorationRole: + return item->icon(); + case Qt::CheckStateRole: + return item->checked(); + case SizeRole: + return item->size() > 0 ? KFormat().formatByteSize(item->size()) : i18n("Unknown"); + case ResourceRole: + return QVariant::fromValue(item->resource()); + case ResourceProgressRole: + return item->progress(); + case ResourceStateRole: + return item->state(); + case ChangelogRole: + return item->changelog(); + case ExtendedRole: + return item->isExtended(); + case SectionRole: { + static const QString appUpdatesSection = i18nc("@item:inlistbox", "Applications"); + static const QString systemUpdateSection = i18nc("@item:inlistbox", "System Software"); + static const QString addonsSection = i18nc("@item:inlistbox", "Addons"); + switch (item->resource()->type()) { + case AbstractResource::Application: + return appUpdatesSection; + case AbstractResource::Technical: + return systemUpdateSection; + case AbstractResource::Addon: + return addonsSection; + } + Q_UNREACHABLE(); + } + case SectionResourceProgressRole: + return (100 - item->progress()) + (101 * item->resource()->type()); + default: + break; + } + + return QVariant(); +} + +void UpdateModel::checkResources(const QList &resource, bool checked) +{ + if (checked) + m_updates->addResources(resource); + else + m_updates->removeResources(resource); +} + +Qt::ItemFlags UpdateModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +int UpdateModel::rowCount(const QModelIndex &parent) const +{ + return !parent.isValid() ? m_updateItems.count() : 0; +} + +bool UpdateModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + if (role == Qt::CheckStateRole) { + UpdateItem *item = itemFromIndex(idx); + const bool newValue = value.toInt() == Qt::Checked; + const QList apps = {item->app()}; + + checkResources(apps, newValue); + Q_ASSERT(idx.data(Qt::CheckStateRole) == value); + + // When un/checking some backends will decide to add or remove a bunch of packages, so refresh it all + auto m = idx.model(); + Q_EMIT dataChanged(m->index(0, 0), m->index(m->rowCount() - 1, 0), {Qt::CheckStateRole}); + Q_EMIT toUpdateChanged(); + + return true; + } else if (role == ExtendedRole) { + UpdateItem *item = itemFromIndex(idx); + if (item->isExtended() != value.toBool()) { + item->setExtended(value.toBool()); + Q_EMIT dataChanged(idx, idx, {ExtendedRole}); + } + } + + return false; +} + +void UpdateModel::fetchUpdateDetails(int row) +{ + UpdateItem *item = itemFromIndex(index(row, 0)); + Q_ASSERT(item); + if (!item) + return; + + item->app()->fetchUpdateDetails(); +} + +void UpdateModel::integrateChangelog(const QString &changelog) +{ + auto app = qobject_cast(sender()); + Q_ASSERT(app); + auto item = itemFromResource(app); + if (!item) + return; + + item->setChangelog(changelog); + + const QModelIndex idx = indexFromItem(item); + Q_ASSERT(idx.isValid()); + Q_EMIT dataChanged(idx, idx, {ChangelogRole}); +} + +void UpdateModel::setResources(const QList &resources) +{ + if (resources == m_resources) { + return; + } + m_resources = resources; + + beginResetModel(); + qDeleteAll(m_updateItems); + m_updateItems.clear(); + + QVector appItems, systemItems, addonItems; + for (AbstractResource *res : resources) { + connect(res, &AbstractResource::changelogFetched, this, &UpdateModel::integrateChangelog, Qt::UniqueConnection); + + UpdateItem *updateItem = new UpdateItem(res); + + switch (res->type()) { + case AbstractResource::Technical: + systemItems += updateItem; + break; + case AbstractResource::Application: + appItems += updateItem; + break; + case AbstractResource::Addon: + addonItems += updateItem; + break; + } + } + const auto sortUpdateItems = [](UpdateItem *a, UpdateItem *b) { + return a->name() < b->name(); + }; + std::sort(appItems.begin(), appItems.end(), sortUpdateItems); + std::sort(systemItems.begin(), systemItems.end(), sortUpdateItems); + std::sort(addonItems.begin(), addonItems.end(), sortUpdateItems); + m_updateItems = (QVector() << appItems << addonItems << systemItems); + endResetModel(); + + Q_EMIT hasUpdatesChanged(!resources.isEmpty()); + Q_EMIT toUpdateChanged(); +} + +bool UpdateModel::hasUpdates() const +{ + return rowCount() > 0; +} + +ResourcesUpdatesModel *UpdateModel::backend() const +{ + return m_updates; +} + +int UpdateModel::toUpdateCount() const +{ + int ret = 0; + QSet packages; + for (UpdateItem *item : qAsConst(m_updateItems)) { + const auto packageName = item->resource()->packageName(); + if (packages.contains(packageName)) { + continue; + } + packages.insert(packageName); + ret += item->checked() != Qt::Unchecked ? 1 : 0; + } + return ret; +} + +int UpdateModel::totalUpdatesCount() const +{ + int ret = 0; + QSet packages; + for (UpdateItem *item : qAsConst(m_updateItems)) { + const auto packageName = item->resource()->packageName(); + if (packages.contains(packageName)) { + continue; + } + packages.insert(packageName); + ret += 1; + } + return ret; +} + +UpdateItem *UpdateModel::itemFromResource(AbstractResource *res) +{ + for (UpdateItem *item : qAsConst(m_updateItems)) { + if (item->app() == res) + return item; + } + return nullptr; +} + +QString UpdateModel::updateSize() const +{ + if (!m_updates) { + return QString(); + } + if (m_updates->updateSize() != 0) { + return KFormat().formatByteSize(m_updates->updateSize()); + } + return i18n("Unknown"); +} + +QModelIndex UpdateModel::indexFromItem(UpdateItem *item) const +{ + return index(m_updateItems.indexOf(item), 0, {}); +} + +UpdateItem *UpdateModel::itemFromIndex(const QModelIndex &index) const +{ + return m_updateItems[index.row()]; +} + +void UpdateModel::resourceDataChanged(AbstractResource *res, const QVector &properties) +{ + auto item = itemFromResource(res); + if (!item) + return; + + const auto index = indexFromItem(item); + if (properties.contains("state")) + Q_EMIT dataChanged(index, index, {SizeRole, UpgradeTextRole}); + else if (properties.contains("size")) { + Q_EMIT dataChanged(index, index, {SizeRole}); + m_updateSizeTimer->start(); + } +} + +void UpdateModel::checkAll() +{ + QList updatedItems; + + for (int i = 0, c = rowCount(); i < c; ++i) { + auto idx = index(i); + if (idx.data(Qt::CheckStateRole) != Qt::Checked) { + updatedItems.append(itemFromIndex(idx)->app()); + } + } + + checkResources(updatedItems, true); + + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {Qt::CheckStateRole}); + Q_EMIT toUpdateChanged(); +} + +void UpdateModel::uncheckAll() +{ + QList updatedItems; + + for (int i = 0, c = rowCount(); i < c; ++i) { + auto idx = index(i); + if (idx.data(Qt::CheckStateRole) != Qt::Unchecked) { + updatedItems.append(itemFromIndex(idx)->app()); + } + } + + checkResources(updatedItems, false); + + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {Qt::CheckStateRole}); + Q_EMIT toUpdateChanged(); +} + +#include "moc_UpdateModel.cpp" diff --git a/libdiscover/UpdateModel/UpdateModel.h b/libdiscover/UpdateModel/UpdateModel.h new file mode 100644 index 0000000..09e9d02 --- /dev/null +++ b/libdiscover/UpdateModel/UpdateModel.h @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2011 Jonathan Thomas + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include "resources/AbstractBackendUpdater.h" +#include + +class QTimer; +class ResourcesUpdatesModel; +class AbstractResource; +class UpdateItem; + +class DISCOVERCOMMON_EXPORT UpdateModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(ResourcesUpdatesModel *backend READ backend WRITE setBackend) + Q_PROPERTY(bool hasUpdates READ hasUpdates NOTIFY hasUpdatesChanged) + Q_PROPERTY(int toUpdateCount READ toUpdateCount NOTIFY toUpdateChanged) + Q_PROPERTY(int totalUpdatesCount READ totalUpdatesCount NOTIFY hasUpdatesChanged) + Q_PROPERTY(QString updateSize READ updateSize NOTIFY updateSizeChanged) +public: + enum Roles { + SizeRole = Qt::UserRole + 1, + ResourceRole, + ResourceProgressRole, + ResourceStateRole, + SectionResourceProgressRole, + ChangelogRole, + SectionRole, + UpgradeTextRole, + ExtendedRole, + }; + Q_ENUM(Roles) + + explicit UpdateModel(QObject *parent = nullptr); + ~UpdateModel() override; + + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + void setResources(const QList &res); + UpdateItem *itemFromIndex(const QModelIndex &index) const; + + void checkResources(const QList &resource, bool checked); + QHash roleNames() const override; + + bool hasUpdates() const; + + /// all upgradeable packages + int totalUpdatesCount() const; + + /// packages marked to upgrade + int toUpdateCount() const; + + Q_SCRIPTABLE void fetchUpdateDetails(int row); + + QString updateSize() const; + + ResourcesUpdatesModel *backend() const; + +public Q_SLOTS: + void checkAll(); + void uncheckAll(); + + void setBackend(ResourcesUpdatesModel *updates); + +Q_SIGNALS: + void hasUpdatesChanged(bool hasUpdates); + void toUpdateChanged(); + void updateSizeChanged(); + +private: + void resourceDataChanged(AbstractResource *res, const QVector &properties); + void integrateChangelog(const QString &changelog); + QModelIndex indexFromItem(UpdateItem *item) const; + UpdateItem *itemFromResource(AbstractResource *res); + void resourceHasProgressed(AbstractResource *res, qreal progress, AbstractBackendUpdater::State state); + void activityChanged(); + + QTimer *const m_updateSizeTimer; + QVector m_updateItems; + ResourcesUpdatesModel *m_updates; + QList m_resources; +}; diff --git a/libdiscover/appstream/AppStreamIntegration.cpp b/libdiscover/appstream/AppStreamIntegration.cpp new file mode 100644 index 0000000..046f8c7 --- /dev/null +++ b/libdiscover/appstream/AppStreamIntegration.cpp @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AppStreamIntegration.h" + +AppStreamIntegration *AppStreamIntegration::global() +{ + static AppStreamIntegration *var = nullptr; + if (!var) { + var = new AppStreamIntegration; + } + + return var; +} + +QSharedPointer AppStreamIntegration::reviews() +{ + QSharedPointer ret; + if (m_reviews) { + ret = m_reviews; + } else { + ret = QSharedPointer(new OdrsReviewsBackend()); + m_reviews = ret; + } + return ret; +} diff --git a/libdiscover/appstream/AppStreamIntegration.h b/libdiscover/appstream/AppStreamIntegration.h new file mode 100644 index 0000000..1c1cf7f --- /dev/null +++ b/libdiscover/appstream/AppStreamIntegration.h @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "OdrsReviewsBackend.h" +#include "discovercommon_export.h" +#include +#include + +class DISCOVERCOMMON_EXPORT AppStreamIntegration : public QObject +{ + Q_OBJECT +public: + static AppStreamIntegration *global(); + + QSharedPointer reviews(); + KOSRelease *osRelease() + { + return &m_osrelease; + } + +private: + QSharedPointer m_reviews; + KOSRelease m_osrelease; + + AppStreamIntegration() + { + } +}; diff --git a/libdiscover/appstream/AppStreamUtils.cpp b/libdiscover/appstream/AppStreamUtils.cpp new file mode 100644 index 0000000..77f117b --- /dev/null +++ b/libdiscover/appstream/AppStreamUtils.cpp @@ -0,0 +1,299 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AppStreamUtils.h" + +#include "utils.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include + +using namespace AppStreamUtils; + +QUrl AppStreamUtils::imageOfKind(const QList &images, AppStream::Image::Kind kind) +{ + QUrl ret; + for (const AppStream::Image &i : images) { + if (i.kind() == kind) { + ret = i.url(); + break; + } + } + return ret; +} + +QString AppStreamUtils::changelogToHtml(const AppStream::Component &appdata) +{ +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto releases = appdata.releasesPlain(); +#else + const auto releases = appdata.releases(); +#endif + if (releases.isEmpty()) { + return {}; + } + +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto release = releases.indexSafe(0).value(); +#else + const auto release = releases.constFirst(); +#endif + if (release.description().isEmpty()) + return {}; + + QString changelog = + QLatin1String("

") + release.version() + QLatin1String("

") + QStringLiteral("

") + release.description() + QStringLiteral("

"); + return changelog; +} + +Screenshots AppStreamUtils::fetchScreenshots(const AppStream::Component &appdata) +{ +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto appdataScreenshots = appdata.screenshotsAll(); +#else + const auto appdataScreenshots = appdata.screenshots(); +#endif + Screenshots ret; + ret.reserve(appdataScreenshots.size()); + for (const AppStream::Screenshot &s : appdataScreenshots) { + const auto images = s.images(); + const QUrl thumbnail = AppStreamUtils::imageOfKind(images, AppStream::Image::KindThumbnail); + const QUrl plain = AppStreamUtils::imageOfKind(images, AppStream::Image::KindSource); + if (plain.isEmpty()) + qWarning() << "invalid screenshot for" << appdata.name(); + + ret.append(Screenshot{plain, thumbnail.isEmpty() ? plain : thumbnail, s.mediaKind() == AppStream::Screenshot::MediaKindVideo}); + } + return ret; +} + +QJsonArray AppStreamUtils::licenses(const AppStream::Component &appdata) +{ + return licenses(appdata.projectLicense()); +} + +QJsonArray AppStreamUtils::licenses(const QString &spdx) +{ + static const QSet tokens = {'&', '+', '|', '^', '(', ')'}; + + QJsonArray ret; + const auto licenses = AppStream::SPDX::tokenizeLicense(spdx); + for (const auto &token : licenses) { + if (token.size() == 1 && tokens.contains(token.at(0))) + continue; + ret += license(token.mid(1)); // tokenize prefixes with an @ for some reason + } + return ret; +} + +QJsonObject AppStreamUtils::license(const QString &license) +{ + bool publicLicense = false; + QString name = license; + if (license.startsWith(QLatin1String("LicenseRef-proprietary"))) { + name = i18n("Proprietary"); + } else if (license == QLatin1String("LicenseRef-public-domain")) { + name = i18n("Public Domain"); + publicLicense = true; + } + + if (license.isEmpty()) { + return { + {QStringLiteral("name"), i18n("Unknown")}, + {QStringLiteral("hasFreedom"), true}, // give it the benefit of the doubt + }; + } + if (!AppStream::SPDX::isLicenseId(license)) + return { + {QStringLiteral("name"), name}, + {QStringLiteral("hasFreedom"), true}, // give it the benefit of the doubt + }; + return { + {QStringLiteral("name"), name}, + {QStringLiteral("url"), {AppStream::SPDX::licenseUrl(license)}}, + {QStringLiteral("hasFreedom"), AppStream::SPDX::isFreeLicense(license) || publicLicense}, + }; +} + +QStringList AppStreamUtils::appstreamIds(const QUrl &appstreamUrl) +{ + QStringList ret; + ret += appstreamUrl.host().isEmpty() ? appstreamUrl.path() : appstreamUrl.host(); + if (appstreamUrl.hasQuery()) { + QUrlQuery query(appstreamUrl); + ret << query.queryItemValue(QStringLiteral("alt")).split(QLatin1Char(','), Qt::SkipEmptyParts); + } + if (ret.removeDuplicates() != 0) { + qDebug() << "received malformed url" << appstreamUrl; + } + return ret; +} + +QString AppStreamUtils::versionString(const QString &version, const AppStream::Component &appdata) +{ + if (version.isEmpty()) { + return {}; + } else { +#if ASQ_CHECK_VERSION(1, 0, 0) + if (appdata.releasesPlain().isEmpty()) { + return version; + } + auto release = appdata.releasesPlain().indexSafe(0).value(); +#else + if (appdata.releases().isEmpty()) { + return version; + } + auto release = appdata.releases().constFirst(); +#endif + + if (release.timestamp().isValid() && version.startsWith(release.version())) { + QLocale l; + return i18n("%1, released on %2", version, l.toString(release.timestamp().date(), QLocale::ShortFormat)); + } else { + return version; + } + } +} + +QString AppStreamUtils::contentRatingDescription(const AppStream::Component &appdata) +{ +#if ASQ_CHECK_VERSION(0, 15, 6) + const auto ratings = appdata.contentRatings(); + QString ret; + for (const auto &r : ratings) { + const auto ratingIds = r.ratingIds(); + for (const auto &id : ratingIds) { + if (r.value(id) != AppStream::ContentRating::RatingValueNone) { + ret += QLatin1String("* ") + r.description(id) + QLatin1Char('\n'); + } + } + } + + return ret; +#else + Q_UNUSED(appdata); + return {}; +#endif +} + +QString AppStreamUtils::contentRatingText(const AppStream::Component &appdata) +{ +#if ASQ_CHECK_VERSION(0, 15, 6) + const auto ratings = appdata.contentRatings(); + AppStream::ContentRating::RatingValue intensity = AppStream::ContentRating::RatingValueUnknown; + for (const auto &r : ratings) { + const auto ratingIds = r.ratingIds(); + for (const auto &id : ratingIds) { + intensity = std::max(r.value(id), intensity); + } + } + + static QStringList texts = { + {}, + i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of content suitable for everyone", "All Audiences"), + i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of content with relatively benign themes only unsuitable for very young " + "children, such as minor cartoon violence or mild profanity", + "Mild Content"), + i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of content with some intense themes, such as somewhat realistic " + "violence, references to sexuality, or adult profanity", + "Moderate Content"), + i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of mature content that could be quite objectionable or unsuitable for " + "young audiences, such as realistic graphic violence, extreme profanity or nudity, or glorification of drug use", + "Intense Content"), + }; + return texts[intensity]; +#else + Q_UNUSED(appdata); + return {}; +#endif +} + +AbstractResource::ContentIntensity AppStreamUtils::contentRatingIntensity(const AppStream::Component &appdata) +{ +#if ASQ_CHECK_VERSION(0, 15, 6) + const auto ratings = appdata.contentRatings(); + AppStream::ContentRating::RatingValue intensity = AppStream::ContentRating::RatingValueUnknown; + for (const auto &r : ratings) { + const auto ratingIds = r.ratingIds(); + for (const auto &id : ratingIds) { + intensity = std::max(r.value(id), intensity); + } + } + + static QVector intensities = { + AbstractResource::Mild, + AbstractResource::Mild, + AbstractResource::Mild, + AbstractResource::Intense, + AbstractResource::Intense, + }; + return intensities[intensity]; +#else + Q_UNUSED(appdata); + return {}; +#endif +} + +uint AppStreamUtils::contentRatingMinimumAge(const AppStream::Component &appdata) +{ +#if ASQ_CHECK_VERSION(0, 15, 6) + const auto ratings = appdata.contentRatings(); + uint minimumAge = 0; + for (const auto &r : ratings) { + minimumAge = std::max(r.minimumAge(), minimumAge); + } + return minimumAge; +#else + Q_UNUSED(appdata); + return 0; +#endif +} + +static void kRemoveDuplicates(QList &input, AppStream::Bundle::Kind kind) +{ + QSet ret; + for (auto it = input.begin(); it != input.end();) { + const auto key = kind == AppStream::Bundle::KindUnknown ? it->id() : it->bundle(kind).id(); + if (!ret.contains(key)) { + ret << key; + ++it; + } else { + it = input.erase(it); + } + } +} + +QList AppStreamUtils::componentsByCategories(AppStream::Pool *pool, Category *cat, AppStream::Bundle::Kind kind) +{ + QList ret; + for (const auto &categoryName : cat->involvedCategories()) { +#if ASQ_CHECK_VERSION(1, 0, 0) + ret += pool->componentsByCategories({categoryName}).toList(); +#else + ret += pool->componentsByCategories({categoryName}); +#endif + } + kRemoveDuplicates(ret, kind); + return ret; +} diff --git a/libdiscover/appstream/AppStreamUtils.h b/libdiscover/appstream/AppStreamUtils.h new file mode 100644 index 0000000..ebb5ecc --- /dev/null +++ b/libdiscover/appstream/AppStreamUtils.h @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#else +#include +#include +#include +#endif + +#include +#include +#include +#include + +namespace AppStreamUtils +{ +Q_DECL_EXPORT QUrl imageOfKind(const QList &images, AppStream::Image::Kind kind); + +Q_DECL_EXPORT QString changelogToHtml(const AppStream::Component &appdata); + +Q_DECL_EXPORT Screenshots fetchScreenshots(const AppStream::Component &appdata); + +Q_DECL_EXPORT QJsonArray licenses(const AppStream::Component &appdata); + +Q_DECL_EXPORT QJsonArray licenses(const QString &spdxExpression); + +Q_DECL_EXPORT QJsonObject license(const QString &spdxId); + +Q_DECL_EXPORT QStringList appstreamIds(const QUrl &appstreamUrl); + +/// Helps implement AbstractResource::versionString +Q_DECL_EXPORT QString versionString(const QString &version, const AppStream::Component &appdata); + +Q_DECL_EXPORT QString contentRatingText(const AppStream::Component &appdata); +Q_DECL_EXPORT QString contentRatingDescription(const AppStream::Component &appdata); +Q_DECL_EXPORT AbstractResource::ContentIntensity contentRatingIntensity(const AppStream::Component &appdata); +Q_DECL_EXPORT uint contentRatingMinimumAge(const AppStream::Component &appdata); + +Q_DECL_EXPORT QList componentsByCategories(AppStream::Pool *pool, Category *cat, AppStream::Bundle::Kind kind); +} diff --git a/libdiscover/appstream/OdrsReviewsBackend.cpp b/libdiscover/appstream/OdrsReviewsBackend.cpp new file mode 100644 index 0000000..c54490b --- /dev/null +++ b/libdiscover/appstream/OdrsReviewsBackend.cpp @@ -0,0 +1,413 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "OdrsReviewsBackend.h" +#include "AppStreamIntegration.h" +#include "CachedNetworkAccessManager.h" + +#include +#include + +#include +#include + +#include +#include +#include + +#include "libdiscover_debug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// #define APIURL "http://127.0.0.1:5000/1.0/reviews/api" +#define APIURL "https://odrs.gnome.org/1.0/reviews/api" + +OdrsReviewsBackend::OdrsReviewsBackend() + : AbstractReviewsBackend(nullptr) +{ + fetchRatings(); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QNetworkConfigurationManager *manager = new QNetworkConfigurationManager(this); + + connect(manager, &QNetworkConfigurationManager::onlineStateChanged, this, [this](bool online) { + if (online && !m_errorMessage.isEmpty()) { + m_errorMessage.clear(); + Q_EMIT errorMessageChanged(); + fetchRatings(); + } + }); +#endif +} + +OdrsReviewsBackend::~OdrsReviewsBackend() noexcept +{ + qDeleteAll(m_ratings); +} + +void OdrsReviewsBackend::fetchRatings() +{ + bool fetchRatings = false; + const QUrl ratingsUrl(QStringLiteral(APIURL "/ratings")); + const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/ratings/ratings")); + const QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + + // Create $HOME/.cache/discover/ratings folder + cacheDir.mkpath(QStringLiteral("ratings")); + + if (QFileInfo::exists(fileUrl.toLocalFile())) { + QFileInfo file(fileUrl.toLocalFile()); + // Refresh the cached ratings if they are older than one day + if (file.lastModified().msecsTo(QDateTime::currentDateTime()) > 1000 * 60 * 60 * 24) { + fetchRatings = true; + } + } else { + fetchRatings = true; + } + + qDebug() << "fetch ratings!" << fetchRatings; + if (fetchRatings) { + setFetching(true); + KIO::FileCopyJob *getJob = KIO::file_copy(ratingsUrl, fileUrl, -1, KIO::Overwrite | KIO::HideProgressInfo); + connect(getJob, &KIO::FileCopyJob::result, this, &OdrsReviewsBackend::ratingsFetched); + } else { + parseRatings(); + } +} + +void OdrsReviewsBackend::setFetching(bool fetching) +{ + if (fetching == m_isFetching) { + return; + } + m_isFetching = fetching; + Q_EMIT fetchingChanged(fetching); +} + +void OdrsReviewsBackend::ratingsFetched(KJob *job) +{ + setFetching(false); + if (job->error()) { + qCWarning(LIBDISCOVER_LOG) << "Failed to fetch ratings " << job->errorString(); + } else { + parseRatings(); + } +} + +static QString osName() +{ + return AppStreamIntegration::global()->osRelease()->name(); +} + +static QString userHash() +{ + QString machineId; + QFile file(QStringLiteral("/etc/machine-id")); + if (file.open(QIODevice::ReadOnly)) { + machineId = QString::fromUtf8(file.readAll()); + file.close(); + } + + if (machineId.isEmpty()) { + return QString(); + } + + QString salted = QStringLiteral("gnome-software[%1:%2]").arg(KUser().loginName(), machineId); + return QString::fromUtf8(QCryptographicHash::hash(salted.toUtf8(), QCryptographicHash::Sha1).toHex()); +} + +void OdrsReviewsBackend::fetchReviews(AbstractResource *app, int page) +{ + if (app->appstreamId().isEmpty()) { + return; + } + Q_UNUSED(page) + const QString version = app->isInstalled() ? app->installedVersion() : app->availableVersion(); + if (version.isEmpty()) { + return; + } + setFetching(true); + + const QJsonDocument document(QJsonObject{ + {QStringLiteral("app_id"), app->appstreamId()}, + {QStringLiteral("distro"), osName()}, + {QStringLiteral("user_hash"), userHash()}, + {QStringLiteral("version"), version}, + {QStringLiteral("locale"), QLocale::system().name()}, + {QStringLiteral("limit"), -1}, + }); + + const auto json = document.toJson(QJsonDocument::Compact); + QNetworkRequest request(QUrl(QStringLiteral(APIURL "/fetch"))); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); + request.setHeader(QNetworkRequest::ContentLengthHeader, json.size()); + // Store reference to the app for which we request reviews + request.setOriginatingObject(app); + + auto reply = nam()->post(request, json); + connect(reply, &QNetworkReply::finished, this, &OdrsReviewsBackend::reviewsFetched); +} + +void OdrsReviewsBackend::reviewsFetched() +{ + QNetworkReply *reply = qobject_cast(sender()); + QScopedPointer replyPtr(reply); + const QByteArray data = reply->readAll(); + const auto networkError = reply->error(); + if (networkError != QNetworkReply::NoError) { + qCWarning(LIBDISCOVER_LOG) << "error fetching reviews:" << reply->errorString() << data; + m_errorMessage = i18n("Error while fetching reviews: %1", reply->errorString()); + Q_EMIT errorMessageChanged(); + setFetching(false); + return; + } + + QJsonParseError error; + const QJsonDocument document = QJsonDocument::fromJson(data, &error); + if (error.error) { + qWarning() << "odrs: error parsing reviews" << reply->url() << error.errorString(); + } + + AbstractResource *resource = qobject_cast(reply->request().originatingObject()); + Q_ASSERT(resource); + parseReviews(document, resource); +} + +Rating *OdrsReviewsBackend::ratingForApplication(AbstractResource *app) const +{ + if (app->appstreamId().isEmpty()) { + return nullptr; + } + + return m_ratings[app->appstreamId()]; +} + +void OdrsReviewsBackend::submitUsefulness(Review *review, bool useful) +{ + const QJsonDocument document(QJsonObject{ + {QStringLiteral("app_id"), review->applicationName()}, + {QStringLiteral("user_skey"), review->getMetadata(QStringLiteral("ODRS::user_skey")).toString()}, + {QStringLiteral("user_hash"), userHash()}, + {QStringLiteral("distro"), osName()}, + {QStringLiteral("review_id"), QJsonValue(double(review->id()))}, // if we really need uint64 we should get it in QJsonValue + }); + + QNetworkRequest request(QUrl(QStringLiteral(APIURL) + (useful ? QLatin1String("/upvote") : QLatin1String("/downvote")))); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); + request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); + + auto reply = nam()->post(request, document.toJson()); + connect(reply, &QNetworkReply::finished, this, &OdrsReviewsBackend::usefulnessSubmitted); +} + +void OdrsReviewsBackend::usefulnessSubmitted() +{ + QNetworkReply *reply = qobject_cast(sender()); + const auto networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + qCWarning(LIBDISCOVER_LOG) << "Usefulness submitted"; + } else { + qCWarning(LIBDISCOVER_LOG) << "Failed to submit usefulness: " << reply->errorString(); + Q_EMIT error(i18n("Error while submitting usefulness: %1", reply->errorString())); + } + reply->deleteLater(); +} + +QString OdrsReviewsBackend::userName() const +{ + return KUser().property(KUser::FullName).toString(); +} + +void OdrsReviewsBackend::sendReview(AbstractResource *res, const QString &summary, const QString &description, const QString &rating, const QString &userName) +{ + Q_ASSERT(res); + QJsonObject map = { + {QStringLiteral("app_id"), res->appstreamId()}, + {QStringLiteral("user_skey"), res->getMetadata(QStringLiteral("ODRS::user_skey")).toString()}, + {QStringLiteral("user_hash"), userHash()}, + {QStringLiteral("version"), res->isInstalled() ? res->installedVersion() : res->availableVersion()}, + {QStringLiteral("locale"), QLocale::system().name()}, + {QStringLiteral("distro"), osName()}, + {QStringLiteral("user_display"), QJsonValue::fromVariant(userName)}, + {QStringLiteral("summary"), summary}, + {QStringLiteral("description"), description}, + {QStringLiteral("rating"), rating.toInt() * 10}, + }; + + const QJsonDocument document(map); + + QNetworkAccessManager *accessManager = nam(); + QNetworkRequest request(QUrl(QStringLiteral(APIURL "/submit"))); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); + request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); + + // Store what we need so we can immediately show our review once it is submitted + // Use review_id 0 for now as odrs starts numbering from 1 and once reviews are re-downloaded we get correct id + map.insert(QStringLiteral("review_id"), 0); + res->addMetadata(QStringLiteral("ODRS::review_map"), map); + request.setOriginatingObject(res); + + accessManager->post(request, document.toJson()); + connect(accessManager, &QNetworkAccessManager::finished, this, &OdrsReviewsBackend::reviewSubmitted); +} + +void OdrsReviewsBackend::reviewSubmitted(QNetworkReply *reply) +{ + const auto networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + AbstractResource *resource = qobject_cast(reply->request().originatingObject()); + Q_ASSERT(resource); + qCWarning(LIBDISCOVER_LOG) << "Review submitted" << resource; + if (resource) { + const QJsonDocument document({resource->getMetadata(QStringLiteral("ODRS::review_map")).toObject()}); + parseReviews(document, resource); + } else { + qCWarning(LIBDISCOVER_LOG) << "Failed to submit review: missing object"; + } + } else { + Q_EMIT error(i18n("Error while submitting review: %1", reply->errorString())); + qCWarning(LIBDISCOVER_LOG) << "Failed to submit review: " << reply->errorString(); + } + reply->deleteLater(); +} + +void OdrsReviewsBackend::parseRatings() +{ + auto fw = new QFutureWatcher(this); + connect(fw, &QFutureWatcher::finished, this, [this, fw] { + const QJsonDocument jsonDocument = fw->result(); + fw->deleteLater(); + const QJsonObject jsonObject = jsonDocument.object(); + m_ratings.reserve(jsonObject.size()); + for (auto it = jsonObject.begin(); it != jsonObject.end(); it++) { + QJsonObject appJsonObject = it.value().toObject(); + + const int ratingCount = appJsonObject.value(QLatin1String("total")).toInt(); + int ratingMap[] = { + appJsonObject.value(QLatin1String("star0")).toInt(), + appJsonObject.value(QLatin1String("star1")).toInt(), + appJsonObject.value(QLatin1String("star2")).toInt(), + appJsonObject.value(QLatin1String("star3")).toInt(), + appJsonObject.value(QLatin1String("star4")).toInt(), + appJsonObject.value(QLatin1String("star5")).toInt(), + }; + + Rating *rating = new Rating(it.key(), ratingCount, ratingMap); + m_ratings.insert(it.key(), rating); + + const auto finder = [rating](Rating *r) { + return r->ratingPoints() < rating->ratingPoints(); + }; + const auto topIt = std::find_if(m_top.begin(), m_top.end(), finder); + if (topIt == m_top.end()) { + if (m_top.size() < 25) { + m_top.append(rating); + } + } else + m_top.insert(topIt, rating); + if (m_top.size() > 25) { + m_top.resize(25); + } + } + Q_EMIT ratingsReady(); + }); + fw->setFuture(QtConcurrent::run([] { + QFile ratingsDocument(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/ratings/ratings")); + if (!ratingsDocument.open(QIODevice::ReadOnly)) { + qWarning() << "odrs: Could not open file" << ratingsDocument.fileName(); + return QJsonDocument::fromJson({}); + } + + QJsonParseError error; + const auto ret = QJsonDocument::fromJson(ratingsDocument.readAll(), &error); + if (error.error) { + qWarning() << "odrs: error parsing ratings" << ratingsDocument.errorString() << error.errorString(); + } + return ret; + })); +} + +void OdrsReviewsBackend::parseReviews(const QJsonDocument &document, AbstractResource *resource) +{ + setFetching(false); + Q_ASSERT(resource); + if (!resource) { + return; + } + + QJsonArray reviews = document.array(); + if (!reviews.isEmpty()) { + QVector reviewList; + for (auto it = reviews.begin(); it != reviews.end(); it++) { + const QJsonObject review = it->toObject(); + if (!review.isEmpty()) { + const int usefulFavorable = review.value(QStringLiteral("karma_up")).toInt(); + const int usefulTotal = review.value(QStringLiteral("karma_down")).toInt() + usefulFavorable; + QDateTime dateTime; + dateTime.setSecsSinceEpoch(review.value(QStringLiteral("date_created")).toInt()); + ReviewPtr r(new Review(review.value(QStringLiteral("app_id")).toString(), + resource->packageName(), + review.value(QStringLiteral("locale")).toString(), + review.value(QStringLiteral("summary")).toString(), + review.value(QStringLiteral("description")).toString(), + review.value(QStringLiteral("user_display")).toString(), + dateTime, + true, + review.value(QStringLiteral("review_id")).toInt(), + review.value(QStringLiteral("rating")).toInt() / 10, + usefulTotal, + usefulFavorable, + review.value(QStringLiteral("version")).toString())); + // We can also receive just a json with app name and user info so filter these out as there is no review + if (!r->summary().isEmpty() && !r->reviewText().isEmpty()) { + reviewList << r; + // Needed for submitting usefulness + r->addMetadata(QStringLiteral("ODRS::user_skey"), review.value(QStringLiteral("user_skey")).toString()); + } + + // We should get at least user_skey needed for posting reviews + resource->addMetadata(QStringLiteral("ODRS::user_skey"), review.value(QStringLiteral("user_skey")).toString()); + } + } + + Q_EMIT reviewsReady(resource, reviewList, false); + } +} + +bool OdrsReviewsBackend::isResourceSupported(AbstractResource *res) const +{ + return !res->appstreamId().isEmpty(); +} + +void OdrsReviewsBackend::emitRatingFetched(AbstractResourcesBackend *b, const QList &resources) const +{ + b->emitRatingsReady(); + for (AbstractResource *res : resources) { + if (m_ratings.contains(res->appstreamId())) { + Q_EMIT res->ratingFetched(); + } + } +} + +QNetworkAccessManager *OdrsReviewsBackend::nam() +{ + if (!m_delayedNam) { + m_delayedNam = new CachedNetworkAccessManager(QStringLiteral("odrs"), this); + } + return m_delayedNam; +} diff --git a/libdiscover/appstream/OdrsReviewsBackend.h b/libdiscover/appstream/OdrsReviewsBackend.h new file mode 100644 index 0000000..8e36af8 --- /dev/null +++ b/libdiscover/appstream/OdrsReviewsBackend.h @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +#include +#include +#include + +class KJob; +class AbstractResourcesBackend; +class CachedNetworkAccessManager; + +class DISCOVERCOMMON_EXPORT OdrsReviewsBackend : public AbstractReviewsBackend +{ + Q_OBJECT +public: + explicit OdrsReviewsBackend(); + ~OdrsReviewsBackend() override; + + void login() override + { + } + void logout() override + { + } + void registerAndLogin() override + { + } + + Rating *ratingForApplication(AbstractResource *app) const override; + bool hasCredentials() const override + { + return false; + } + bool supportsNameChange() const override + { + return true; + } + void deleteReview(Review *) override + { + } + void fetchReviews(AbstractResource *app, int page = 1) override; + bool isFetching() const override + { + return m_isFetching; + } + void flagReview(Review *, const QString &, const QString &) override + { + } + void submitUsefulness(Review *review, bool useful) override; + bool isResourceSupported(AbstractResource *res) const override; + void emitRatingFetched(AbstractResourcesBackend *backend, const QList &res) const; + QString errorMessage() const override + { + return m_errorMessage; + } + + QVector top() const + { + return m_top; + } + +private Q_SLOTS: + void ratingsFetched(KJob *job); + void reviewsFetched(); + void reviewSubmitted(QNetworkReply *reply); + void usefulnessSubmitted(); + +Q_SIGNALS: + void ratingsReady(); + +protected: + void sendReview(AbstractResource *app, const QString &summary, const QString &review_text, const QString &rating, const QString &userName) override; + QString userName() const override; + +private: + void fetchRatings(); + void setFetching(bool); + QNetworkAccessManager *nam(); + void parseRatings(); + void parseReviews(const QJsonDocument &document, AbstractResource *resource); + + QString m_errorMessage; + QHash m_ratings; + bool m_isFetching = false; + CachedNetworkAccessManager *m_delayedNam = nullptr; + + QVector m_top; +}; diff --git a/libdiscover/backends/CMakeLists.txt b/libdiscover/backends/CMakeLists.txt new file mode 100644 index 0000000..7305fdb --- /dev/null +++ b/libdiscover/backends/CMakeLists.txt @@ -0,0 +1,56 @@ +function(add_unit_test name) + add_executable(${name} ${ARGN}) + add_test(${name} dbus-run-session ${CMAKE_BINARY_DIR}/bin/${name}) + ecm_mark_as_test(${name}) + target_link_libraries(${name} + Discover::Common + + Qt::Test Qt::Core ${EXTRA_LIBS}) +endfunction() + +if(KF5Attica_FOUND AND KF5NewStuff_FOUND AND DISCOVER_AppStreamQt_FOUND) + add_subdirectory(KNSBackend) +endif() + +if(packagekitqt5_FOUND AND DISCOVER_AppStreamQt_FOUND) + add_subdirectory(PackageKitBackend) +endif() + +option(BUILD_DummyBackend "Build the DummyBackend" "OFF") +if(BUILD_DummyBackend) + add_subdirectory(DummyBackend) +endif() + +option(BUILD_FlatpakBackend "Build Flatpak support" "ON") +if(Flatpak_FOUND AND DISCOVER_AppStreamQt_FOUND AND BUILD_FlatpakBackend) + add_subdirectory(FlatpakBackend) +elseif(BUILD_FlatpakBackend) + message(WARNING "BUILD_FlatpakBackend enabled but Flatpak=${Flatpak_FOUND} or AppStreamQt=${DISCOVER_AppStreamQt_FOUND} not found") +endif() + +find_package(Snapd) +set_package_properties(Snapd PROPERTIES + DESCRIPTION "Library that exposes Snapd" + URL "https://www.snapcraft.io" + PURPOSE "Required to build the Snap backend" + TYPE OPTIONAL) + +option(BUILD_SteamOSBackend "Build SteamOS support." "OFF") +if(BUILD_SteamOSBackend) + add_subdirectory(SteamOSBackend) +endif() + +option(BUILD_SnapBackend "Build Snap support." "ON") +if(BUILD_SnapBackend AND DISCOVER_AppStreamQt_FOUND AND Snapd_FOUND) + add_subdirectory(SnapBackend) +endif() + +option(BUILD_FwupdBackend "Build Fwupd support." "ON") +if(BUILD_FwupdBackend AND TARGET PkgConfig::Fwupd) + add_subdirectory(FwupdBackend) +endif() + +option(BUILD_RpmOstreeBackend "Build rpm-ostree support." "ON") +if(BUILD_RpmOstreeBackend AND Ostree_FOUND AND RpmOstree_FOUND) + add_subdirectory(RpmOstreeBackend) +endif() diff --git a/libdiscover/backends/DummyBackend/CMakeLists.txt b/libdiscover/backends/DummyBackend/CMakeLists.txt new file mode 100644 index 0000000..5b4aef1 --- /dev/null +++ b/libdiscover/backends/DummyBackend/CMakeLists.txt @@ -0,0 +1,21 @@ +add_subdirectory(tests) + +set(dummy-backend_SRCS + DummyResource.cpp + DummyBackend.cpp + DummyReviewsBackend.cpp + DummyTransaction.cpp + DummySourcesBackend.cpp +) + +add_library(dummy-backend MODULE ${dummy-backend_SRCS}) +target_link_libraries(dummy-backend Qt::Core Qt::Widgets KF5::CoreAddons KF5::ConfigCore Discover::Common) + +install(TARGETS dummy-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) +install(FILES dummy-backend-categories.xml DESTINATION ${KDE_INSTALL_DATADIR}/libdiscover/categories) + +add_library(DummyNotifier MODULE DummyNotifier.cpp) +target_link_libraries(DummyNotifier Discover::Notifiers) +set_target_properties(DummyNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) + +install(TARGETS DummyNotifier DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover-notifier) diff --git a/libdiscover/backends/DummyBackend/DummyBackend.cpp b/libdiscover/backends/DummyBackend/DummyBackend.cpp new file mode 100644 index 0000000..36f4312 --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyBackend.cpp @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummyBackend.h" +#include "DummyResource.h" +#include "DummyReviewsBackend.h" +#include "DummySourcesBackend.h" +#include "DummyTransaction.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +DISCOVER_BACKEND_PLUGIN(DummyBackend) + +DummyBackend::DummyBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , m_updater(new StandardBackendUpdater(this)) + , m_reviews(new DummyReviewsBackend(this)) + , m_fetching(true) + , m_startElements(120) +{ + QTimer::singleShot(500, this, &DummyBackend::toggleFetching); + connect(m_reviews, &DummyReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady); + connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &DummyBackend::updatesCountChanged); + + populate(QStringLiteral("Dummy")); + if (!m_fetching) + m_reviews->initialize(); + + m_updater->setErrorMessage(QStringLiteral("I am super broken")); + SourcesModel::global()->addSourcesBackend(new DummySourcesBackend(this)); +} + +void DummyBackend::populate(const QString &n) +{ + const int start = m_resources.count(); + for (int i = start; i < start + m_startElements; i++) { + const QString name = n + QLatin1Char(' ') + QString::number(i); + DummyResource *res = new DummyResource(name, AbstractResource::Application, this); + res->setSize(100 + (m_startElements - i)); + res->setState(AbstractResource::State(1 + (i % 3))); + m_resources.insert(name.toLower(), res); + connect(res, &DummyResource::stateChanged, this, &DummyBackend::updatesCountChanged); + } + + for (int i = start; i < start + m_startElements; i++) { + const QString name = QLatin1String("addon") + QString::number(i); + DummyResource *res = new DummyResource(name, AbstractResource::Addon, this); + res->setState(AbstractResource::State(1 + (i % 3))); + res->setSize(300 + (m_startElements - i)); + m_resources.insert(name, res); + connect(res, &DummyResource::stateChanged, this, &DummyBackend::updatesCountChanged); + } + + for (int i = start; i < start + m_startElements; i++) { + const QString name = QLatin1String("techie") + QString::number(i); + DummyResource *res = new DummyResource(name, AbstractResource::Technical, this); + res->setState(AbstractResource::State(1 + (i % 3))); + res->setSize(300 + (m_startElements - i)); + m_resources.insert(name, res); + connect(res, &DummyResource::stateChanged, this, &DummyBackend::updatesCountChanged); + } +} + +void DummyBackend::toggleFetching() +{ + m_fetching = !m_fetching; + // qDebug() << "fetching..." << m_fetching; + Q_EMIT fetchingChanged(); + if (!m_fetching) + m_reviews->initialize(); + + DiscoverAction *celebrate = new DiscoverAction("wine", QStringLiteral("To who?"), this); + connect(celebrate, &DiscoverAction::triggered, this, [this] { + Q_EMIT passiveMessage(QStringLiteral("To you!")); + }); + + Q_EMIT inlineMessageChanged(QSharedPointer::create(InlineMessage::Warning, + QStringLiteral("dialog-warning"), + QStringLiteral("Happy unbirthday 🎂, probably."), + celebrate)); +} + +int DummyBackend::updatesCount() const +{ + return m_updater->updatesCount(); +} + +ResultsStream *DummyBackend::search(const AbstractResourcesBackend::Filters &filter) +{ + QVector ret; + if (!filter.resourceUrl.isEmpty()) + return findResourceByPackageName(filter.resourceUrl); + else + for (AbstractResource *r : qAsConst(m_resources)) { + if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable) { + continue; + } + + if (r->state() < filter.state) + continue; + + if (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)) + ret += r; + } + return new ResultsStream(QStringLiteral("DummyStream"), ret); +} + +ResultsStream *DummyBackend::findResourceByPackageName(const QUrl &search) +{ + if (search.isLocalFile()) { + DummyResource *res = new DummyResource(search.fileName(), AbstractResource::Technical, this); + res->setSize(666); + res->setState(AbstractResource::None); + m_resources.insert(res->packageName(), res); + connect(res, &DummyResource::stateChanged, this, &DummyBackend::updatesCountChanged); + return new ResultsStream(QStringLiteral("DummyStream-local"), {res}); + } + + auto res = search.scheme() == QLatin1String("dummy") ? m_resources.value(search.host().replace(QLatin1Char('.'), QLatin1Char(' '))) : nullptr; + if (!res) { + return new ResultsStream(QStringLiteral("DummyStream"), {}); + } else + return new ResultsStream(QStringLiteral("DummyStream"), {res}); +} + +AbstractBackendUpdater *DummyBackend::backendUpdater() const +{ + return m_updater; +} + +AbstractReviewsBackend *DummyBackend::reviewsBackend() const +{ + return m_reviews; +} + +Transaction *DummyBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + return new DummyTransaction(qobject_cast(app), addons, Transaction::InstallRole); +} + +Transaction *DummyBackend::installApplication(AbstractResource *app) +{ + return new DummyTransaction(qobject_cast(app), Transaction::InstallRole); +} + +Transaction *DummyBackend::removeApplication(AbstractResource *app) +{ + return new DummyTransaction(qobject_cast(app), Transaction::RemoveRole); +} + +void DummyBackend::checkForUpdates() +{ + if (m_fetching) + return; + toggleFetching(); + populate(QStringLiteral("Moar")); + QTimer::singleShot(500, this, &DummyBackend::toggleFetching); + qDebug() << "DummyBackend::checkForUpdates"; + + Q_EMIT passiveMessage("Dummy: Checking for updates"); +} + +QString DummyBackend::displayName() const +{ + return QStringLiteral("Dummy"); +} + +bool DummyBackend::hasApplications() const +{ + return true; +} + +InlineMessage *DummyBackend::explainDysfunction() const +{ + return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), QStringLiteral("There are no Dummy sources.")); +} + +#include "DummyBackend.moc" diff --git a/libdiscover/backends/DummyBackend/DummyBackend.h b/libdiscover/backends/DummyBackend/DummyBackend.h new file mode 100644 index 0000000..1cac4af --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyBackend.h @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +class DummyReviewsBackend; +class StandardBackendUpdater; +class DummyResource; +class DummyBackend : public AbstractResourcesBackend +{ + Q_OBJECT + Q_PROPERTY(int startElements MEMBER m_startElements) +public: + explicit DummyBackend(QObject *parent = nullptr); + + int updatesCount() const override; + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + ResultsStream *findResourceByPackageName(const QUrl &search); + QHash resources() const + { + return m_resources; + } + bool isValid() const override + { + return true; + } // No external file dependencies that could cause runtime errors + + Transaction *installApplication(AbstractResource *app) override; + Transaction *installApplication(AbstractResource *app, const AddonList &addons) override; + Transaction *removeApplication(AbstractResource *app) override; + bool isFetching() const override + { + return m_fetching; + } + void checkForUpdates() override; + QString displayName() const override; + bool hasApplications() const override; + InlineMessage *explainDysfunction() const override; +public Q_SLOTS: + void toggleFetching(); + +private: + void populate(const QString &name); + + QHash m_resources; + StandardBackendUpdater *m_updater; + DummyReviewsBackend *m_reviews; + bool m_fetching; + int m_startElements; +}; diff --git a/libdiscover/backends/DummyBackend/DummyNotifier.cpp b/libdiscover/backends/DummyBackend/DummyNotifier.cpp new file mode 100644 index 0000000..99d10be --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyNotifier.cpp @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#include "DummyNotifier.h" + +#include + +DummyNotifier::DummyNotifier(QObject *parent) + : BackendNotifierModule(parent) +{ +} + +DummyNotifier::~DummyNotifier() +{ +} + +void DummyNotifier::recheckSystemUpdateNeeded() +{ + Q_EMIT foundUpdates(); +} diff --git a/libdiscover/backends/DummyBackend/DummyNotifier.h b/libdiscover/backends/DummyBackend/DummyNotifier.h new file mode 100644 index 0000000..9b2929f --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyNotifier.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#pragma once + +#include + +class DummyNotifier : public BackendNotifierModule +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.discover.BackendNotifierModule") + Q_INTERFACES(BackendNotifierModule) +public: + explicit DummyNotifier(QObject *parent = nullptr); + ~DummyNotifier() override; + + void recheckSystemUpdateNeeded() override; + bool hasSecurityUpdates() override + { + return false; + } + + bool hasUpdates() override + { + return false; + } + + bool needsReboot() const override + { + return false; + } +}; diff --git a/libdiscover/backends/DummyBackend/DummyResource.cpp b/libdiscover/backends/DummyBackend/DummyResource.cpp new file mode 100644 index 0000000..4a98f14 --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyResource.cpp @@ -0,0 +1,220 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummyResource.h" +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC_WITH_ARGS( + QVector, + s_icons, + ({QLatin1String("kdevelop"), QLatin1String("kalgebra"), QLatin1String("kmail"), QLatin1String("akregator"), QLatin1String("korganizer")})) + +DummyResource::DummyResource(QString name, AbstractResource::Type type, AbstractResourcesBackend *parent) + : AbstractResource(parent) + , m_name(std::move(name)) + , m_state(State::Broken) + , m_iconName((*s_icons)[KRandom::random() % s_icons->size()]) + , m_addons({PackageState(QStringLiteral("a"), QStringLiteral("aaaaaa"), false), + PackageState(QStringLiteral("b"), QStringLiteral("aaaaaa"), false), + PackageState(QStringLiteral("c"), QStringLiteral("aaaaaa"), false)}) + , m_type(type) +{ + const int nofScreenshots = KRandom::random() % 5; + m_screenshots = + Screenshots{ + QUrl(QStringLiteral("https://screenshots.debian.net/screenshots/000/014/863/large.png")), + QUrl(QStringLiteral("https://c1.staticflickr.com/9/8479/8166397343_b78106f353_k.jpg")), + QUrl(QStringLiteral("https://c2.staticflickr.com/4/3685/9954407993_dad10a6943_k.jpg")), + QUrl(QStringLiteral("https://c1.staticflickr.com/1/653/22527103378_8ce572e1de_k.jpg")), + QUrl(QStringLiteral( + "https://images.unsplash.com/photo-1528744598421-b7b93e12df15?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60")), + QUrl(QStringLiteral( + "https://images.unsplash.com/photo-1552385430-53e6f2028760?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80")), + QUrl(QStringLiteral( + "https://images.unsplash.com/photo-1506810172640-8a9f77cb1472?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80")), + + } + .mid(nofScreenshots); +} + +QList DummyResource::addonsInformation() +{ + return m_addons; +} + +QString DummyResource::availableVersion() const +{ + return QStringLiteral("3.0"); +} + +QStringList DummyResource::categories() +{ + return {QStringLiteral("dummy"), m_name.endsWith(QLatin1Char('3')) ? QStringLiteral("three") : QStringLiteral("notthree")}; +} + +QString DummyResource::comment() +{ + return QStringLiteral("A reasonably short comment ") + name(); +} + +quint64 DummyResource::size() +{ + return m_size; +} + +QUrl DummyResource::homepage() +{ + return QUrl(QStringLiteral("https://kde.org")); +} + +QUrl DummyResource::helpURL() +{ + return QUrl(QStringLiteral("http://very-very-excellent-docs.lol")); +} + +QUrl DummyResource::bugURL() +{ + return QUrl(QStringLiteral("file:///dev/null")); +} + +QUrl DummyResource::donationURL() +{ + return QUrl(QStringLiteral("https://youtu.be/0o8XMlL8rqY")); +} + +QUrl DummyResource::contributeURL() +{ + return QUrl(QStringLiteral("https://techbase.kde.org/Contribute")); +} + +QVariant DummyResource::icon() const +{ + static const QVector icons = {QStringLiteral("device-notifier"), QStringLiteral("media-floppy"), QStringLiteral("drink-beer")}; + return icons[type()]; +} + +QString DummyResource::installedVersion() const +{ + return QStringLiteral("2.3"); +} + +QJsonArray DummyResource::licenses() +{ + return {QJsonObject{{QStringLiteral("name"), QStringLiteral("GPL")}, {QStringLiteral("url"), QStringLiteral("https://kde.org")}}}; +} + +QString DummyResource::longDescription() +{ + return QStringLiteral( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies consequat nulla, ut vulputate nulla ultricies ac. Suspendisse lacinia " + "commodo lacus, non tristique mauris dictum vitae. Sed adipiscing augue nec nisi aliquet viverra. Etiam sit amet nulla in tellus consectetur feugiat. " + "Cras in sem tortor. Fusce a nulla at justo accumsan gravida. Maecenas dui felis, lacinia at ornare sed, aliquam et purus. Sed ut sagittis lacus. " + "Etiam dictum pharetra rhoncus. Suspendisse auctor orci ipsum. Pellentesque vitae urna nec felis consequat lobortis dictum in urna. Phasellus a mi ac " + "leo adipiscing varius eget a felis. Cras magna augue, commodo sed placerat vel, tempus vel ligula. In feugiat quam quis est lobortis sed accumsan " + "nunc malesuada. Mauris quis massa sit amet felis tempus suscipit a quis diam.\n\n" + + "Aenean quis nulla erat, vel sagittis sem. Praesent vitae mauris arcu. Cras porttitor, ante at scelerisque sodales, nibh felis consectetur orci, ut " + "hendrerit urna urna non urna. Duis eu magna id mi scelerisque adipiscing. Aliquam sed quam in eros sodales accumsan. Phasellus tempus sagittis " + "suscipit. Aliquam rutrum dictum justo ut viverra. Nulla felis sem, molestie sed scelerisque non, consequat vitae nulla. Aliquam ullamcorper malesuada " + "mi, vel vestibulum magna vulputate eget. In hac habitasse platea dictumst. Cras sed lacus dui, vel semper sem. Aenean sodales porta leo vel " + "fringilla.\n\n" + + "Ut tempus massa et urna porta non mollis metus ultricies. Duis nec nulla ac metus auctor porta id et mi. Mauris aliquam nibh a ligula malesuada sed " + "tincidunt nibh varius. Sed felis metus, porta et adipiscing non, faucibus id leo. Donec ipsum nibh, hendrerit eget aliquam nec, tempor ut mauris. " + "Suspendisse potenti. Vestibulum scelerisque adipiscing libero tristique eleifend. Donec quis tortor eget elit mollis iaculis ac sit amet nisi. Proin " + "non massa sed nunc rutrum pellentesque. Sed dui lectus, laoreet sed condimentum id, commodo sed urna.\n\n" + + "Praesent tincidunt mattis massa mattis porta. Nullam posuere neque at mauris vestibulum vitae elementum leo sodales. Quisque condimentum lectus in " + "libero luctus egestas. Fusce tempor neque ac dui tincidunt eget viverra quam suscipit. In hac habitasse platea dictumst. Etiam metus mi, adipiscing " + "nec suscipit id, aliquet sed sem. Duis urna ligula, ornare sed vestibulum vel, molestie ac nisi. Morbi varius iaculis ligula. Nunc in augue leo, sit " + "amet aliquam elit. Suspendisse rutrum sem diam. Proin eu orci nisl. Praesent porttitor dignissim est, id fermentum arcu venenatis vitae.\n\n" + + "Integer in sapien eget quam vulputate lobortis. Morbi nibh elit, elementum vitae vehicula sed, consequat nec erat. Donec placerat porttitor est ut " + "dapibus. Fusce augue orci, dictum et convallis vel, blandit eu tortor. Phasellus non eros nulla. In iaculis nulla fermentum nulla gravida eu mattis " + "purus consectetur. Integer dui nunc, sollicitudin ac tincidunt nec, hendrerit bibendum nunc. Proin sit amet augue ac velit egestas varius. Sed eu " + "ante quis orci vestibulum sagittis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus vitae urna " + "odio, at molestie leo. In convallis neque vel mi dictum convallis lobortis turpis sagittis.\n\n"); +} + +QString DummyResource::name() const +{ + return m_name; +} + +QString DummyResource::origin() const +{ + return QStringLiteral("DummySource1"); +} + +QString DummyResource::packageName() const +{ + return m_name; +} + +QString DummyResource::section() +{ + return QStringLiteral("dummy"); +} + +AbstractResource::State DummyResource::state() +{ + return m_state; +} + +void DummyResource::fetchChangelog() +{ + QString log = longDescription(); + log.replace(QLatin1Char('\n'), QLatin1String("
")); + + Q_EMIT changelogFetched(log); +} + +void DummyResource::fetchScreenshots() +{ + Q_EMIT screenshotsFetched(m_screenshots); +} + +void DummyResource::setState(AbstractResource::State state) +{ + m_state = state; + Q_EMIT stateChanged(); +} + +void DummyResource::setAddons(const AddonList &addons) +{ + const auto addonsToInstall = addons.addonsToInstall(); + for (const QString &toInstall : addonsToInstall) { + setAddonInstalled(toInstall, true); + } + const auto addonsToRemove = addons.addonsToRemove(); + for (const QString &toRemove : addonsToRemove) { + setAddonInstalled(toRemove, false); + } +} + +void DummyResource::setAddonInstalled(const QString &addon, bool installed) +{ + for (auto &elem : m_addons) { + if (elem.name() == addon) { + elem.setInstalled(installed); + } + } +} + +void DummyResource::invokeApplication() const +{ + QDesktopServices d; + d.openUrl(QUrl(QStringLiteral("https://projects.kde.org/projects/extragear/sysadmin/muon"))); +} + +QUrl DummyResource::url() const +{ + return QUrl(QLatin1String("dummy://") + packageName().replace(QLatin1Char(' '), QLatin1Char('.'))); +} diff --git a/libdiscover/backends/DummyBackend/DummyResource.h b/libdiscover/backends/DummyBackend/DummyResource.h new file mode 100644 index 0000000..1d400a2 --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyResource.h @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +class AddonList; +class DummyResource : public AbstractResource +{ + Q_OBJECT +public: + explicit DummyResource(QString name, AbstractResource::Type type, AbstractResourcesBackend *parent); + + QList addonsInformation() override; + QString section() override; + QString origin() const override; + QString longDescription() override; + QString availableVersion() const override; + QString installedVersion() const override; + QJsonArray licenses() override; + quint64 size() override; + QUrl homepage() override; + QUrl helpURL() override; + QUrl bugURL() override; + QUrl donationURL() override; + QUrl contributeURL() override; + QStringList categories() override; + AbstractResource::State state() override; + QVariant icon() const override; + QString comment() override; + QString name() const override; + QString packageName() const override; + AbstractResource::Type type() const override + { + return m_type; + } + bool canExecute() const override + { + return true; + } + void invokeApplication() const override; + void fetchChangelog() override; + void fetchScreenshots() override; + QUrl url() const override; + QString author() const override + { + return QStringLiteral("BananaPerson"); + } + void setState(State state); + void setSize(quint64 size) + { + m_size = size; + } + void setAddons(const AddonList &addons); + + void setAddonInstalled(const QString &addon, bool installed); + QString sourceIcon() const override + { + return QStringLiteral("player-time"); + } + QDate releaseDate() const override + { + return {}; + } + +public: + const QString m_name; + AbstractResource::State m_state; + Screenshots m_screenshots; + QString m_iconName; + QList m_addons; + const AbstractResource::Type m_type; + quint64 m_size; +}; diff --git a/libdiscover/backends/DummyBackend/DummyReviewsBackend.cpp b/libdiscover/backends/DummyBackend/DummyReviewsBackend.cpp new file mode 100644 index 0000000..6a0d5eb --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyReviewsBackend.cpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummyReviewsBackend.h" +#include "DummyBackend.h" +#include "DummyResource.h" +#include +#include +#include +#include +#include +#include + +DummyReviewsBackend::DummyReviewsBackend(DummyBackend *parent) + : AbstractReviewsBackend(parent) +{ +} + +DummyReviewsBackend::~DummyReviewsBackend() noexcept +{ + qDeleteAll(m_ratings); +} + +void DummyReviewsBackend::fetchReviews(AbstractResource *app, int page) +{ + if (page >= 5) + return; + + QVector review; + for (int i = 0; i < 33; i++) { + review += ReviewPtr(new Review(app->name(), + app->packageName(), + QStringLiteral("en_US"), + QStringLiteral("good morning"), + QStringLiteral("the morning is very good"), + QStringLiteral("dummy"), + QDateTime(), + true, + page + i, + i % 5, + 1, + 1, + app->packageName())); + } + Q_EMIT reviewsReady(app, review, false); +} + +Rating *DummyReviewsBackend::ratingForApplication(AbstractResource *app) const +{ + return m_ratings[app]; +} + +void DummyReviewsBackend::initialize() +{ + int i = 11; + DummyBackend *b = qobject_cast(parent()); + const auto resources = b->resources(); + for (DummyResource *app : resources) { + if (m_ratings.contains(app)) + continue; + + int ratings[] = {0, 0, 0, 0, 0, QRandomGenerator::global()->bounded(0, 10)}; + Rating *rating = new Rating(app->packageName(), ++i, ratings); + m_ratings.insert(app, rating); + Q_EMIT app->ratingFetched(); + } + Q_EMIT ratingsReady(); +} + +void DummyReviewsBackend::submitUsefulness(Review *r, bool useful) +{ + qDebug() << "usefulness..." << r->applicationName() << r->reviewer() << useful; + r->setUsefulChoice(useful ? ReviewsModel::Yes : ReviewsModel::No); +} + +void DummyReviewsBackend::sendReview(AbstractResource *res, const QString &a, const QString &b, const QString &c, const QString &d) +{ + qDebug() << "dummy submit review" << res->name() << a << b << c << d; +} + +bool DummyReviewsBackend::isResourceSupported(AbstractResource * /*res*/) const +{ + return true; +} diff --git a/libdiscover/backends/DummyBackend/DummyReviewsBackend.h b/libdiscover/backends/DummyBackend/DummyReviewsBackend.h new file mode 100644 index 0000000..7cc671a --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyReviewsBackend.h @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +class DummyBackend; +class DummyReviewsBackend : public AbstractReviewsBackend +{ + Q_OBJECT +public: + explicit DummyReviewsBackend(DummyBackend *parent = nullptr); + ~DummyReviewsBackend() override; + + QString userName() const override + { + return QStringLiteral("dummy"); + } + void login() override + { + } + void logout() override + { + } + void registerAndLogin() override + { + } + + Rating *ratingForApplication(AbstractResource *app) const override; + bool hasCredentials() const override + { + return false; + } + void deleteReview(Review *) override + { + } + void fetchReviews(AbstractResource *app, int page = 1) override; + bool isFetching() const override + { + return false; + } + + void flagReview(Review *, const QString &, const QString &) override + { + } + void submitUsefulness(Review *, bool) override; + + void initialize(); + bool isResourceSupported(AbstractResource *res) const override; + +Q_SIGNALS: + void ratingsReady(); + +protected: + void sendReview(AbstractResource *, const QString &, const QString &, const QString &, const QString &) override; + +private: + QHash m_ratings; +}; diff --git a/libdiscover/backends/DummyBackend/DummySourcesBackend.cpp b/libdiscover/backends/DummyBackend/DummySourcesBackend.cpp new file mode 100644 index 0000000..7fc8c15 --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummySourcesBackend.cpp @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummySourcesBackend.h" +#include "resources/DiscoverAction.h" +#include + +DummySourcesBackend::DummySourcesBackend(AbstractResourcesBackend *parent) + : AbstractSourcesBackend(parent) + , m_sources(new QStandardItemModel(this)) + , m_testAction(new DiscoverAction(QStringLiteral("kalgebra"), QStringLiteral("DummyAction"), this)) +{ + for (int i = 0; i < 10; ++i) + addSource(QStringLiteral("DummySource%1").arg(i)); + + connect(m_testAction, &DiscoverAction::triggered, []() { + qDebug() << "action triggered!"; + }); + connect(m_sources, &QStandardItemModel::itemChanged, this, [](QStandardItem *item) { + qDebug() << "DummySource changed" << item << item->checkState(); + }); +} + +QAbstractItemModel *DummySourcesBackend::sources() +{ + return m_sources; +} + +bool DummySourcesBackend::addSource(const QString &id) +{ + if (id.isEmpty()) + return false; + + QStandardItem *it = new QStandardItem(id); + it->setData(id, AbstractSourcesBackend::IdRole); + it->setData(QVariant(id + QLatin1Char(' ') + id), Qt::ToolTipRole); + it->setCheckable(true); + it->setCheckState(Qt::Checked); + m_sources->appendRow(it); + return true; +} + +QStandardItem *DummySourcesBackend::sourceForId(const QString &id) const +{ + for (int i = 0, c = m_sources->rowCount(); i < c; ++i) { + const auto it = m_sources->item(i, 0); + if (it->text() == id) + return it; + } + return nullptr; +} + +bool DummySourcesBackend::removeSource(const QString &id) +{ + const auto it = sourceForId(id); + if (!it) { + Q_EMIT passiveMessage(QStringLiteral("Could not find %1").arg(id)); + return false; + } + return m_sources->removeRow(it->row()); +} + +QVariantList DummySourcesBackend::actions() const +{ + return QVariantList() << QVariant::fromValue(m_testAction); +} + +bool DummySourcesBackend::moveSource(const QString &sourceId, int delta) +{ + int row = sourceForId(sourceId)->row(); + auto prevRow = m_sources->takeRow(row); + Q_ASSERT(!prevRow.isEmpty()); + + const auto destRow = row + delta; + m_sources->insertRow(destRow, prevRow); + if (destRow == 0 || row == 0) + Q_EMIT firstSourceIdChanged(); + if (destRow == m_sources->rowCount() - 1 || row == m_sources->rowCount() - 1) + Q_EMIT lastSourceIdChanged(); + return true; +} diff --git a/libdiscover/backends/DummyBackend/DummySourcesBackend.h b/libdiscover/backends/DummyBackend/DummySourcesBackend.h new file mode 100644 index 0000000..a0f391f --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummySourcesBackend.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +class DiscoverAction; + +class DummySourcesBackend : public AbstractSourcesBackend +{ +public: + explicit DummySourcesBackend(AbstractResourcesBackend *parent); + + QAbstractItemModel *sources() override; + bool addSource(const QString &id) override; + bool removeSource(const QString &id) override; + QString idDescription() override + { + return QStringLiteral("Random weird text"); + } + QVariantList actions() const override; + bool supportsAdding() const override + { + return true; + } + + bool canMoveSources() const override + { + return true; + } + bool moveSource(const QString &sourceId, int delta) override; + +private: + QStandardItem *sourceForId(const QString &id) const; + + QStandardItemModel *m_sources; + DiscoverAction *m_testAction; +}; diff --git a/libdiscover/backends/DummyBackend/DummyTransaction.cpp b/libdiscover/backends/DummyBackend/DummyTransaction.cpp new file mode 100644 index 0000000..a99b53b --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyTransaction.cpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummyTransaction.h" +#include "DummyBackend.h" +#include "DummyResource.h" +#include +#include +#include + +// #define TEST_PROCEED + +DummyTransaction::DummyTransaction(DummyResource *app, Role role) + : DummyTransaction(app, {}, role) +{ +} + +DummyTransaction::DummyTransaction(DummyResource *app, const AddonList &addons, Transaction::Role role) + : Transaction(app->backend(), app, role, addons) + , m_app(app) +{ + setCancellable(true); + setStatus(DownloadingStatus); + + iterateTransaction(); +} + +void DummyTransaction::iterateTransaction() +{ + if (!m_iterate) + return; + + if (progress() < 100) { + setProgress(qBound(0, progress() + (KRandom::random() % 5), 100)); + QTimer::singleShot(/*KRandom::random()%*/ 100, this, &DummyTransaction::iterateTransaction); + } else if (status() == DownloadingStatus) { + setStatus(CommittingStatus); + QTimer::singleShot(/*KRandom::random()%*/ 100, this, &DummyTransaction::iterateTransaction); +#ifdef TEST_PROCEED + } else if (resource()->name() == "Dummy 101") { + Q_EMIT proceedRequest(QStringLiteral("yadda yadda"), + QStringLiteral("Biii BOooo
  • A
  • A
  • ") + QStringLiteral("
  • A
  • ").repeated(2) + + QStringLiteral("
  • A
")); +#endif + } else { + finishTransaction(); + } +} + +void DummyTransaction::proceed() +{ + finishTransaction(); +} + +void DummyTransaction::cancel() +{ + m_iterate = false; + + setStatus(CancelledStatus); +} + +void DummyTransaction::finishTransaction() +{ + AbstractResource::State newState; + switch (role()) { + case InstallRole: + case ChangeAddonsRole: + newState = AbstractResource::Installed; + break; + case RemoveRole: + newState = AbstractResource::None; + break; + } + m_app->setAddons(addons()); + m_app->setState(newState); + setStatus(DoneStatus); + deleteLater(); +} diff --git a/libdiscover/backends/DummyBackend/DummyTransaction.h b/libdiscover/backends/DummyBackend/DummyTransaction.h new file mode 100644 index 0000000..4e57af0 --- /dev/null +++ b/libdiscover/backends/DummyBackend/DummyTransaction.h @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +class DummyResource; +class DummyTransaction : public Transaction +{ + Q_OBJECT +public: + DummyTransaction(DummyResource *app, Role role); + DummyTransaction(DummyResource *app, const AddonList &list, Role role); + + void cancel() override; + void proceed() override; + +private Q_SLOTS: + void iterateTransaction(); + void finishTransaction(); + +private: + bool m_iterate = true; + DummyResource *m_app; +}; diff --git a/libdiscover/backends/DummyBackend/dummy-backend-categories.xml b/libdiscover/backends/DummyBackend/dummy-backend-categories.xml new file mode 100644 index 0000000..d96eb54 --- /dev/null +++ b/libdiscover/backends/DummyBackend/dummy-backend-categories.xml @@ -0,0 +1,84 @@ + + + + Dummy Category + applications + + dummy + + + + dummy + kalarm + + dummy1 + + + + dummy addons + plasma + + + dummy2 + + + + dummy 1 + kontact + + dummy3 + + + + dummy with stuff + kmail + + dummy + + + + dummy 2.1 + kalgebra + + dummy + + + + + dummy with quite some stuff + kmail + + dummy2 + + + + dummy 2.1 + kalgebra + + dummy + + + + + + dummy 3 + kig + + + dummy + three + + + + + dummy 4 + kdeconnect + + + dummy + notthree + + + + + diff --git a/libdiscover/backends/DummyBackend/tests/CMakeLists.txt b/libdiscover/backends/DummyBackend/tests/CMakeLists.txt new file mode 100644 index 0000000..04d7f6a --- /dev/null +++ b/libdiscover/backends/DummyBackend/tests/CMakeLists.txt @@ -0,0 +1,4 @@ +add_unit_test(dummytest DummyTest.cpp) +add_unit_test(updatedummytest UpdateDummyTest.cpp) + +target_link_libraries(updatedummytest KF5::CoreAddons) diff --git a/libdiscover/backends/DummyBackend/tests/DummyTest.cpp b/libdiscover/backends/DummyBackend/tests/DummyTest.cpp new file mode 100644 index 0000000..12226da --- /dev/null +++ b/libdiscover/backends/DummyBackend/tests/DummyTest.cpp @@ -0,0 +1,286 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummyTest.h" +#include "DiscoverBackendsFactory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +QTEST_MAIN(DummyTest) + +AbstractResourcesBackend *backendByName(ResourcesModel *m, const QString &name) +{ + const QVector backends = m->backends(); + for (AbstractResourcesBackend *backend : backends) { + if (QString::fromLatin1(backend->metaObject()->className()) == name) { + return backend; + } + } + return nullptr; +} + +DummyTest::DummyTest(QObject *parent) + : QObject(parent) +{ + DiscoverBackendsFactory::setRequestedBackends({QStringLiteral("dummy-backend")}); + + m_model = new ResourcesModel(QStringLiteral("dummy-backend"), this); + m_appBackend = backendByName(m_model, QStringLiteral("DummyBackend")); + + CategoryModel::global()->populateCategories(); +} + +void DummyTest::initTestCase() +{ + QVERIFY(m_appBackend); + while (m_appBackend->isFetching()) { + QSignalSpy spy(m_appBackend, &AbstractResourcesBackend::fetchingChanged); + QVERIFY(spy.wait()); + } +} + +QVector fetchResources(ResultsStream *stream) +{ + QVector ret; + QObject::connect(stream, &ResultsStream::resourcesFound, stream, [&ret](const QVector &res) { + ret += res; + }); + QSignalSpy spy(stream, &ResultsStream::destroyed); + Q_ASSERT(spy.wait()); + return ret; +} + +void DummyTest::testReadData() +{ + const auto resources = fetchResources(m_appBackend->search({})); + + QCOMPARE(m_appBackend->property("startElements").toInt() * 2, resources.size()); + QBENCHMARK { + for (AbstractResource *res : resources) { + QVERIFY(!res->name().isEmpty()); + } + } +} + +void DummyTest::testProxy() +{ + ResourcesProxyModel pm; + QSignalSpy spy(&pm, &ResourcesProxyModel::busyChanged); + // QVERIFY(spy.wait()); + QVERIFY(!pm.isBusy()); + + pm.setFiltersFromCategory(CategoryModel::global()->rootCategories().first()); + pm.componentComplete(); + QVERIFY(pm.isBusy()); + QVERIFY(spy.wait()); + QVERIFY(!pm.isBusy()); + + QCOMPARE(pm.rowCount(), m_appBackend->property("startElements").toInt() * 2); + pm.setSearch(QStringLiteral("techie")); + QVERIFY(pm.isBusy()); + QVERIFY(spy.wait()); + QVERIFY(!pm.isBusy()); + QCOMPARE(0, pm.rowCount()); + QCOMPARE(pm.subcategories().count(), 7); + pm.setSearch(QString()); + QVERIFY(pm.isBusy()); + QVERIFY(spy.wait()); + QVERIFY(!pm.isBusy()); + QCOMPARE(pm.rowCount(), m_appBackend->property("startElements").toInt() * 2); +} + +void DummyTest::testProxySorting() +{ + ResourcesProxyModel pm; + QSignalSpy spy(&pm, &ResourcesProxyModel::busyChanged); + // QVERIFY(spy.wait()); + QVERIFY(!pm.isBusy()); + + pm.setFiltersFromCategory(CategoryModel::global()->rootCategories().first()); + pm.setSortOrder(Qt::DescendingOrder); + pm.setSortRole(ResourcesProxyModel::RatingCountRole); + pm.componentComplete(); + QVERIFY(pm.isBusy()); + QVERIFY(spy.wait()); + QVERIFY(!pm.isBusy()); + + QCOMPARE(m_appBackend->property("startElements").toInt() * 2, pm.rowCount()); + QVariant lastRatingCount; + for (int i = 0, rc = pm.rowCount(); i < rc; ++i) { + const QModelIndex mi = pm.index(i, 0); + + const auto value = mi.data(pm.sortRole()); + QVERIFY(i == 0 || value <= lastRatingCount); + lastRatingCount = value; + } +} + +void DummyTest::testFetch() +{ + const auto resources = fetchResources(m_appBackend->search({})); + QCOMPARE(m_appBackend->property("startElements").toInt() * 2, resources.count()); + + // fetches updates, adds new things + m_appBackend->checkForUpdates(); + QSignalSpy spy(m_model, &ResourcesModel::allInitialized); + QVERIFY(spy.wait(80000)); + auto resources2 = fetchResources(m_appBackend->search({})); + QCOMPARE(m_appBackend->property("startElements").toInt() * 4, resources2.count()); +} + +void DummyTest::testSort() +{ + ResourcesProxyModel pm; + + QCollator c; + QBENCHMARK_ONCE { + pm.setSortRole(ResourcesProxyModel::NameRole); + pm.sort(0); + QCOMPARE(pm.sortOrder(), Qt::AscendingOrder); + QString last; + for (int i = 0, count = pm.rowCount(); i < count; ++i) { + const QString current = pm.index(i, 0).data(pm.sortRole()).toString(); + if (!last.isEmpty()) { + QCOMPARE(c.compare(last, current), -1); + } + last = current; + } + } + + QBENCHMARK_ONCE { + pm.setSortRole(ResourcesProxyModel::SortableRatingRole); + int last = -1; + for (int i = 0, count = pm.rowCount(); i < count; ++i) { + const int current = pm.index(i, 0).data(pm.sortRole()).toInt(); + QVERIFY(last <= current); + last = current; + } + } +} + +void DummyTest::testInstallAddons() +{ + AbstractResourcesBackend::Filters filter; + filter.resourceUrl = QUrl(QStringLiteral("dummy://Dummy.1")); + + const auto resources = fetchResources(m_appBackend->search(filter)); + QCOMPARE(resources.count(), 1); + AbstractResource *res = resources.first(); + QVERIFY(res); + + ApplicationAddonsModel m; + new QAbstractItemModelTester(&m, &m); + m.setApplication(res); + QCOMPARE(m.rowCount(), res->addonsInformation().count()); + QCOMPARE(res->addonsInformation().at(0).isInstalled(), false); + + QString firstAddonName = m.data(m.index(0, 0)).toString(); + m.changeState(firstAddonName, true); + QVERIFY(m.hasChanges()); + + m.applyChanges(); + QSignalSpy sR(TransactionModel::global(), &TransactionModel::transactionRemoved); + QVERIFY(sR.wait()); + QVERIFY(!m.hasChanges()); + + QCOMPARE(m.data(m.index(0, 0)).toString(), firstAddonName); + QCOMPARE(res->addonsInformation().at(0).name(), firstAddonName); + QCOMPARE(res->addonsInformation().at(0).isInstalled(), true); + + m.changeState(m.data(m.index(1, 0)).toString(), true); + QVERIFY(m.hasChanges()); + for (int i = 0, c = m.rowCount(); i < c; ++i) { + const auto idx = m.index(i, 0); + QCOMPARE(idx.data(Qt::CheckStateRole).toInt(), int(i <= 1 ? Qt::Checked : Qt::Unchecked)); + QVERIFY(!idx.data(ApplicationAddonsModel::PackageNameRole).toString().isEmpty()); + } + m.discardChanges(); + QVERIFY(!m.hasChanges()); +} + +void DummyTest::testReviewsModel() +{ + AbstractResourcesBackend::Filters filter; + filter.resourceUrl = QUrl(QStringLiteral("dummy://Dummy.1")); + + const auto resources = fetchResources(m_appBackend->search(filter)); + QCOMPARE(resources.count(), 1); + AbstractResource *res = resources.first(); + QVERIFY(res); + + ReviewsModel m; + new QAbstractItemModelTester(&m, &m); + m.setResource(res); + m.fetchMore(); + + QVERIFY(m.rowCount() > 0); + + QCOMPARE(ReviewsModel::UserChoice(m.data(m.index(0, 0), ReviewsModel::UsefulChoice).toInt()), ReviewsModel::None); + m.markUseful(0, true); + QCOMPARE(ReviewsModel::UserChoice(m.data(m.index(0, 0), ReviewsModel::UsefulChoice).toInt()), ReviewsModel::Yes); + m.markUseful(0, false); + QCOMPARE(ReviewsModel::UserChoice(m.data(m.index(0, 0), ReviewsModel::UsefulChoice).toInt()), ReviewsModel::No); + + const auto resources2 = fetchResources(m_appBackend->search(filter)); + QCOMPARE(resources2.count(), 1); + res = resources2.first(); + m.setResource(res); + m.fetchMore(); + + QSignalSpy spy(&m, &ReviewsModel::rowsChanged); + QVERIFY(m.rowCount() > 0); +} + +void DummyTest::testUpdateModel() +{ + const auto backend = m_model->backends().first(); + + ResourcesUpdatesModel ruModel; + new QAbstractItemModelTester(&ruModel, &ruModel); + UpdateModel model; + new QAbstractItemModelTester(&model, &model); + model.setBackend(&ruModel); + + QCOMPARE(model.rowCount(), backend->property("startElements").toInt() * 2); + QCOMPARE(model.hasUpdates(), true); +} + +void DummyTest::testScreenshotsModel() +{ + AbstractResourcesBackend::Filters filter; + filter.resourceUrl = QUrl(QStringLiteral("dummy://Dummy.1")); + + ScreenshotsModel m; + new QAbstractItemModelTester(&m, &m); + + const auto resources = fetchResources(m_appBackend->search(filter)); + QCOMPARE(resources.count(), 1); + AbstractResource *res = resources.first(); + QVERIFY(res); + m.setResource(res); + QCOMPARE(res, m.resource()); + + int c = m.rowCount(); + for (int i = 0; i < c; ++i) { + const auto idx = m.index(i, 0); + QVERIFY(!idx.data(ScreenshotsModel::ThumbnailUrl).isNull()); + QVERIFY(!idx.data(ScreenshotsModel::ScreenshotUrl).isNull()); + } +} + +// TODO test cancel transaction diff --git a/libdiscover/backends/DummyBackend/tests/DummyTest.h b/libdiscover/backends/DummyBackend/tests/DummyTest.h new file mode 100644 index 0000000..97e455c --- /dev/null +++ b/libdiscover/backends/DummyBackend/tests/DummyTest.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +class ResourcesModel; +class AbstractResourcesBackend; + +class DummyTest : public QObject +{ + Q_OBJECT +public: + explicit DummyTest(QObject *parent = nullptr); + +private Q_SLOTS: + void initTestCase(); + + void testReadData(); + void testProxy(); + void testProxySorting(); + void testFetch(); + void testSort(); + void testInstallAddons(); + void testReviewsModel(); + void testUpdateModel(); + void testScreenshotsModel(); + +private: + AbstractResourcesBackend *m_appBackend; + ResourcesModel *m_model; +}; diff --git a/libdiscover/backends/DummyBackend/tests/UpdateDummyTest.cpp b/libdiscover/backends/DummyBackend/tests/UpdateDummyTest.cpp new file mode 100644 index 0000000..ba40805 --- /dev/null +++ b/libdiscover/backends/DummyBackend/tests/UpdateDummyTest.cpp @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DummyTest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class UpdateDummyTest : public QObject +{ + Q_OBJECT +public: + AbstractResourcesBackend *backendByName(ResourcesModel *m, const QString &name) + { + const QVector backends = m->backends(); + for (AbstractResourcesBackend *backend : backends) { + if (QLatin1String(backend->metaObject()->className()) == name) { + return backend; + } + } + return nullptr; + } + + UpdateDummyTest(QObject *parent = nullptr) + : QObject(parent) + { + m_model = new ResourcesModel(QStringLiteral("dummy-backend"), this); + m_appBackend = backendByName(m_model, QStringLiteral("DummyBackend")); + } + +private Q_SLOTS: + void init() + { + QVERIFY(m_appBackend); + while (m_appBackend->isFetching()) { + QSignalSpy spy(m_appBackend, &AbstractResourcesBackend::fetchingChanged); + QVERIFY(spy.wait()); + } + } + + void testInformation() + { + ResourcesUpdatesModel *rum = new ResourcesUpdatesModel(this); + new QAbstractItemModelTester(rum, rum); + + UpdateModel *m = new UpdateModel(this); + new QAbstractItemModelTester(m, m); + m->setBackend(rum); + + rum->prepare(); + QSignalSpy spySetup(m_appBackend->backendUpdater(), &AbstractBackendUpdater::progressingChanged); + QVERIFY(!m_appBackend->backendUpdater()->isProgressing() || spySetup.wait()); + QCOMPARE(m_appBackend->updatesCount(), m_appBackend->property("startElements").toInt()); + QCOMPARE(m->hasUpdates(), true); + + QCOMPARE(m->index(0, 0).data(UpdateModel::ChangelogRole).toString(), QString{}); + + QSignalSpy spy(m, &QAbstractItemModel::dataChanged); + m->fetchUpdateDetails(0); + QVERIFY(spy.count() || spy.wait()); + QCOMPARE(spy.count(), 1); + delete m; + } + + void testUpdate() + { + ResourcesUpdatesModel *rum = new ResourcesUpdatesModel(this); + new QAbstractItemModelTester(rum, rum); + + UpdateModel *m = new UpdateModel(this); + new QAbstractItemModelTester(m, m); + m->setBackend(rum); + + rum->prepare(); + QSignalSpy spySetup(m_appBackend->backendUpdater(), &AbstractBackendUpdater::progressingChanged); + QVERIFY(!m_appBackend->backendUpdater()->isProgressing() || spySetup.wait()); + QCOMPARE(m_appBackend->updatesCount(), m_appBackend->property("startElements").toInt()); + QCOMPARE(m->hasUpdates(), true); + + for (int i = 0, c = m->rowCount(); i < c; ++i) { + const QModelIndex resourceIdx = m->index(i, 0); + QVERIFY(resourceIdx.isValid()); + + AbstractResource *res = qobject_cast(resourceIdx.data(UpdateModel::ResourceRole).value()); + QVERIFY(res); + + QCOMPARE(Qt::CheckState(resourceIdx.data(Qt::CheckStateRole).toInt()), Qt::Checked); + QVERIFY(m->setData(resourceIdx, int(Qt::Unchecked), Qt::CheckStateRole)); + QCOMPARE(Qt::CheckState(resourceIdx.data(Qt::CheckStateRole).toInt()), Qt::Unchecked); + QCOMPARE(resourceIdx.data(Qt::DisplayRole).toString(), res->name()); + + if (i != 0) { + QVERIFY(m->setData(resourceIdx, int(Qt::Checked), Qt::CheckStateRole)); + } + } + + QSignalSpy spy(rum, &ResourcesUpdatesModel::progressingChanged); + QVERIFY(!rum->isProgressing() || spy.wait()); + QCOMPARE(rum->isProgressing(), false); + + QCOMPARE(m_appBackend->updatesCount(), m->rowCount()); + QCOMPARE(m->hasUpdates(), true); + + rum->prepare(); + + spy.clear(); + QCOMPARE(rum->isProgressing(), false); + rum->updateAll(); + QVERIFY(spy.count() || spy.wait()); + QCOMPARE(rum->isProgressing(), true); + + QCOMPARE(TransactionModel::global()->rowCount(), 1); + connect(TransactionModel::global(), &TransactionModel::progressChanged, this, []() { + const int progress = TransactionModel::global()->progress(); + static int lastProgress = -1; + Q_ASSERT(progress >= lastProgress || (TransactionModel::global()->rowCount() == 0 && progress == 0)); + lastProgress = progress; + }); + + QTest::qWait(20); + QScopedPointer rum2(new ResourcesUpdatesModel(this)); + new QAbstractItemModelTester(rum2.data(), rum2.data()); + + QScopedPointer m2(new UpdateModel(this)); + new QAbstractItemModelTester(m2.data(), m2.data()); + m->setBackend(rum2.data()); + + QCOMPARE(rum->isProgressing(), true); + QVERIFY(spy.wait()); + QCOMPARE(rum->isProgressing(), false); + + QCOMPARE(m_appBackend->updatesCount(), 0); + QCOMPARE(m->hasUpdates(), false); + } + +private: + ResourcesModel *m_model; + AbstractResourcesBackend *m_appBackend; +}; + +QTEST_MAIN(UpdateDummyTest) + +#include "UpdateDummyTest.moc" diff --git a/libdiscover/backends/FlatpakBackend/CMakeLists.txt b/libdiscover/backends/FlatpakBackend/CMakeLists.txt new file mode 100644 index 0000000..2872111 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/CMakeLists.txt @@ -0,0 +1,45 @@ +add_subdirectory(tests) + +set(flatpak-backend_SRCS + FlatpakResource.cpp + FlatpakBackend.cpp + FlatpakFetchDataJob.cpp + FlatpakSourcesBackend.cpp + FlatpakJobTransaction.cpp + FlatpakTransactionThread.cpp + FlatpakRefreshAppstreamMetadataJob.cpp + FlatpakPermission.cpp + resources.qrc +) + +add_library(flatpak-backend MODULE ${flatpak-backend_SRCS}) +target_link_libraries(flatpak-backend Qt::Core Qt::Widgets Qt::Concurrent KF5::CoreAddons KF5::ConfigCore KF5::KIOGui Discover::Common AppStreamQt PkgConfig::Flatpak) + +target_compile_definitions(flatpak-backend PRIVATE -DAPPSTREAM_NEW_POOL_API) + +if (NOT Flatpak_VERSION VERSION_LESS 1.1.2) + target_compile_definitions(flatpak-backend PRIVATE -DFLATPAK_VERBOSE_PROGRESS -DFLATPAK_LIST_UNUSED_REFS) +endif() + +install(TARGETS flatpak-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) +install(FILES flatpak-backend-categories.xml DESTINATION ${KDE_INSTALL_DATADIR}/libdiscover/categories) + +add_library(FlatpakNotifier MODULE FlatpakNotifier.cpp) +target_link_libraries(FlatpakNotifier Discover::Notifiers Qt::Concurrent PkgConfig::Flatpak) +set_target_properties(FlatpakNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) + +if (Flatpak_VERSION VERSION_LESS 1.10.2) + target_compile_definitions(flatpak-backend PRIVATE -DFLATPAK_EXTERNC_REQUIRED) + target_compile_definitions(FlatpakNotifier PRIVATE -DFLATPAK_EXTERNC_REQUIRED) +endif() + +install(TARGETS FlatpakNotifier DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover-notifier) +install(PROGRAMS org.kde.discover-flatpak.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) +install(FILES org.kde.discover.flatpak.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) + +ecm_install_icons( + ICONS + sc-apps-flatpak-discover.svg + DESTINATION ${KDE_INSTALL_ICONDIR} + THEME hicolor +) diff --git a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp new file mode 100644 index 0000000..ef133cc --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp @@ -0,0 +1,1944 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakBackend.h" +#include "FlatpakFetchDataJob.h" +#include "FlatpakJobTransaction.h" +#include "FlatpakRefreshAppstreamMetadataJob.h" +#include "FlatpakSourcesBackend.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +DISCOVER_BACKEND_PLUGIN(FlatpakBackend) + +class FlatpakSource +{ +public: + FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation) + : m_remote(nullptr) + , m_installation(installation) + , m_backend(backend) + { + g_object_ref(m_installation); + } + + FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation, FlatpakRemote *remote) + : m_remote(remote) + , m_installation(installation) + , m_backend(backend) + , m_appstreamIconsDir(appstreamDir() + QLatin1String("/icons")) + { + g_object_ref(m_remote); + g_object_ref(m_installation); + } + + ~FlatpakSource() + { + if (m_remote) { + g_object_unref(m_remote); + } + g_object_unref(m_installation); + } + + QString url() const + { + return m_remote ? flatpak_remote_get_url(m_remote) : QString(); + } + + bool isEnabled() const + { + return m_remote && !flatpak_remote_get_disabled(m_remote); + } + + QString appstreamIconsDir() const + { + return m_appstreamIconsDir; + } + QString appstreamDir() const + { + Q_ASSERT(m_remote); + g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr); + if (!appstreamDir) { + qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote); + return {}; + } + g_autofree char *path_str = g_file_get_path(appstreamDir); + return QString::fromUtf8(path_str); + } + + QString name() const + { + return m_remote ? QString::fromUtf8(flatpak_remote_get_name(m_remote)) : QString(); + } + + QString title() const + { + auto ret = m_remote ? QString::fromUtf8(flatpak_remote_get_title(m_remote)) : QString(); + if (flatpak_installation_get_is_user(m_installation)) { + ret = i18nc("user denotes this as user-scoped flatpak repo", "%1 (user)", ret); + } + return ret; + } + + FlatpakInstallation *installation() const + { + return m_installation; + } + + void addResource(FlatpakResource *resource) + { + // Update app with all possible information we have + if (!m_backend->parseMetadataFromAppBundle(resource)) { + qWarning() << "Failed to parse metadata from app bundle for" << resource->name(); + } + + m_backend->updateAppState(resource); + + Q_ASSERT(!m_resources.contains(resource->uniqueId()) || m_resources.value(resource->uniqueId()) == resource); + m_resources.insert(resource->uniqueId(), resource); + if (!resource->extends().isEmpty()) { + m_backend->m_extends.append(resource->extends()); + m_backend->m_extends.removeDuplicates(); + } + + QObject::connect(resource, &FlatpakResource::sizeChanged, m_backend, [this, resource] { + if (!m_backend->isFetching()) + Q_EMIT m_backend->resourcesChanged(resource, {"size", "sizeDescription"}); + }); + } + + FlatpakRemote *remote() const + { + return m_remote; + } + + QList componentsByName(const QString &name) + { + auto comps = m_pool->componentsById(name); + if (!comps.isEmpty()) { +#if ASQ_CHECK_VERSION(1, 0, 0) + return comps.toList(); +#else + return comps; +#endif + } + + comps = m_pool->componentsByProvided(AppStream::Provided::KindId, name); + +#if ASQ_CHECK_VERSION(1, 0, 0) + return comps.toList(); +#else + return comps; +#endif + } + + QList componentsByFlatpakId(const QString &ref) + { +#if ASQ_CHECK_VERSION(1, 0, 0) + QList comps = m_pool->componentsByBundleId(AppStream::Bundle::KindFlatpak, ref, false).toList(); +#elif ASQ_CHECK_VERSION(0, 16, 0) + QList comps = m_pool->componentsByBundleId(AppStream::Bundle::KindFlatpak, ref, false); +#else + QList comps = m_pool->components(); + comps = kFilter>(comps, [&ref](const AppStream::Component &component) { + const QString id = component.bundle(AppStream::Bundle::KindFlatpak).id(); + return id == ref; + }); +#endif + if (!comps.isEmpty()) + return comps; +#if ASQ_CHECK_VERSION(1, 0, 0) + comps = m_pool->componentsByProvided(AppStream::Provided::KindId, ref.section('/', 1, 1)).toList(); +#else + comps = m_pool->componentsByProvided(AppStream::Provided::KindId, ref.section('/', 1, 1)); +#endif + return comps; + } + + AppStream::Pool *m_pool = nullptr; + QHash m_resources; + +private: + FlatpakRemote *const m_remote; + FlatpakInstallation *const m_installation; + FlatpakBackend *const m_backend; + const QString m_appstreamIconsDir; +}; + +static void populateRemote(FlatpakRemote *remote, const QString &name, const QString &url, const QString &gpgKey) +{ + flatpak_remote_set_url(remote, url.toUtf8().constData()); + flatpak_remote_set_noenumerate(remote, false); + flatpak_remote_set_title(remote, name.toUtf8().constData()); + + if (!gpgKey.isEmpty()) { + gsize dataLen = 0; + g_autofree guchar *data = nullptr; + g_autoptr(GBytes) bytes = nullptr; + data = g_base64_decode(gpgKey.toUtf8().constData(), &dataLen); + bytes = g_bytes_new(data, dataLen); + flatpak_remote_set_gpg_verify(remote, true); + flatpak_remote_set_gpg_key(remote, bytes); + } else { + flatpak_remote_set_gpg_verify(remote, false); + } +} + +QDebug operator<<(QDebug debug, const FlatpakResource::Id &id) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "FlatpakResource::Id("; + debug.nospace() << "name:" << id.id << ','; + debug.nospace() << "branch:" << id.branch; + debug.nospace() << ')'; + return debug; +} + +static FlatpakResource::Id idForComponent(const AppStream::Component &component) +{ + // app/app.getspace.Space/x86_64/stable + const auto bundleId = component.bundle(AppStream::Bundle::KindFlatpak).id(); + auto parts = bundleId.splitRef('/'); + Q_ASSERT(parts.size() == 4); + + return { + component.id(), + parts[3].toString(), + parts[2].toString(), + }; +} + +static FlatpakResource::Id idForInstalledRef(FlatpakInstalledRef *ref, const QString &postfix) +{ + const QString appId = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + postfix; + const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref))); + const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref))); + + return {appId, branch, arch}; +} + +static std::optional metadataFromBytes(GBytes *appstreamGz, GCancellable *cancellable) +{ + g_autoptr(GError) localError = nullptr; + g_autoptr(GZlibDecompressor) decompressor = nullptr; + g_autoptr(GInputStream) streamGz = nullptr; + g_autoptr(GInputStream) streamData = nullptr; + g_autoptr(GBytes) appstream = nullptr; + + /* decompress data */ + decompressor = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP); + streamGz = g_memory_input_stream_new_from_bytes(appstreamGz); + if (!streamGz) { + return {}; + } + + streamData = g_converter_input_stream_new(streamGz, G_CONVERTER(decompressor)); + + appstream = g_input_stream_read_bytes(streamData, 0x100000, cancellable, &localError); + if (!appstream) { + qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message; + return {}; + } + + gsize len = 0; + gconstpointer data = g_bytes_get_data(appstream, &len); + + AppStream::Metadata metadata; +#if ASQ_CHECK_VERSION(0, 16, 0) + metadata.setFormatStyle(AppStream::Metadata::FormatStyleCatalog); +#else + metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); +#endif + AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char *)data, len), AppStream::Metadata::FormatKindXml); + if (error != AppStream::Metadata::MetadataErrorNoError) { + qWarning() << "Failed to parse appstream metadata: " << error; + return {}; + } + return metadata; +} + +FlatpakBackend::FlatpakBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , m_updater(new StandardBackendUpdater(this)) + , m_reviews(AppStreamIntegration::global()->reviews()) + , m_cancellable(g_cancellable_new()) + , m_checkForUpdatesTimer(new QTimer(this)) +{ + g_autoptr(GError) error = nullptr; + + connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged); + + // Load flatpak installation + if (!setupFlatpakInstallations(&error)) { + qWarning() << "Failed to setup flatpak installations:" << error->message; + } else { + m_sources = new FlatpakSourcesBackend(m_installations, this); + loadAppsFromAppstreamData(); + + SourcesModel::global()->addSourcesBackend(m_sources); + } + + connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { + m_reviews->emitRatingFetched(this, kAppend>(m_flatpakSources, [](const auto &source) { + return kTransform>(source->m_resources.values()); + })); + }); + + m_checkForUpdatesTimer->setInterval(1000); + m_checkForUpdatesTimer->setSingleShot(true); + connect(m_checkForUpdatesTimer, &QTimer::timeout, this, &FlatpakBackend::checkForUpdates); + + /* Override the umask to 022 to make it possible to share files between + * the plasma-discover process and flatpak system helper process. + * + * See https://github.com/flatpak/flatpak/pull/2856/ + */ + umask(022); +} + +FlatpakBackend::~FlatpakBackend() +{ + g_cancellable_cancel(m_cancellable); + if (!m_threadPool.waitForDone(200)) { + qDebug() << "could not kill them all" << m_threadPool.activeThreadCount(); + } + m_threadPool.clear(); + + for (auto inst : qAsConst(m_installations)) + g_object_unref(inst); + m_installations.clear(); + g_object_unref(m_cancellable); +} + +bool FlatpakBackend::isValid() const +{ + return m_sources && !m_installations.isEmpty(); +} + +class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager +{ + Q_OBJECT +public: + FlatpakFetchRemoteResourceJob(const QUrl &url, ResultsStream *stream, FlatpakBackend *backend) + : QNetworkAccessManager(backend) + , m_backend(backend) + , m_stream(stream) + , m_url(url) + { + connect(stream, &ResultsStream::destroyed, this, &QObject::deleteLater); + } + + void start() + { + if (m_url.isLocalFile()) { + QTimer::singleShot(0, m_stream, [this] { + processFile(m_url); + }); + return; + } + + QNetworkRequest req(m_url); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + auto replyGet = get(req); + connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] { + QScopedPointer replyPtr(replyGet); + if (replyGet->error() != QNetworkReply::NoError) { + qWarning() << "couldn't download" << m_url << replyGet->errorString(); + m_stream->finish(); + return; + } + const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) // + + QLatin1Char('/') + m_url.fileName()); + auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll()); + connect(replyPut, &QNetworkReply::finished, this, [this, fileUrl, replyPut]() { + QScopedPointer replyPtr(replyPut); + if (replyPut->error() != QNetworkReply::NoError) { + qWarning() << "couldn't save" << m_url << replyPut->errorString(); + m_stream->finish(); + return; + } + if (!fileUrl.isLocalFile()) { + m_stream->finish(); + return; + } + + processFile(fileUrl); + }); + }); + } + + void processFile(const QUrl &fileUrl) + { + const auto path = fileUrl.toLocalFile(); + if (path.endsWith(QLatin1String(".flatpak"))) { + m_backend->addAppFromFlatpakBundle(fileUrl, m_stream); + } else if (path.endsWith(QLatin1String(".flatpakref"))) { + m_backend->addAppFromFlatpakRef(fileUrl, m_stream); + } else if (path.endsWith(QLatin1String(".flatpakrepo"))) { + m_backend->addSourceFromFlatpakRepo(fileUrl, m_stream); + } else { + qWarning() << "unrecognized format" << fileUrl; + } + } + +private: + FlatpakBackend *const m_backend; + ResultsStream *const m_stream; + const QUrl m_url; +}; + +FlatpakRemote *FlatpakBackend::getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const +{ + auto remotes = flatpak_installation_list_remotes(installation, m_cancellable, nullptr); + if (!remotes) { + return nullptr; + } + + const QByteArray comparableUrl = url.toUtf8(); + for (uint i = 0; i < remotes->len; i++) { + FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); + + if (comparableUrl == flatpak_remote_get_url(remote)) { + return remote; + } + } + return nullptr; +} + +FlatpakInstalledRef *FlatpakBackend::getInstalledRefForApp(const FlatpakResource *resource) const +{ + Q_ASSERT(resource->resourceType() != FlatpakResource::Source); + g_autoptr(GError) localError = nullptr; + + const auto type = resource->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME; + + FlatpakInstalledRef *ref = flatpak_installation_get_installed_ref(resource->installation(), + type, + resource->flatpakName().toUtf8().constData(), + resource->arch().toUtf8().constData(), + resource->branch().toUtf8().constData(), + m_cancellable, + &localError); + return ref; +} + +QString refToBundleId(FlatpakRef *ref) +{ + return QString(flatpak_ref_get_kind(ref) == FLATPAK_REF_KIND_APP ? "app/" : "runtime/") + flatpak_ref_get_name(ref) + '/' + flatpak_ref_get_arch(ref) + '/' + + flatpak_ref_get_branch(ref); +} + +FlatpakResource *FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref, bool *freshResource) const +{ + if (freshResource) + *freshResource = false; + const QString origin = QString::fromUtf8(flatpak_installed_ref_get_origin(ref)); + auto source = findSource(installation, origin); + if (source) { + auto ret = source->m_resources.value(idForInstalledRef(ref, {})); + if (ret) { + return ret; + } + } + + const QLatin1String name(flatpak_ref_get_name(FLATPAK_REF(ref))); + const QLatin1String branch(flatpak_ref_get_branch(FLATPAK_REF(ref))); + const QString pathExports = FlatpakResource::installationPath(installation) + QLatin1String("/exports/"); + const QString pathApps = pathExports + QLatin1String("share/applications/"); + const QString refId = refToBundleId(FLATPAK_REF(ref)); + AppStream::Component cid; + if (source && source->m_pool) { + QList comps = source->componentsByFlatpakId(refId); + if (comps.isEmpty()) { + g_autoptr(GBytes) metadata = flatpak_installed_ref_load_appdata(ref, 0, 0); + if (metadata) { + auto meta = metadataFromBytes(metadata, m_cancellable); +#if ASQ_CHECK_VERSION(1, 0, 0) + comps = meta->components().toList(); +#else + comps = meta->components(); +#endif + } + } + + if (comps.count() >= 1) { + Q_ASSERT(comps.count() == 1); + cid = comps.constFirst(); + } + } + + if (!cid.isValid()) { + AppStream::Metadata metadata; + const QString fnDesktop = pathApps + name + QLatin1String(".desktop"); + AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry); + if (error != AppStream::Metadata::MetadataErrorNoError) { + if (QFile::exists(fnDesktop)) + qDebug() << "Failed to parse appstream metadata:" << error << fnDesktop; + + cid.setId(name); +#if FLATPAK_CHECK_VERSION(1, 1, 2) + cid.setName(QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref))); +#endif + } else + cid = metadata.component(); + } + + if (cid.bundle(AppStream::Bundle::KindFlatpak).isEmpty()) { + AppStream::Bundle b; + b.setKind(AppStream::Bundle::KindFlatpak); + b.setId(refId); + cid.addBundle(b); + } + + if (source && cid.isValid()) { + auto ret = source->m_resources.value(idForComponent(cid)); + if (ret) { + return ret; + } + } + + if (!source) { + return nullptr; + } + + FlatpakResource *resource = new FlatpakResource(cid, source->installation(), const_cast(this)); + resource->setOrigin(source->name()); + resource->setDisplayOrigin(source->title()); + resource->setIconPath(pathExports); + resource->updateFromRef(FLATPAK_REF(ref)); + resource->setState(AbstractResource::Installed); + source->addResource(resource); + + if (freshResource) + *freshResource = true; + + Q_ASSERT(resource->uniqueId() == idForInstalledRef(ref, {}) || resource->uniqueId() == idForInstalledRef(ref, {".desktop"})); + return resource; +} + +QSharedPointer FlatpakBackend::findSource(FlatpakInstallation *installation, const QString &origin) const +{ + for (const auto &source : m_flatpakSources) { + if (source->installation() == installation && source->name() == origin) { + return source; + } + } + for (const auto &source : m_flatpakLoadingSources) { + if (source->installation() == installation && source->name() == origin) { + return source; + } + } + + qWarning() << "Could not find source:" << installation << origin; + return {}; +} + +FlatpakResource *FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const +{ + FlatpakResource *runtime = nullptr; + const QString runtimeName = resource->runtime(); + const auto runtimeInfo = runtimeName.splitRef(QLatin1Char('/')); + + if (runtimeInfo.count() != 3) { + return runtime; + } + + for (const auto &source : m_flatpakSources) { + for (auto it = source->m_resources.constBegin(), itEnd = source->m_resources.constEnd(); it != itEnd; ++it) { + const auto &id = it.key(); + if ((*it)->resourceType() == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) { + runtime = *it; + break; + } + } + } + + for (auto installation : m_installations) { + auto instref = flatpak_installation_get_installed_ref(installation, + FLATPAK_REF_KIND_RUNTIME, + runtimeInfo.at(0).toUtf8().constData(), + runtimeInfo.at(1).toUtf8().constData(), + runtimeInfo.at(2).toUtf8().constData(), + m_cancellable, + nullptr); + if (instref) { + return getAppForInstalledRef(installation, instref); + } + } + + // TODO if runtime wasn't found, create a new one from available info + if (!runtime) { + qWarning() << "could not find runtime" << runtimeName << resource; + } + + return runtime; +} + +void FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url, ResultsStream *stream) +{ + auto x = qScopeGuard([stream] { + stream->finish(); + }); + g_autoptr(GBytes) appstreamGz = nullptr; + g_autoptr(GError) localError = nullptr; + g_autoptr(GFile) file = nullptr; + g_autoptr(FlatpakBundleRef) bundleRef = nullptr; + AppStream::Component asComponent; + + file = g_file_new_for_path(url.toLocalFile().toUtf8().constData()); + bundleRef = flatpak_bundle_ref_new(file, &localError); + + if (!bundleRef) { + qWarning() << "Failed to load bundle:" << localError->message; + return; + } + + gsize len = 0; + g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef); + const QByteArray metadataContent((char *)g_bytes_get_data(metadata, &len)); + + appstreamGz = flatpak_bundle_ref_get_appstream(bundleRef); + if (appstreamGz) { + const auto metadata = metadataFromBytes(appstreamGz, m_cancellable); + if (!metadata.has_value()) { + return; + } + +#if ASQ_CHECK_VERSION(1, 0, 0) + const QList components = metadata->components().toList(); +#else + const QList components = metadata->components(); +#endif + if (components.size()) { + asComponent = components.first(); + } else { + qWarning() << "Failed to parse appstream metadata"; + return; + } + } else { + qWarning() << "No appstream metadata in bundle"; + + QTemporaryFile tempFile; + tempFile.setAutoRemove(false); + if (!tempFile.open()) { + qWarning() << "Failed to get metadata file"; + return; + } + + tempFile.write(metadataContent); + tempFile.close(); + + // Parse the temporary file + QSettings setting(tempFile.fileName(), QSettings::NativeFormat); + setting.beginGroup(QLatin1String("Application")); + + asComponent.setName(setting.value(QLatin1String("name")).toString()); + + tempFile.remove(); + } + + g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(preferredInstallation(), m_cancellable, &localError); + if (!refs) { + qWarning() << "Failed to get list of installed refs for listing local updates:" << localError->message; + return; + } + + for (uint i = 0; i < refs->len; i++) { + FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i)); + FlatpakInstalledRef *iref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); + if (qstrcmp(flatpak_ref_get_commit(ref), flatpak_ref_get_commit(FLATPAK_REF(bundleRef))) == 0) { + auto res = getAppForInstalledRef(preferredInstallation(), iref, nullptr); + if (res) { + Q_EMIT stream->resourcesFound({res}); + } + return; + } + } + + FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this); + if (!updateAppMetadata(resource, metadataContent)) { + delete resource; + qWarning() << "Failed to update metadata from app bundle"; + return; + } + + g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128); + if (!iconData) { + iconData = flatpak_bundle_ref_get_icon(bundleRef, 64); + } + + if (iconData) { + gsize len = 0; + char *data = (char *)g_bytes_get_data(iconData, &len); + + QPixmap pixmap; + pixmap.loadFromData(QByteArray(data, len), "PNG"); + resource->setBundledIcon(pixmap); + } + + const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef)); + resource->updateFromRef(FLATPAK_REF(bundleRef)); + resource->setDownloadSize(0); + resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef)); + resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown); + resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown); + resource->setFlatpakFileType(FlatpakResource::FileFlatpak); + resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin); + resource->setResourceFile(url); + resource->setState(FlatpakResource::None); + + if (!m_localSource) { + m_localSource.reset(new FlatpakSource(this, preferredInstallation())); + m_flatpakSources += m_localSource; + } + m_localSource->addResource(resource); + Q_EMIT stream->resourcesFound({resource}); +} + +QString composeRef(bool isRuntime, const QString &name, const QString &branch) +{ + return (isRuntime ? "runtime/" : "app/") + name + '/' + flatpak_get_default_arch() + '/' + branch; +} + +AppStream::Component fetchComponentFromRemote(const QSettings &settings, GCancellable *cancellable) +{ + const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString(); + const QString branch = settings.value(QStringLiteral("Flatpak Ref/Branch")).toString(); + const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString(); + const bool isRuntime = settings.value(QStringLiteral("Flatpak Ref/IsRuntime")).toBool(); + + AppStream::Component asComponent; + asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Ref/Homepage")).toString()); + asComponent.setDescription(settings.value(QStringLiteral("Flatpak Ref/Description")).toString()); + asComponent.setName(settings.value(QStringLiteral("Flatpak Ref/Title")).toString()); + asComponent.setSummary(settings.value(QStringLiteral("Flatpak Ref/Comment")).toString()); + asComponent.setId(name); + + AppStream::Bundle b; + b.setKind(AppStream::Bundle::KindFlatpak); + b.setId(composeRef(isRuntime, asComponent.name(), branch)); + asComponent.addBundle(b); + + // We are going to create a temporary installation and add the remote to it. + // There we will fetch the appstream metadata and then delete that temporary installation. + + g_autoptr(GError) localError = nullptr; + const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-temporary-") + remoteName; + qDebug() << "Creating temporary installation" << path; + g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData()); + g_autoptr(FlatpakInstallation) tempInstallation = flatpak_installation_new_for_path(file, true, cancellable, &localError); + if (!tempInstallation) { + return asComponent; + } + auto x = qScopeGuard([path] { + QDir(path).removeRecursively(); + }); + + g_autoptr(FlatpakRemote) tempRemote = flatpak_remote_new(remoteName.toUtf8()); + populateRemote(tempRemote, + remoteName, + settings.value(QStringLiteral("Flatpak Ref/Url")).toString(), + settings.value(QStringLiteral("Flatpak Ref/GPGKey")).toString().toUtf8()); + if (!flatpak_installation_modify_remote(tempInstallation, tempRemote, cancellable, &localError)) { + qDebug() << "error adding temporary remote" << localError->message; + return {asComponent}; + } + + auto cb = [](const char *status, guint progress, gboolean /*estimating*/, gpointer /*user_data*/) { + qDebug() << "Progress..." << status << progress; + }; + + gboolean changed; + if (!flatpak_installation_update_appstream_full_sync(tempInstallation, remoteName.toUtf8(), nullptr, cb, nullptr, &changed, cancellable, &localError)) { + qDebug() << "error fetching appstream" << localError->message; + return {asComponent}; + } + Q_ASSERT(changed); + const QString appstreamLocation = path + "/appstream/" + remoteName + '/' + flatpak_get_default_arch() + "/active"; + + AppStream::Pool pool; +#ifdef APPSTREAM_NEW_POOL_API + pool.setLoadStdDataLocations(false); +#if ASQ_CHECK_VERSION(0, 16, 0) + pool.addExtraDataLocation(appstreamLocation, AppStream::Metadata::FormatStyleCatalog); +#else + pool.addExtraDataLocation(appstreamLocation, AppStream::Metadata::FormatStyleCollection); +#endif +#else + pool.clearMetadataLocations(); + pool.addMetadataLocation(appstreamLocation); + pool.setFlags(AppStream::Pool::FlagReadCollection); + pool.setCacheFlags(AppStream::Pool::CacheFlagUseUser); + + const QString subdir = flatpak_installation_get_id(tempInstallation) + QLatin1Char('/') + remoteName; + pool.setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream-temp/" + subdir); + QDir().mkpath(pool.cacheLocation()); +#endif + + if (!pool.load()) { + qDebug() << "error loading pool" << pool.lastError(); + return {asComponent}; + } + + // TODO optimise, this lookup should happen in libappstream +#if ASQ_CHECK_VERSION(1, 0, 0) + auto comps = pool.components().toList(); +#else + auto comps = pool.components(); +#endif + comps = kFilter>(comps, [name, branch](const AppStream::Component &component) { + const QString id = component.bundle(AppStream::Bundle::KindFlatpak).id(); + // app/app.getspace.Space/x86_64/stable + return id.section(QLatin1Char('/'), 1, 1) == name && (branch.isEmpty() || id.section(QLatin1Char('/'), 3, 3) == branch); + }); + if (comps.isEmpty()) { + qDebug() << "could not find" << name << "in" << remoteName; + return asComponent; + } + return comps.constFirst(); +} + +void FlatpakBackend::addAppFromFlatpakRef(const QUrl &url, ResultsStream *stream) +{ + Q_ASSERT(url.isLocalFile()); + QSettings settings(url.toLocalFile(), QSettings::NativeFormat); + const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString(); + const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString(); + const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString(); + const QString branch = settings.value(QStringLiteral("Flatpak Ref/Branch")).toString(); + const bool isRuntime = settings.value(QStringLiteral("Flatpak Ref/IsRuntime")).toBool(); + g_autoptr(GError) error = nullptr; + + // If we already added the remote, just go with it + g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(preferredInstallation(), remoteName.toUtf8().constData(), m_cancellable, &error); + if (remote && flatpak_remote_get_url(remote) != refurl) { + remote = nullptr; + } + if (remote) { + Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(remote)); + m_refreshAppstreamMetadataJobs.insert(remote); + auto source = integrateRemote(preferredInstallation(), remote); + if (source) { + const QString ref = composeRef(isRuntime, name, branch); + auto searchComponent = [this, stream, source, ref] { + auto comps = source->componentsByFlatpakId(ref); + auto resources = kTransform>(comps, [this, source](const auto &comp) { + return resourceForComponent(comp, source); + }); + Q_EMIT stream->resourcesFound(resources); + stream->finish(); + }; + if (source->m_pool) { + QTimer::singleShot(0, this, searchComponent); + } else { + connect(this, &FlatpakBackend::initialized, stream, searchComponent); + } + return; + } + } + + AppStream::Component asComponent = fetchComponentFromRemote(settings, m_cancellable); + const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString(); + if (!iconUrl.isEmpty()) { + AppStream::Icon icon; + icon.setKind(AppStream::Icon::KindRemote); + icon.setUrl(QUrl(iconUrl)); + asComponent.addIcon(icon); + } + + auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); + resource->setFlatpakFileType(FlatpakResource::FileFlatpakRef); + resource->setResourceFile(url); + resource->setResourceLocation(QUrl(refurl)); + resource->setOrigin(remoteName); + resource->setDisplayOrigin(remote ? QString::fromUtf8(flatpak_remote_get_title(remote)) : QString()); + resource->setFlatpakName(name); + resource->setArch(flatpak_get_default_arch()); + resource->setBranch(branch); + resource->setType(isRuntime ? FlatpakResource::Runtime : FlatpakResource::DesktopApp); + + QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString()); + auto refSource = QSharedPointer::create(this, preferredInstallation()); + resource->setTemporarySource(refSource); + m_flatpakSources += refSource; + if (!runtimeUrl.isEmpty()) { + // We need to fetch metadata to find information about required runtime + auto fw = new QFutureWatcher(this); + connect(fw, &QFutureWatcher::finished, this, [this, resource, fw, runtimeUrl, stream, refSource]() { + fw->deleteLater(); + const auto metadata = fw->result(); + // Even when we failed to fetch information about runtime we still want to show the application + if (metadata.isEmpty()) { + onFetchMetadataFinished(resource, metadata); + } else { + updateAppMetadata(resource, metadata); + + auto runtime = getRuntimeForApp(resource); + if (!runtime || (runtime && !runtime->isInstalled())) { + auto repoStream = new ResultsStream(QLatin1String("FlatpakStream-searchrepo-") + runtimeUrl.toString()); + connect(repoStream, + &ResultsStream::resourcesFound, + this, + [this, resource, stream, refSource](const QVector &resources) { + for (auto res : resources) { + installApplication(res); + } + refSource->addResource(resource); + Q_EMIT stream->resourcesFound({resource}); + stream->finish(); + }); + + auto fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, repoStream, this); + fetchRemoteResource->start(); + return; + } else { + refSource->addResource(resource); + } + } + Q_EMIT stream->resourcesFound({resource}); + stream->finish(); + }); + fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable)); + } else { + refSource->addResource(resource); + Q_EMIT stream->resourcesFound({resource}); + stream->finish(); + } +} + +void FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url, ResultsStream *stream) +{ + auto x = qScopeGuard([stream] { + stream->finish(); + }); + Q_ASSERT(url.isLocalFile()); + QSettings settings(url.toLocalFile(), QSettings::NativeFormat); + + const QString gpgKey = settings.value(QStringLiteral("Flatpak Repo/GPGKey")).toString(); + const QString title = settings.value(QStringLiteral("Flatpak Repo/Title")).toString(); + const QString repoUrl = settings.value(QStringLiteral("Flatpak Repo/Url")).toString(); + + if (gpgKey.isEmpty() || title.isEmpty() || repoUrl.isEmpty()) { + return; + } + + if (gpgKey.startsWith(QLatin1String("http://")) || gpgKey.startsWith(QLatin1String("https://"))) { + return; + } + + AppStream::Component asComponent; + asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Repo/Homepage")).toString()); + asComponent.setSummary(settings.value(QStringLiteral("Flatpak Repo/Comment")).toString()); + asComponent.setDescription(settings.value(QStringLiteral("Flatpak Repo/Description")).toString()); + asComponent.setName(title); + asComponent.setId(settings.value(QStringLiteral("Flatpak Repo/Title")).toString()); + + const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString(); + if (!iconUrl.isEmpty()) { + AppStream::Icon icon; + icon.setKind(AppStream::Icon::KindRemote); + icon.setUrl(QUrl(iconUrl)); + asComponent.addIcon(icon); + } + + auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); + // Use metadata only for stuff which are not common for all resources + resource->addMetadata(QStringLiteral("gpg-key"), gpgKey); + resource->addMetadata(QStringLiteral("repo-url"), repoUrl); + resource->setBranch(settings.value(QStringLiteral("Flatpak Repo/DefaultBranch")).toString()); + resource->setFlatpakName(url.fileName().remove(QStringLiteral(".flatpakrepo"))); + resource->setType(FlatpakResource::Source); + + g_autoptr(FlatpakRemote) repo = + flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr); + if (!repo) { + resource->setState(AbstractResource::State::None); + } else { + resource->setState(AbstractResource::State::Installed); + } + + Q_EMIT stream->resourcesFound({resource}); +} + +void FlatpakBackend::loadAppsFromAppstreamData() +{ + for (auto installation : qAsConst(m_installations)) { + // Load applications from appstream metadata + if (g_cancellable_is_cancelled(m_cancellable)) + break; + + if (!loadAppsFromAppstreamData(installation)) { + qWarning() << "Failed to load packages from appstream data from installation" << installation; + } + } +} + +bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation) +{ + Q_ASSERT(flatpakInstallation); + + GError *error = nullptr; + g_autoptr(GPtrArray) remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, &error); + if (!remotes) { + qWarning() << "failed to list remotes" << error->message; + return false; + } + + for (uint i = 0; i < remotes->len; i++) { + FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); + loadRemote(flatpakInstallation, remote); + } + return true; +} + +void FlatpakBackend::loadRemote(FlatpakInstallation *installation, FlatpakRemote *remote) +{ + g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, flatpak_get_default_arch()); + + Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(remote)); + m_refreshAppstreamMetadataJobs.insert(remote); + + g_autofree char *path_str = g_file_get_path(fileTimestamp); + QFileInfo fileInfo(QFile::encodeName(path_str)); + if (!fileInfo.exists() || fileInfo.lastModified().toUTC().secsTo(QDateTime::currentDateTimeUtc()) > 21600) { + // Refresh appstream metadata in case they have never been refreshed or the cache is older than 6 hours + checkForRemoteUpdates(installation, remote); + } else { + auto source = integrateRemote(installation, remote); + Q_ASSERT(findSource(installation, flatpak_remote_get_name(remote)) == source); + } +} + +void FlatpakBackend::unloadRemote(FlatpakInstallation *installation, FlatpakRemote *remote) +{ + acquireFetching(true); + for (auto it = m_flatpakSources.begin(); it != m_flatpakSources.end();) { + if ((*it)->url() == flatpak_remote_get_url(remote) && (*it)->installation() == installation) { + qDebug() << "unloading remote" << (*it) << remote; + it = m_flatpakSources.erase(it); + } else { + ++it; + } + } + acquireFetching(false); +} + +void FlatpakBackend::metadataRefreshed(FlatpakRemote *remote) +{ + const bool removed = m_refreshAppstreamMetadataJobs.remove(remote); + Q_ASSERT(removed); + if (m_refreshAppstreamMetadataJobs.isEmpty()) { + for (auto installation : qAsConst(m_installations)) { + // Load local updates, comparing current and latest commit + loadLocalUpdates(installation); + + if (g_cancellable_is_cancelled(m_cancellable)) + break; + } + } +} + +void FlatpakBackend::createPool(QSharedPointer source) +{ + if (source->m_pool) { + return; + } + + const QString appstreamDirPath = source->appstreamDir(); + if (!QFile::exists(appstreamDirPath)) { + qWarning() << "No" << appstreamDirPath << "appstream metadata found for" << source->name(); + metadataRefreshed(source->remote()); + return; + } + + AppStream::Pool *pool = new AppStream::Pool(this); + auto fw = new QFutureWatcher(this); + connect(fw, &QFutureWatcher::finished, this, [this, fw, pool, source]() { + source->m_pool = pool; + m_flatpakLoadingSources.removeAll(source); + if (fw->result()) { + m_flatpakSources += source; + } else { + qWarning() << "Could not open the AppStream metadata pool" << pool->lastError(); + } + metadataRefreshed(source->remote()); + acquireFetching(false); + fw->deleteLater(); + }); + acquireFetching(true); + +#ifdef APPSTREAM_NEW_POOL_API + pool->setLoadStdDataLocations(false); +#if ASQ_CHECK_VERSION(0, 16, 0) + pool->addExtraDataLocation(appstreamDirPath, AppStream::Metadata::FormatStyleCatalog); +#else + pool->addExtraDataLocation(appstreamDirPath, AppStream::Metadata::FormatStyleCollection); +#endif +#else + pool->clearMetadataLocations(); + pool->addMetadataLocation(appstreamDirPath); + pool->setFlags(AppStream::Pool::FlagReadCollection); + pool->setCacheFlags(AppStream::Pool::CacheFlagUseUser); + + const QString subdir = flatpak_installation_get_id(source->installation()) + QLatin1Char('/') + source->name(); + pool->setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream/" + subdir); + QDir().mkpath(pool->cacheLocation()); +#endif + + fw->setFuture(QtConcurrent::run(&m_threadPool, pool, &AppStream::Pool::load)); +} + +QSharedPointer FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote) +{ + Q_ASSERT(m_refreshAppstreamMetadataJobs.contains(remote)); + m_sources->addRemote(remote, flatpakInstallation); + for (auto source : qAsConst(m_flatpakSources)) { + if (source->url() == flatpak_remote_get_url(remote) && source->installation() == flatpakInstallation + && source->name() == flatpak_remote_get_name(remote)) { + createPool(source); + return source; + } + } + for (auto source : qAsConst(m_flatpakLoadingSources)) { + if (source->url() == flatpak_remote_get_url(remote) && source->installation() == flatpakInstallation + && source->name() == flatpak_remote_get_name(remote)) { + createPool(source); + return source; + } + } + + auto source = QSharedPointer::create(this, flatpakInstallation, remote); + if (!source->isEnabled() || flatpak_remote_get_noenumerate(remote)) { + m_flatpakSources += source; + metadataRefreshed(remote); + return source; + } + + createPool(source); + m_flatpakLoadingSources << source; + return source; +} + +void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation) +{ + g_autoptr(GError) localError = nullptr; + g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); + if (!refs) { + qWarning() << "Failed to get list of installed refs for listing local updates:" << localError->message; + return; + } + + for (uint i = 0; i < refs->len; i++) { + FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); + + const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref); + + if (!latestCommit) { + qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref)); + continue; + } + + const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref)); + if (g_strcmp0(commit, latestCommit) == 0) { + continue; + } + + FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); + if (resource) { + resource->setState(AbstractResource::Upgradeable); + updateAppSize(resource); + } + Q_ASSERT(!resource->temporarySource()); + } +} + +bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource) +{ + g_autoptr(GError) localError = nullptr; + g_autoptr(FlatpakRef) ref = flatpak_ref_parse(resource->ref().toUtf8().constData(), &localError); + if (!ref) { + qWarning() << "Failed to parse" << resource->ref() << localError->message; + return false; + } else { + resource->updateFromRef(ref); + } + + return true; +} + +bool FlatpakBackend::setupFlatpakInstallations(GError **error) +{ + if (qEnvironmentVariableIsSet("FLATPAK_TEST_MODE")) { + const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-test"); + qDebug() << "running flatpak backend on test mode" << path; + g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData()); + m_installations << flatpak_installation_new_for_path(file, true, m_cancellable, error); + return m_installations.constLast() != nullptr; + } + + g_autoptr(GPtrArray) installations = flatpak_get_system_installations(m_cancellable, error); + if (*error) { + qWarning() << "Failed to call flatpak_get_system_installations:" << (*error)->message; + } + for (uint i = 0; installations && i < installations->len; i++) { + auto installation = FLATPAK_INSTALLATION(g_ptr_array_index(installations, i)); + g_object_ref(installation); + m_installations << installation; + } + + auto user = flatpak_installation_new_user(m_cancellable, error); + if (user) { + m_installations << user; + } + + return !m_installations.isEmpty(); +} + +void FlatpakBackend::updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource) +{ + // Update the rest + resource->updateFromRef(FLATPAK_REF(installedRef)); + resource->setInstalledSize(flatpak_installed_ref_get_installed_size(installedRef)); + resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(installedRef))); + if (resource->state() < AbstractResource::Installed) + resource->setState(AbstractResource::Installed); +} + +bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource) +{ + if (resource->resourceType() != FlatpakResource::DesktopApp) { + return true; + } + + const QString path = resource->installPath() + QStringLiteral("/metadata"); + + if (QFile::exists(path)) { + return updateAppMetadata(resource, path); + } else { + auto fw = new QFutureWatcher(this); + connect(fw, &QFutureWatcher::finished, this, [this, resource, fw]() { + const auto metadata = fw->result(); + if (!metadata.isEmpty()) + onFetchMetadataFinished(resource, metadata); + fw->deleteLater(); + }); + fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable)); + + // Return false to indicate we cannot continue (right now used only in updateAppSize()) + return false; + } +} + +void FlatpakBackend::onFetchMetadataFinished(FlatpakResource *resource, const QByteArray &metadata) +{ + updateAppMetadata(resource, metadata); + + // Right now we attempt to update metadata for calculating the size so call updateSizeFromRemote() + // as it's what we want. In future if there are other reason to update metadata we will need to somehow + // distinguish between these calls + updateAppSizeFromRemote(resource); +} + +bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QString &path) +{ + // Parse the temporary file + QSettings setting(path, QSettings::NativeFormat); + setting.beginGroup(QLatin1String("Application")); + // Set the runtime in form of name/arch/version which can be later easily parsed + resource->setRuntime(setting.value(QLatin1String("runtime")).toString()); + // TODO get more information? + return true; +} + +bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QByteArray &data) +{ + // We just find the runtime with a regex, QSettings only can read from disk (and so does KConfig) + const QRegularExpression rx(QStringLiteral("runtime=(.*)")); + const auto match = rx.match(QString::fromUtf8(data)); + if (!match.isValid()) { + return false; + } + + resource->setRuntime(match.captured(1)); + return true; +} + +bool FlatpakBackend::updateAppSize(FlatpakResource *resource) +{ + // Check if the size is already set, we should also distinguish between download and installed size, + // right now it doesn't matter whether we get size for installed or not installed app, but if we + // start making difference then for not installed app check download and install size separately + + if (resource->state() == AbstractResource::Installed) { + // The size appears to be already set (from updateAppInstalledMetadata() apparently) + if (resource->installedSize() > 0) { + return true; + } + } else { + if (resource->installedSize() > 0 && resource->downloadSize() > 0) { + return true; + } + } + + // Check if we know the needed runtime which is needed for calculating the size + if (resource->runtime().isEmpty()) { + if (!updateAppMetadata(resource)) { + return false; + } + } + + return updateAppSizeFromRemote(resource); +} + +bool FlatpakBackend::updateAppSizeFromRemote(FlatpakResource *resource) +{ + // Calculate the runtime size + if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) { + auto runtime = getRuntimeForApp(resource); + if (runtime) { + // Re-check runtime state if case a new one was created + updateAppState(runtime); + + if (!runtime->isInstalled()) { + if (!updateAppSize(runtime)) { + qWarning() << "Failed to get runtime size needed for total size of" << resource->name(); + return false; + } + // Set required download size to include runtime size even now, in case we fail to + // get the app size (e.g. when installing bundles where download size is 0) + resource->setDownloadSize(runtime->downloadSize()); + } + } + } + + if (resource->state() == AbstractResource::Installed) { + g_autoptr(FlatpakInstalledRef) ref = nullptr; + ref = getInstalledRefForApp(resource); + if (!ref) { + qWarning() << "Failed to get installed size of" << resource->name(); + return false; + } + resource->setInstalledSize(flatpak_installed_ref_get_installed_size(ref)); + } else if (resource->resourceType() != FlatpakResource::Source) { + if (resource->origin().isEmpty()) { + qWarning() << "Failed to get size of" << resource->name() << " because of missing origin"; + return false; + } + + if (resource->propertyState(FlatpakResource::DownloadSize) == FlatpakResource::Fetching) { + return true; + } + + auto futureWatcher = new QFutureWatcher(this); + connect(futureWatcher, &QFutureWatcher::finished, this, [this, resource, futureWatcher]() { + g_autoptr(FlatpakRemoteRef) remoteRef = futureWatcher->result(); + if (remoteRef) { + onFetchSizeFinished(resource, flatpak_remote_ref_get_download_size(remoteRef), flatpak_remote_ref_get_installed_size(remoteRef)); + } else { + resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed); + resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed); + } + futureWatcher->deleteLater(); + }); + resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::Fetching); + resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::Fetching); + + futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::findRemoteRef, resource, m_cancellable)); + } + + return true; +} + +void FlatpakBackend::onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize) +{ + FlatpakResource *runtime = nullptr; + if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) { + runtime = getRuntimeForApp(resource); + } + + if (runtime && !runtime->isInstalled()) { + resource->setDownloadSize(runtime->downloadSize() + downloadSize); + } else { + resource->setDownloadSize(downloadSize); + } + resource->setInstalledSize(installedSize); +} + +void FlatpakBackend::updateAppState(FlatpakResource *resource) +{ + g_autoptr(FlatpakInstalledRef) ref = getInstalledRefForApp(resource); + if (ref) { + // If the app is installed, we can set information about commit, arch etc. + updateAppInstalledMetadata(ref, resource); + } else { + // TODO check if the app is actually still available + resource->setState(AbstractResource::None); + } +} + +void FlatpakBackend::acquireFetching(bool f) +{ + if (f) + m_isFetching++; + else + m_isFetching--; + + if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) { + Q_EMIT fetchingChanged(); + } + + if (m_isFetching == 0) + Q_EMIT initialized(); +} + +int FlatpakBackend::updatesCount() const +{ + return m_updater->updatesCount(); +} + +bool FlatpakBackend::flatpakResourceLessThan(AbstractResource *l, AbstractResource *r) const +{ + // clang-format off + return (l->isInstalled() != r->isInstalled()) ? l->isInstalled() + : (l->origin() != r->origin()) ? m_sources->originIndex(l->origin()) < m_sources->originIndex(r->origin()) + : (l->rating() && r->rating() && l->rating()->ratingPoints() != r->rating()->ratingPoints()) ? l->rating()->ratingPoints() > r->rating()->ratingPoints() + : l < r; + // clang-format on +} + +ResultsStream *FlatpakBackend::search(const AbstractResourcesBackend::Filters &filter) +{ + const auto fileName = filter.resourceUrl.fileName(); + if (fileName.endsWith(QLatin1String(".flatpakrepo")) || fileName.endsWith(QLatin1String(".flatpakref")) || fileName.endsWith(QLatin1String(".flatpak"))) { + auto stream = new ResultsStream(QLatin1String("FlatpakStream-http-") + fileName); + FlatpakFetchRemoteResourceJob *fetchResourceJob = new FlatpakFetchRemoteResourceJob(filter.resourceUrl, stream, this); + fetchResourceJob->start(); + return stream; + } else if (filter.resourceUrl.scheme() == QLatin1String("appstream")) { + return findResourceByPackageName(filter.resourceUrl); + } else if (!filter.resourceUrl.isEmpty() || (!filter.extends.isEmpty() && !m_extends.contains(filter.extends))) + return new ResultsStream(QStringLiteral("FlatpakStream-void"), {}); + else if (filter.state == AbstractResource::Upgradeable) { + auto stream = new ResultsStream(QStringLiteral("FlatpakStream-upgradeable")); + auto f = [this, stream] { + auto fw = new QFutureWatcher>>(this); + connect(fw, &QFutureWatcher::finished, this, [this, fw, stream]() { + if (g_cancellable_is_cancelled(m_cancellable)) { + stream->finish(); + fw->deleteLater(); + return; + } + + const auto refs = fw->result(); + QVector resources; + for (auto it = refs.constBegin(), itEnd = refs.constEnd(); it != itEnd; ++it) { + resources.reserve(resources.size() + it->size()); + for (auto ref : qAsConst(it.value())) { + bool fresh; + auto resource = getAppForInstalledRef(it.key(), ref, &fresh); + g_object_unref(ref); + if (resource) { + resource->setState(AbstractResource::Upgradeable, !fresh); + updateAppSize(resource); + if (resource->resourceType() == FlatpakResource::Runtime) { + resources.prepend(resource); + } else { + resources.append(resource); + } + } + } + } + + if (!resources.isEmpty()) + Q_EMIT stream->resourcesFound(resources); + stream->finish(); + fw->deleteLater(); + }); + + QVector installations = m_installations; + auto cancellable = m_cancellable; + fw->setFuture(QtConcurrent::run(&m_threadPool, [installations, cancellable] { + QHash> ret; + if (g_cancellable_is_cancelled(cancellable)) { + qWarning() << "Job cancelled"; + return ret; + } + + for (auto installation : std::as_const(installations)) { + g_autoptr(GError) localError = nullptr; + g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs_for_update(installation, cancellable, &localError); + if (!refs) { + qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message; + continue; + } + if (g_cancellable_is_cancelled(cancellable)) { + qWarning() << "Job cancelled"; + ret.clear(); + break; + } + + if (refs->len == 0) { + continue; + } + + auto ¤t = ret[installation]; + current.reserve(refs->len); + for (uint i = 0; i < refs->len; i++) { + FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); + g_object_ref(ref); + current.append(ref); + } + } + return ret; + })); + }; + + if (isFetching()) { + connect(this, &FlatpakBackend::initialized, stream, f); + } else { + QTimer::singleShot(0, this, f); + } + return stream; + } else if (filter.state == AbstractResource::Installed) { + auto stream = new ResultsStream(QStringLiteral("FlatpakStream-installed")); + auto f = [this, stream, filter] { + QVector resources; + for (auto installation : std::as_const(m_installations)) { + g_autoptr(GError) localError = nullptr; + g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(installation, m_cancellable, &localError); + if (!refs) { + qWarning() << "Failed to get list of installed refs for listing installed:" << localError->message; + continue; + } + + resources.reserve(resources.size() + refs->len); + for (uint i = 0; i < refs->len; i++) { + FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); + QString name = QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref)); + if (name.endsWith(QLatin1String(".Debug")) || name.endsWith(QLatin1String(".Locale")) || name.endsWith(QLatin1String(".BaseApp")) + || name.endsWith(QLatin1String(".Docs"))) + continue; + + auto resource = getAppForInstalledRef(installation, ref); + if (!resource) { + continue; + } + if (!filter.search.isEmpty() && !resource->name().contains(filter.search, Qt::CaseInsensitive)) + continue; + + if (resource->resourceType() == FlatpakResource::Runtime) { + resources.prepend(resource); + } else { + resources.append(resource); + } + } + } + if (!resources.isEmpty()) + Q_EMIT stream->resourcesFound(resources); + stream->finish(); + }; + + if (isFetching()) { + connect(this, &FlatpakBackend::initialized, stream, f); + } else { + QTimer::singleShot(0, this, f); + } + return stream; + } + + auto stream = new ResultsStream(QStringLiteral("FlatpakStream")); + auto f = [this, stream, filter]() { + QVector prioritary, rest; + for (const auto &source : qAsConst(m_flatpakSources)) { + QList resources; + if (source->m_pool) { + QList components; + if (!filter.search.isEmpty()) { +#if ASQ_CHECK_VERSION(1, 0, 0) + components = source->m_pool->search(filter.search).toList(); +#else + components = source->m_pool->search(filter.search); +#endif +#if ASQ_CHECK_VERSION(0, 15, 6) + } else if (filter.category) { + components = AppStreamUtils::componentsByCategories(source->m_pool, filter.category, AppStream::Bundle::KindFlatpak); +#endif + } else { +#if ASQ_CHECK_VERSION(1, 0, 0) + components = source->m_pool->components().toList(); +#else + components = source->m_pool->components(); +#endif + } + resources = kTransform>(components, [this, &source](const auto &comp) { + return resourceForComponent(comp, source); + }); + } else { + resources = source->m_resources.values(); + } + + for (auto r : std::as_const(resources)) { + const bool matchById = r->appstreamId().compare(filter.search, Qt::CaseInsensitive) == 0; + if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable && !matchById) { + continue; + } + if (r->state() < filter.state) + continue; + + if (!filter.extends.isEmpty() && !r->extends().contains(filter.extends)) + continue; + + if (!filter.mimetype.isEmpty() && !r->mimetypes().contains(filter.mimetype)) + continue; + + if (filter.search.isEmpty() || matchById) { + rest += r; + } else if (r->name().contains(filter.search, Qt::CaseInsensitive)) { + prioritary += r; + } else if (r->comment().contains(filter.search, Qt::CaseInsensitive)) { + rest += r; + } + } + } + auto f = [this](AbstractResource *l, AbstractResource *r) { + return flatpakResourceLessThan(l, r); + }; + std::sort(rest.begin(), rest.end(), f); + std::sort(prioritary.begin(), prioritary.end(), f); + rest = prioritary + rest; + if (!rest.isEmpty()) + Q_EMIT stream->resourcesFound(rest); + stream->finish(); + }; + if (isFetching()) { + connect(this, &FlatpakBackend::initialized, stream, f); + } else { + QTimer::singleShot(0, this, f); + } + return stream; +} + +bool FlatpakBackend::isTracked(FlatpakResource *resource) const +{ + const auto uid = resource->uniqueId(); + for (const auto &source : m_flatpakSources) { + if (source->m_resources.contains(uid)) { + return true; + } + } + return false; +} + +QVector FlatpakBackend::resourcesByAppstreamName(const QString &name) const +{ + QVector resources; + for (const auto &source : m_flatpakSources) { + if (source->m_pool) { + auto comps = source->componentsByName(name); + resources << kTransform>(comps, [this, source](const auto &comp) { + return resourceForComponent(comp, source); + }); + } + } + auto f = [this](AbstractResource *l, AbstractResource *r) { + return flatpakResourceLessThan(l, r); + }; + std::sort(resources.begin(), resources.end(), f); + return resources; +} + +ResultsStream *FlatpakBackend::findResourceByPackageName(const QUrl &url) +{ + if (url.scheme() == QLatin1String("appstream")) { + const auto appstreamIds = AppStreamUtils::appstreamIds(url); + if (appstreamIds.isEmpty()) + Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); + else { + auto stream = new ResultsStream(QStringLiteral("FlatpakStream-AppStreamUrl")); + auto f = [this, stream, appstreamIds] { + std::set resources; + QVector resourcesVector; + for (const auto &appstreamId : appstreamIds) { + const auto resourcesFound = resourcesByAppstreamName(appstreamId); + for (auto res : resourcesFound) { + auto [x, inserted] = resources.insert(res); + if (inserted) { + resourcesVector.append(res); + } + } + } + if (!resourcesVector.isEmpty()) + Q_EMIT stream->resourcesFound(resourcesVector); + stream->finish(); + }; + + if (isFetching()) { + connect(this, &FlatpakBackend::initialized, stream, f); + } else { + QTimer::singleShot(0, this, f); + } + return stream; + } + } + return new ResultsStream(QStringLiteral("FlatpakStream-packageName-void"), {}); +} + +FlatpakResource *FlatpakBackend::resourceForComponent(const AppStream::Component &component, const QSharedPointer &source) const +{ + const auto ref = idForComponent(component); + auto resource = source->m_resources.value(ref); + if (resource) { + return resource; + } + + FlatpakResource *res = new FlatpakResource(component, source->installation(), const_cast(this)); + res->setOrigin(source->name()); + res->setDisplayOrigin(source->title()); + res->setIconPath(source->appstreamIconsDir()); + res->updateFromAppStream(); + source->addResource(res); + Q_ASSERT(ref == res->uniqueId()); + return res; +} + +AbstractBackendUpdater *FlatpakBackend::backendUpdater() const +{ + return m_updater; +} + +AbstractReviewsBackend *FlatpakBackend::reviewsBackend() const +{ + return m_reviews.data(); +} + +void FlatpakBackend::checkRepositories(const QMap &names) +{ + auto flatpakInstallationByPath = [this](const QString &path) -> FlatpakInstallation * { + for (auto inst : std::as_const(m_installations)) { + if (FlatpakResource::installationPath(inst) == path) + return inst; + } + return nullptr; + }; + + g_autoptr(GError) localError = nullptr; + for (auto it = names.begin(), itEnd = names.end(); it != itEnd; ++it) { + FlatpakInstallation *installation = flatpakInstallationByPath(it.key()); + for (const QString &name : qAsConst(*it)) { + auto remote = flatpak_installation_get_remote_by_name(installation, name.toUtf8(), m_cancellable, &localError); + if (!remote) { + qWarning() << "Could not find remote" << name << "in" << it.key(); + continue; + } + loadRemote(installation, remote); + } + } +} + +FlatpakRemote *FlatpakBackend::installSource(FlatpakResource *resource) +{ + g_autoptr(GCancellable) cancellable = g_cancellable_new(); + + auto remote = flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), cancellable, nullptr); + if (remote) { + qWarning() << "Source " << resource->flatpakName() << " already exists in" << flatpak_installation_get_path(preferredInstallation()); + return nullptr; + } + + remote = flatpak_remote_new(resource->flatpakName().toUtf8().constData()); + populateRemote(remote, + resource->comment(), + resource->getMetadata(QStringLiteral("repo-url")).toString(), + resource->getMetadata(QStringLiteral("gpg-key")).toString()); + if (!resource->branch().isEmpty()) { + flatpak_remote_set_default_branch(remote, resource->branch().toUtf8().constData()); + } + + g_autoptr(GError) error = nullptr; + if (!flatpak_installation_add_remote(preferredInstallation(), remote, false, cancellable, &error)) { + Q_EMIT passiveMessage(i18n("Failed to add source '%1': %2", resource->flatpakName(), error->message)); + qWarning() << "Failed to add source " << resource->flatpakName() << error->message; + return nullptr; + } + return remote; +} + +Transaction *FlatpakBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + Q_UNUSED(addons); + + FlatpakResource *resource = qobject_cast(app); + + if (resource->resourceType() == FlatpakResource::Source) { + // Let source backend handle this + FlatpakRemote *remote = installSource(resource); + if (remote) { + resource->setState(AbstractResource::Installed); + auto name = flatpak_remote_get_name(remote); + g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(resource->installation(), name, m_cancellable, nullptr); + loadRemote(resource->installation(), remote); + } + return nullptr; + } + + FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::InstallRole); + connect(transaction, &FlatpakJobTransaction::repositoriesAdded, this, &FlatpakBackend::checkRepositories); + connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource](Transaction::Status status) { + if (status == Transaction::Status::DoneStatus) { + if (auto tempSource = resource->temporarySource()) { + auto source = findSource(resource->installation(), resource->origin()); + if (!source) { + // It could mean that it's still integrating after checkRepositories It should update itself + return; + } + resource->setTemporarySource({}); + const auto id = resource->uniqueId(); + source->m_resources.insert(id, resource); + + tempSource->m_resources.remove(id); + if (tempSource->m_resources.isEmpty()) { + const bool removed = m_flatpakSources.removeAll(tempSource) || m_flatpakLoadingSources.removeAll(tempSource); + Q_ASSERT(removed); + } + } + updateAppState(resource); + } + }); + return transaction; +} + +Transaction *FlatpakBackend::installApplication(AbstractResource *app) +{ + return installApplication(app, {}); +} + +Transaction *FlatpakBackend::removeApplication(AbstractResource *app) +{ + FlatpakResource *resource = qobject_cast(app); + + if (resource->resourceType() == FlatpakResource::Source) { + // Let source backend handle this + if (m_sources->removeSource(resource->flatpakName())) { + resource->setState(AbstractResource::None); + } + return nullptr; + } + + FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::RemoveRole); + connect(transaction, &FlatpakJobTransaction::repositoriesAdded, this, &FlatpakBackend::checkRepositories); + connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource](Transaction::Status status) { + if (status == Transaction::Status::DoneStatus) { + updateAppSize(resource); + } + }); + return transaction; +} + +void FlatpakBackend::checkForUpdates() +{ + disconnect(this, &FlatpakBackend::initialized, m_checkForUpdatesTimer, qOverload<>(&QTimer::start)); + for (const auto &source : qAsConst(m_flatpakSources)) { + if (source->remote()) { + Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(source->remote())); + m_refreshAppstreamMetadataJobs.insert(source->remote()); + checkForRemoteUpdates(source->installation(), source->remote()); + } + } +} + +void FlatpakBackend::checkForRemoteUpdates(FlatpakInstallation *installation, FlatpakRemote *remote) +{ + Q_ASSERT(remote); + const bool needsIntegration = m_refreshAppstreamMetadataJobs.contains(remote); + if (flatpak_remote_get_disabled(remote) || flatpak_remote_get_noenumerate(remote)) { + if (needsIntegration) { + integrateRemote(installation, remote); + } + return; + } + + FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote); + if (needsIntegration) { + connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote); + } + connect(job, &FlatpakRefreshAppstreamMetadataJob::finished, this, [=] { + acquireFetching(false); + }); + + acquireFetching(true); + job->start(); +} + +QString FlatpakBackend::displayName() const +{ + return QStringLiteral("Flatpak"); +} + +InlineMessage *FlatpakBackend::explainDysfunction() const +{ + if (m_flatpakSources.isEmpty()) { + return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), i18n("There are no Flatpak sources."), m_sources->actions()); + } + for (const auto &source : m_flatpakSources) { + if (source->m_pool && !source->m_pool->lastError().isEmpty()) { + return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), i18n("Failed to load \"%1\" source", source->name())); + } + } + return AbstractResourcesBackend::explainDysfunction(); +} + +#include "FlatpakBackend.moc" diff --git a/libdiscover/backends/FlatpakBackend/FlatpakBackend.h b/libdiscover/backends/FlatpakBackend/FlatpakBackend.h new file mode 100644 index 0000000..f205db8 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.h @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "FlatpakResource.h" + +#include +#include +#include +#include + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#else +#include +#endif + +#include "flatpak-helper.h" + +class FlatpakSourcesBackend; +class FlatpakSource; +class StandardBackendUpdater; +class OdrsReviewsBackend; + +namespace AppStream +{ +class Pool; +} + +class FlatpakBackend : public AbstractResourcesBackend +{ + Q_OBJECT +public: + explicit FlatpakBackend(QObject *parent = nullptr); + ~FlatpakBackend(); + + int updatesCount() const override; + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + ResultsStream *findResourceByPackageName(const QUrl &search); + bool isValid() const override; + + Transaction *installApplication(AbstractResource *app) override; + Transaction *installApplication(AbstractResource *app, const AddonList &addons) override; + Transaction *removeApplication(AbstractResource *app) override; + bool isFetching() const override + { + return m_isFetching > 0; + } + void checkForUpdates() override; + QString displayName() const override; + bool hasApplications() const override + { + return true; + } + QStringList extends() const override + { + return m_extends; + } + + void addSourceFromFlatpakRepo(const QUrl &url, ResultsStream *stream); + void addAppFromFlatpakBundle(const QUrl &url, ResultsStream *stream); + void addAppFromFlatpakRef(const QUrl &url, ResultsStream *stream); + FlatpakResource *getAppForInstalledRef(FlatpakInstallation *flatpakInstallation, FlatpakInstalledRef *ref, bool *freshResource = nullptr) const; + + FlatpakSourcesBackend *sources() const + { + return m_sources; + } + + bool updateAppSize(FlatpakResource *resource); + FlatpakInstalledRef *getInstalledRefForApp(const FlatpakResource *resource) const; + void loadRemote(FlatpakInstallation *installation, FlatpakRemote *remote); + void unloadRemote(FlatpakInstallation *installation, FlatpakRemote *remote); + + InlineMessage *explainDysfunction() const override; + QVector installations() const + { + return m_installations; + } + + bool isTracked(FlatpakResource *resource) const; + QThreadPool *threadPool() + { + return &m_threadPool; + } + + GCancellable *cancellable() const + { + return m_cancellable; + } + +private Q_SLOTS: + void onFetchMetadataFinished(FlatpakResource *resource, const QByteArray &metadata); + void onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize); + +Q_SIGNALS: // for tests + void initialized(); + +private: + friend class FlatpakSource; + + void metadataRefreshed(FlatpakRemote *remote); + bool flatpakResourceLessThan(AbstractResource *l, AbstractResource *r) const; + void announceRatingsReady(); + FlatpakInstallation *preferredInstallation() const + { + return m_installations.constFirst(); + } + QSharedPointer integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote); + FlatpakRemote *getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const; + FlatpakResource *getRuntimeForApp(FlatpakResource *resource) const; + FlatpakResource *resourceForComponent(const AppStream::Component &component, const QSharedPointer &source) const; + void checkRepositories(const QMap &names); + + void loadAppsFromAppstreamData(); + bool loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation); + void loadLocalUpdates(FlatpakInstallation *flatpakInstallation); + bool parseMetadataFromAppBundle(FlatpakResource *resource); + bool setupFlatpakInstallations(GError **error); + void updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource); + bool updateAppMetadata(FlatpakResource *resource); + bool updateAppMetadata(FlatpakResource *resource, const QByteArray &data); + bool updateAppMetadata(FlatpakResource *resource, const QString &path); + bool updateAppSizeFromRemote(FlatpakResource *resource); + void updateAppState(FlatpakResource *resource); + QSharedPointer findSource(FlatpakInstallation *installation, const QString &origin) const; + + QVector resourcesByAppstreamName(const QString &name) const; + void acquireFetching(bool f); + void checkForRemoteUpdates(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote); + void createPool(QSharedPointer source); + FlatpakRemote *installSource(FlatpakResource *resource); + + StandardBackendUpdater *m_updater; + FlatpakSourcesBackend *m_sources = nullptr; + QSharedPointer m_reviews; + uint m_isFetching = 0; + QSet m_refreshAppstreamMetadataJobs; + QStringList m_extends; + + GCancellable *m_cancellable; + QVector m_installations; + QThreadPool m_threadPool; + QVector> m_flatpakSources; + QVector> m_flatpakLoadingSources; + QSharedPointer m_localSource; + QTimer *const m_checkForUpdatesTimer; +}; diff --git a/libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.cpp b/libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.cpp new file mode 100644 index 0000000..dcae19e --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakFetchDataJob.h" +#include "FlatpakResource.h" + +namespace FlatpakRunnables +{ +FlatpakRemoteRef *findRemoteRef(FlatpakResource *app, GCancellable *cancellable) +{ + if (app->origin().isEmpty()) { + qWarning("Failed to get metadata file because of missing origin"); + return nullptr; + } + + g_autoptr(GError) localError = nullptr; + const auto kind = app->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME; + const QByteArray origin = app->origin().toUtf8(), name = app->flatpakName().toUtf8(), arch = app->arch().toUtf8(), branch = app->branch().toUtf8(); + auto ret = flatpak_installation_fetch_remote_ref_sync_full(app->installation(), + origin.constData(), + kind, + name.constData(), + arch.constData(), + branch.constData(), + FLATPAK_QUERY_FLAGS_ONLY_CACHED, + cancellable, + &localError); + if (localError) { + qWarning() << "Failed to find remote ref:" << localError->message; + } + return ret; +} + +QByteArray fetchMetadata(FlatpakResource *app, GCancellable *cancellable) +{ + FlatpakRemoteRef *remoteRef = findRemoteRef(app, cancellable); + if (!remoteRef) { + if (!g_cancellable_is_cancelled(cancellable)) { + qDebug() << "failed to find the remote" << app->name(); + } + + return {}; + } + + g_autoptr(GBytes) data = flatpak_remote_ref_get_metadata(remoteRef); + gsize len = 0; + auto buff = g_bytes_get_data(data, &len); + const QByteArray metadataContent((const char *)buff, len); + + if (metadataContent.isEmpty()) { + qWarning() << "Failed to get metadata file: empty metadata"; + return {}; + } + return metadataContent; +} + +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.h b/libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.h new file mode 100644 index 0000000..397ec72 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakFetchDataJob.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "flatpak-helper.h" +#include +#include + +class FlatpakResource; + +namespace FlatpakRunnables +{ +FlatpakRemoteRef *findRemoteRef(FlatpakResource *app, GCancellable *cancellable); + +QByteArray fetchMetadata(FlatpakResource *app, GCancellable *cancellable); +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.cpp b/libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.cpp new file mode 100644 index 0000000..b2c1fcc --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.cpp @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * SPDX-FileCopyrightText: 2023 Harald Sitter + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakJobTransaction.h" +#include "FlatpakBackend.h" +#include "FlatpakResource.h" +#include "FlatpakTransactionThread.h" + +#include + +#include +#include + +namespace +{ +class ThreadPool : public QThreadPool +{ +public: + ThreadPool() + { + // Cap the amount of concurrency to prevent too many in-flight transactions. This in particular + // prevents running out of file descriptors or other limited resources. + // https://bugs.kde.org/show_bug.cgi?id=474231 + constexpr auto arbitraryMaxConcurrency = 4U; + const auto concurrency = std::min(std::thread::hardware_concurrency(), arbitraryMaxConcurrency); + setMaxThreadCount(std::make_signed_t(concurrency)); + } +}; +} // namespace + +Q_GLOBAL_STATIC(ThreadPool, s_pool); + +FlatpakJobTransaction::FlatpakJobTransaction(FlatpakResource *app, Role role, bool delayStart) + : Transaction(app->backend(), app, role, {}) + , m_app(app) +{ + setCancellable(true); + setStatus(QueuedStatus); + + if (!delayStart) { + QTimer::singleShot(0, this, &FlatpakJobTransaction::start); + } +} + +FlatpakJobTransaction::~FlatpakJobTransaction() +{ + cancel(); + if (s_pool->tryTake(m_appJob)) { // immediately delete if the runnable hasn't started yet + delete m_appJob; + } else { // otherwise defer cleanup to the pool + m_appJob->setAutoDelete(true); + } +} + +void FlatpakJobTransaction::cancel() +{ + m_appJob->cancel(); +} + +void FlatpakJobTransaction::start() +{ + setStatus(DownloadingStatus); + + // App job will be added every time + m_appJob = new FlatpakTransactionThread(m_app, role()); + m_appJob->setAutoDelete(false); + connect(m_appJob, &FlatpakTransactionThread::finished, this, &FlatpakJobTransaction::finishTransaction); + connect(m_appJob, &FlatpakTransactionThread::progressChanged, this, &FlatpakJobTransaction::setProgress); + connect(m_appJob, &FlatpakTransactionThread::speedChanged, this, &FlatpakJobTransaction::setDownloadSpeed); + connect(m_appJob, &FlatpakTransactionThread::passiveMessage, this, &FlatpakJobTransaction::passiveMessage); + connect(m_appJob, &FlatpakTransactionThread::webflowStarted, this, &FlatpakJobTransaction::webflowStarted); + connect(m_appJob, &FlatpakTransactionThread::webflowDone, this, &FlatpakJobTransaction::webflowDone); + + s_pool->start(m_appJob); +} + +void FlatpakJobTransaction::finishTransaction() +{ + if (static_cast(m_app->backend())->getInstalledRefForApp(m_app)) { + m_app->setState(AbstractResource::Installed); + } else { + m_app->setState(AbstractResource::None); + } + + if (!m_appJob->addedRepositories().isEmpty()) { + Q_EMIT repositoriesAdded(m_appJob->addedRepositories()); + } + + if (!m_appJob->cancelled() && !m_appJob->errorMessage().isEmpty()) { + Q_EMIT passiveMessage(m_appJob->errorMessage()); + } + + if (m_appJob->result()) { + setStatus(DoneStatus); + } else { + setStatus(m_appJob->cancelled() ? CancelledStatus : DoneWithErrorStatus); + } +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.h b/libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.h new file mode 100644 index 0000000..f1184ec --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakJobTransaction.h @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "flatpak-helper.h" +#include +#include + +#include +#include + +class FlatpakResource; +class FlatpakTransactionThread; +class FlatpakJobTransaction : public Transaction +{ + Q_OBJECT +public: + FlatpakJobTransaction(FlatpakResource *app, Role role, bool delayStart = false); + + ~FlatpakJobTransaction(); + + void cancel() override; + +public Q_SLOTS: + void finishTransaction(); + void start(); + +Q_SIGNALS: + void repositoriesAdded(const QMap &repositoryNames); + +private: + void updateProgress(); + + QPointer m_app; + QPointer m_appJob; +}; diff --git a/libdiscover/backends/FlatpakBackend/FlatpakNotifier.cpp b/libdiscover/backends/FlatpakBackend/FlatpakNotifier.cpp new file mode 100644 index 0000000..fa2bd55 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakNotifier.cpp @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakNotifier.h" + +#include + +#include +#include +#include +#include + +using namespace std::chrono_literals; + +static void installationChanged(GFileMonitor *monitor, GFile *child, GFile *other_file, GFileMonitorEvent event_type, gpointer self) +{ + Q_UNUSED(monitor); + Q_UNUSED(child); + Q_UNUSED(other_file); + Q_UNUSED(event_type); + + FlatpakNotifier::Installation *installation = (FlatpakNotifier::Installation *)self; + if (!installation) + return; + + FlatpakNotifier *notifier = installation->m_notifier; + notifier->loadRemoteUpdates(installation); +} + +FlatpakNotifier::FlatpakNotifier(QObject *parent) + : BackendNotifierModule(parent) + , m_user(this) + , m_system(this) + , m_cancellable(g_cancellable_new()) +{ + QTimer *dailyCheck = new QTimer(this); + dailyCheck->setInterval(24h); // refresh at least once every day + connect(dailyCheck, &QTimer::timeout, this, &FlatpakNotifier::recheckSystemUpdateNeeded); +} + +FlatpakNotifier::Installation::Installation(FlatpakNotifier *notifier) + : m_notifier(notifier) +{ +} + +FlatpakNotifier::Installation::~Installation() +{ + if (m_monitor) + g_object_unref(m_monitor); + if (m_installation) + g_object_unref(m_installation); +} + +FlatpakNotifier::~FlatpakNotifier() +{ + g_object_unref(m_cancellable); +} + +void FlatpakNotifier::recheckSystemUpdateNeeded() +{ + g_autoptr(GError) error = nullptr; + + // Load flatpak installation + if (!setupFlatpakInstallations(&error)) { + qWarning() << "Failed to setup flatpak installations: " << error->message; + } else { + // Load updates from remote repositories + loadRemoteUpdates(&m_system); + loadRemoteUpdates(&m_user); + } +} + +void FlatpakNotifier::onFetchUpdatesFinished(Installation *installation, bool hasUpdates) +{ + if (installation->m_hasUpdates == hasUpdates) { + return; + } + bool hadUpdates = this->hasUpdates(); + installation->m_hasUpdates = hasUpdates; + + if (hadUpdates != this->hasUpdates()) { + Q_EMIT foundUpdates(); + } +} + +void FlatpakNotifier::loadRemoteUpdates(Installation *installation) +{ + auto fw = new QFutureWatcher(this); + connect(fw, &QFutureWatcher::finished, this, [this, installation, fw]() { + onFetchUpdatesFinished(installation, fw->result()); + fw->deleteLater(); + }); + fw->setFuture(QtConcurrent::run([installation]() -> bool { + g_autoptr(GCancellable) cancellable = g_cancellable_new(); + g_autoptr(GError) localError = nullptr; + g_autoptr(GPtrArray) fetchedUpdates = flatpak_installation_list_installed_refs_for_update(installation->m_installation, cancellable, &localError); + bool hasUpdates = false; + + if (!fetchedUpdates) { + qWarning() << "Failed to get list of installed refs for listing updates: " << localError->message; + return false; + } + for (uint i = 0; !hasUpdates && i < fetchedUpdates->len; i++) { + FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(fetchedUpdates, i)); + const QString refName = QString::fromUtf8(flatpak_ref_get_name(FLATPAK_REF(ref))); + // FIXME right now I can't think of any other filter than this, in FlatpakBackend updates are matched + // with apps so .Locale/.Debug subrefs are not shown and updated automatically. Also this will show + // updates for refs we don't show in Discover if appstream metadata or desktop file for them is not found + if (refName.endsWith(QLatin1String(".Locale")) || refName.endsWith(QLatin1String(".Debug"))) { + continue; + } + hasUpdates = true; + } + return hasUpdates; + })); +} + +bool FlatpakNotifier::hasUpdates() +{ + return m_system.m_hasUpdates || m_user.m_hasUpdates; +} + +bool FlatpakNotifier::Installation::ensureInitialized(std::function func, GCancellable *cancellable, GError **error) +{ + if (!m_installation) { + m_installation = func(); + m_monitor = flatpak_installation_create_monitor(m_installation, cancellable, error); + g_signal_connect(m_monitor, "changed", G_CALLBACK(installationChanged), this); + } + return m_installation && m_monitor; +} + +bool FlatpakNotifier::setupFlatpakInstallations(GError **error) +{ + if (!m_system.ensureInitialized( + [this, error] { + return flatpak_installation_new_system(m_cancellable, error); + }, + m_cancellable, + error)) + return false; + if (!m_user.ensureInitialized( + [this, error] { + return flatpak_installation_new_user(m_cancellable, error); + }, + m_cancellable, + error)) + return false; + + return true; +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakNotifier.h b/libdiscover/backends/FlatpakBackend/FlatpakNotifier.h new file mode 100644 index 0000000..c13e677 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakNotifier.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#pragma once + +#include +#include + +#include "flatpak-helper.h" + +class FlatpakNotifier : public BackendNotifierModule +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.discover.BackendNotifierModule") + Q_INTERFACES(BackendNotifierModule) +public: + explicit FlatpakNotifier(QObject *parent = nullptr); + ~FlatpakNotifier() override; + + bool hasUpdates() override; + bool hasSecurityUpdates() override + { + return false; + } + void recheckSystemUpdateNeeded() override; + bool needsReboot() const override + { + return false; + } + + struct Installation { + explicit Installation(FlatpakNotifier *notifier); + ~Installation(); + + bool ensureInitialized(std::function func, GCancellable *, GError **error); + + FlatpakNotifier *m_notifier; + bool m_hasUpdates = false; + GFileMonitor *m_monitor = nullptr; + FlatpakInstallation *m_installation = nullptr; + }; + + void onFetchUpdatesFinished(Installation *flatpakInstallation, bool hasUpdates); + void loadRemoteUpdates(Installation *installation); + bool setupFlatpakInstallations(GError **error); + Installation m_user; + Installation m_system; + GCancellable *const m_cancellable; + bool m_lastHasUpdates = false; +}; diff --git a/libdiscover/backends/FlatpakBackend/FlatpakPermission.cpp b/libdiscover/backends/FlatpakBackend/FlatpakPermission.cpp new file mode 100644 index 0000000..958434b --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakPermission.cpp @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2022 Suhaas Joshi + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakPermission.h" + +#include + +FlatpakPermission::FlatpakPermission(QString brief, QString description, QString icon) + : m_brief(brief) + , m_description(description) + , m_icon(icon) +{ +} + +QString FlatpakPermission::icon() const +{ + return m_icon; +} + +QString FlatpakPermission::brief() const +{ + return m_brief; +} + +QString FlatpakPermission::description() const +{ + return m_description; +} + +FlatpakPermissionsModel::FlatpakPermissionsModel(QVector permissions) + : QAbstractListModel() + , m_permissions(permissions) +{ +} + +int FlatpakPermissionsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_permissions.count(); +} + +QVariant FlatpakPermissionsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case Roles::BriefRole: + return m_permissions.at(index.row()).brief(); + case Roles::DescriptionRole: + return m_permissions.at(index.row()).description(); + case Roles::IconRole: + return m_permissions.at(index.row()).icon(); + } + return QVariant(); +} + +QHash FlatpakPermissionsModel::roleNames() const +{ + QHash roles; + roles[Roles::BriefRole] = "brief"; + roles[Roles::DescriptionRole] = "description"; + roles[Roles::IconRole] = "icon"; + return roles; +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakPermission.h b/libdiscover/backends/FlatpakBackend/FlatpakPermission.h new file mode 100644 index 0000000..1b0ee6c --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakPermission.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2022 Suhaas Joshi + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +class FlatpakPermission +{ +public: + FlatpakPermission(QString brief, QString description, QString icon); + QString icon() const; + QString brief() const; + QString description() const; + +private: + const QString m_brief; + const QString m_description; + const QString m_icon; +}; + +class FlatpakPermissionsModel : public QAbstractListModel +{ +public: + FlatpakPermissionsModel(QVector permissions); + + enum Roles { + BriefRole = Qt::UserRole + 1, + DescriptionRole, + ListRole, + IconRole, + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + virtual QHash roleNames() const override; + +private: + QVector m_permissions; +}; diff --git a/libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.cpp b/libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.cpp new file mode 100644 index 0000000..49ca644 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.cpp @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakRefreshAppstreamMetadataJob.h" +#include + +FlatpakRefreshAppstreamMetadataJob::FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote) + : QThread() + , m_cancellable(g_cancellable_new()) + , m_installation(installation) + , m_remote(remote) +{ + g_object_ref(m_remote); + connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater); +} + +FlatpakRefreshAppstreamMetadataJob::~FlatpakRefreshAppstreamMetadataJob() +{ + g_object_unref(m_remote); + g_object_unref(m_cancellable); +} + +void FlatpakRefreshAppstreamMetadataJob::cancel() +{ + g_cancellable_cancel(m_cancellable); +} + +void FlatpakRefreshAppstreamMetadataJob::run() +{ + g_autoptr(GError) localError = nullptr; + +#if FLATPAK_CHECK_VERSION(0, 9, 4) + // With Flatpak 0.9.4 we can use flatpak_installation_update_appstream_full_sync() providing progress reporting which we don't use at this moment, but + // still better to use newer function in case the previous one gets deprecated + if (!flatpak_installation_update_appstream_full_sync(m_installation, + flatpak_remote_get_name(m_remote), + nullptr, + nullptr, + nullptr, + nullptr, + m_cancellable, + &localError)) { +#else + if (!flatpak_installation_update_appstream_sync(m_installation, flatpak_remote_get_name(m_remote), nullptr, nullptr, m_cancellable, &localError)) { +#endif + const QString error = localError ? QString::fromUtf8(localError->message) : QStringLiteral(""); + qWarning() << "Failed to refresh appstream metadata for " << flatpak_remote_get_name(m_remote) << ": " << error; + } + Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote); +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.h b/libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.h new file mode 100644 index 0000000..cb58137 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakRefreshAppstreamMetadataJob.h @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "flatpak-helper.h" +#include + +class FlatpakRefreshAppstreamMetadataJob : public QThread +{ + Q_OBJECT +public: + FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote); + ~FlatpakRefreshAppstreamMetadataJob() override; + + void cancel(); + void run() override; + +Q_SIGNALS: + void jobRefreshAppstreamMetadataFinished(FlatpakInstallation *installation, FlatpakRemote *remote); + +private: + GCancellable *m_cancellable; + FlatpakInstallation *m_installation; + FlatpakRemote *m_remote; +}; diff --git a/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp b/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp new file mode 100644 index 0000000..544047c --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp @@ -0,0 +1,1029 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakResource.h" +#include "FlatpakBackend.h" +#include "FlatpakFetchDataJob.h" +#include "FlatpakSourcesBackend.h" +#include "config-paths.h" + +#include + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static QString iconCachePath(const AppStream::Icon &icon) +{ + Q_ASSERT(icon.kind() == AppStream::Icon::KindRemote); + return QStringLiteral("%1/icons/%2").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), icon.url().fileName()); +} + +const QStringList FlatpakResource::s_objects({ + QStringLiteral("qrc:/qml/FlatpakAttention.qml"), + QStringLiteral("qrc:/qml/FlatpakRemoveData.qml"), + QStringLiteral("qrc:/qml/FlatpakOldBeta.qml"), + QStringLiteral("qrc:/qml/FlatpakEolReason.qml"), +}); +const QStringList FlatpakResource::s_bottomObjects({QStringLiteral("qrc:/qml/PermissionsList.qml")}); +Q_GLOBAL_STATIC(QNetworkAccessManager, manager) + +FlatpakResource::FlatpakResource(const AppStream::Component &component, FlatpakInstallation *installation, FlatpakBackend *parent) + : AbstractResource(parent) + , m_appdata(component) + , m_id({component.id(), QString(), QString()}) + , m_downloadSize(0) + , m_installedSize(0) + , m_propertyStates({{DownloadSize, NotKnownYet}, {InstalledSize, NotKnownYet}, {RequiredRuntime, NotKnownYet}}) + , m_state(AbstractResource::None) + , m_installation(installation) +{ + setObjectName(packageName()); + + // Start fetching remote icons during initialization + const auto icons = m_appdata.icons(); + if (icons.count() == 1 && icons.constFirst().kind() == AppStream::Icon::KindRemote) { + const auto icon = icons.constFirst(); + const QString fileName = iconCachePath(icon); + if (!QFileInfo::exists(fileName)) { + const QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + // Create $HOME/.cache/discover/icons folder + cacheDir.mkdir(QStringLiteral("icons")); + auto reply = manager->get(QNetworkRequest(icon.url())); + connect(reply, &QNetworkReply::finished, this, [this, icon, fileName, reply] { + if (reply->error() == QNetworkReply::NoError) { + QByteArray iconData = reply->readAll(); + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) { + file.write(iconData); + } else { + qDebug() << "could not find icon for" << packageName() << reply->url(); + QIcon::fromTheme(QStringLiteral("package-x-generic")).pixmap(32, 32).toImage().save(fileName); + } + file.close(); + Q_EMIT iconChanged(); + reply->deleteLater(); + } + }); + } + } + + connect(this, &FlatpakResource::stateChanged, this, &FlatpakResource::hasDataChanged); +} + +AppStream::Component FlatpakResource::appstreamComponent() const +{ + return m_appdata; +} + +QList FlatpakResource::addonsInformation() +{ + return {}; +} + +QString FlatpakResource::availableVersion() const +{ + if (m_availableVersion.isEmpty()) { +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto releases = m_appdata.releasesPlain().entries(); +#else + const auto releases = m_appdata.releases(); +#endif + if (!releases.isEmpty()) { + auto latestVersion = releases.constFirst().version(); + for (const auto &release : releases) { + if (AppStream::Utils::vercmpSimple(release.version(), latestVersion) > 0) { + latestVersion = release.version(); + } + }; + m_availableVersion = latestVersion; + return m_availableVersion; + } + } else { + return m_availableVersion; + } + return branch(); +} + +QString FlatpakResource::appstreamId() const +{ + return m_id.id; +} + +QString FlatpakResource::arch() const +{ + return m_id.arch; +} + +QString FlatpakResource::branch() const +{ + return m_id.branch; +} + +bool FlatpakResource::canExecute() const +{ + return (m_type == DesktopApp && (m_state == AbstractResource::Installed || m_state == AbstractResource::Upgradeable)); +} + +void FlatpakResource::updateFromRef(FlatpakRef *ref) +{ + setArch(QString::fromUtf8(flatpak_ref_get_arch(ref))); + setBranch(QString::fromUtf8(flatpak_ref_get_branch(ref))); + setCommit(QString::fromUtf8(flatpak_ref_get_commit(ref))); + setFlatpakName(QString::fromUtf8(flatpak_ref_get_name(ref))); + setType(flatpak_ref_get_kind(ref) == FLATPAK_REF_KIND_APP ? DesktopApp : extends().isEmpty() ? Runtime : Extension); + setObjectName(packageName()); +} + +void FlatpakResource::updateFromAppStream() +{ + const QString refstr = m_appdata.bundle(AppStream::Bundle::KindFlatpak).id(); + g_autoptr(GError) localError = nullptr; + g_autoptr(FlatpakRef) ref = flatpak_ref_parse(refstr.toUtf8().constData(), &localError); + if (!ref) { + qDebug() << "failed to obtain ref" << refstr << localError->message; + return; + } + updateFromRef(ref); +} + +QString FlatpakResource::ref() const +{ + return typeAsString() + QLatin1Char('/') + flatpakName() + QLatin1Char('/') + arch() + QLatin1Char('/') + branch(); +} + +QStringList FlatpakResource::categories() +{ + auto cats = m_appdata.categories(); + if (m_appdata.kind() != AppStream::Component::KindAddon) + cats.append(QStringLiteral("Application")); + return cats; +} + +QString FlatpakResource::comment() +{ + const auto summary = m_appdata.summary(); + if (!summary.isEmpty()) { + return summary; + } + + return QString(); +} + +QString FlatpakResource::commit() const +{ + return m_commit; +} + +quint64 FlatpakResource::downloadSize() const +{ + return m_downloadSize; +} + +QVariant FlatpakResource::icon() const +{ + QIcon ret; + const auto icons = m_appdata.icons(); + + if (!m_bundledIcon.isNull()) { + ret = QIcon(m_bundledIcon); + } else if (icons.isEmpty()) { + ret = QIcon::fromTheme(QStringLiteral("package-x-generic")); + } else + for (const AppStream::Icon &icon : icons) { + switch (icon.kind()) { + case AppStream::Icon::KindLocal: + case AppStream::Icon::KindCached: { + const QString path = icon.url().toLocalFile(); + if (QDir::isRelativePath(path)) { + const QString appstreamLocation = installationPath() + "/appstream/" + origin() + '/' + flatpak_get_default_arch() + "/active/icons/"; + QDirIterator dit(appstreamLocation, QDirIterator::Subdirectories); + while (dit.hasNext()) { + const auto currentPath = dit.next(); + if (dit.fileName() == path) { + ret.addFile(currentPath, icon.size()); + } + } + } else { + ret.addFile(path, icon.size()); + } + } break; + case AppStream::Icon::KindStock: { + const auto ret = QIcon::fromTheme(icon.name()); + if (!ret.isNull()) + return ret; + break; + } + case AppStream::Icon::KindRemote: { + const QString fileName = iconCachePath(icon); + if (QFileInfo::exists(fileName)) { + ret.addFile(fileName, icon.size()); + } + break; + } + case AppStream::Icon::KindUnknown: + break; + } + } + + if (ret.isNull()) { + ret = QIcon::fromTheme(QStringLiteral("package-x-generic")); + } + + return ret; +} + +QString FlatpakResource::installedVersion() const +{ + g_autoptr(FlatpakInstalledRef) ref = qobject_cast(backend())->getInstalledRefForApp(this); + if (ref) { + const char *appdataVersion = flatpak_installed_ref_get_appdata_version(ref); + + if (appdataVersion) { + return appdataVersion; + } + } + return branch(); +} + +quint64 FlatpakResource::installedSize() const +{ + return m_installedSize; +} + +AbstractResource::Type FlatpakResource::type() const +{ + switch (m_type) { + case FlatpakResource::Runtime: + return Technical; + case FlatpakResource::Extension: + return Addon; + default: + return Application; + } +} + +QUrl FlatpakResource::homepage() +{ + return m_appdata.url(AppStream::Component::UrlKindHomepage); +} + +QUrl FlatpakResource::helpURL() +{ + return m_appdata.url(AppStream::Component::UrlKindHelp); +} + +QUrl FlatpakResource::bugURL() +{ + return m_appdata.url(AppStream::Component::UrlKindBugtracker); +} + +QUrl FlatpakResource::donationURL() +{ + return m_appdata.url(AppStream::Component::UrlKindDonation); +} + +QUrl FlatpakResource::contributeURL() +{ + return m_appdata.url(AppStream::Component::UrlKindContribute); +} + +FlatpakResource::FlatpakFileType FlatpakResource::flatpakFileType() const +{ + return m_flatpakFileType; +} + +QString FlatpakResource::flatpakName() const +{ + // If the flatpak name is not known (known only for installed apps), then use + // appstream id instead; + if (m_flatpakName.isEmpty()) { + return m_id.id; + } + + return m_flatpakName; +} + +QJsonArray FlatpakResource::licenses() +{ + return AppStreamUtils::licenses(m_appdata); +} + +QString FlatpakResource::longDescription() +{ + return m_appdata.description(); +} + +QString FlatpakResource::attentionText() const +{ + if (m_flatpakFileType == FlatpakResource::FileFlatpakRef) { + QUrl loc = m_resourceLocation; + loc.setPath({}); + loc.setQuery(QUrlQuery()); + return xi18nc("@info", + "This application comes from \"%1\" (hosted at %2). Other software in this repository will also be made be available " + "in Discover " + "when the application is " + "installed.", + m_origin, + loc.toDisplayString()); + } + return {}; +} + +QAbstractListModel *FlatpakResource::permissionsModel() +{ + if (m_permissions.empty()) { + loadPermissions(); + } + return new FlatpakPermissionsModel(m_permissions); +} + +QString FlatpakResource::name() const +{ + QString name = m_appdata.name(); + if (name.isEmpty()) { + name = flatpakName(); + } + + if (name.startsWith(QLatin1String("(Nightly) "))) { + return name.mid(10); + } + + return name; +} + +QString FlatpakResource::origin() const +{ + return m_origin; +} + +QString FlatpakResource::displayOrigin() const +{ + return !m_displayOrigin.isEmpty() ? m_displayOrigin : m_origin; +} + +QString FlatpakResource::packageName() const +{ + return flatpakName() + QLatin1Char('/') + arch() + QLatin1Char('/') + branch(); +} + +FlatpakResource::PropertyState FlatpakResource::propertyState(FlatpakResource::PropertyKind kind) const +{ + return m_propertyStates[kind]; +} + +QUrl FlatpakResource::resourceFile() const +{ + return m_resourceFile; +} + +QString FlatpakResource::runtime() const +{ + return m_runtime; +} + +QString FlatpakResource::section() +{ + return QString(); +} + +quint64 FlatpakResource::size() +{ + if (m_state == Installed) { + return m_installedSize; + } else { + return m_downloadSize; + } +} + +QString FlatpakResource::sizeDescription() +{ + KFormat f; + if (!isInstalled() || canUpgrade()) { + if (propertyState(DownloadSize) == NotKnownYet || propertyState(InstalledSize) == NotKnownYet || propertyState(DownloadSize) == Fetching + || propertyState(InstalledSize) == Fetching) { + qobject_cast(backend())->updateAppSize(this); + return i18n("Retrieving size information"); + } else if (propertyState(DownloadSize) == UnknownOrFailed || propertyState(InstalledSize) == UnknownOrFailed) { + return i18n("Unknown size"); + } else { + return i18nc("@info app size", "%1 to download, %2 on disk", f.formatByteSize(downloadSize()), f.formatByteSize(installedSize())); + } + } else { + if (propertyState(InstalledSize) == NotKnownYet || propertyState(InstalledSize) == Fetching) { + return i18n("Retrieving size information"); + } else if (propertyState(InstalledSize) == UnknownOrFailed) { + return i18n("Unknown size"); + } else { + return i18nc("@info app size", "%1 on disk", f.formatByteSize(installedSize())); + } + } +} + +AbstractResource::State FlatpakResource::state() +{ + return m_state; +} + +FlatpakResource::ResourceType FlatpakResource::resourceType() const +{ + return m_type; +} + +QString FlatpakResource::typeAsString() const +{ + switch (m_type) { + case FlatpakResource::Runtime: + case FlatpakResource::Extension: + return QLatin1String("runtime"); + case FlatpakResource::DesktopApp: + case FlatpakResource::Source: + default: + return QLatin1String("app"); + } +} + +FlatpakResource::Id FlatpakResource::uniqueId() const +{ + return m_id; +} + +void FlatpakResource::invokeApplication() const +{ + QString desktopFileName; + auto launchables = m_appdata.launchable(AppStream::Launchable::KindDesktopId).entries(); + if (!launchables.isEmpty()) { + desktopFileName = launchables.constFirst(); + } else { + qWarning() << "Failed to find launchable for " << m_appdata.name() << ", using AppStream identifier instead"; + desktopFileName = appstreamId(); + } + + KService::Ptr service = KService::serviceByStorageId(desktopFileName); + + if (!service) { + qWarning() << "Failed to find service" << desktopFileName; + return; + } + + auto *job = new KIO::ApplicationLauncherJob(service); + connect(job, &KJob::finished, this, [this, service](KJob *job) { + if (job->error()) { + Q_EMIT backend()->passiveMessage(i18n("Failed to start '%1': %2", service->name(), job->errorString())); + } + }); + + job->start(); +} + +void FlatpakResource::fetchChangelog() +{ + Q_EMIT changelogFetched(AppStreamUtils::changelogToHtml(m_appdata)); +} + +void FlatpakResource::fetchScreenshots() +{ + Q_EMIT screenshotsFetched(AppStreamUtils::fetchScreenshots(m_appdata)); +} + +void FlatpakResource::setArch(const QString &arch) +{ + m_id.arch = arch; +} + +void FlatpakResource::setBranch(const QString &branch) +{ + m_id.branch = branch; +} + +void FlatpakResource::setBundledIcon(const QPixmap &pixmap) +{ + m_bundledIcon = pixmap; +} + +void FlatpakResource::setCommit(const QString &commit) +{ + m_commit = commit; +} + +void FlatpakResource::setDownloadSize(quint64 size) +{ + m_downloadSize = size; + + setPropertyState(DownloadSize, AlreadyKnown); + + Q_EMIT sizeChanged(); +} + +void FlatpakResource::setFlatpakFileType(FlatpakFileType fileType) +{ + m_flatpakFileType = fileType; +} + +void FlatpakResource::setFlatpakName(const QString &name) +{ + m_flatpakName = name; +} + +void FlatpakResource::setIconPath(const QString &path) +{ + m_iconPath = path; +} + +void FlatpakResource::setInstalledSize(quint64 size) +{ + m_installedSize = size; + + setPropertyState(InstalledSize, AlreadyKnown); + + Q_EMIT sizeChanged(); +} + +void FlatpakResource::setOrigin(const QString &origin) +{ + m_origin = origin; +} + +void FlatpakResource::setDisplayOrigin(const QString &displayOrigin) +{ + m_displayOrigin = displayOrigin; +} + +void FlatpakResource::setPropertyState(FlatpakResource::PropertyKind kind, FlatpakResource::PropertyState newState) +{ + auto &state = m_propertyStates[kind]; + if (state != newState) { + state = newState; + + Q_EMIT propertyStateChanged(kind, newState); + } +} + +void FlatpakResource::setResourceFile(const QUrl &url) +{ + m_resourceFile = url; +} + +void FlatpakResource::setRuntime(const QString &runtime) +{ + m_runtime = runtime; + + setPropertyState(RequiredRuntime, AlreadyKnown); +} + +void FlatpakResource::setState(AbstractResource::State state, bool shouldEmit) +{ + if (m_state != state) { + m_state = state; + + if (shouldEmit && qobject_cast(backend())->isTracked(this)) { + Q_EMIT stateChanged(); + } + } +} + +void FlatpakResource::setType(FlatpakResource::ResourceType type) +{ + m_type = type; +} + +QString FlatpakResource::installationPath() const +{ + return installationPath(m_installation); +} + +QString FlatpakResource::installationPath(FlatpakInstallation *flatpakInstallation) +{ + g_autoptr(GFile) path = flatpak_installation_get_path(flatpakInstallation); + g_autofree char *path_str = g_file_get_path(path); + return QString::fromUtf8(path_str); +} + +QString FlatpakResource::installPath() const +{ + return installationPath() + QStringLiteral("/app/%1/%2/%3/active").arg(flatpakName(), arch(), branch()); +} + +QUrl FlatpakResource::url() const +{ + if (!m_resourceFile.isEmpty()) { + return m_resourceFile; + } + + QUrl ret(QStringLiteral("appstream://") + appstreamId()); + const AppStream::Provided::Kind AppStream_Provided_KindId = (AppStream::Provided::Kind)12; // Should be AppStream::Provided::KindId when released + const auto provided = m_appdata.provided(AppStream_Provided_KindId).items(); + if (!provided.isEmpty()) { + QUrlQuery qq; + qq.addQueryItem("alt", provided.join(QLatin1Char(','))); + ret.setQuery(qq); + } + return ret; +} + +QDate FlatpakResource::releaseDate() const +{ +#if ASQ_CHECK_VERSION(1, 0, 0) + if (const auto optional = m_appdata.releasesPlain().indexSafe(0); optional.has_value()) { + auto release = optional.value(); +#else + if (const auto releases = m_appdata.releases(); !releases.isEmpty()) { + auto release = releases.constFirst(); +#endif + return release.timestamp().date(); + } + + return {}; +} + +QString FlatpakResource::sourceIcon() const +{ + const auto sourceItem = qobject_cast(backend())->sources()->sourceById(origin()); + if (!sourceItem) { + qWarning() << "Could not find source " << origin(); + return QStringLiteral("flatpak-discover"); + } + + const auto iconUrl = sourceItem->data(FlatpakSourcesBackend::IconUrlRole).toString(); + if (iconUrl.isEmpty()) + return QStringLiteral("flatpak-discover"); + return iconUrl; +} + +QString FlatpakResource::author() const +{ +#if ASQ_CHECK_VERSION(1, 0, 0) + QString name = m_appdata.developer().name(); +#else + QString name = m_appdata.developerName(); +#endif + + if (name.isEmpty()) { + name = m_appdata.projectGroup(); + } + + return name; +} + +QStringList FlatpakResource::extends() const +{ + return m_appdata.extends(); +} + +QSet FlatpakResource::alternativeAppstreamIds() const +{ + const AppStream::Provided::Kind AppStream_Provided_KindId = (AppStream::Provided::Kind)12; // Should be AppStream::Provided::KindId when released + const auto ret = m_appdata.provided(AppStream_Provided_KindId).items(); + + return QSet(ret.begin(), ret.end()); +} + +QStringList FlatpakResource::mimetypes() const +{ + return m_appdata.provided(AppStream::Provided::KindMimetype).items(); +} + +QString FlatpakResource::versionString() +{ + QString version; + if (resourceType() == Source) { + return {}; + } + if (isInstalled()) { + auto ref = qobject_cast(backend())->getInstalledRefForApp(this); + if (ref) { + version = flatpak_installed_ref_get_appdata_version(ref); + } +#if ASQ_CHECK_VERSION(1, 0, 0) + } else if (!m_appdata.releasesPlain().isEmpty()) { + const auto release = m_appdata.releasesPlain().indexSafe(0).value(); +#else + } else if (!m_appdata.releases().isEmpty()) { + const auto release = m_appdata.releases().constFirst(); +#endif + version = release.version(); + } else { + version = m_id.branch; + } + + return AppStreamUtils::versionString(version, m_appdata); +} + +QString translateSymbolicName(const QStringView &name) +{ + if (name == QLatin1String("host")) { + return i18n("All Files"); + } else if (name == QLatin1String("home")) { + return i18n("Home"); + } else if (name == QLatin1String("xdg-download")) { + return i18n("Downloads"); + } else if (name == QLatin1String("xdg-music")) { + return i18n("Music"); + } + return name.toString(); +} + +QString FlatpakResource::eolReason() +{ + if (!m_eolReason.has_value()) { + auto futureWatcher = new QFutureWatcher(this); + connect(futureWatcher, &QFutureWatcher::finished, this, [this, futureWatcher]() { + g_autoptr(FlatpakRemoteRef) rref = futureWatcher->result(); + if (rref) { + m_eolReason = QString::fromUtf8(flatpak_remote_ref_get_eol(rref)); + Q_EMIT eolReasonChanged(); + } + futureWatcher->deleteLater(); + }); + + futureWatcher->setFuture(QtConcurrent::run(qobject_cast(backend())->threadPool(), + &FlatpakRunnables::findRemoteRef, + this, + qobject_cast(backend())->cancellable())); + return {}; + } + return m_eolReason.value_or(QString()); +} + +void FlatpakResource::loadPermissions() +{ + QByteArray metaDataBytes = FlatpakRunnables::fetchMetadata(this, NULL); + + QTemporaryFile f; + if (!f.open()) { + return; + } + f.write(metaDataBytes); + f.close(); + + KDesktopFile parser(f.fileName()); + + QString brief, description; + + bool fullSessionBusAccess = false; + bool fullSystemBusAccess = false; + + const KConfigGroup contextGroup = parser.group("Context"); + const QString shared = contextGroup.readEntry("shared", QString()); + if (shared.contains("network")) { + brief = i18n("Network Access"); + description = i18n("Can access the internet"); + m_permissions.append(FlatpakPermission(brief, description, "network-wireless")); + } + + const QString sockets = contextGroup.readEntry("sockets", QString()); + if (sockets.contains("session-bus")) { + brief = i18n("Session Bus Access"); + description = i18n("Access is granted to the entire Session Bus"); + m_permissions.append(FlatpakPermission(brief, description, "system-save-session")); + fullSessionBusAccess = true; + } + if (sockets.contains("system-bus")) { + brief = i18n("System Bus Access"); + description = i18n("Access is granted to the entire System Bus"); + m_permissions.append(FlatpakPermission(brief, description, "system-save-session")); + fullSystemBusAccess = true; + } + if (sockets.contains("ssh-auth")) { + brief = i18n("Remote Login Access"); + description = i18n("Can initiate remote login requests using the SSH protocol"); + m_permissions.append(FlatpakPermission(brief, description, "x-shape-connection")); + } + if (sockets.contains("pcsc")) { + brief = i18n("Smart Card Access"); + description = i18n("Can integrate and communicate with smart cards"); + m_permissions.append(FlatpakPermission(brief, description, "network-card")); + } + if (sockets.contains("cups")) { + brief = i18n("Printer Access"); + description = i18n("Can integrate and communicate with printers"); + m_permissions.append(FlatpakPermission(brief, description, "document-print")); + } + if (sockets.contains("gpg-agent")) { + brief = i18n("GPG Agent"); + description = i18n("Allows access to the GPG cryptography service, generally used for signing and reading signed documents"); + m_permissions.append(FlatpakPermission(brief, description, "gpg")); + } + + const QString features = contextGroup.readEntry("features", QString()); + if (features.contains("bluetooth")) { + brief = i18n("Bluetooth Access"); + description = i18n("Can integrate and communicate with Bluetooth devices"); + m_permissions.append(FlatpakPermission(brief, description, "network-bluetooth")); + } + if (features.contains("devel")) { + brief = i18n("Low-Level System Access"); + description = i18n("Can make low-level system calls (e.g. ptrace)"); + m_permissions.append(FlatpakPermission(brief, description, "project-development")); + } + + const QString devices = contextGroup.readEntry("devices", QString()); + if (devices.contains("all")) { + brief = i18n("Device Access"); + description = i18n("Can communicate with and control built-in or connected hardware devices"); + m_permissions.append(FlatpakPermission(brief, description, "preferences-devices-tree")); + } + if (devices.contains("kvm")) { + brief = i18n("Kernel-based Virtual Machine Access"); + description = i18n("Allows running other operating systems as guests in virtual machines"); + m_permissions.append(FlatpakPermission(brief, description, "virtualbox")); + } + + const QString filesystems = contextGroup.readEntry("filesystems", QString()); + const auto dirs = QStringView(filesystems).split(';', Qt::SkipEmptyParts); + QStringList homeList, systemList; + bool home_ro = false, home_rw = false, home_cr = false, homeAccess = false; + bool system_ro = false, system_rw = false, system_cr = false, systemAccess = false; + + bool hasHostRW = false; + + for (const QStringView &dir : dirs) { + if (dir == QLatin1String("xdg-config/kdeglobals:ro")) { + // Ignore notifying about the global file being visible, since it's intended by design + continue; + } + + int separator = dir.lastIndexOf(':'); + const QStringView postfix = separator > 0 ? dir.mid(separator) : QStringView(); + const QStringView symbolicName = dir.left(separator); + const QString id = translateSymbolicName(symbolicName); + if ((dir.contains(QLatin1String("home")) || dir.contains(QChar('~')))) { + if (postfix == QLatin1String(":ro")) { + homeList << i18n("%1 (read-only)", id); + home_ro = true; + } else if (postfix == QLatin1String(":create")) { + homeList << i18n("%1 (can create files)", id); + home_cr = true; + } else { + homeList << i18n("%1 (read & write) ", id); + home_rw = true; + } + homeAccess = true; + } else if (!hasHostRW) { + if (postfix == QLatin1String(":ro")) { + systemList << i18n("%1 (read-only)", id); + system_ro = true; + } else if (postfix == QLatin1String(":create")) { + systemList << i18n("%1 (can create files)", id); + system_cr = true; + } else { + // Once we have host in rw, no need to list the rest + if (symbolicName == QLatin1String("host")) { + hasHostRW = true; + systemList.clear(); + } + + systemList << i18n("%1 (read & write) ", id); + system_rw = true; + } + systemAccess = true; + } + } + + QString appendText = "\n- " + homeList.join("\n- "); + if (homeAccess) { + brief = i18n("Home Folder Access"); + if (home_rw && home_ro && home_cr) { + description = + i18n("Can read, write, and create files in the following locations in your home folder without asking permission first: %1", appendText); + } else if (home_rw && !home_cr) { + description = i18n("Can read and write files in the following locations in your home folder without asking permission first: %1", appendText); + } else if (home_ro && !home_cr && !home_rw) { + description = i18n("Can read files in the following locations in your home folder without asking permission first: %1", appendText); + } else { + description = i18n("Can access files in the following locations in your home folder without asking permission first: %1", appendText); + } + m_permissions.append(FlatpakPermission(brief, description, "inode-directory")); + } + appendText = "\n- " + systemList.join("\n- "); + if (systemAccess) { + brief = i18n("System Folder Access"); + if (system_rw && system_ro && system_cr) { + description = i18n("Can read, write, and create system files in the following locations without asking permission first: %1", appendText); + } else if (system_rw && !system_cr) { + description = i18n("Can read and write system files in the following locations without asking permission first: %1", appendText); + } else if (system_ro && !system_cr && !system_rw) { + description = i18n("Can read system files in the following locations without asking permission first: %1", appendText); + } else { + description = i18n("Can access system files in the following locations without asking permission first: %1", appendText); + } + m_permissions.append(FlatpakPermission(brief, description, "inode-directory")); + } + + if (!fullSessionBusAccess) { + const KConfigGroup sessionBusGroup = parser.group("Session Bus Policy"); + if (sessionBusGroup.exists()) { + const QStringList busList = sessionBusGroup.keyList(); + brief = i18n("Session Bus Access"); + description = i18n("Can communicate with other applications and processes in the same desktop session using the following communication protocols: %1", + "\n- " + busList.join("\n- ")); + m_permissions.append(FlatpakPermission(brief, description, "system-save-session")); + } + } + + if (!fullSystemBusAccess) { + const KConfigGroup systemBusGroup = parser.group("System Bus Policy"); + if (systemBusGroup.exists()) { + const QStringList busList = systemBusGroup.keyList(); + brief = i18n("System Bus Access"); + description = + i18n("Can communicate with all applications and system services using the following communication protocols: %1", "\n- " + busList.join("\n- ")); + m_permissions.append(FlatpakPermission(brief, description, "system-save-session")); + } + } +} + +QString FlatpakResource::dataLocation() const +{ + auto id = m_appdata.bundle(AppStream::Bundle::KindFlatpak).id().section('/', 0, 1); + if (id.isEmpty()) { + return {}; + } + return QDir::homePath() + QLatin1String("/.var/") + id; +} + +bool FlatpakResource::hasData() const +{ + return !dataLocation().isEmpty() && QDir(dataLocation()).exists(); +} + +void FlatpakResource::clearUserData() +{ + const auto location = dataLocation(); + if (location.isEmpty()) { + qWarning() << "Failed find location for" << name(); + return; + } + + if (!QDir(location).removeRecursively()) { + qWarning() << "Failed to remove location" << location; + } + Q_EMIT hasDataChanged(); +} + +int FlatpakResource::versionCompare(FlatpakResource *resource) const +{ + const QString other = resource->availableVersion(); + return AppStream::Utils::vercmpSimple(availableVersion(), other); +} + +QString FlatpakResource::contentRatingDescription() const +{ + return AppStreamUtils::contentRatingDescription(m_appdata); +} + +QString FlatpakResource::contentRatingText() const +{ + return AppStreamUtils::contentRatingText(m_appdata); +} + +AbstractResource::ContentIntensity FlatpakResource::contentRatingIntensity() const +{ + return AppStreamUtils::contentRatingIntensity(m_appdata); +} + +uint FlatpakResource::contentRatingMinimumAge() const +{ + return AppStreamUtils::contentRatingMinimumAge(m_appdata); +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakResource.h b/libdiscover/backends/FlatpakBackend/FlatpakResource.h new file mode 100644 index 0000000..072008b --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakResource.h @@ -0,0 +1,253 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include "FlatpakPermission.h" +#include "flatpak-helper.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#else +#include +#endif + +#include +#include +#include + +class AddonList; +class FlatpakBackend; +class FlatpakSource; + +class FlatpakResource : public AbstractResource +{ + Q_OBJECT + Q_PROPERTY(QStringList topObjects MEMBER s_objects CONSTANT) + Q_PROPERTY(QStringList objects MEMBER s_bottomObjects CONSTANT) + Q_PROPERTY(QString attentionText READ attentionText CONSTANT) + Q_PROPERTY(QString dataLocation READ dataLocation CONSTANT) + Q_PROPERTY(QString branch READ branch CONSTANT) + Q_PROPERTY(bool isDesktopApp READ isDesktopApp CONSTANT) + Q_PROPERTY(QString eolReason READ eolReason NOTIFY eolReasonChanged) + Q_PROPERTY(bool hasData READ hasData NOTIFY hasDataChanged) +public: + explicit FlatpakResource(const AppStream::Component &component, FlatpakInstallation *installation, FlatpakBackend *parent); + + enum PropertyKind { + DownloadSize = 0, + InstalledSize, + RequiredRuntime, + }; + Q_ENUM(PropertyKind) + + enum PropertyState { + NotKnownYet = 0, + AlreadyKnown, + UnknownOrFailed, + Fetching, + }; + Q_ENUM(PropertyState) + + enum ResourceType { + DesktopApp = 0, + Runtime, + Extension, + Source, + }; + Q_ENUM(ResourceType) + + enum FlatpakFileType { + NotAFile, + FileFlatpak, + FileFlatpakRef, + }; + Q_ENUM(FlatpakFileType) + + struct Id { + const QString id; + QString branch; + QString arch; + bool operator!=(const Id &other) const + { + return !operator==(other); + } + bool operator==(const Id &other) const + { + return &other == this + || (other.id == id // + && other.branch == branch // + && other.arch == arch // + ); + } + }; + + static QString typeAsString(ResourceType type) + { + if (type == DesktopApp) { + return QLatin1String("app"); + } + return QLatin1String("runtime"); + } + + QString installationPath() const; + static QString installationPath(FlatpakInstallation *installation); + + AppStream::Component appstreamComponent() const; + QList addonsInformation() override; + QString availableVersion() const override; + QString appstreamId() const override; + QString arch() const; + QString branch() const; + bool canExecute() const override; + QStringList categories() override; + QString comment() override; + QString commit() const; + quint64 downloadSize() const; + QVariant icon() const override; + QString installedVersion() const override; + quint64 installedSize() const; + AbstractResource::Type type() const override; + QUrl homepage() override; + QUrl helpURL() override; + QUrl bugURL() override; + QUrl donationURL() override; + QUrl contributeURL() override; + FlatpakFileType flatpakFileType() const; + QString flatpakName() const; + QJsonArray licenses() override; + QString longDescription() override; + QString name() const override; + QString origin() const override; + QString displayOrigin() const override; + QString packageName() const override; + PropertyState propertyState(PropertyKind kind) const; + QUrl resourceFile() const; + QString runtime() const; + QString section() override; + quint64 size() override; + QString sizeDescription() override; + AbstractResource::State state() override; + ResourceType resourceType() const; + QString typeAsString() const; + FlatpakResource::Id uniqueId() const; + QUrl url() const override; + QDate releaseDate() const override; + QString author() const override; + QStringList extends() const override; + QString versionString() override; + + FlatpakInstallation *installation() const + { + return m_installation; + } + + void invokeApplication() const override; + void fetchChangelog() override; + void fetchScreenshots() override; + QSet alternativeAppstreamIds() const override; + QStringList mimetypes() const override; + + void setBranch(const QString &branch); + void setBundledIcon(const QPixmap &pixmap); + void setDownloadSize(quint64 size); + void setIconPath(const QString &path); + void setInstalledSize(quint64 size); + void setFlatpakFileType(FlatpakFileType fileType); + void setFlatpakName(const QString &name); + void setOrigin(const QString &origin); + void setDisplayOrigin(const QString &displayOrigin); + void setPropertyState(PropertyKind kind, PropertyState state); + void setResourceFile(const QUrl &url); + void setRuntime(const QString &runtime); + void setState(State state, bool shouldEmit = true); + void setType(ResourceType type); + void setResourceLocation(const QUrl &location) + { + m_resourceLocation = location; + } + + void updateFromRef(FlatpakRef *ref); + QString ref() const; + QString sourceIcon() const override; + QString installPath() const; + void updateFromAppStream(); + void setArch(const QString &arch); + QString attentionText() const; + QString dataLocation() const; + bool hasData() const; + Q_INVOKABLE QAbstractListModel *permissionsModel(); + + void setTemporarySource(const QSharedPointer &temp) + { + m_temp = temp; + } + QSharedPointer temporarySource() const + { + return m_temp; + } + + Q_INVOKABLE void clearUserData(); + Q_INVOKABLE int versionCompare(FlatpakResource *resource) const; + + const AppStream::Component appdata() const + { + return m_appdata; + } + + QString contentRatingText() const override; + QString contentRatingDescription() const override; + ContentIntensity contentRatingIntensity() const override; + uint contentRatingMinimumAge() const override; + bool isDesktopApp() const + { + return m_type == DesktopApp; + } + QString eolReason(); + +Q_SIGNALS: + void hasDataChanged(); + void propertyStateChanged(FlatpakResource::PropertyKind kind, FlatpakResource::PropertyState state); + void eolReasonChanged(); + +private: + void setCommit(const QString &commit); + void loadPermissions(); + + const AppStream::Component m_appdata; + FlatpakResource::Id m_id; + FlatpakRefKind m_flatpakRefKind; + QPixmap m_bundledIcon; + QString m_commit; + qint64 m_downloadSize; + FlatpakFileType m_flatpakFileType = FlatpakResource::NotAFile; + QString m_flatpakName; + QString m_iconPath; + qint64 m_installedSize; + QHash m_propertyStates; + QUrl m_resourceFile; + QUrl m_resourceLocation; + QString m_runtime; + AbstractResource::State m_state; + FlatpakInstallation *const m_installation; + QString m_origin; + QString m_displayOrigin; + mutable QString m_availableVersion; + FlatpakResource::ResourceType m_type = DesktopApp; + QSharedPointer m_temp; + QVector m_permissions; + std::optional m_eolReason; + static const QStringList s_objects; + static const QStringList s_bottomObjects; +}; + +inline uint qHash(const FlatpakResource::Id &key) +{ + return qHash(key.id) ^ qHash(key.branch) ^ qHash(key.arch); +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp new file mode 100644 index 0000000..9a02c8b --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp @@ -0,0 +1,430 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakSourcesBackend.h" +#include "FlatpakBackend.h" +#include "FlatpakResource.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +class FlatpakSourceItem : public QStandardItem +{ +public: + FlatpakSourceItem(const QString &text, FlatpakRemote *remote, FlatpakBackend *backend) + : QStandardItem(text) + , m_remote(remote) + , m_backend(backend) + { + g_object_ref(remote); + } + ~FlatpakSourceItem() + { + g_object_unref(m_remote); + } + + void setFlatpakInstallation(FlatpakInstallation *installation) + { + m_installation = installation; + } + + FlatpakInstallation *flatpakInstallation() const + { + return m_installation; + } + + void setData(const QVariant &value, int role) override + { + // We check isCheckable() so the initial setting of the item doesn't trigger a change + if (role == Qt::CheckStateRole && isCheckable()) { + const bool disabled = flatpak_remote_get_disabled(m_remote); + const bool requestedDisabled = Qt::Unchecked == value; + if (disabled != requestedDisabled) { + flatpak_remote_set_disabled(m_remote, requestedDisabled); + g_autoptr(GError) error = nullptr; + if (!flatpak_installation_modify_remote(m_installation, m_remote, nullptr, &error)) { + qWarning() << "set disabled failed" << error->message; + return; + } + + if (requestedDisabled) { + m_backend->unloadRemote(m_installation, m_remote); + } else { + m_backend->loadRemote(m_installation, m_remote); + } + } + } + QStandardItem::setData(value, role); + } + + FlatpakRemote *remote() const + { + return m_remote; + } + +private: + FlatpakInstallation *m_installation = nullptr; + FlatpakRemote *const m_remote; + FlatpakBackend *const m_backend; +}; + +FlatpakSourcesBackend::FlatpakSourcesBackend(const QVector &installations, AbstractResourcesBackend *parent) + : AbstractSourcesBackend(parent) + , m_preferredInstallation(installations.constFirst()) + , m_sources(new QStandardItemModel(this)) + , m_flathubAction(new DiscoverAction("flatpak-discover", i18n("Add Flathub"), this)) + , m_saveAction(new DiscoverAction("dialog-ok-apply", i18n("Apply Changes"), this)) + , m_noSourcesItem(new QStandardItem(QStringLiteral("-"))) +{ + m_saveAction->setVisible(false); + m_saveAction->setToolTip(i18n("Changes to the priority of Flatpak sources must be applied before they will take effect.")); + connect(m_saveAction, &DiscoverAction::triggered, this, &FlatpakSourcesBackend::save); + + m_flathubAction->setObjectName(QStringLiteral("flathub")); + m_flathubAction->setToolTip(i18n("Makes it possible to easily install the applications listed in https://flathub.org")); + connect(m_flathubAction, &DiscoverAction::triggered, this, [this]() { + addSource(QStringLiteral("https://dl.flathub.org/repo/flathub.flatpakrepo")); + }); + + m_noSourcesItem->setEnabled(false); + if (m_sources->rowCount() == 0) { + m_sources->appendRow(m_noSourcesItem); + } +} + +FlatpakSourcesBackend::~FlatpakSourcesBackend() +{ + QStringList ids; + for (int i = 0, c = m_sources->rowCount(); i < c; ++i) { + auto it = m_sources->item(i); + ids << it->data(IdRole).toString(); + } + + auto conf = KSharedConfig::openConfig(); + KConfigGroup group = conf->group("FlatpakSources"); + group.writeEntry("Sources", ids); + + if (!m_noSourcesItem->model()) + delete m_noSourcesItem; +} + +void FlatpakSourcesBackend::save() +{ + int last = INT_MIN; + for (int i = m_sources->rowCount() - 1; i >= 0; --i) { + auto it = m_sources->item(i); + const int prio = it->data(PrioRole).toInt(); + if (prio <= last) { + FlatpakSourceItem *sourceItem = static_cast(it); + flatpak_remote_set_prio(sourceItem->remote(), ++last); + g_autoptr(GError) error = nullptr; + if (!flatpak_installation_modify_remote(sourceItem->flatpakInstallation(), sourceItem->remote(), nullptr, &error)) { + qDebug() << "failed setting priorities" << error->message; + } + + it->setData(last, PrioRole); + } else { + last = prio; + } + } + m_saveAction->setVisible(false); +} + +QAbstractItemModel *FlatpakSourcesBackend::sources() +{ + return m_sources; +} + +bool FlatpakSourcesBackend::addSource(const QString &id) +{ + FlatpakBackend *backend = qobject_cast(parent()); + const QUrl flatpakrepoUrl(id); + + if (id.isEmpty() || !flatpakrepoUrl.isValid()) + return false; + + auto addSource = [=](AbstractResource *res) { + if (res) + backend->installApplication(res); + else + Q_EMIT backend->passiveMessage(i18n("Could not add the source %1", flatpakrepoUrl.toDisplayString())); + }; + + if (flatpakrepoUrl.isLocalFile()) { + auto stream = new ResultsStream(QStringLiteral("FlatpakSource-") + flatpakrepoUrl.toDisplayString()); + backend->addSourceFromFlatpakRepo(flatpakrepoUrl, stream); + connect(stream, &ResultsStream::resourcesFound, this, [addSource](const QVector &res) { + addSource(res.constFirst()); + }); + } else { + AbstractResourcesBackend::Filters filter; + filter.resourceUrl = flatpakrepoUrl; + auto stream = new StoredResultsStream({backend->search(filter)}); + connect(stream, &StoredResultsStream::finished, this, [addSource, stream]() { + const auto res = stream->resources(); + addSource(res.value(0)); + }); + } + return true; +} + +QStandardItem *FlatpakSourcesBackend::sourceById(const QString &id) const +{ + QStandardItem *sourceIt = nullptr; + for (int i = 0, c = m_sources->rowCount(); i < c; ++i) { + auto it = m_sources->item(i); + if (it->data(IdRole) == id) { + sourceIt = it; + break; + } + } + return sourceIt; +} + +QStandardItem *FlatpakSourcesBackend::sourceByUrl(const QString &_url) const +{ + QUrl url(_url); + + QStandardItem *sourceIt = nullptr; + for (int i = 0, c = m_sources->rowCount(); i < c && !sourceIt; ++i) { + auto it = m_sources->item(i); + if (url.matches(it->data(Qt::StatusTipRole).toUrl(), QUrl::StripTrailingSlash)) { + sourceIt = it; + break; + } + } + return sourceIt; +} + +bool FlatpakSourcesBackend::removeSource(const QString &id) +{ + auto sourceIt = sourceById(id); + if (sourceIt) { + FlatpakSourceItem *sourceItem = static_cast(sourceIt); + g_autoptr(GCancellable) cancellable = g_cancellable_new(); + g_autoptr(GError) error = nullptr; + const auto installation = sourceItem->flatpakInstallation(); + + g_autoptr(GPtrArray) refs = flatpak_installation_list_remote_refs_sync(installation, id.toUtf8().constData(), cancellable, &error); + if (refs) { + QHash toRemoveHash; + toRemoveHash.reserve(refs->len); + QStringList toRemoveRefs; + toRemoveRefs.reserve(refs->len); + FlatpakBackend *backend = qobject_cast(parent()); + for (uint i = 0; i < refs->len; i++) { + FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i)); + + g_autoptr(GError) error = nullptr; + FlatpakInstalledRef *installedRef = flatpak_installation_get_installed_ref(installation, + flatpak_ref_get_kind(ref), + flatpak_ref_get_name(ref), + flatpak_ref_get_arch(ref), + flatpak_ref_get_branch(ref), + cancellable, + &error); + if (installedRef) { + auto res = backend->getAppForInstalledRef(installation, installedRef); + const auto name = QString::fromUtf8(flatpak_ref_get_name(ref)); + const auto refString = QString::fromUtf8(flatpak_ref_format_ref(ref)); + if (!name.endsWith(QLatin1String(".Locale"))) { + if (res) + toRemoveHash[res->name()] << refString; + else + toRemoveHash[refString] << refString; + } + toRemoveRefs << refString; + } + } + QStringList toRemove; + toRemove.reserve(toRemoveHash.count()); + for (auto it = toRemoveHash.constBegin(), itEnd = toRemoveHash.constEnd(); it != itEnd; ++it) { + if (it.value().count() > 1) + toRemove << QStringLiteral("%1 - %2").arg(it.key(), it.value().join(QLatin1String(", "))); + else + toRemove << it.key(); + } + toRemove.sort(); + + if (!toRemove.isEmpty()) { + m_proceedFunctions.push([this, toRemoveRefs, installation, id] { + g_autoptr(GError) localError = nullptr; + g_autoptr(GCancellable) cancellable = g_cancellable_new(); + g_autoptr(FlatpakTransaction) transaction = flatpak_transaction_new_for_installation(installation, cancellable, &localError); + for (const QString &instRef : qAsConst(toRemoveRefs)) { + const QByteArray refString = instRef.toUtf8(); + flatpak_transaction_add_uninstall(transaction, refString.constData(), &localError); + if (localError) + return; + } + + if (flatpak_transaction_run(transaction, cancellable, &localError)) { + removeSource(id); + } + }); + + Q_EMIT proceedRequest(i18n("Removing '%1'", id), + i18n("To remove this repository, the following applications must be uninstalled:
  • %1
", + toRemove.join(QStringLiteral("
  • ")))); + return false; + } + } else { + qWarning() << "could not list refs in repo" << id << error->message; + } + + g_autoptr(GError) errorRemoveRemote = nullptr; + if (flatpak_installation_remove_remote(installation, id.toUtf8().constData(), cancellable, &errorRemoveRemote)) { + m_sources->removeRow(sourceItem->row()); + + if (m_sources->rowCount() == 0) { + m_sources->appendRow(m_noSourcesItem); + } + return true; + } else { + Q_EMIT passiveMessage(i18n("Failed to remove %1 remote repository: %2", id, QString::fromUtf8(errorRemoveRemote->message))); + return false; + } + } else { + Q_EMIT passiveMessage(i18n("Could not find %1", id)); + return false; + } + + return false; +} + +QVariantList FlatpakSourcesBackend::actions() const +{ + return {QVariant::fromValue(m_flathubAction)}; +} + +void FlatpakSourcesBackend::addRemote(FlatpakRemote *remote, FlatpakInstallation *installation) +{ + if (flatpak_remote_get_noenumerate(remote)) { + return; + } + const QString id = QString::fromUtf8(flatpak_remote_get_name(remote)); + const QString title = QString::fromUtf8(flatpak_remote_get_title(remote)); + const QUrl remoteUrl(QString::fromUtf8(flatpak_remote_get_url(remote))); + + const auto theActions = actions(); + for (const QVariant &act : theActions) { + DiscoverAction *action = qobject_cast(act.value()); + if (action->objectName() == id) { + action->setEnabled(false); + action->setVisible(false); + } + } + + QString label = !title.isEmpty() ? title : id; + if (flatpak_installation_get_is_user(installation)) { + label = i18n("%1 (user)", label); + } + + for (int i = 0, c = m_sources->rowCount(); i < c; ++i) { + auto genItem = m_sources->item(i); + if (genItem == m_noSourcesItem) { + continue; + } + + FlatpakSourceItem *item = static_cast(m_sources->item(i)); + if (item->data(Qt::StatusTipRole) == remoteUrl && item->flatpakInstallation() == installation) { + qDebug() << "we already have an item for this" << remoteUrl; + return; + } + } + + FlatpakBackend *backend = qobject_cast(parent()); + FlatpakSourceItem *it = new FlatpakSourceItem(label, remote, backend); + const int prio = flatpak_remote_get_prio(remote); + it->setData(remoteUrl.isLocalFile() ? remoteUrl.toLocalFile() : remoteUrl.host(), Qt::ToolTipRole); + it->setData(remoteUrl, Qt::StatusTipRole); + it->setData(id, IdRole); + it->setData(prio, PrioRole); + it->setCheckState(flatpak_remote_get_disabled(remote) ? Qt::Unchecked : Qt::Checked); +#if FLATPAK_CHECK_VERSION(1, 4, 0) + it->setData(QString::fromUtf8(flatpak_remote_get_icon(remote)), IconUrlRole); +#endif + it->setCheckable(true); + it->setFlatpakInstallation(installation); + + // Add the remotes before those with lower priorities, after the rest. + // We disambiguate with internal discover settings + const auto conf = KSharedConfig::openConfig(); + const KConfigGroup group = conf->group("FlatpakSources"); + const auto ids = group.readEntry("Sources", QStringList()); + const int ourIdx = ids.indexOf(id); + + int idx, c; + for (c = m_sources->rowCount(), idx = c; idx < c; ++idx) { + const auto compIt = m_sources->item(idx); + if (prio > compIt->data(PrioRole).toInt()) { + break; + } + const int compIdx = ids.indexOf(compIt->data(IdRole).toString()); + if (compIdx >= ourIdx) { + break; + } + } + + m_sources->insertRow(idx, it); + if (m_sources->rowCount() == 1) + Q_EMIT firstSourceIdChanged(); + Q_EMIT lastSourceIdChanged(); + + if (m_sources->rowCount() > 0) { + m_sources->takeRow(m_noSourcesItem->row()); + } +} + +QString FlatpakSourcesBackend::idDescription() +{ + return i18n("Enter a Flatpak repository URI (*.flatpakrepo):"); +} + +bool FlatpakSourcesBackend::moveSource(const QString &sourceId, int delta) +{ + auto item = sourceById(sourceId); + if (!item) + return false; + const auto row = item->row(); + auto prevRow = m_sources->takeRow(row); + Q_ASSERT(!prevRow.isEmpty()); + + const auto destRow = row + delta; + m_sources->insertRow(destRow, prevRow); + if (destRow == 0 || row == 0) + Q_EMIT firstSourceIdChanged(); + if (destRow == m_sources->rowCount() - 1 || row == m_sources->rowCount() - 1) + Q_EMIT lastSourceIdChanged(); + m_saveAction->setVisible(true); + return true; +} + +int FlatpakSourcesBackend::originIndex(const QString &sourceId) const +{ + auto item = sourceById(sourceId); + return item ? item->row() : INT_MAX; +} + +void FlatpakSourcesBackend::cancel() +{ + m_proceedFunctions.pop(); +} + +void FlatpakSourcesBackend::proceed() +{ + m_proceedFunctions.pop()(); +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.h b/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.h new file mode 100644 index 0000000..c372c98 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.h @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2017 Jan Grulich + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +#include "flatpak-helper.h" + +class DiscoverAction; +class FlatpakResource; +class FlatpakSourcesBackend : public AbstractSourcesBackend +{ + Q_OBJECT +public: + explicit FlatpakSourcesBackend(const QVector &installations, AbstractResourcesBackend *parent); + ~FlatpakSourcesBackend() override; + + enum Roles { + IconUrlRole = LastRole + 1, + PrioRole, + }; + + QAbstractItemModel *sources() override; + bool addSource(const QString &id) override; + bool removeSource(const QString &id) override; + QString idDescription() override; + QVariantList actions() const override; + bool supportsAdding() const override + { + return true; + } + bool canFilterSources() const override + { + return true; + } + + FlatpakRemote *installSource(FlatpakResource *resource); + bool canMoveSources() const override + { + return true; + } + + bool moveSource(const QString &sourceId, int delta) override; + int originIndex(const QString &sourceId) const; + QStandardItem *sourceByUrl(const QString &url) const; + QStandardItem *sourceById(const QString &sourceId) const; + DiscoverAction *inlineAction() const override + { + return m_saveAction; + } + + void cancel() override; + void proceed() override; + + void save(); + void addRemote(FlatpakRemote *remote, FlatpakInstallation *installation); + +private: + FlatpakInstallation *m_preferredInstallation; + QStandardItemModel *m_sources; + DiscoverAction *const m_flathubAction; + DiscoverAction *const m_saveAction; + QStandardItem *m_noSourcesItem; + QStack> m_proceedFunctions; +}; diff --git a/libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.cpp b/libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.cpp new file mode 100644 index 0000000..738a97a --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.cpp @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2017 Jan Grulich + * SPDX-FileCopyrightText: 2023 Harald Sitter + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FlatpakTransactionThread.h" +#include "FlatpakResource.h" + +#include +#include +#include +#include + +static int FLATPAK_CLI_UPDATE_FREQUENCY = 150; + +gboolean FlatpakTransactionThread::add_new_remote_cb(FlatpakTransaction *object, + gint /*reason*/, + gchar *from_id, + gchar *suggested_remote_name, + gchar *url, + gpointer user_data) +{ + FlatpakTransactionThread *obj = (FlatpakTransactionThread *)user_data; + + // TODO ask instead + auto name = QString::fromUtf8(suggested_remote_name); + obj->m_addedRepositories[FlatpakResource::installationPath(flatpak_transaction_get_installation(object))].append(name); + Q_EMIT obj->passiveMessage(i18n("Adding remote '%1' in %2 from %3", name, QString::fromUtf8(url), QString::fromUtf8(from_id))); + return true; +} + +void FlatpakTransactionThread::progress_changed_cb(FlatpakTransactionProgress *progress, gpointer user_data) +{ + FlatpakTransactionThread *obj = (FlatpakTransactionThread *)user_data; + + g_autolist(GObject) ops = flatpak_transaction_get_operations(obj->m_transaction); + g_autoptr(FlatpakTransactionOperation) op = flatpak_transaction_get_current_operation(obj->m_transaction); + const int idx = g_list_index(ops, op); + obj->setProgress(qMin(99, (100 * idx + flatpak_transaction_progress_get_progress(progress)) / g_list_length(ops))); + +#ifdef FLATPAK_VERBOSE_PROGRESS + guint64 start_time = flatpak_transaction_progress_get_start_time(progress); + guint64 elapsed_time = (g_get_monotonic_time() - start_time) / G_USEC_PER_SEC; + if (elapsed_time > 0) { + guint64 transferred = flatpak_transaction_progress_get_bytes_transferred(progress); + obj->setSpeed(transferred / elapsed_time); + } +#endif +} + +void FlatpakTransactionThread::new_operation_cb(FlatpakTransaction * /*object*/, + FlatpakTransactionOperation * /*operation*/, + FlatpakTransactionProgress *progress, + gpointer user_data) +{ + FlatpakTransactionThread *obj = (FlatpakTransactionThread *)user_data; + + g_signal_connect(progress, "changed", G_CALLBACK(&FlatpakTransactionThread::progress_changed_cb), obj); + flatpak_transaction_progress_set_update_frequency(progress, FLATPAK_CLI_UPDATE_FREQUENCY); +} + +void operation_error_cb(FlatpakTransaction * /*object*/, FlatpakTransactionOperation * /*operation*/, GError *error, gint /*details*/, gpointer user_data) +{ + FlatpakTransactionThread *obj = (FlatpakTransactionThread *)user_data; + if (error) { + obj->addErrorMessage(QString::fromUtf8(error->message)); + } +} + +gboolean +FlatpakTransactionThread::webflowStart(FlatpakTransaction *transaction, const char *remote, const char *url, GVariant *options, guint id, gpointer user_data) +{ + Q_UNUSED(transaction); + Q_UNUSED(options); + + QUrl webflowUrl(QString::fromUtf8(url)); + qDebug() << "starting web flow" << webflowUrl << remote << id; + FlatpakTransactionThread *obj = (FlatpakTransactionThread *)user_data; + obj->m_webflows << id; + Q_EMIT obj->webflowStarted(webflowUrl, id); + return true; +} + +void FlatpakTransactionThread::webflowDoneCallback(FlatpakTransaction *transaction, GVariant *options, guint id, gpointer user_data) +{ + Q_UNUSED(transaction); + Q_UNUSED(options); + FlatpakTransactionThread *obj = (FlatpakTransactionThread *)user_data; + obj->m_webflows << id; + Q_EMIT obj->webflowDone(id); + qDebug() << "webflow done" << id; +} + +FlatpakTransactionThread::FlatpakTransactionThread(FlatpakResource *app, Transaction::Role role) + : m_result(false) + , m_app(app) + , m_role(role) +{ + m_cancellable = g_cancellable_new(); + + g_autoptr(GError) localError = nullptr; + m_transaction = flatpak_transaction_new_for_installation(app->installation(), m_cancellable, &localError); + if (localError) { + addErrorMessage(QString::fromUtf8(localError->message)); + qWarning() << "Failed to create transaction" << m_errorMessage; + } else { + g_signal_connect(m_transaction, "add-new-remote", G_CALLBACK(add_new_remote_cb), this); + g_signal_connect(m_transaction, "new-operation", G_CALLBACK(new_operation_cb), this); + g_signal_connect(m_transaction, "operation-error", G_CALLBACK(operation_error_cb), this); + + if (qEnvironmentVariableIntValue("DISCOVER_FLATPAK_WEBFLOW")) { + g_signal_connect(m_transaction, "webflow-start", G_CALLBACK(webflowStart), this); + g_signal_connect(m_transaction, "webflow-done", G_CALLBACK(webflowDoneCallback), this); + } + } +} + +FlatpakTransactionThread::~FlatpakTransactionThread() +{ + g_object_unref(m_transaction); + g_object_unref(m_cancellable); +} + +void FlatpakTransactionThread::cancel() +{ + for (int id : std::as_const(m_webflows)) + flatpak_transaction_abort_webflow(m_transaction, id); + g_cancellable_cancel(m_cancellable); +} + +void FlatpakTransactionThread::run() +{ + auto finish = qScopeGuard([this] { + Q_EMIT finished(); + }); + + if (!m_transaction) + return; + g_autoptr(GError) localError = nullptr; + + const QString refName = m_app->ref(); + + if (m_role == Transaction::Role::InstallRole) { + bool correct = false; + if (m_app->state() == AbstractResource::Upgradeable && m_app->isInstalled()) { + correct = flatpak_transaction_add_update(m_transaction, refName.toUtf8().constData(), nullptr, nullptr, &localError); + } else if (m_app->flatpakFileType() == FlatpakResource::FileFlatpak) { + g_autoptr(GFile) file = g_file_new_for_path(m_app->resourceFile().toLocalFile().toUtf8().constData()); + if (!file) { + qWarning() << "Failed to install bundled application" << refName; + m_result = false; + return; + } + correct = flatpak_transaction_add_install_bundle(m_transaction, file, nullptr, &localError); + } else if (m_app->flatpakFileType() == FlatpakResource::FileFlatpakRef && m_app->resourceFile().isLocalFile()) { + g_autoptr(GFile) file = g_file_new_for_path(m_app->resourceFile().toLocalFile().toUtf8().constData()); + if (!file) { + qWarning() << "Failed to install flatpakref application" << refName; + m_result = false; + return; + } + g_autoptr(GBytes) bytes = g_file_load_bytes(file, m_cancellable, nullptr, &localError); + correct = flatpak_transaction_add_install_flatpakref(m_transaction, bytes, &localError); + } else { + correct = flatpak_transaction_add_install(m_transaction, // + m_app->origin().toUtf8().constData(), + refName.toUtf8().constData(), + nullptr, + &localError); + } + + if (!correct) { + m_result = false; + m_errorMessage = QString::fromUtf8(localError->message); + // We are done so we can set the progress to 100 + setProgress(100); + qWarning() << "Failed to install" << m_app->flatpakFileType() << refName << ':' << m_errorMessage; + return; + } + } else if (m_role == Transaction::Role::RemoveRole) { + if (!flatpak_transaction_add_uninstall(m_transaction, refName.toUtf8().constData(), &localError)) { + m_result = false; + m_errorMessage = QString::fromUtf8(localError->message); + // We are done so we can set the progress to 100 + setProgress(100); + qWarning() << "Failed to uninstall" << refName << ':' << m_errorMessage; + return; + } + } + + m_result = flatpak_transaction_run(m_transaction, m_cancellable, &localError); + if (!m_result) { + if (localError->code == FLATPAK_ERROR_REF_NOT_FOUND) { + m_errorMessage = i18n("Could not find '%1' in '%2'; please make sure it's available.", refName, m_app->origin()); + } else { + m_errorMessage = QString::fromUtf8(localError->message); + } +#if defined(FLATPAK_LIST_UNUSED_REFS) + } else { + const auto installation = flatpak_transaction_get_installation(m_transaction); + g_autoptr(GError) refsError = nullptr; + g_autoptr(GPtrArray) refs = flatpak_installation_list_unused_refs(installation, nullptr, m_cancellable, &refsError); + if (!refs) { + qWarning() << "could not fetch unused refs" << refsError->message; + } else if (refs->len > 0) { + g_autoptr(GError) localError = nullptr; + qDebug() << "found unused refs:" << refs->len; + auto transaction = flatpak_transaction_new_for_installation(installation, m_cancellable, &localError); + for (uint i = 0; i < refs->len; i++) { + FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i)); + g_autofree gchar *strRef = flatpak_ref_format_ref(ref); + qDebug() << "unused ref:" << strRef; + if (!flatpak_transaction_add_uninstall(transaction, strRef, &localError)) { + qDebug() << "failed to uninstall unused ref" << refName << localError->message; + break; + } + } + if (!flatpak_transaction_run(transaction, m_cancellable, &localError)) { + qWarning() << "could not properly clean the elements" << refs->len << localError->message; + } + } +#endif + } + // We are done so we can set the progress to 100 + setProgress(100); +} + +void FlatpakTransactionThread::setProgress(int progress) +{ + Q_ASSERT(qBound(0, progress, 100) == progress); + if (m_progress != progress) { + m_progress = progress; + Q_EMIT progressChanged(m_progress); + } +} + +void FlatpakTransactionThread::setSpeed(quint64 speed) +{ + if (m_speed != speed) { + m_speed = speed; + Q_EMIT speedChanged(m_speed); + } +} + +QString FlatpakTransactionThread::errorMessage() const +{ + return m_errorMessage; +} + +bool FlatpakTransactionThread::result() const +{ + return m_result; +} + +void FlatpakTransactionThread::addErrorMessage(const QString &error) +{ + if (!m_errorMessage.isEmpty()) + m_errorMessage.append(QLatin1Char('\n')); + m_errorMessage.append(error); +} + +bool FlatpakTransactionThread::cancelled() const +{ + return g_cancellable_is_cancelled(m_cancellable); +} diff --git a/libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.h b/libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.h new file mode 100644 index 0000000..8e3d0e2 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.h @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2017 Jan Grulich + * SPDX-FileCopyrightText: 2023 Harald Sitter + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "flatpak-helper.h" +#include +#include + +#include +#include +#include +#include +#include + +class FlatpakResource; +class FlatpakTransactionThread : public QObject, public QRunnable +{ + Q_OBJECT +public: + FlatpakTransactionThread(FlatpakResource *app, Transaction::Role role); + ~FlatpakTransactionThread() override; + + void cancel(); + void run() override; + + int progress() const + { + return m_progress; + } + void setProgress(int progress); + void setSpeed(quint64 speed); + + QString errorMessage() const; + bool result() const; + bool cancelled() const; + + void addErrorMessage(const QString &error); + QMap addedRepositories() const + { + return m_addedRepositories; + } + +Q_SIGNALS: + void progressChanged(int progress); + void speedChanged(quint64 speed); + void passiveMessage(const QString &msg); + void webflowStarted(const QUrl &url, int id); + void webflowDone(int id); + void finished(); + +private: + static gboolean + add_new_remote_cb(FlatpakTransaction * /*object*/, gint /*reason*/, gchar *from_id, gchar *suggested_remote_name, gchar *url, gpointer user_data); + static void progress_changed_cb(FlatpakTransactionProgress *progress, gpointer user_data); + static void + new_operation_cb(FlatpakTransaction * /*object*/, FlatpakTransactionOperation *operation, FlatpakTransactionProgress *progress, gpointer user_data); + + static gboolean webflowStart(FlatpakTransaction *transaction, const char *remote, const char *url, GVariant *options, guint id, gpointer user_data); + static void webflowDoneCallback(FlatpakTransaction *transaction, GVariant *options, guint id, gpointer user_data); + + FlatpakTransaction *m_transaction; + bool m_result = false; + int m_progress = 0; + quint64 m_speed = 0; + QString m_errorMessage; + GCancellable *m_cancellable; + FlatpakResource *const m_app; + const Transaction::Role m_role; + QMap m_addedRepositories; + + QVector m_webflows; +}; diff --git a/libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml b/libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml new file mode 100644 index 0000000..e29165a --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml @@ -0,0 +1,551 @@ + + + + + All Applications + applications-all + + + + + Accessibility + AudioVideo + Development + Education + Engineering + Game + Graphics + Network + Office + Science + Settings + System + Utility + + + + + + Accessories + applications-utilities + + + Utility + Accessibility + + + + + + Accessibility + preferences-desktop-accessibility + + + Accessibility + Settings + + + + + + Developer Tools + applications-development + + + Development + + + + + Debugging + tools-report-bug + + + Debugger + + + + + + Graphic Interface Design + + + GUIDesigner + + + + + + IDEs + + + IDE + + + + + + Localization + preferences-desktop-locale + + + Translation + + + + + + Profiling + + + Profiling + + + + + + Web Development + + + WebDevelopment + + + applications-internet + + + + + + + Education + applications-education + + + Education + + + + + + Science and Engineering + applications-science + + + Science + Engineering + + + + Astronomy + + + Astronomy + + + + + Biology + + + Biology + + + + + Chemistry + applications-science + + + Chemistry + + + + + Computer Science and Robotics + computer + + + ArtificialIntelligence + ComputerScience + Robotics + + + + + Electronics + audio-card + + + Electronics + + + + + Engineering + applications-engineering + + + Engineering + + + + + Geography + + + Geography + + + + + Geology + + + Geology + Geoscience + + + + + Mathematics + applications-education-mathematics + + + DataVisualization + Math + NumericalAnalysis + + + + + Physics + step + + + Physics + + + + + + + Games + applications-games + + + Game + + + + + Arcade + applications-games-arcade + + + ArcadeGame + + + + + Board Games + applications-games-board + + + BoardGame + + + + + Card Games + applications-games-card + + + CardGame + + + + + Puzzles + applications-games + + + LogicGame + + + + + Role Playing + applications-games + + + RolePlaying + + + + + Simulation + applications-games-strategy + + + Simulation + + + + + Strategy + applications-games-strategy + + + StrategyGame + + + + + Sports + applications-games + + + SportsGame + + + + + Action + applications-games + + + ActionGame + + + + + Emulators + applications-games + + + Emulator + + + + + + + + Graphics + applications-graphics + + + Graphics + + + + 3D + + + 3DGraphics + + + + + Drawing + draw-freehand + + + VectorGraphics + + Viewer + + + + + + Painting and Editing + draw-brush + + + RasterGraphics + + Viewer + Scanning + + + + + + Photography + image-x-generic + + + Photography + + + + + Publishing + document-export + + + Publishing + + + + + Scanning and OCR + scanner + + + Scanning + OCR + + + + + Viewers + graphics-viewer-document + + + Viewer + + + + + + + + Internet + applications-internet + + + Network + + + + Chat + kopete + + + InstantMessaging + IRCClient + + + + + File Sharing + ktorrent + + + FileTransfer + + + + + Mail + internet-mail + + + Email + + + + + Web Browsers + internet-web-browser + + + WebBrowser + + + + + + + + Multimedia + applications-multimedia + + + AudioVideo + + + + + Audio and Video Editors + edit-cut + + + AudioVideoEditing + + + + + Audio Players + audio-headphones + + + + AudioVideo + Audio + + + Video + AudioVideoEditing + DiscBurning + Music + Sequencer + Mixer + Utility + + + + + + Video Players + emblem-videos-symbolic + + + + AudioVideo + Video + + + Audio + AudioVideoEditing + DiscBurning + Utility + + + + + + CD and DVD + media-optical + + + DiscBurning + + + + + + + + Office + applications-office + + + Office + + + + + + System Settings + preferences-system + + + Settings + System + + + + diff --git a/libdiscover/backends/FlatpakBackend/flatpak-helper.h b/libdiscover/backends/FlatpakBackend/flatpak-helper.h new file mode 100644 index 0000000..6f0c4ff --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/flatpak-helper.h @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#ifdef FLATPAK_EXTERNC_REQUIRED +extern "C" { +#endif +#include +#ifdef FLATPAK_EXTERNC_REQUIRED +} +#endif diff --git a/libdiscover/backends/FlatpakBackend/org.kde.discover-flatpak.desktop b/libdiscover/backends/FlatpakBackend/org.kde.discover-flatpak.desktop new file mode 100644 index 0000000..aa6d002 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/org.kde.discover-flatpak.desktop @@ -0,0 +1,62 @@ +[Desktop Entry] +Name=Discover +Name[ar]=المستكشف +Name[az]=Discover +Name[bg]=Discover +Name[ca]=Discover +Name[ca@valencia]=Discover +Name[cs]=Discover +Name[da]=Discover +Name[de]=Discover +Name[el]=Discover +Name[en_GB]=Discover +Name[es]=Discover +Name[et]=Discover +Name[eu]=Discover +Name[fi]=Discover +Name[fr]=Discover +Name[gl]=Descubrir +Name[he]=Discover +Name[hi]=डिस्कवर +Name[hu]=Discover +Name[ia]=Discover (Discoperi) +Name[id]=Discover +Name[ie]=Discover +Name[it]=Discover +Name[ja]=Discover +Name[ka]=Discover +Name[ko]=Discover +Name[lt]=Discover +Name[ml]=കണ്ടെത്തുക +Name[my]=ဒစ်(စ)ကာဗာ +Name[nb]=Discover +Name[nl]=Ontdekken +Name[nn]=Discover +Name[pa]=ਡਿਸਕਵਰ +Name[pl]=Odkrywca +Name[pt]=Discover +Name[pt_BR]=Discover +Name[ro]=Discover +Name[ru]=Discover +Name[sk]=Discover +Name[sl]=Discover +Name[sr]=Oткривач +Name[sr@ijekavian]=Oткривач +Name[sr@ijekavianlatin]=Otkrivač +Name[sr@latin]=Otkrivač +Name[sv]=Upptäck +Name[ta]=டிஸ்கவர் +Name[tg]=Кашфиёт +Name[tr]=Keşfet +Name[uk]=Discover +Name[x-test]=xxDiscoverxx +Name[zh_CN]=Discover 软件管理中心 +Name[zh_TW]=Discover +Exec=plasma-discover %U +Icon=plasmadiscover +Type=Application +X-DocPath=plasma-discover/index.html +Categories=Qt;KDE;System; +NoDisplay=true + +MimeType=application/vnd.flatpak.ref;application/vnd.flatpak;application/vnd.flatpak.repo diff --git a/libdiscover/backends/FlatpakBackend/org.kde.discover.flatpak.appdata.xml b/libdiscover/backends/FlatpakBackend/org.kde.discover.flatpak.appdata.xml new file mode 100644 index 0000000..0d88a33 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/org.kde.discover.flatpak.appdata.xml @@ -0,0 +1,152 @@ + + + org.kde.discover.flatpak + Flatpak backend + سَند فلاتباك + Flatpak geri izləri + Бекенд на Flatpak + Dorsal del Flatpak + Dorsal de Flatpak + Podpůrná vrstva Flatpak + Flatpak-motor + Flatpak-Backend + Flatpak backend + Motor Flatpak + Flatpaki taustaprogramm + Flatpak bizkarraldeakoa + Flatpak-taustaosa + Moteur « Flatpak » + Motor de Flatpak + फ्लॅटपैक पृष्ठभाग + Flatpak backend + Retro-Administration de Flatpak + Backend Flatpak + Infrastructura Flatpak + Motore Flatpak + Flatpak -ის უკანაბოლო + Flatpak 백엔드 + Flatpak vidinė pusė + ഫ്ലാറ്റ്പ്പാക്ക് ബാക്കെൻഡ് + ဖလက်ပတ်ခ် အုတ်မြစ်ပရိုဂရမ် + Baksystem for Flatpak + Flatpak-backend + Flatpak-motor + ਫਲੈਟਪੈਕ ਬੈਕਐਡ + Silnik Flatpak + Infra-estrutura do Flatpak + Infraestrutura Flatpak + Platformă Flatpak + Модуль для работы с форматом Flatpak + Podporný program pre Flatpak + Zaledje Flatpak + Gränssnitt för Flatpak + ஃபலாட்பாக் பின்நிலை + Коркардкунандаи Flatpak + Flatpak arka ucu + Модуль Flatpak + xxFlatpak backendxx + Flatpak 后端程序 + Flatpak 後端 + Integrates Flatpak applications into Discover + يُكامل تطبيقات ”فلاتباك“ في «استكشف» + Flatpak tətbiqlərini Discoveriyə daxil edir + Интегрира приложения на Flatpak в Discover + Integra les aplicacions del Flatpak al Discover + Integra les aplicacions de Flatpak a dins de Discover + Integruje aplikace Flatpaku do Discover + Integrerer Flatpak-programmer i Discover + Integriert Flatpak-Anwendungen in Discover + Integrates Flatpak applications into Discover + Integra aplicaciones Flatpak en Discover + Flatpaki rakenduste lõimimine Discoverisse + Flatpak aplikazioak Discover-ren integratzen ditu + Yhdistää Flatpak-sovellukset Discoveriin + Intègre les applications « Flatpak » au sein de Discover + Integra aplicacións de Flatpak con Discover. + फ्लॅटपैक अनुप्रयोगों को डिस्कवर में एकीकृत करता है + Flatpak alkalmazások integrálása a Discoverbe + Integra pplicationes de Flatpak in Discover + Aplikasi Flatpak terintegrasi ke dalam Discover + Integra applicationes Flatpak con Discover + Integra le applicazioni Flatpak in Discover + Discover-ში Flatpak-ის ინტეგრაცია + Flatpak 앱을 Discover에 통합 + Integruoja Flatpak programas į Discover + ഡിസ്കവറിൽ ഫ്ലാറ്റ്പ്പാക്ക് ആപ്ലിക്കേഷൻ സംയോജിപ്പിക്കുന്നു + ဖလက်ပတ်ခ် အပ္ပလီကေးရှင်းများကို ဒစ်(စ)ကာဗာနှင့် ပူးပေါင်းဆက်နွယ်ပေးသည် + Integrerer Flatpak-programmer i Discover + Integreert Flatpak-toepassingen in Ontdekken + Integrerer Flatpak-program i Discover + ਡਿਸਕਵਰ ਵਿੱਚ ਫਲੈਟਪੈਕ ਐਪਲੀਕੇਸ਼ਨਾਂ ਨੂੰ ਜੋੜਦਾ ਹੈ + Integruje aplikacje Flatpak w Odkrywcy + Integra as aplicações do Flatpak no Discover + Integra aplicativos Flatpak no Discover + Integrează aplicații Flatpak în Discover + Добавление поддержки формата Flatpak в центр программ Discover + Integruje aplikácie Flatpad do aplikácie Discover + V Discover vgradi programe Flatpack + Integrerar Flatpak-program i Discover + ஃபலாட்பாக் செயலிகளை டிஸ்கவருக்குள் ஒருங்கிணைக்கும் + Барномаҳои Flatpak-ро ба барномаи Кашфиёт дарунсохт мекунад + Flatpak uygulamalarını Keşfet ile bütünleştirir + Інтеграція програм Flatpak з Discover + xxIntegrates Flatpak applications into Discoverxx + 为 Discover 提供 Flatpak 应用程序的集成功能 + 將 Flatpak 應用程式整合進 Discover 商店 + org.kde.discover.desktop + CC0-1.0 + GPL-2.0+ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + एलिक्स पॉल गोंज़ालेज़ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + အဲလက်ပိုဂွန်ဇလက် + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + ਐਲਿਕਸ ਪੋਲ ਗੋਨਜ਼ਾਵੇਜ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + அலேயிக்சு போல் கொன்ஸாலெசு + Алейкс Пол Гонзалес (Aleix Pol Gonzalez) + Aleix Pol Gonzalez + Aleix Pol Gonzalez + xxAleix Pol Gonzalezxx + Aleix Pol Gonzalez + Aleix Pol Gonzalez + system-software-install + + + + + + + diff --git a/libdiscover/backends/FlatpakBackend/qml/FlatpakAttention.qml b/libdiscover/backends/FlatpakBackend/qml/FlatpakAttention.qml new file mode 100644 index 0000000..5b4e845 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/qml/FlatpakAttention.qml @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.10 as Kirigami + +Kirigami.InlineMessage +{ + // resource is set by the creator of the element in ApplicationPage. + //required property AbstractResource resource + Layout.fillWidth: true + text: resource.attentionText + visible: resource && text.length > 0 + onLinkActivated: Qt.openUrlExternally(link) +} diff --git a/libdiscover/backends/FlatpakBackend/qml/FlatpakEolReason.qml b/libdiscover/backends/FlatpakBackend/qml/FlatpakEolReason.qml new file mode 100644 index 0000000..1ce5f25 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/qml/FlatpakEolReason.qml @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.10 as Kirigami +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import "navigation.js" as Navigation + +Kirigami.InlineMessage +{ + // resource is set by the creator of the element in ApplicationPage. + //required property AbstractResource resource + Layout.fillWidth: true + text: resource.eolReason + height: visible ? implicitHeight : 0 + visible: text.length > 0 + type: Kirigami.MessageType.Warning +} diff --git a/libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml b/libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml new file mode 100644 index 0000000..e662348 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.10 as Kirigami +import org.kde.discover 2.0 +import org.kde.discover.app 1.0 +import "navigation.js" as Navigation + +Kirigami.InlineMessage +{ + id: oldBetaItem + // resource is set by the creator of the element in ApplicationPage. + //required property AbstractResource resource + Layout.fillWidth: true + text: betaOlderThanStable ? i18ndc("libdiscover", "@label %1 is the name of an application", "This development version of %1 is outdated. Using the stable version is highly recommended.", resource.name) : i18ndc("libdiscover", "@label %1 is the name of an application", "A more stable version of %1 is available.", resource.name) + height: visible ? implicitHeight : 0 + visible: actionsArray.filter(action => action.visible).length > 0 + type: betaOlderThanStable ? Kirigami.MessageType.Warning : Kirigami.MessageType.Information + + property bool betaOlderThanStable: false + property var app: resource + onAppChanged: { + betaOlderThanStable = false + for (const action in actionsArray) { + actionsArray[action].reset() + } + } + + Instantiator { + id: inst + model: ResourcesProxyModel { + allBackends: true + backendFilter: resource.backend + resourcesUrl: resource.url + } + active: resource.isDesktopApp + delegate: Kirigami.Action { + visible: inst.active && model.application !== resource && model.application.branch !== "beta" && model.application.branch !== "master" && versionCompare !== 0 + text: i18ndc("libdiscover", "@action: button %1 is the name of a Flatpak repo", "View Stable Version on %1", displayOrigin) + onTriggered: { + applicationWindow().pageStack.pop(); + Navigation.openApplication(model.application) + } + readonly property int versionCompare: resource.versionCompare(model.application) + Component.onCompleted: reset() + function reset() { + oldBetaItem.betaOlderThanStable |= versionCompare < 0 + } + } + + onObjectAdded: { + oldBetaItem.actionsArray.splice(index, 0, object) + oldBetaItem.actions = oldBetaItem.actionsArray = oldBetaItem.actionsArray + } + onObjectRemoved: { + oldBetaItem.actionsArray.splice(index, 1) + oldBetaItem.actions = oldBetaItem.actionsArray = oldBetaItem.actionsArray + } + } + + property var actionsArray: [] + actions: actionsArray +} diff --git a/libdiscover/backends/FlatpakBackend/qml/FlatpakRemoveData.qml b/libdiscover/backends/FlatpakBackend/qml/FlatpakRemoveData.qml new file mode 100644 index 0000000..c53ddd2 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/qml/FlatpakRemoveData.qml @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import org.kde.discover 2.0 +import org.kde.kirigami 2.10 as Kirigami + +Kirigami.InlineMessage +{ + // resource is set by the creator of the element in ApplicationPage. + //required property AbstractResource resource + Layout.fillWidth: true + text: i18nd("libdiscover", "%1 is not installed but it still has data present.", resource.name) + visible: resource.hasData && query.count.number === 0 + height: visible ? implicitHeight : 0 + + ResourcesProxyModel { + id: query + backendFilter: resource.backend + resourcesUrl: resource.url + stateFilter: AbstractResource.Installed + } + + actions: [ + Kirigami.Action { + icon.name: "delete" + text: i18nd("libdiscover", "Delete settings and user data") + onTriggered: { + resource.clearUserData() + } + } + ] +} diff --git a/libdiscover/backends/FlatpakBackend/qml/PermissionsList.qml b/libdiscover/backends/FlatpakBackend/qml/PermissionsList.qml new file mode 100644 index 0000000..5082194 --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/qml/PermissionsList.qml @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2022 Suhaas Joshi + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.12 +import QtQml.Models 2.15 +import org.kde.kirigami 2.14 as Kirigami + +ColumnLayout { + visible: list.model.rowCount() > 0 + spacing: 0 + + Kirigami.Heading { + Layout.fillWidth: true + Layout.bottomMargin: Kirigami.Units.largeSpacing + text: i18ndc("libdiscover", "%1 is the name of the application", "Permissions for %1", resource.name) + level: 2 + type: Kirigami.Heading.Type.Primary + wrapMode: Text.Wrap + } + + Repeater { + id: list + model: resource.permissionsModel() + + delegate: Kirigami.BasicListItem { + Layout.fillWidth: true + text: model.brief + subtitle: model.description + icon: model.icon + subtitleItem.wrapMode: Text.WordWrap + hoverEnabled: false + } + } +} diff --git a/libdiscover/backends/FlatpakBackend/resources.qrc b/libdiscover/backends/FlatpakBackend/resources.qrc new file mode 100644 index 0000000..29b604f --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/resources.qrc @@ -0,0 +1,10 @@ + + + + qml/FlatpakAttention.qml + qml/FlatpakEolReason.qml + qml/FlatpakRemoveData.qml + qml/FlatpakOldBeta.qml + qml/PermissionsList.qml + + diff --git a/libdiscover/backends/FlatpakBackend/sc-apps-flatpak-discover.svg b/libdiscover/backends/FlatpakBackend/sc-apps-flatpak-discover.svg new file mode 100644 index 0000000..2b8494e --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/sc-apps-flatpak-discover.svg @@ -0,0 +1,8 @@ + + + + diff --git a/libdiscover/backends/FlatpakBackend/tests/CMakeLists.txt b/libdiscover/backends/FlatpakBackend/tests/CMakeLists.txt new file mode 100644 index 0000000..306619b --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/tests/CMakeLists.txt @@ -0,0 +1 @@ +add_unit_test(flatpaktest FlatpakTest.cpp) diff --git a/libdiscover/backends/FlatpakBackend/tests/FlatpakTest.cpp b/libdiscover/backends/FlatpakBackend/tests/FlatpakTest.cpp new file mode 100644 index 0000000..500761a --- /dev/null +++ b/libdiscover/backends/FlatpakBackend/tests/FlatpakTest.cpp @@ -0,0 +1,188 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez * + * * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class FlatpakTest : public QObject +{ + Q_OBJECT +public: + AbstractResourcesBackend *backendByName(ResourcesModel *m, const QString &name) + { + const QVector backends = m->backends(); + for (AbstractResourcesBackend *backend : backends) { + if (QLatin1String(backend->metaObject()->className()) == name) { + return backend; + } + } + return nullptr; + } + + FlatpakTest(QObject *parent = nullptr) + : QObject(parent) + { + QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-test")).removeRecursively(); + + QStandardPaths::setTestModeEnabled(true); + qputenv("FLATPAK_TEST_MODE", "ON"); + m_model = new ResourcesModel(QStringLiteral("flatpak-backend"), this); + m_appBackend = backendByName(m_model, QStringLiteral("FlatpakBackend")); + } + +private Q_SLOTS: + void initTestCase() + { + QVERIFY(m_appBackend); + while (m_appBackend->isFetching()) { + QSignalSpy spy(m_appBackend, &AbstractResourcesBackend::fetchingChanged); + QVERIFY(spy.wait()); + } + } + + void testAddSource() + { + auto res = getAllResources(m_appBackend); + QCOMPARE(res.count(), 0); + + auto m = SourcesModel::global(); + auto bk = qobject_cast(m->index(0, 0).data(SourcesModel::SourcesBackend).value()); + + QSignalSpy initializedSpy(m_appBackend, SIGNAL(initialized())); + if (m->rowCount() == 1) { + QSignalSpy spy(m, &SourcesModel::rowsInserted); + qobject_cast(bk->actions().constFirst().value())->trigger(); + QVERIFY(spy.count() || spy.wait(200000)); + } + QVERIFY(initializedSpy.count() || initializedSpy.wait(200000)); + auto resFlathub = getAllResources(m_appBackend); + QVERIFY(resFlathub.count() > 0); + } + + void testListOrigin() + { + AbstractResourcesBackend::Filters f; + f.origin = QStringLiteral("flathub"); + auto resources = getResources(m_appBackend->search(f), true); + QVERIFY(resources.count() > 0); + } + + void testInstallApp() + { + AbstractResourcesBackend::Filters f; + f.resourceUrl = QUrl(QStringLiteral("appstream://dosbox.desktop")); + const auto res = getResources(m_appBackend->search(f)); + QCOMPARE(res.count(), 1); + + const auto ourResource = res.constFirst(); + QCOMPARE(ourResource->state(), AbstractResource::None); + QCOMPARE(waitTransaction(m_appBackend->installApplication(ourResource)), Transaction::DoneStatus); + QCOMPARE(ourResource->state(), AbstractResource::Installed); + QCOMPARE(waitTransaction(m_appBackend->removeApplication(ourResource)), Transaction::DoneStatus); + QCOMPARE(ourResource->state(), AbstractResource::None); + } + + void testFlatpakref() + { + AbstractResourcesBackend::Filters f; + f.resourceUrl = QUrl(QStringLiteral("https://dl.flathub.org/repo/appstream/com.dosbox.DOSBox.flatpakref")); + const auto res = getResources(m_appBackend->search(f)); + QCOMPARE(res.count(), 1); + + f.resourceUrl = QUrl(QStringLiteral("appstream://dosbox.desktop")); + const auto res2 = getResources(m_appBackend->search(f)); + QCOMPARE(res2, res); + + f.resourceUrl = QUrl(QStringLiteral("appstream://com.dosbox.DOSBox.desktop")); + const auto res3 = getResources(m_appBackend->search(f)); + QCOMPARE(res3, res); + } + + /* + void testCancelInstallation() + { + AbstractResourcesBackend::Filters f; + f.resourceUrl = QUrl(QStringLiteral("appstream://com.github.rssguard.desktop")); + const auto res = getResources(m_appBackend->search(f)); + QCOMPARE(res.count(), 1); + + const auto resRssguard = res.constFirst(); + QCOMPARE(resRssguard->state(), AbstractResource::None); + auto t = m_appBackend->installApplication(resRssguard); + QSignalSpy spy(t, &Transaction::statusChanged); + QVERIFY(spy.wait()); + QCOMPARE(t->status(), Transaction::CommittingStatus); + t->cancel(); + QVERIFY(spy.wait()); + QCOMPARE(t->status(), Transaction::CancelledStatus); + }*/ + +private: + Transaction::Status waitTransaction(Transaction *t) + { + int lastProgress = -1; + connect(t, &Transaction::progressChanged, this, [t, &lastProgress] { + Q_ASSERT(lastProgress <= t->progress()); + lastProgress = t->progress(); + }); + + TransactionModel::global()->addTransaction(t); + QSignalSpy spyInstalled(TransactionModel::global(), &TransactionModel::transactionRemoved); + QSignalSpy destructionSpy(t, &QObject::destroyed); + + Transaction::Status ret = t->status(); + connect(TransactionModel::global(), &TransactionModel::transactionRemoved, t, [t, &ret](Transaction *trans) { + if (trans == t) { + ret = trans->status(); + } + }); + while (t && spyInstalled.count() == 0) { + qDebug() << "waiting, currently" << ret << spyInstalled.count() << destructionSpy.count(); + spyInstalled.wait(1000); + } + Q_ASSERT(destructionSpy.count() || destructionSpy.wait()); + return ret; + } + + QVector getResources(ResultsStream *stream, bool canBeEmpty = true) + { + Q_ASSERT(stream); + QSignalSpy spyResources(stream, &ResultsStream::destroyed); + QVector resources; + connect(stream, &ResultsStream::resourcesFound, this, [&resources](const QVector &res) { + resources += res; + }); + Q_ASSERT(spyResources.wait(100000)); + Q_ASSERT(!resources.isEmpty() || canBeEmpty); + return resources; + } + + QVector getAllResources(AbstractResourcesBackend *backend) + { + AbstractResourcesBackend::Filters f; + if (CategoryModel::global()->rootCategories().isEmpty()) + CategoryModel::global()->populateCategories(); + f.category = CategoryModel::global()->rootCategories().constFirst(); + return getResources(backend->search(f), true); + } + + ResourcesModel *m_model; + AbstractResourcesBackend *m_appBackend; +}; + +QTEST_GUILESS_MAIN(FlatpakTest) + +#include "FlatpakTest.moc" diff --git a/libdiscover/backends/FwupdBackend/CMakeLists.txt b/libdiscover/backends/FwupdBackend/CMakeLists.txt new file mode 100644 index 0000000..2542cb8 --- /dev/null +++ b/libdiscover/backends/FwupdBackend/CMakeLists.txt @@ -0,0 +1,17 @@ +add_definitions( -DPROJECT_NAME=${PROJECT_NAME} -DPROJECT_VERSION=${PROJECT_VERSION}) + +set(fwupd-backend_SRCS + FwupdResource.cpp + FwupdBackend.cpp + FwupdTransaction.cpp + FwupdSourcesBackend.cpp +) + +add_library(fwupd-backend MODULE ${fwupd-backend_SRCS}) +target_link_libraries(fwupd-backend Qt::Core KF5::CoreAddons KF5::ConfigCore Discover::Common PkgConfig::Fwupd) +if (Fwupd_VERSION VERSION_LESS 1.5.8) + target_compile_definitions(fwupd-backend PRIVATE -DFWUPD_EXTERNC_REQUIRED) +endif() + +install(TARGETS fwupd-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) + diff --git a/libdiscover/backends/FwupdBackend/FwupdBackend.cpp b/libdiscover/backends/FwupdBackend/FwupdBackend.cpp new file mode 100644 index 0000000..263d83d --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdBackend.cpp @@ -0,0 +1,471 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FwupdBackend.h" +#include "../DiscoverVersion.h" +#include "FwupdResource.h" +#include "FwupdSourcesBackend.h" +#include "FwupdTransaction.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +DISCOVER_BACKEND_PLUGIN(FwupdBackend) + +FwupdBackend::FwupdBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , client(fwupd_client_new()) + , m_updater(new StandardBackendUpdater(this)) + , m_cancellable(g_cancellable_new()) +{ + g_autoptr(GError) error = nullptr; + if (!fwupd_client_connect(client, m_cancellable, &error)) { + handleError(error); + m_isValid = false; + return; + } + fwupd_client_set_user_agent_for_package(client, "plasma-discover", version.data()); + connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FwupdBackend::updatesCountChanged); + + SourcesModel::global()->addSourcesBackend(new FwupdSourcesBackend(this)); + QTimer::singleShot(0, this, &FwupdBackend::checkForUpdates); +} + +QMap FwupdBackend::gchecksumToQChryptographicHash() +{ + static QMap map; + if (map.isEmpty()) { + map.insert(G_CHECKSUM_SHA1, QCryptographicHash::Sha1); + map.insert(G_CHECKSUM_SHA256, QCryptographicHash::Sha256); + map.insert(G_CHECKSUM_SHA512, QCryptographicHash::Sha512); + map.insert(G_CHECKSUM_MD5, QCryptographicHash::Md5); + } + return map; +} + +FwupdBackend::~FwupdBackend() +{ + g_cancellable_cancel(m_cancellable); + g_object_unref(m_cancellable); + + g_object_unref(client); +} + +void FwupdBackend::addResource(FwupdResource *res) +{ + res->setParent(this); + auto &r = m_resources[res->packageName()]; + if (r) { + Q_EMIT resourceRemoved(r); + delete r; + } + r = res; + Q_ASSERT(m_resources.value(res->packageName()) == res); +} + +FwupdResource *FwupdBackend::createRelease(FwupdDevice *device) +{ + FwupdRelease *release = fwupd_device_get_release_default(device); + FwupdResource *res = new FwupdResource(device, QString::fromUtf8(fwupd_release_get_appstream_id(release)), this); + res->setReleaseDetails(release); + + /* the same as we have already */ + if (qstrcmp(fwupd_device_get_version(device), fwupd_release_get_version(release)) == 0) { + qWarning() << "Fwupd Error: same firmware version as installed"; + } + + return res; +} + +void FwupdBackend::addUpdates() +{ + g_autoptr(GError) error = nullptr; + g_autoptr(GPtrArray) devices = fwupd_client_get_devices(client, m_cancellable, &error); + + if (!devices) { + if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) + qDebug() << "Fwupd Info: No Devices Found"; + else + handleError(error); + return; + } + + for (uint i = 0; i < devices->len && !g_cancellable_is_cancelled(m_cancellable); i++) { + FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, i); + + if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)) + continue; + + if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_LOCKED)) + continue; + + if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)) + continue; + + g_autoptr(GError) error2 = nullptr; + g_autoptr(GPtrArray) rels = fwupd_client_get_upgrades(client, fwupd_device_get_id(device), m_cancellable, &error2); + if (rels) { + fwupd_device_add_release(device, (FwupdRelease *)g_ptr_array_index(rels, 0)); + auto res = createApp(device); + if (!res) { + qWarning() << "Fwupd Error: Cannot Create App From Device" << fwupd_device_get_name(device); + } else { + QString longdescription; + for (uint j = 0; j < rels->len; j++) { + FwupdRelease *release = (FwupdRelease *)g_ptr_array_index(rels, j); + if (!fwupd_release_get_description(release)) + continue; + if (rels->len > 1) { + longdescription += QStringLiteral("Version %1\n").arg(QString::fromUtf8(fwupd_release_get_version(release))); + } + longdescription += QString::fromUtf8(fwupd_release_get_description(release)); + if (rels->len > 1) { + longdescription += QLatin1Char('\n'); + } + } + res->setDescription(longdescription); + + // Make sure to set the installed version of the current thing so + // they can both be shown in the update page UI + auto installedResource = m_resources[res->packageName()]; + if (installedResource) { + res->setInstalledVersion(installedResource->availableVersion()); + } + addResource(res); + } + } else { + if (g_error_matches(error2, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { + qWarning() << "fwupd: Device not supported:" << fwupd_device_get_name(device); + } else if (!g_error_matches(error2, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { + handleError(error2); + } + } + } +} + +QByteArray FwupdBackend::getChecksum(const QString &filename, QCryptographicHash::Algorithm hashAlgorithm) +{ + QFile f(filename); + if (!f.open(QFile::ReadOnly)) { + qWarning() << "could not open to check" << filename; + return {}; + } + + QCryptographicHash hash(hashAlgorithm); + if (!hash.addData(&f)) { + qWarning() << "could not read to check" << filename; + return {}; + } + + return hash.result().toHex(); +} + +FwupdResource *FwupdBackend::createApp(FwupdDevice *device) +{ + FwupdRelease *release = fwupd_device_get_release_default(device); + QScopedPointer app(createRelease(device)); + + if (!app->isLiveUpdatable()) { + qWarning() << "Fwupd Error: " << app->name() << "[" << app->id() << "]" + << "cannot be updated"; + return nullptr; + } + + if (app->id().isNull()) { + qWarning() << "Fwupd Error: No id for firmware"; + return nullptr; + } + + if (app->availableVersion().isNull()) { + qWarning() << "Fwupd Error: No version! for " << app->id(); + return nullptr; + } + + GPtrArray *checksums = fwupd_release_get_checksums(release); + if (checksums->len == 0) { + qWarning() << "Fwupd Error: " << app->name() << "[" << app->id() << "] has no checksums, ignoring as unsafe"; + return nullptr; + } + + const QUrl update_uri(QString::fromUtf8(fwupd_release_get_uri(release))); + if (!update_uri.isValid()) { + qWarning() << "Fwupd Error: No Update URI available for" << app->name() << "[" << app->id() << "]"; + return nullptr; + } + + /* Checking for firmware in the cache? */ + const QString filename_cache = app->cacheFile(); + if (QFile::exists(filename_cache)) { + /* Currently LVFS supports SHA1 only*/ + const QByteArray checksum_tmp(fwupd_checksum_get_by_kind(checksums, G_CHECKSUM_SHA1)); + const QByteArray checksum = getChecksum(filename_cache, QCryptographicHash::Sha1); + if (checksum_tmp != checksum) { + QFile::remove(filename_cache); + } + } + + app->setState(AbstractResource::Upgradeable); + return app.take(); +} + +void FwupdBackend::handleError(GError *perror) +{ + // TODO: localise the error message + if (perror && !g_error_matches(perror, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE) && !g_error_matches(perror, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { + const QString msg = QString::fromUtf8(perror->message); + QTimer::singleShot(0, this, [this, msg]() { + Q_EMIT passiveMessage(msg); + }); + qWarning() << "Fwupd Error" << perror->code << perror->message; + } + // else + // qDebug() << "Fwupd skipped" << perror->code << perror->message; +} + +QString FwupdBackend::cacheFile(const QString &kind, const QString &basename) +{ + const QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)); + const QString cacheDirFile = cacheDir.filePath(kind); + + if (!QFileInfo::exists(cacheDirFile) && !cacheDir.mkpath(kind)) { + qWarning() << "Fwupd Error: cannot make cache directory!"; + return {}; + } + + return cacheDir.filePath(kind + QLatin1Char('/') + basename); +} + +static void fwupd_client_get_devices_cb(GObject * /*source*/, GAsyncResult *res, gpointer user_data) +{ + FwupdBackend *helper = (FwupdBackend *)user_data; + g_autoptr(GError) error = nullptr; + auto array = fwupd_client_get_devices_finish(helper->client, res, &error); + if (!error) + helper->setDevices(array); + else + helper->handleError(error); +} + +void FwupdBackend::setDevices(GPtrArray *devices) +{ + for (uint i = 0; devices && i < devices->len; i++) { + FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, i); + + if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)) + continue; + + g_autoptr(GError) error = nullptr; + g_autoptr(GPtrArray) releases = fwupd_client_get_releases(client, fwupd_device_get_id(device), m_cancellable, &error); + + if (error) { + if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { + qWarning() << "fwupd: Device not supported:" << fwupd_device_get_name(device) << error->message; + continue; + } + if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { + continue; + } + + handleError(error); + } + + auto res = new FwupdResource(device, this); + for (uint i = 0; releases && i < releases->len; ++i) { + FwupdRelease *release = (FwupdRelease *)g_ptr_array_index(releases, i); + if (res->installedVersion().toUtf8() == fwupd_release_get_version(release)) { + res->setReleaseDetails(release); + break; + } + } + addResource(res); + } + g_ptr_array_unref(devices); + + addUpdates(); + + m_fetching = false; + Q_EMIT fetchingChanged(); + Q_EMIT initialized(); +} + +static void fwupd_client_get_remotes_cb(GObject * /*source*/, GAsyncResult *res, gpointer user_data) +{ + FwupdBackend *helper = (FwupdBackend *)user_data; + g_autoptr(GError) error = nullptr; + auto array = fwupd_client_get_remotes_finish(helper->client, res, &error); + if (!error) + helper->setRemotes(array); + else + helper->handleError(error); +} + +static void fwupd_client_refresh_remote_cb(GObject * /*source*/, GAsyncResult *res, gpointer user_data) +{ + FwupdBackend *helper = (FwupdBackend *)user_data; + g_autoptr(GError) error = nullptr; + const bool successful = fwupd_client_refresh_remote_finish(helper->client, res, &error); + if (!successful) + helper->handleError(error); +} + +void FwupdBackend::setRemotes(GPtrArray *remotes) +{ + for (uint i = 0; remotes && i < remotes->len; i++) { + FwupdRemote *remote = (FwupdRemote *)g_ptr_array_index(remotes, i); + if (!fwupd_remote_get_enabled(remote)) + continue; + + if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_LOCAL + || fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DIRECTORY) { + continue; + } + + fwupd_client_refresh_remote_async(client, remote, m_cancellable, fwupd_client_refresh_remote_cb, this); + } +} + +void FwupdBackend::checkForUpdates() +{ + if (m_fetching) + return; + + m_fetching = true; + Q_EMIT fetchingChanged(); + + fwupd_client_get_devices_async(client, m_cancellable, fwupd_client_get_devices_cb, this); + fwupd_client_get_remotes_async(client, m_cancellable, fwupd_client_get_remotes_cb, this); +} + +int FwupdBackend::updatesCount() const +{ + return m_updater->updatesCount(); +} + +ResultsStream *FwupdBackend::search(const AbstractResourcesBackend::Filters &filter) +{ + if (!filter.resourceUrl.isEmpty()) { + if (filter.resourceUrl.scheme() == QLatin1String("fwupd")) { + return findResourceByPackageName(filter.resourceUrl); + } else if (filter.resourceUrl.isLocalFile()) { + return resourceForFile(filter.resourceUrl); + } + return new ResultsStream(QStringLiteral("FwupdStream-empty"), {}); + } + + auto stream = new ResultsStream(QStringLiteral("FwupdStream")); + auto f = [this, stream, filter]() { + QVector ret; + for (AbstractResource *r : qAsConst(m_resources)) { + if (r->state() < filter.state) + continue; + + if (filter.search.isEmpty() || r->name().contains(filter.search, Qt::CaseInsensitive) + || r->comment().contains(filter.search, Qt::CaseInsensitive)) { + ret += r; + } + } + if (!ret.isEmpty()) + Q_EMIT stream->resourcesFound(ret); + stream->finish(); + }; + if (isFetching()) { + connect(this, &FwupdBackend::initialized, stream, f); + } else { + QTimer::singleShot(0, this, f); + } + return stream; +} + +ResultsStream *FwupdBackend::findResourceByPackageName(const QUrl &search) +{ + auto res = search.scheme() == QLatin1String("fwupd") ? m_resources.value(search.host().replace(QLatin1Char('.'), QLatin1Char(' '))) : nullptr; + if (!res) { + return new ResultsStream(QStringLiteral("FwupdStream"), {}); + } else + return new ResultsStream(QStringLiteral("FwupdStream"), {res}); +} + +AbstractBackendUpdater *FwupdBackend::backendUpdater() const +{ + return m_updater; +} + +AbstractReviewsBackend *FwupdBackend::reviewsBackend() const +{ + return nullptr; +} + +Transaction *FwupdBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + Q_ASSERT(addons.isEmpty()); + return installApplication(app); +} + +Transaction *FwupdBackend::installApplication(AbstractResource *app) +{ + return new FwupdTransaction(qobject_cast(app), this); +} + +Transaction *FwupdBackend::removeApplication(AbstractResource * /*app*/) +{ + qWarning() << "should not have reached here, it's not possible to uninstall a firmware"; + return nullptr; +} + +ResultsStream *FwupdBackend::resourceForFile(const QUrl &path) +{ + if (!path.isLocalFile()) + return new ResultsStream(QStringLiteral("FwupdStream-void"), {}); + + g_autoptr(GError) error = nullptr; + + const QString fileName = path.fileName(); + QMimeDatabase db; + QMimeType type = db.mimeTypeForFile(fileName); + FwupdResource *app = nullptr; + + if (type.isValid() && type.inherits(QStringLiteral("application/vnd.ms-cab-compressed"))) { + g_autofree gchar *filename = fileName.toUtf8().data(); + g_autoptr(GPtrArray) devices = fwupd_client_get_details(client, filename, nullptr, &error); + + if (devices) { + FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, 0); + app = createRelease(device); + app->setState(AbstractResource::None); + for (uint i = 1; i < devices->len; i++) { + FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, i); + FwupdResource *app_ = createRelease(device); + app_->setState(AbstractResource::None); + } + addResource(app); + connect(app, &FwupdResource::stateChanged, this, &FwupdBackend::updatesCountChanged); + return new ResultsStream(QStringLiteral("FwupdStream-file"), {app}); + } else { + handleError(error); + } + } + return new ResultsStream(QStringLiteral("FwupdStream-void"), {}); +} + +QString FwupdBackend::displayName() const +{ + return QStringLiteral("Firmware Updates"); +} + +bool FwupdBackend::hasApplications() const +{ + return false; +} + +#include "FwupdBackend.moc" diff --git a/libdiscover/backends/FwupdBackend/FwupdBackend.h b/libdiscover/backends/FwupdBackend/FwupdBackend.h new file mode 100644 index 0000000..ac92a94 --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdBackend.h @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef FWUPD_EXTERNC_REQUIRED +extern "C" { +#endif + +#include +#ifdef FWUPD_EXTERNC_REQUIRED +} +#endif +#include + +class DiscoverAction; +class StandardBackendUpdater; +class FwupdResource; +class FwupdBackend : public AbstractResourcesBackend +{ + Q_OBJECT + Q_PROPERTY(int startElements MEMBER m_startElements) +public: + explicit FwupdBackend(QObject *parent = nullptr); + ~FwupdBackend(); + + int updatesCount() const override; + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + ResultsStream *findResourceByPackageName(const QUrl &search); + QHash resources() const + { + return m_resources; + } + bool isValid() const override + { + return m_isValid; + } + + Transaction *installApplication(AbstractResource *app) override; + Transaction *installApplication(AbstractResource *app, const AddonList &addons) override; + Transaction *removeApplication(AbstractResource *app) override; + bool isFetching() const override + { + return m_fetching; + } + void checkForUpdates() override; + QString displayName() const override; + bool hasApplications() const override; + FwupdClient *client; + void handleError(GError *perror); + + static QString cacheFile(const QString &kind, const QString &baseName); + void setDevices(GPtrArray *); + void setRemotes(GPtrArray *); + +Q_SIGNALS: + void initialized(); + +private: + ResultsStream *resourceForFile(const QUrl &); + void addUpdates(); + void addResource(FwupdResource *res); + + static QMap gchecksumToQChryptographicHash(); + static QByteArray getChecksum(const QString &filename, QCryptographicHash::Algorithm hashAlgorithm); + + FwupdResource *createRelease(FwupdDevice *device); + FwupdResource *createApp(FwupdDevice *device); + + QHash m_resources; + StandardBackendUpdater *m_updater; + bool m_fetching = false; + int m_startElements; + QList m_toUpdate; + GCancellable *m_cancellable; + bool m_isValid = true; +}; diff --git a/libdiscover/backends/FwupdBackend/FwupdResource.cpp b/libdiscover/backends/FwupdBackend/FwupdResource.cpp new file mode 100644 index 0000000..c7fffec --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdResource.cpp @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FwupdResource.h" + +#include +#include +#include +#include + +FwupdResource::FwupdResource(FwupdDevice *device, AbstractResourcesBackend *parent) + : FwupdResource(device, + QStringLiteral("org.fwupd.%1.device").arg(QString::fromUtf8(fwupd_device_get_id(device)).replace(QLatin1Char('/'), QLatin1Char('_'))), + parent) +{ +} + +FwupdResource::FwupdResource(FwupdDevice *device, const QString &id, AbstractResourcesBackend *parent) + : AbstractResource(parent) + , m_id(id) + , m_name(QString::fromUtf8(fwupd_device_get_name(device))) + , m_deviceID(QString::fromUtf8(fwupd_device_get_id(device))) +{ + Q_ASSERT(!m_name.isEmpty()); + setObjectName(m_name); + setDeviceDetails(device); +} + +QString FwupdResource::availableVersion() const +{ + return m_availableVersion; +} + +QStringList FwupdResource::categories() +{ + return m_categories; +} + +QString FwupdResource::comment() +{ + return m_summary; +} + +quint64 FwupdResource::size() +{ + return m_size; +} + +QUrl FwupdResource::homepage() +{ + return m_homepage; +} + +QUrl FwupdResource::helpURL() +{ + return {}; +} + +QUrl FwupdResource::bugURL() +{ + return {}; +} + +QUrl FwupdResource::donationURL() +{ + return {}; +} + +QVariant FwupdResource::icon() const +{ + return m_iconName; +} + +QString FwupdResource::installedVersion() const +{ + return m_installedVersion; +} + +QJsonArray FwupdResource::licenses() +{ + return {QJsonObject{{QStringLiteral("name"), m_license}}}; +} + +QString FwupdResource::longDescription() +{ + return m_description; +} + +QString FwupdResource::name() const +{ + return m_displayName.isEmpty() ? m_name : m_displayName; +} + +QString FwupdResource::vendor() const +{ + return m_vendor; +} + +QString FwupdResource::origin() const +{ + return m_origin; +} + +QString FwupdResource::packageName() const +{ + return m_name; +} + +QString FwupdResource::section() +{ + return QStringLiteral("Firmware Updates"); +} + +AbstractResource::State FwupdResource::state() +{ + return m_state; +} + +void FwupdResource::fetchChangelog() +{ + QString log = longDescription(); + log.replace(QLatin1Char('\n'), QLatin1String("
    ")); + + Q_EMIT changelogFetched(log); +} + +void FwupdResource::setState(AbstractResource::State state) +{ + if (m_state != state) { + m_state = state; + Q_EMIT stateChanged(); + } +} + +void FwupdResource::invokeApplication() const +{ + qWarning() << "Not Launchable"; +} + +QUrl FwupdResource::url() const +{ + return m_homepage; +} + +QString FwupdResource::executeLabel() const +{ + return QStringLiteral("Not Invokable"); +} + +void FwupdResource::setReleaseDetails(FwupdRelease *release) +{ + m_origin = QString::fromUtf8(fwupd_release_get_remote_id(release)); + m_summary = QString::fromUtf8(fwupd_release_get_summary(release)); + m_vendor = QString::fromUtf8(fwupd_release_get_vendor(release)); + m_size = fwupd_release_get_size(release); + m_availableVersion = QString::fromUtf8(fwupd_release_get_version(release)); + m_description = QString::fromUtf8((fwupd_release_get_description(release))); + m_homepage = QUrl(QString::fromUtf8(fwupd_release_get_homepage(release))); + m_license = QString::fromUtf8(fwupd_release_get_license(release)); + m_updateURI = QString::fromUtf8(fwupd_release_get_uri(release)); +} + +void FwupdResource::setDeviceDetails(FwupdDevice *dev) +{ + m_isLiveUpdatable = fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE); + m_isOnlyOffline = fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_ONLY_OFFLINE); + m_needsReboot = fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); + + if (fwupd_device_get_name(dev)) { + QString vendorDesc = QString::fromUtf8(fwupd_device_get_name(dev)); + const QString vendorName = QString::fromUtf8(fwupd_device_get_vendor(dev)); + + if (!vendorDesc.startsWith(vendorName)) + vendorDesc = vendorName + QLatin1Char(' ') + vendorDesc; + m_displayName = vendorDesc; + } + m_summary = QString::fromUtf8(fwupd_device_get_summary(dev)); + m_vendor = QString::fromUtf8(fwupd_device_get_vendor(dev)); + m_releaseDate = QDateTime::fromSecsSinceEpoch(fwupd_device_get_created(dev)).date(); + m_availableVersion = QString::fromUtf8(fwupd_device_get_version(dev)); + m_description = QString::fromUtf8((fwupd_device_get_description(dev))); + + if (fwupd_device_get_icons(dev)->len >= 1) + m_iconName = QString::fromUtf8((const gchar *)g_ptr_array_index(fwupd_device_get_icons(dev), 0)); // Check whether given icon exists or not! + else + m_iconName = QStringLiteral("device-notifier"); +} + +QString FwupdResource::cacheFile() const +{ + const auto filename_cache = FwupdBackend::cacheFile(QStringLiteral("fwupd"), QFileInfo(QUrl(m_updateURI).path()).fileName()); + Q_ASSERT(!filename_cache.isEmpty()); + return filename_cache; +} diff --git a/libdiscover/backends/FwupdBackend/FwupdResource.h b/libdiscover/backends/FwupdBackend/FwupdResource.h new file mode 100644 index 0000000..db25328 --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdResource.h @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "FwupdBackend.h" + +#include +#include + +class FwupdResource : public AbstractResource +{ + Q_OBJECT +public: + explicit FwupdResource(FwupdDevice *device, AbstractResourcesBackend *parent); + explicit FwupdResource(FwupdDevice *device, const QString &id, AbstractResourcesBackend *parent); + + QList addonsInformation() override + { + return {}; + } + QString section() override; + QString origin() const override; + QString longDescription() override; + QString availableVersion() const override; + QString installedVersion() const override; + QJsonArray licenses() override; + quint64 size() override; + QUrl homepage() override; + QUrl helpURL() override; + QUrl bugURL() override; + QUrl donationURL() override; + QStringList categories() override; + AbstractResource::State state() override; + QVariant icon() const override; + QString comment() override; + QString name() const override; + QString packageName() const override; + QString vendor() const; + AbstractResource::Type type() const override + { + return Technical; + } + bool canExecute() const override + { + return false; + } + void invokeApplication() const override; + void fetchChangelog() override; + QUrl url() const override; + QString executeLabel() const override; + QDate releaseDate() const override + { + return m_releaseDate; + } + QString sourceIcon() const override + { + return {}; + } + QString author() const override + { + return {}; + } + + void setIsDeviceLocked(bool locked) + { + m_isDeviceLocked = locked; + } + void setDescription(const QString &description) + { + m_description = description; + } + void setInstalledVersion(const QString &version) + { + m_installedVersion = version; + } + + void setState(AbstractResource::State state); + void setReleaseDetails(FwupdRelease *release); + + QString id() const + { + return m_id; + } + + QString deviceId() const + { + return m_deviceID; + } + + QUrl updateURI() const + { + return QUrl(m_updateURI); + } + + bool isDeviceLocked() const + { + return m_isDeviceLocked; + } + + bool isOnlyOffline() const + { + return m_isOnlyOffline; + } + + bool isLiveUpdatable() const + { + return m_isLiveUpdatable; + } + + bool needsReboot() const + { + return m_needsReboot; + } + + QString cacheFile() const; + bool isRemovable() const override + { + return false; + } + +private: + void setDeviceDetails(FwupdDevice *device); + + const QString m_id; + const QString m_name; + const QString m_deviceID; + QString m_summary; + QString m_description; + QString m_installedVersion; + QString m_availableVersion; + QString m_vendor; + QStringList m_categories; + QString m_license; + QString m_displayName; + QDate m_releaseDate; + + AbstractResource::State m_state = None; + QUrl m_homepage; + QString m_iconName; + quint64 m_size = 0; + + QString m_updateURI; + bool m_isDeviceLocked = false; // True if device is locked! + bool m_isOnlyOffline = false; // True if only offline updates + bool m_isLiveUpdatable = false; // True if device is live updatable + bool m_needsReboot = false; // True if device needs Reboot + QString m_origin; +}; diff --git a/libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp b/libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp new file mode 100644 index 0000000..449c4bc --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FwupdSourcesBackend.h" + +#include +#include + +class FwupdSourcesModel : public QStandardItemModel +{ + Q_OBJECT +public: + FwupdSourcesModel(FwupdSourcesBackend *backend) + : QStandardItemModel(backend) + , m_backend(backend) + { + } + + bool setData(const QModelIndex &index, const QVariant &value, int role) override + { + auto item = itemFromIndex(index); + if (!item) + return false; + + FwupdRemote *remote = fwupd_client_get_remote_by_id(m_backend->backend->client, + item->data(AbstractSourcesBackend::IdRole).toString().toUtf8().constData(), + nullptr, + nullptr); + switch (role) { + case Qt::CheckStateRole: { + if (value == Qt::Checked) { + m_backend->m_currentItem = item; + if (fwupd_remote_get_approval_required(remote)) { + QString eulaText = i18n("The remote %1 require that you accept their license:\n %2", + QString::fromUtf8(fwupd_remote_get_title(remote)), + QString::fromUtf8(fwupd_remote_get_agreement(remote))); + Q_EMIT m_backend->proceedRequest(i18n("Review EULA"), eulaText); + } else { + m_backend->proceed(); + } + } else if (value.toInt() == Qt::Unchecked) { + g_autoptr(GError) error = nullptr; + if (fwupd_client_modify_remote(m_backend->backend->client, fwupd_remote_get_id(remote), "Enabled", "false", nullptr, &error)) + item->setCheckState(Qt::Unchecked); + else + qWarning() << "could not disable remote" << remote << error->message; + } + return true; + } + } + return false; + } + +private: + FwupdSourcesBackend *const m_backend; +}; + +FwupdSourcesBackend::FwupdSourcesBackend(AbstractResourcesBackend *parent) + : AbstractSourcesBackend(parent) + , backend(qobject_cast(parent)) + , m_sources(new FwupdSourcesModel(this)) +{ + populateSources(); +} + +void FwupdSourcesBackend::populateSources() +{ + g_autoptr(GError) error = nullptr; + g_autoptr(GPtrArray) remotes = fwupd_client_get_remotes(backend->client, nullptr, &error); + if (!remotes) { + qWarning() << "could not list fwupd remotes" << error->message; + return; + } + + for (uint i = 0; i < remotes->len; i++) { + FwupdRemote *remote = (FwupdRemote *)g_ptr_array_index(remotes, i); + if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_LOCAL) + continue; + const QString id = QString::fromUtf8(fwupd_remote_get_id(remote)); + if (id.isEmpty()) + continue; + + QStandardItem *it = new QStandardItem(id); + it->setData(id, AbstractSourcesBackend::IdRole); + it->setData(QVariant(QString::fromUtf8(fwupd_remote_get_title(remote))), Qt::ToolTipRole); + it->setCheckable(true); + it->setCheckState(fwupd_remote_get_enabled(remote) ? Qt::Checked : Qt::Unchecked); + m_sources->appendRow(it); + } +} + +QAbstractItemModel *FwupdSourcesBackend::sources() +{ + return m_sources; +} + +bool FwupdSourcesBackend::addSource(const QString &id) +{ + qWarning() << "Fwupd Error: Custom Addition of Sources Not Allowed" + << "Remote-ID" << id; + return false; +} + +bool FwupdSourcesBackend::removeSource(const QString &id) +{ + qWarning() << "Fwupd Error: Removal of Sources Not Allowed" + << "Remote-ID" << id; + return false; +} + +QVariantList FwupdSourcesBackend::actions() const +{ + return {}; +} + +void FwupdSourcesBackend::cancel() +{ + FwupdRemote *remote = + fwupd_client_get_remote_by_id(backend->client, m_currentItem->data(AbstractSourcesBackend::IdRole).toString().toUtf8().constData(), nullptr, nullptr); + m_currentItem->setCheckState(fwupd_remote_get_enabled(remote) ? Qt::Checked : Qt::Unchecked); + + m_currentItem = nullptr; +} + +void FwupdSourcesBackend::proceed() +{ + const QString id = m_currentItem->data(AbstractSourcesBackend::IdRole).toString(); + FwupdRemote *remote = fwupd_client_get_remote_by_id(backend->client, id.toUtf8().constData(), nullptr, nullptr); + g_autoptr(GError) error = nullptr; + if (fwupd_client_modify_remote(backend->client, fwupd_remote_get_id(remote), "Enabled", "true", nullptr, &error)) + m_currentItem->setData(Qt::Checked, Qt::CheckStateRole); + else + Q_EMIT passiveMessage(i18n("Could not enable remote %1: %2", id, (error ? error->message : ""))); + + m_currentItem = nullptr; +} + +#include "FwupdSourcesBackend.moc" diff --git a/libdiscover/backends/FwupdBackend/FwupdSourcesBackend.h b/libdiscover/backends/FwupdBackend/FwupdSourcesBackend.h new file mode 100644 index 0000000..c98f6d4 --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdSourcesBackend.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "FwupdBackend.h" +#include +#include + +class FwupdSourcesModel; + +class FwupdSourcesBackend : public AbstractSourcesBackend +{ + Q_OBJECT +public: + explicit FwupdSourcesBackend(AbstractResourcesBackend *parent); + + FwupdBackend *backend; + QAbstractItemModel *sources() override; + bool addSource(const QString &id) override; + bool removeSource(const QString &id) override; + QString idDescription() override + { + return QString(); + } + QVariantList actions() const override; + bool supportsAdding() const override + { + return false; + } + void eulaRequired(const QString &remoteName, const QString &licenseAgreement); + void populateSources(); + + void proceed() override; + void cancel() override; + + QStandardItem *m_currentItem = nullptr; + +private: + FwupdSourcesModel *m_sources; +}; diff --git a/libdiscover/backends/FwupdBackend/FwupdTransaction.cpp b/libdiscover/backends/FwupdBackend/FwupdTransaction.cpp new file mode 100644 index 0000000..cd7d6cb --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdTransaction.cpp @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "FwupdTransaction.h" +#include "../DiscoverVersion.h" + +#include +#include + +FwupdTransaction::FwupdTransaction(FwupdResource *app, FwupdBackend *backend) + : Transaction(backend, app, Transaction::InstallRole, {}) + , m_app(app) + , m_backend(backend) +{ + setCancellable(true); + setStatus(QueuedStatus); + + Q_ASSERT(!m_app->deviceId().isEmpty()); + QTimer::singleShot(0, this, &FwupdTransaction::install); +} + +FwupdTransaction::~FwupdTransaction() = default; + +void FwupdTransaction::install() +{ + g_autoptr(GError) error = nullptr; + + if (m_app->isDeviceLocked()) { + QString device_id = m_app->deviceId(); + if (device_id.isEmpty()) { + qWarning() << "Fwupd Error: No Device ID set, cannot unlock device " << this << m_app->name(); + } else if (!fwupd_client_unlock(m_backend->client, device_id.toUtf8().constData(), nullptr, &error)) { + m_backend->handleError(error); + } + setStatus(DoneWithErrorStatus); + return; + } + + const QString fileName = m_app->cacheFile(); + if (!QFileInfo::exists(fileName)) { + const QUrl uri(m_app->updateURI()); + setStatus(DownloadingStatus); + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + auto req = QNetworkRequest(uri); + req.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("plasma-discover/%1").arg(version)); + req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + auto reply = manager->get(req); + QFile *file = new QFile(fileName); + if (!file->open(QFile::WriteOnly)) { + qWarning() << "Fwupd Error: Could not open to write" << fileName << uri; + setStatus(DoneWithErrorStatus); + file->deleteLater(); + return; + } + + connect(reply, &QNetworkReply::finished, this, [this, file, reply]() { + file->close(); + file->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Fwupd Error: Could not download" << reply->url() << reply->errorString(); + file->remove(); + setStatus(DoneWithErrorStatus); + } else { + fwupdInstall(file->fileName()); + } + }); + connect(reply, &QNetworkReply::readyRead, this, [file, reply]() { + file->write(reply->readAll()); + }); + } else { + fwupdInstall(fileName); + } +} + +void FwupdTransaction::fwupdInstall(const QString &file) +{ + FwupdInstallFlags install_flags = FWUPD_INSTALL_FLAG_NONE; + g_autoptr(GError) error = nullptr; + + /* only offline supported */ + if (m_app->isOnlyOffline()) + install_flags = static_cast(install_flags | FWUPD_INSTALL_FLAG_OFFLINE); + + if (!fwupd_client_install(m_backend->client, m_app->deviceId().toUtf8().constData(), file.toUtf8().constData(), install_flags, nullptr, &error)) { + m_backend->handleError(error); + setStatus(DoneWithErrorStatus); + } else + finishTransaction(); +} + +void FwupdTransaction::updateProgress() +{ + setProgress(fwupd_client_get_percentage(m_backend->client)); +} + +void FwupdTransaction::proceed() +{ + finishTransaction(); +} + +void FwupdTransaction::cancel() +{ + setStatus(CancelledStatus); +} + +void FwupdTransaction::finishTransaction() +{ + AbstractResource::State newState; + switch (role()) { + case InstallRole: + case ChangeAddonsRole: + newState = AbstractResource::Installed; + break; + case RemoveRole: + newState = AbstractResource::None; + break; + } + m_app->setState(newState); + if (m_app->needsReboot()) { + m_app->backend()->backendUpdater()->enableNeedsReboot(); + } + setStatus(DoneStatus); + deleteLater(); +} diff --git a/libdiscover/backends/FwupdBackend/FwupdTransaction.h b/libdiscover/backends/FwupdBackend/FwupdTransaction.h new file mode 100644 index 0000000..24de299 --- /dev/null +++ b/libdiscover/backends/FwupdBackend/FwupdTransaction.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2018 Abhijeet Sharma + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "FwupdBackend.h" +#include "FwupdResource.h" +#include + +class FwupdResource; +class FwupdTransaction : public Transaction +{ + Q_OBJECT +public: + FwupdTransaction(FwupdResource *app, FwupdBackend *backend); + ~FwupdTransaction(); + void cancel() override; + void proceed() override; + +private Q_SLOTS: + void updateProgress(); + void finishTransaction(); + void fwupdInstall(const QString &file); + +private: + void install(); + + FwupdResource *const m_app; + FwupdBackend *const m_backend; +}; diff --git a/libdiscover/backends/KNSBackend/CMakeLists.txt b/libdiscover/backends/KNSBackend/CMakeLists.txt new file mode 100644 index 0000000..8c48e4f --- /dev/null +++ b/libdiscover/backends/KNSBackend/CMakeLists.txt @@ -0,0 +1,12 @@ +add_subdirectory(tests) + +add_library(kns-backend MODULE + KNSBackend.cpp + KNSResource.cpp + KNSReviews.cpp + KNSTransaction.cpp +) + +target_link_libraries(kns-backend Discover::Common AppStreamQt KF5::ConfigCore KF5::Attica KF5::NewStuffCore KF5::WidgetsAddons Qt::Xml) + +install(TARGETS kns-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) diff --git a/libdiscover/backends/KNSBackend/KNSBackend.cpp b/libdiscover/backends/KNSBackend/KNSBackend.cpp new file mode 100644 index 0000000..6ee9329 --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSBackend.cpp @@ -0,0 +1,707 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +// Qt includes +#include +#include +#include +#include +#include +#include + +// Attica includes +#include +#include + +// KDE includes +#include +#include +#include +#include +#include +#include + +// DiscoverCommon includes +#include "Category/Category.h" +#include "Transaction/Transaction.h" +#include "Transaction/TransactionModel.h" + +// Own includes +#include "KNSBackend.h" +#include "KNSResource.h" +#include "KNSReviews.h" +#include "KNSTransaction.h" +#include "utils.h" +#include + +static const int ENGINE_PAGE_SIZE = 100; + +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 *question) { + const auto transactions = TransactionModel::global()->transactions(); + for (auto t : transactions) { + const auto transaction = dynamic_cast(t); + if (!transaction) { + continue; + } + + if (question->entry().uniqueId() == transaction->uniqueId()) { + switch (question->questionType()) { + case KNSCore::Question::ContinueCancelQuestion: + transaction->addQuestion(question); + return; + default: + transaction->passiveMessage(i18n("Unsupported question:\n%1", question->question())); + question->setResponse(KNSCore::Question::InvalidResponse); + transaction->setStatus(Transaction::CancelledStatus); + break; + } + return; + } + } + qWarning() << "Question for unknown resource" << question->question() << question->questionType(); + question->setResponse(KNSCore::Question::InvalidResponse); + }); + } + + QVector newInstance(QObject *parent, const QString & /*name*/) const override + { + QVector ret; + const QStringList availableConfigFiles = KNSCore::Engine::availableConfigFiles(); + for (const QString &configFile : availableConfigFiles) { + auto bk = new KNSBackend(parent, QStringLiteral("plasma"), configFile); + if (bk->isValid()) + ret += bk; + else + delete bk; + } + return ret; + } +}; + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +Q_DECLARE_METATYPE(KNSCore::EntryInternal) +#endif + +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, KConfig::SimpleConfig); + 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()); + + 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)); + setResponsePending(false); + } + }); + + const CategoryFilter filter = {CategoryFilter::CategoryNameFilter, 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::CategoryNameFilter, cat}, backendName, {}, true); + else + categories << new Category(cat, QStringLiteral("plasma"), {CategoryFilter::CategoryNameFilter, cat}, backendName, {}, true); + } + } + + QVector topCategories{categories}; + for (const auto &cat : qAsConst(categories)) { + const QString catName = cat->name().append(QLatin1Char('/')); + for (const auto &potentialSubCat : qAsConst(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::slotErrorCode); + connect(m_engine, &KNSCore::Engine::signalEntryEvent, this, &KNSBackend::slotEntryEvent); + connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection); + connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled); + connect(m_engine, &KNSCore::Engine::signalUpdateableEntriesLoaded, this, [this] { + if (m_responsePending) { + setResponsePending(false); + } + }); + connect(m_engine, &KNSCore::Engine::signalCategoriesMetadataLoded, this, [categories](const QList &categoryMetadatas) { + for (const KNSCore::Provider::CategoryMetadata &category : categoryMetadatas) { + for (Category *cat : qAsConst(categories)) { + if (cat->matchesCategoryName(category.name)) { + cat->setName(category.displayName); + break; + } + } + } + }); + m_engine->setPageSize(ENGINE_PAGE_SIZE); + m_engine->init(m_name); + + if (m_hasApplications) { + auto actualCategory = new Category(m_displayName, QStringLiteral("applications-other"), filter, backendName, topCategories, false); + auto applicationCategory = new Category(i18n("Applications"), // + QStringLiteral("applications-internet"), + filter, + backendName, + {actualCategory}, + false); + const QVector filters = {{CategoryFilter::CategoryNameFilter, QLatin1String("Application")}, filter}; + applicationCategory->setFilter({CategoryFilter::AndFilter, filters}); + 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("wallpaper-mobile.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, filter, backendName, categories, true); + actualCategory->setParent(this); + + const auto topLevelName = knsrcPlasma.contains(fileName) ? i18n("Plasma Addons") : i18n("Application Addons"); + auto addonsCategory = new Category(topLevelName, iconName, filter, backendName, {actualCategory}, true); + m_rootCategories = {addonsCategory}; + } + + connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &KNSBackend::updatesCountChanged); +} + +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::setResponsePending(bool pending) +{ + Q_ASSERT(m_responsePending != pending); + m_responsePending = pending; + if (pending) { + Q_EMIT startingSearch(); + } else { + Q_EMIT availableForQueries(); + setFetching(false); + m_onePage = false; + } +} + +void KNSBackend::fetchInstalled() +{ + auto search = new OneTimeAction( + 666, + [this]() { + // First we ensure we've got data loaded on what we've got installed already + if (m_responsePending) { + // Slot already taken, will need to wait again + return false; + } + m_onePage = true; + setResponsePending(true); + m_engine->checkForInstalled(); + // And then we check for updates - we could do only one, if all we cared about was updates, + // but to have both a useful initial list, /and/ information on updates, we want to get both. + // The reason we are not doing a checkUpdates() overload for this is that the caching for this + // information is done by KNSEngine, and we want to actually load it every time we initialize. + auto updateChecker = new OneTimeAction( + 666, + [this] { + // No need to check for updates if there's no resources + if (m_resourcesByName.isEmpty()) { + return true; + } + + if (m_responsePending) { + // Slot already taken, will need to wait again + return false; + } + + m_onePage = true; + setResponsePending(true); + m_engine->checkForUpdates(); + return true; + }, + this); + connect(this, &KNSBackend::availableForQueries, updateChecker, &OneTimeAction::trigger, Qt::QueuedConnection); + return true; + }, + this); + + if (m_responsePending) { + connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection); + } else { + search->trigger(); + } +} + +void KNSBackend::checkForUpdates() +{ + // Since we load the updates during initialization already, don't overburden + // the machine with multiple of these, because that would just be silly. + if (m_initialized) { + auto updateChecker = new OneTimeAction( + 666, + [this] { + if (m_responsePending) { + // Slot already taken, will need to wait again + return false; + } + + m_onePage = true; + setResponsePending(true); + m_engine->checkForUpdates(); + return true; + }, + this); + + if (m_responsePending) { + connect(this, &KNSBackend::availableForQueries, updateChecker, &OneTimeAction::trigger, Qt::QueuedConnection); + } else { + updateChecker->trigger(); + } + } +} + +void KNSBackend::setFetching(bool f) +{ + if (m_fetching != f) { + m_fetching = f; + Q_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) +{ + if (!m_isValid) + return; + + 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); + } + + setResponsePending(false); + if (m_onePage || resources.count() < ENGINE_PAGE_SIZE) { + Q_EMIT searchFinished(); + } +} + +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. + setResponsePending(true); + m_engine->requestMoreData(); +} + +void KNSBackend::statusChanged(const KNSCore::EntryInternal &entry) +{ + resourceForEntry(entry); +} + +void KNSBackend::slotErrorCode(const KNSCore::ErrorCode &errorCode, const QString &message, const QVariant &metadata) +{ + QString error = message; + qWarning() << "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: { + KNSResource *r = static_cast(m_resourcesByName.value(metadata.toString())); + if (r) { + // If the following is true, then we can safely assume that the entry was + // attempted updated, but the update was aborted. + // Specifically, we can also likely expect that the update failed because + // KNSCore::Engine was unable to deduce which payload to use (which will + // happen when an entry has more than one payload, and none of those match + // the name of the originally downloaded file). + // We cannot complete this in Discover (as we've no way to forward that + // query to the user) but we can give them an idea of how to deal with the + // situation some other way. + // TODO: Once Discover has a way to forward queries to the user from transactions, this likely will no longer be needed + if (r->entry().status() == KNS3::Entry::Updateable) { + error = i18n( + "Unable to complete the update of %1. You can try and perform this action through the Get Hot New Stuff dialog, which grants tighter " + "control. The reported error was:\n%2", + r->name(), + 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; + } + if (m_responsePending) { + setResponsePending(false); + } + qWarning() << "kns error" << objectName() << error; + if (!invalidFile) + Q_EMIT passiveMessage(i18n("%1: %2", name(), error)); +} + +void KNSBackend::slotEntryEvent(const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event) +{ + switch (event) { + case KNSCore::EntryInternal::StatusChangedEvent: + statusChanged(entry); + break; + case KNSCore::EntryInternal::DetailsLoadedEvent: + detailsLoaded(entry); + break; + case KNSCore::EntryInternal::AdoptedEvent: + case KNSCore::EntryInternal::UnknownEvent: + default: + break; + } +} + +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 ((!filter.category && !filter.search.isEmpty()) // Accept global searches + // 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(); + + stream->setProperty("alreadyStarted", false); + auto start = [this, stream, searchText]() { + Q_ASSERT(!isFetching()); + if (!m_isValid) { + qWarning() << "querying an invalid backend"; + stream->finish(); + return; + } + + if (m_responsePending || stream->property("alreadyStarted").toBool()) { + return; + } + stream->setProperty("alreadyStarted", true); + setResponsePending(true); + + // No need to explicitly launch a search, setting the search term already does that for us + m_engine->setSearchTerm(searchText); + m_onePage = false; + + 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, Qt::QueuedConnection); + connect(this, &KNSBackend::availableForQueries, stream, start, Qt::QueuedConnection); + } 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('/'), Qt::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]() { + if (m_responsePending) { + // Slot already taken, will need to wait again + return; + } + setResponsePending(true); + m_engine->fetchEntryById(entryid); + m_onePage = true; + + connect(m_engine, &KNSCore::Engine::signalErrorCode, stream, &ResultsStream::finish); + connect(m_engine, + &KNSCore::Engine::signalEntryEvent, + stream, + [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event) { + switch (event) { + case KNSCore::EntryInternal::StatusChangedEvent: + 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(); + QTimer::singleShot(0, this, [this] { + setResponsePending(false); + }); + stream->finish(); + break; + case KNSCore::EntryInternal::DetailsLoadedEvent: + case KNSCore::EntryInternal::AdoptedEvent: + case KNSCore::EntryInternal::UnknownEvent: + default: + break; + } + }); + }; + 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); + Q_EMIT res->longDescriptionChanged(); +} + +#include "KNSBackend.moc" diff --git a/libdiscover/backends/KNSBackend/KNSBackend.h b/libdiscover/backends/KNSBackend/KNSBackend.h new file mode 100644 index 0000000..93fb4df --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSBackend.h @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +#include "Transaction/AddonList.h" +#include "discovercommon_export.h" +#include + +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 slotErrorCode(const KNSCore::ErrorCode &errorCode, const QString &message, const QVariant &metadata); + void slotEntryEvent(const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event); + +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(); + void setResponsePending(bool pending); + + bool m_onePage = false; + bool m_responsePending = false; + QString m_pendingSearchQuery; + bool m_fetching; + bool m_isValid; + KNSCore::Engine *m_engine; + QHash m_resourcesByName; + KNSReviews *const m_reviews; + QString m_name; + const 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; +}; diff --git a/libdiscover/backends/KNSBackend/KNSResource.cpp b/libdiscover/backends/KNSBackend/KNSResource.cpp new file mode 100644 index 0000000..e4016f9 --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSResource.cpp @@ -0,0 +1,270 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "KNSResource.h" +#include "KNSBackend.h" +#include +#include +#include +#include +#include + +#include "ReviewsBackend/Rating.h" +#include +#include +#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)(http[-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 {{AppStreamUtils::license(m_entry.license())}}; +} + +quint64 KNSResource::size() +{ + const auto downloadInfo = m_entry.downloadLinkInformationList(); + return downloadInfo.isEmpty() ? 0 : downloadInfo.at(0).size * 1024; +} + +QString KNSResource::installedVersion() const +{ + return !m_entry.version().isEmpty() ? m_entry.version() : m_entry.releaseDate().toString(); +} + +QString KNSResource::availableVersion() const +{ + return !m_entry.updateVersion().isEmpty() ? m_entry.updateVersion() + : !m_entry.updateReleaseDate().isNull() ? m_entry.updateReleaseDate().toString() + : !m_entry.version().isEmpty() ? m_entry.version() + : releaseDate().toString(); +} + +QString KNSResource::origin() const +{ + return m_entry.providerId(); +} + +QString KNSResource::displayOrigin() const +{ + if (auto providers = knsBackend()->engine()->atticaProviders(); !providers.isEmpty()) { + auto provider = providers.constFirst(); + if (provider->name() == QLatin1String("api.kde-look.org")) { + return i18nc("The name of the KDE Store", "KDE Store"); + } + return providers.constFirst()->name(); + } + return QUrl(m_entry.providerId()).host(); +} + +QString KNSResource::section() +{ + return m_entry.category(); +} + +static bool isAnimated(const QString &path) +{ + static const QVector s_extensions = {QLatin1String(".gif"), QLatin1String(".apng"), QLatin1String(".webp"), QLatin1String(".avif")}; + return kContains(s_extensions, [path](const QLatin1String &postfix) { + return path.endsWith(postfix); + }); +} + +static void appendIfValid(Screenshots &list, const QUrl &thumbnail, const QUrl &screenshot) +{ + if (thumbnail.isEmpty() || screenshot.isEmpty()) { + return; + } + list += {thumbnail, screenshot, isAnimated(thumbnail.path())}; +} + +void KNSResource::fetchScreenshots() +{ + Screenshots ret; + appendIfValid(ret, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1)), QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewBig1))); + appendIfValid(ret, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall2)), QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewBig2))); + appendIfValid(ret, QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewSmall3)), QUrl(m_entry.previewUrl(KNSCore::EntryInternal::PreviewBig3))); + Q_EMIT screenshotsFetched(ret); +} + +void KNSResource::fetchChangelog() +{ + Q_EMIT changelogFetched(m_entry.changelog()); +} + +QStringList KNSResource::extends() const +{ + return knsBackend()->extends(); +} + +QUrl KNSResource::url() const +{ + return QUrl(QStringLiteral("kns://") + knsBackend()->name() + QLatin1Char('/') + QUrl(m_entry.providerId()).host() + QLatin1Char('/') + m_entry.uniqueId()); +} + +bool KNSResource::canExecute() const +{ + return knsBackend()->engine()->hasAdoptionCommand(); +} + +void KNSResource::invokeApplication() const +{ + knsBackend()->engine()->adoptEntry(m_entry); +} + +QString KNSResource::executeLabel() const +{ + return knsBackend()->engine()->useLabel(); +} + +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); + m_rating.reset(new Rating(packageName(), noc, rating / 10)); + } + return m_rating.data(); +} + +QString KNSResource::author() const +{ + return m_entry.author().name(); +} diff --git a/libdiscover/backends/KNSBackend/KNSResource.h b/libdiscover/backends/KNSBackend/KNSResource.h new file mode 100644 index 0000000..42a21e5 --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSResource.h @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +#include "discovercommon_export.h" + +class KNSBackend; +class DISCOVERCOMMON_EXPORT KNSResource : public AbstractResource +{ + Q_OBJECT +public: + explicit KNSResource(const KNSCore::EntryInternal &c, QStringList categories, KNSBackend *parent); + ~KNSResource() override; + + AbstractResource::State state() override; + QVariant icon() const override; + QString comment() override; + QString name() const override; + QString packageName() const override; + QStringList categories() override; + QUrl homepage() override; + QJsonArray licenses() override; + QString longDescription() override; + QList addonsInformation() override + { + return QList(); + } + QString availableVersion() const override; + QString installedVersion() const override; + QString origin() const override; + QString displayOrigin() const override; + QString section() override; + void fetchScreenshots() override; + quint64 size() override; + void fetchChangelog() override; + QStringList extends() const override; + AbstractResource::Type type() const override + { + return Addon; + } + QString author() const override; + + KNSBackend *knsBackend() const; + + void setEntry(const KNSCore::EntryInternal &entry); + KNSCore::EntryInternal entry() const; + + bool canExecute() const override; + void invokeApplication() const override; + + QUrl url() const override; + QString executeLabel() const override; + QString sourceIcon() const override + { + return QStringLiteral("get-hot-new-stuff"); + } + QDate releaseDate() const override; + QVector linkIds() const; + QUrl donationURL() override; + + Rating *ratingInstance(); + +private: + const QStringList m_categories; + KNSCore::EntryInternal m_entry; + KNS3::Entry::Status m_lastStatus; + QScopedPointer m_rating; +}; diff --git a/libdiscover/backends/KNSBackend/KNSReviews.cpp b/libdiscover/backends/KNSBackend/KNSReviews.cpp new file mode 100644 index 0000000..3fa8b28 --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSReviews.cpp @@ -0,0 +1,179 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "KNSReviews.h" +#include "KNSBackend.h" +#include "KNSResource.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +KNSReviews::KNSReviews(KNSBackend *backend) + : AbstractReviewsBackend(backend) + , m_backend(backend) +{ +} + +Rating *KNSReviews::ratingForApplication(AbstractResource *app) const +{ + KNSResource *resource = qobject_cast(app); + if (!resource) { + qDebug() << app->packageName() << "<= couldn't find resource"; + return nullptr; + } + + return resource->ratingInstance(); +} + +void KNSReviews::fetchReviews(AbstractResource *app, int page) +{ + Attica::ListJob *job = provider().requestComments(Attica::Comment::ContentComment, app->packageName(), QStringLiteral("0"), page - 1, 10); + if (!job) { + Q_EMIT reviewsReady(app, {}, false); + return; + } + job->setProperty("app", QVariant::fromValue(app)); + connect(job, &Attica::BaseJob::finished, this, &KNSReviews::commentsReceived); + job->start(); + acquireFetching(true); +} + +void KNSReviews::acquireFetching(bool f) +{ + if (f) + m_fetching++; + else + m_fetching--; + + if ((!f && m_fetching == 0) || (f && m_fetching == 1)) { + Q_EMIT fetchingChanged(m_fetching != 0); + } + Q_ASSERT(m_fetching >= 0); +} + +static QVector createReviewList(AbstractResource *app, const Attica::Comment::List comments, int depth = 0) +{ + QVector reviews; + for (const Attica::Comment &comment : comments) { + // TODO: language lookup? + ReviewPtr r(new Review(app->name(), + app->packageName(), + QStringLiteral("en"), + comment.subject(), + comment.text(), + comment.user(), + comment.date(), + true, + comment.id().toInt(), + comment.score() / 10, + 0, + 0, + QString())); + r->addMetadata(QStringLiteral("NumberOfParents"), depth); + reviews += r; + if (comment.childCount() > 0) { + reviews += createReviewList(app, comment.children(), depth + 1); + } + } + return reviews; +} + +void KNSReviews::commentsReceived(Attica::BaseJob *j) +{ + acquireFetching(false); + Attica::ListJob *job = static_cast *>(j); + + AbstractResource *app = job->property("app").value(); + QVector reviews = createReviewList(app, job->itemList()); + + Q_EMIT reviewsReady(app, reviews, !reviews.isEmpty()); +} + +bool KNSReviews::isFetching() const +{ + return m_fetching > 0; +} + +void KNSReviews::flagReview(Review * /*r*/, const QString & /*reason*/, const QString & /*text*/) +{ + qWarning() << "cannot flag reviews"; +} + +void KNSReviews::deleteReview(Review * /*r*/) +{ + qWarning() << "cannot delete comments"; +} + +void KNSReviews::sendReview(AbstractResource *app, const QString &summary, const QString &review_text, const QString &rating, const QString &userName) +{ + Q_UNUSED(userName); + provider().voteForContent(app->packageName(), rating.toUInt() * 20); + if (!summary.isEmpty()) + provider().addNewComment(Attica::Comment::ContentComment, app->packageName(), QString(), QString(), summary, review_text); +} + +void KNSReviews::submitUsefulness(Review *r, bool useful) +{ + provider().voteForComment(QString::number(r->id()), useful * 5); +} + +void KNSReviews::logout() +{ + bool b = provider().saveCredentials(QString(), QString()); + if (!b) + qWarning() << "couldn't log out"; +} + +void KNSReviews::registerAndLogin() +{ + QDesktopServices::openUrl(provider().baseUrl()); +} + +void KNSReviews::login() +{ + KPasswordDialog *dialog = new KPasswordDialog; + dialog->setPrompt(i18n("Log in information for %1", provider().name())); + connect(dialog, &KPasswordDialog::gotUsernameAndPassword, this, &KNSReviews::credentialsReceived); +} + +void KNSReviews::credentialsReceived(const QString &user, const QString &password) +{ + bool b = provider().saveCredentials(user, password); + if (!b) + qWarning() << "couldn't save" << user << "credentials for" << provider().name(); +} + +bool KNSReviews::hasCredentials() const +{ + return provider().hasCredentials(); +} + +QString KNSReviews::userName() const +{ + QString user, password; + provider().loadCredentials(user, password); + return user; +} + +Attica::Provider KNSReviews::provider() const +{ + if (m_backend->engine()->atticaProviders().isEmpty()) { + return {}; + } + return *m_backend->engine()->atticaProviders().constFirst(); +} + +bool KNSReviews::isResourceSupported(AbstractResource *res) const +{ + return qobject_cast(res); +} diff --git a/libdiscover/backends/KNSBackend/KNSReviews.h b/libdiscover/backends/KNSBackend/KNSReviews.h new file mode 100644 index 0000000..2dc9a35 --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSReviews.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +class KNSBackend; +class QUrl; +namespace Attica +{ +class BaseJob; +} + +class KNSReviews : public AbstractReviewsBackend +{ + Q_OBJECT +public: + explicit KNSReviews(KNSBackend *backend); + + void fetchReviews(AbstractResource *app, int page = 1) override; + bool isFetching() const override; + void flagReview(Review *r, const QString &reason, const QString &text) override; + void deleteReview(Review *r) override; + void submitUsefulness(Review *r, bool useful) override; + void logout() override; + void registerAndLogin() override; + void login() override; + Rating *ratingForApplication(AbstractResource *app) const override; + bool hasCredentials() const override; + + bool isResourceSupported(AbstractResource *res) const override; + +protected: + void sendReview(AbstractResource *app, const QString &summary, const QString &review_text, const QString &rating, const QString &userName) override; + QString userName() const override; + +private Q_SLOTS: + void commentsReceived(Attica::BaseJob *job); + void credentialsReceived(const QString &user, const QString &password); + +private: + Attica::Provider provider() const; + void acquireFetching(bool f); + + KNSBackend *const m_backend; + int m_fetching = 0; +}; diff --git a/libdiscover/backends/KNSBackend/KNSTransaction.cpp b/libdiscover/backends/KNSBackend/KNSTransaction.cpp new file mode 100644 index 0000000..065cac0 --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSTransaction.cpp @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "KNSTransaction.h" +#include "KNSBackend.h" +#include +#include +#include + +#include +#include + +KNSTransaction::KNSTransaction(QObject *parent, KNSResource *res, Role role) + + : Transaction(parent, res, role) + , m_id(res->entry().uniqueId()) +{ + setCancellable(false); + + auto manager = res->knsBackend()->engine(); + connect(manager, &KNSCore::Engine::signalEntryEvent, this, [this](const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event) { + switch (event) { + case KNSCore::EntryInternal::StatusChangedEvent: + anEntryChanged(entry); + break; + case KNSCore::EntryInternal::DetailsLoadedEvent: + case KNSCore::EntryInternal::AdoptedEvent: + case KNSCore::EntryInternal::UnknownEvent: + default: + break; + } + }); + 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->entry().status() == KNS3::Entry::Updateable) + actionFunction = [res, engine]() { + engine->install(res->entry(), -1); + }; + else if (res->linkIds().isEmpty()) + actionFunction = [res]() { + qWarning() << "No installable candidates in the KNewStuff entry" << res->entry().name() << "with id" << res->entry().uniqueId() << "on the backend" + << res->backend()->name() + << "There should always be at least one downloadable item in an OCS entry, and if there isn't, we should consider it broken. OCS " + "can technically show them, but if there is nothing to install, it cannot be installed."; + }; + else + actionFunction = [res, engine]() { + engine->install(res->entry()); + }; + QTimer::singleShot(0, res, actionFunction); +} + +void KNSTransaction::addQuestion(KNSCore::Question *question) +{ + Q_ASSERT(question->questionType() == KNSCore::Question::ContinueCancelQuestion); + m_questions << question; + + Q_EMIT proceedRequest(question->title(), question->question()); +} + +void KNSTransaction::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 KNSTransaction::cancel() +{ + for (auto q : m_questions) { + q->setResponse(KNSCore::Question::CancelResponse); + } + setStatus(CancelledStatus); +} + +void KNSTransaction::proceed() +{ + m_questions.takeFirst()->setResponse(KNSCore::Question::ContinueResponse); +} diff --git a/libdiscover/backends/KNSBackend/KNSTransaction.h b/libdiscover/backends/KNSBackend/KNSTransaction.h new file mode 100644 index 0000000..b64f9ae --- /dev/null +++ b/libdiscover/backends/KNSBackend/KNSTransaction.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "KNSResource.h" +#include "Transaction/Transaction.h" +#include + +class KNSTransaction; + +class KNSTransaction : public Transaction +{ +public: + KNSTransaction(QObject *parent, KNSResource *res, Transaction::Role role); + + void anEntryChanged(const KNSCore::EntryInternal &entry); + + void addQuestion(KNSCore::Question *question); + void cancel() override; + void proceed() override; + + QString uniqueId() const + { + return m_id; + } + +private: + const QString m_id; + QVector m_questions; +}; diff --git a/libdiscover/backends/KNSBackend/tests/CMakeLists.txt b/libdiscover/backends/KNSBackend/tests/CMakeLists.txt new file mode 100644 index 0000000..1462667 --- /dev/null +++ b/libdiscover/backends/KNSBackend/tests/CMakeLists.txt @@ -0,0 +1,4 @@ +include_directories(..) + +add_executable(knsbackendtest KNSBackendTest.cpp) +target_link_libraries(knsbackendtest PRIVATE Discover::Common Qt::Core Qt::Test KF5::Attica KF5::NewStuff) diff --git a/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp b/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp new file mode 100644 index 0000000..ba948e3 --- /dev/null +++ b/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "KNSBackendTest.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +QTEST_MAIN(KNSBackendTest) + +static QString s_knsrcName = QStringLiteral("ksplash.knsrc"); + +KNSBackendTest::KNSBackendTest(QObject *parent) + : QObject(parent) + , m_r(nullptr) +{ + QStandardPaths::setTestModeEnabled(true); + QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)).removeRecursively(); + + ResourcesModel *model = new ResourcesModel(QStringLiteral("kns-backend"), this); + Q_ASSERT(!model->backends().isEmpty()); + auto findTestBackend = [](AbstractResourcesBackend *backend) { + return backend->name() == QLatin1String("ksplash.knsrc"); + }; + m_backend = kFilter>(model->backends(), findTestBackend).value(0); + + if (!m_backend || !m_backend->isValid()) { + qWarning() << "couldn't run the test"; + exit(0); + } + + connect(m_backend->reviewsBackend(), &AbstractReviewsBackend::reviewsReady, this, &KNSBackendTest::reviewsArrived); +} + +QVector KNSBackendTest::getResources(ResultsStream *stream, bool canBeEmpty) +{ + Q_ASSERT(stream); + Q_ASSERT(stream->objectName() != QLatin1String("KNS-void")); + QSignalSpy spyResources(stream, &ResultsStream::destroyed); + QVector resources; + connect(stream, &ResultsStream::resourcesFound, this, [&resources, stream](const QVector &res) { + resources += res; + Q_EMIT stream->fetchMore(); + }); + bool waited = spyResources.wait(10000); + if (!waited) { + if (auto x = qobject_cast(stream)) + qDebug() << "waited" << x->streams(); + } + Q_ASSERT(waited); + Q_ASSERT(!resources.isEmpty() || canBeEmpty); + return resources; +} + +QVector KNSBackendTest::getAllResources(AbstractResourcesBackend *backend) +{ + AbstractResourcesBackend::Filters f; + if (CategoryModel::global()->rootCategories().isEmpty()) + CategoryModel::global()->populateCategories(); + f.category = CategoryModel::global()->rootCategories().constFirst(); + return getResources(backend->search(f)); +} + +void KNSBackendTest::testRetrieval() +{ + QVERIFY(m_backend->backendUpdater()); + QCOMPARE(m_backend->updatesCount(), m_backend->backendUpdater()->toUpdate().count()); + + QSignalSpy spy(m_backend, &AbstractResourcesBackend::fetchingChanged); + QVERIFY(!m_backend->isFetching() || spy.wait()); + + const auto resources = getAllResources(m_backend); + for (AbstractResource *res : resources) { + QVERIFY(!res->name().isEmpty()); + QVERIFY(!res->categories().isEmpty()); + QVERIFY(!res->origin().isEmpty()); + QVERIFY(!res->icon().isNull()); + // QVERIFY(!res->comment().isEmpty()); + // QVERIFY(!res->longDescription().isEmpty()); + // QVERIFY(!res->license().isEmpty()); + QVERIFY(res->homepage().isValid() && !res->homepage().isEmpty()); + QVERIFY(res->state() > AbstractResource::Broken); + QVERIFY(res->addonsInformation().isEmpty()); + + QSignalSpy spy(res, &AbstractResource::screenshotsFetched); + res->fetchScreenshots(); + QVERIFY(spy.count() || spy.wait()); + + QSignalSpy spy1(res, &AbstractResource::changelogFetched); + res->fetchChangelog(); + QVERIFY(spy1.count() || spy1.wait()); + } +} + +void KNSBackendTest::testReviews() +{ + const QVector resources = getAllResources(m_backend); + AbstractReviewsBackend *rev = m_backend->reviewsBackend(); + QVERIFY(!rev->hasCredentials()); + for (AbstractResource *res : resources) { + Rating *r = rev->ratingForApplication(res); + QVERIFY(r); + QCOMPARE(r->packageName(), res->packageName()); + QVERIFY(r->rating() > 0 && r->rating() <= 10); + } + + auto res = resources.first(); + QSignalSpy spy(rev, &AbstractReviewsBackend::reviewsReady); + rev->fetchReviews(res); + QVERIFY(spy.count() || spy.wait()); +} + +void KNSBackendTest::reviewsArrived(AbstractResource *r, const QVector &revs) +{ + m_r = r; + m_revs = revs; +} + +void KNSBackendTest::testResourceByUrl() +{ + AbstractResourcesBackend::Filters f; + f.resourceUrl = QUrl(QLatin1String("kns://") + m_backend->name() + QLatin1String("/api.kde-look.org/1136471")); + const QVector resources = getResources(m_backend->search(f)); + const QVector res = kTransform>(resources, [](AbstractResource *res) { + return res->url(); + }); + QCOMPARE(res.count(), 1); + QCOMPARE(f.resourceUrl, res.constFirst()); + + auto resource = resources.constFirst(); + QVERIFY(!resource->isInstalled()); // Make sure .qttest is clean before running the test + + QSignalSpy spy(resource, &AbstractResource::stateChanged); + auto b = resource->backend(); + b->installApplication(resource); + QVERIFY(spy.wait()); + b->removeApplication(resource); + QVERIFY(spy.wait()); + QCOMPARE(spy.count(), 2); + QVERIFY(!resource->isInstalled()); +} + +void KNSBackendTest::testResourceByUrlResourcesModel() +{ + AbstractResourcesBackend::Filters filter; + filter.resourceUrl = QUrl(QStringLiteral("kns://plasmoids.knsrc/store.kde.org/1169537")); // Wrong domain + + auto resources = getResources(ResourcesModel::global()->search(filter), true); + const QVector res = kTransform>(resources, [](AbstractResource *res) { + return res->url(); + }); + QCOMPARE(res.count(), 0); +} + +#include "moc_KNSBackendTest.cpp" diff --git a/libdiscover/backends/KNSBackend/tests/KNSBackendTest.h b/libdiscover/backends/KNSBackend/tests/KNSBackendTest.h new file mode 100644 index 0000000..ad02ebd --- /dev/null +++ b/libdiscover/backends/KNSBackend/tests/KNSBackendTest.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "ReviewsBackend/ReviewsModel.h" +#include +#include + +class AbstractResourcesBackend; +class AbstractResource; +class ResultsStream; + +class KNSBackendTest : public QObject +{ + Q_OBJECT +public: + explicit KNSBackendTest(QObject *parent = nullptr); + +private Q_SLOTS: + void testRetrieval(); + void testReviews(); + void testResourceByUrl(); + void testResourceByUrlResourcesModel(); + +public Q_SLOTS: + void reviewsArrived(AbstractResource *r, const QVector &revs); + +private: + QVector getResources(ResultsStream *stream, bool canBeEmpty = false); + QVector getAllResources(AbstractResourcesBackend *backend); + QPointer m_backend; + QPointer m_r; + QVector m_revs; +}; diff --git a/libdiscover/backends/KNSBackend/tests/testplasmoids.knsrc b/libdiscover/backends/KNSBackend/tests/testplasmoids.knsrc new file mode 100644 index 0000000..e648ae6 --- /dev/null +++ b/libdiscover/backends/KNSBackend/tests/testplasmoids.knsrc @@ -0,0 +1,6 @@ +[KNewStuff3] +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=Plasma 5 Plasmoid +StandardResource=tmp +InstallationCommand=plasmapkg2 -i %f +UninstallCommand=plasmapkg2 -r %f diff --git a/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp b/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp new file mode 100644 index 0000000..2ea8362 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp @@ -0,0 +1,307 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AppPackageKitResource.h" +#include "utils.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +AppPackageKitResource::AppPackageKitResource(const AppStream::Component &data, const QString &packageName, PackageKitBackend *parent) + : PackageKitResource(packageName, QString(), parent) + , m_appdata(data) +{ + Q_ASSERT(data.isValid()); +} + +QString AppPackageKitResource::name() const +{ + if (m_name.isEmpty()) { + if (!m_appdata.extends().isEmpty()) { + const auto components = backend()->componentsById(m_appdata.extends().constFirst()); + + if (components.isEmpty()) + qWarning() << "couldn't find" << m_appdata.extends() << "which is supposedly extended by" << m_appdata.id(); + else + m_name = components.constFirst().name() + QLatin1String(" - ") + m_appdata.name(); + } + + if (m_name.isEmpty()) + m_name = m_appdata.name(); + } + return m_name; +} + +QString AppPackageKitResource::longDescription() +{ + const auto desc = m_appdata.description(); + if (!desc.isEmpty()) + return desc; + + return PackageKitResource::longDescription(); +} + +static QIcon componentIcon(const AppStream::Component &comp) +{ + QIcon ret; + const auto icons = comp.icons(); + for (const AppStream::Icon &icon : icons) { + QStringList stock; + switch (icon.kind()) { + case AppStream::Icon::KindLocal: + ret.addFile(icon.url().toLocalFile(), icon.size()); + break; + case AppStream::Icon::KindCached: + ret.addFile(icon.url().toLocalFile(), icon.size()); + break; + case AppStream::Icon::KindStock: { + const auto ret = QIcon::fromTheme(icon.name()); + if (!ret.isNull()) + return ret; + break; + } + default: + break; + } + } + if (ret.isNull()) { + ret = QIcon::fromTheme(QStringLiteral("package-x-generic")); + } + return ret; +} + +QVariant AppPackageKitResource::icon() const +{ + return componentIcon(m_appdata); +} + +QJsonArray AppPackageKitResource::licenses() +{ + return m_appdata.projectLicense().isEmpty() ? PackageKitResource::licenses() : AppStreamUtils::licenses(m_appdata); +} + +QStringList AppPackageKitResource::mimetypes() const +{ + return m_appdata.provided(AppStream::Provided::KindMimetype).items(); +} + +static constexpr auto s_addonKinds = {AppStream::Component::KindAddon, AppStream::Component::KindCodec}; + +QStringList AppPackageKitResource::categories() +{ + auto cats = m_appdata.categories(); + if (!kContainsValue(s_addonKinds, m_appdata.kind())) + cats.append(QStringLiteral("Application")); + return cats; +} + +QString AppPackageKitResource::comment() +{ + const auto summary = m_appdata.summary(); + if (!summary.isEmpty()) + return summary; + + return PackageKitResource::comment(); +} + +QString AppPackageKitResource::appstreamId() const +{ + return m_appdata.id(); +} + +QSet AppPackageKitResource::alternativeAppstreamIds() const +{ + const AppStream::Provided::Kind AppStream_Provided_KindId = (AppStream::Provided::Kind)12; // Should be AppStream::Provided::KindId when released + const auto ret = m_appdata.provided(AppStream_Provided_KindId).items(); + return QSet(ret.begin(), ret.end()); +} + +QUrl AppPackageKitResource::url() const +{ + QUrl ret(QStringLiteral("appstream://") + appstreamId()); + const AppStream::Provided::Kind AppStream_Provided_KindId = (AppStream::Provided::Kind)12; // Should be AppStream::Provided::KindId when released + auto provided = m_appdata.provided(AppStream_Provided_KindId).items(); + provided.removeAll(appstreamId()); // Just in case, it has happened before + if (!provided.isEmpty()) { + QUrlQuery qq; + qq.addQueryItem("alt", provided.join(QLatin1Char(','))); + ret.setQuery(qq); + } + return ret; +} + +QUrl AppPackageKitResource::homepage() +{ + return m_appdata.url(AppStream::Component::UrlKindHomepage); +} + +QUrl AppPackageKitResource::helpURL() +{ + return m_appdata.url(AppStream::Component::UrlKindHelp); +} + +QUrl AppPackageKitResource::bugURL() +{ + return m_appdata.url(AppStream::Component::UrlKindBugtracker); +} + +QUrl AppPackageKitResource::donationURL() +{ + return m_appdata.url(AppStream::Component::UrlKindDonation); +} + +QUrl AppPackageKitResource::contributeURL() +{ + return m_appdata.url(AppStream::Component::UrlKindContribute); +} + +AbstractResource::Type AppPackageKitResource::type() const +{ + static QString desktop = QString::fromUtf8(qgetenv("XDG_CURRENT_DESKTOP")); + const auto desktops = m_appdata.compulsoryForDesktops(); + return kContainsValue(s_addonKinds, m_appdata.kind()) ? Addon : (desktops.isEmpty() || !desktops.contains(desktop)) ? Application : Technical; +} + +void AppPackageKitResource::fetchScreenshots() +{ + Q_EMIT screenshotsFetched(AppStreamUtils::fetchScreenshots(m_appdata)); +} + +QStringList AppPackageKitResource::allPackageNames() const +{ + auto ret = m_appdata.packageNames(); + if (ret.isEmpty()) { + ret = QStringList{PackageKit::Daemon::packageName(availablePackageId())}; + } + return ret; +} + +QList AppPackageKitResource::addonsInformation() +{ + const auto res = kFilter>(backend()->extendedBy(m_appdata.id()), [this](AppPackageKitResource *r) { + return r->allPackageNames() != allPackageNames(); + }); + return kTransform>(res, [](AppPackageKitResource *r) { + return PackageState(r->packageName(), r->name(), r->comment(), r->isInstalled()); + }); +} + +QStringList AppPackageKitResource::extends() const +{ + return m_appdata.extends(); +} + +QString AppPackageKitResource::changelog() const +{ + return PackageKitResource::changelog() + QLatin1String("
    ") + AppStreamUtils::changelogToHtml(m_appdata); +} + +bool AppPackageKitResource::canExecute() const +{ + return !m_appdata.launchable(AppStream::Launchable::KindDesktopId).entries().isEmpty(); +} + +void AppPackageKitResource::invokeApplication() const +{ + const QString launchable = m_appdata.launchable(AppStream::Launchable::KindDesktopId).entries().constFirst(); + + KService::Ptr service = KService::serviceByStorageId(launchable); + + if (!service) { + Q_EMIT backend()->passiveMessage(i18n("Cannot launch %1", name())); + return; + } + runService(service); +} + +QString AppPackageKitResource::versionString() +{ + const QString version = isInstalled() ? installedVersion() : availableVersion(); + return AppStreamUtils::versionString(version, m_appdata); +} + +QDate AppPackageKitResource::releaseDate() const +{ +#if ASQ_CHECK_VERSION(1, 0, 0) + if (const auto optional = m_appdata.releasesPlain().indexSafe(0); optional.has_value()) { + auto release = optional.value(); +#else + if (!m_appdata.releases().isEmpty()) { + const auto release = m_appdata.releases().constFirst(); +#endif + return release.timestamp().date(); + } + + return {}; +} + +QString AppPackageKitResource::author() const +{ +#if ASQ_CHECK_VERSION(1, 0, 0) + QString name = m_appdata.developer().name(); +#else + QString name = m_appdata.developerName(); +#endif + + if (name.isEmpty()) { + name = m_appdata.projectGroup(); + } + + return name; +} + +void AppPackageKitResource::fetchChangelog() +{ + Q_EMIT changelogFetched(changelog()); +} + +bool AppPackageKitResource::isCritical() const +{ + return m_appdata.isCompulsoryForDesktop(qEnvironmentVariable("XDG_CURRENT_DESKTOP")); +} + +QString AppPackageKitResource::contentRatingDescription() const +{ + return AppStreamUtils::contentRatingDescription(m_appdata); +} + +QString AppPackageKitResource::contentRatingText() const +{ + return AppStreamUtils::contentRatingText(m_appdata); +} + +AbstractResource::ContentIntensity AppPackageKitResource::contentRatingIntensity() const +{ + return AppStreamUtils::contentRatingIntensity(m_appdata); +} + +uint AppPackageKitResource::contentRatingMinimumAge() const +{ + return AppStreamUtils::contentRatingMinimumAge(m_appdata); +} diff --git a/libdiscover/backends/PackageKitBackend/AppPackageKitResource.h b/libdiscover/backends/PackageKitBackend/AppPackageKitResource.h new file mode 100644 index 0000000..d5bb808 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/AppPackageKitResource.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "PackageKitBackend.h" +#include "PackageKitResource.h" + +class AppPackageKitResource : public PackageKitResource +{ + Q_OBJECT +public: + explicit AppPackageKitResource(const AppStream::Component &data, const QString &packageName, PackageKitBackend *parent); + + QString appstreamId() const override; + + AbstractResource::Type type() const override; + QString name() const override; + QVariant icon() const override; + QStringList mimetypes() const override; + QStringList categories() override; + QString longDescription() override; + QUrl url() const override; + QUrl homepage() override; + QUrl helpURL() override; + QUrl bugURL() override; + QUrl donationURL() override; + QUrl contributeURL() override; + QString comment() override; + QJsonArray licenses() override; + QStringList allPackageNames() const override; + QList addonsInformation() override; + QStringList extends() const override; + void fetchScreenshots() override; + void invokeApplication() const override; + bool canExecute() const override; + QDate releaseDate() const override; + QString changelog() const override; + QString author() const override; + QString versionString() override; + void fetchChangelog() override; + QSet alternativeAppstreamIds() const override; + bool isCritical() const override; + + QString contentRatingText() const override; + QString contentRatingDescription() const override; + ContentIntensity contentRatingIntensity() const override; + uint contentRatingMinimumAge() const override; + +private: + const AppStream::Component m_appdata; + mutable QString m_name; +}; diff --git a/libdiscover/backends/PackageKitBackend/CMakeLists.txt b/libdiscover/backends/PackageKitBackend/CMakeLists.txt new file mode 100644 index 0000000..415407f --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/CMakeLists.txt @@ -0,0 +1,43 @@ +find_package(KF5 REQUIRED Notifications) + +#packagekit-backend +set (packagekit-backend_SRCS + PackageKitBackend.cpp + PackageKitResource.cpp + AppPackageKitResource.cpp + PKTransaction.cpp + PackageKitUpdater.cpp + PackageKitMessages.cpp + PackageKitSourcesBackend.cpp + LocalFilePKResource.cpp + PKResolveTransaction.cpp + pkui.qrc + ) +ecm_qt_declare_logging_category(packagekit-backend_SRCS HEADER libdiscover_backend_debug.h IDENTIFIER LIBDISCOVER_BACKEND_LOG CATEGORY_NAME org.kde.plasma.libdiscover.backend DESCRIPTION "libdiscover backend" EXPORT DISCOVER) + +add_library(packagekit-backend MODULE ${packagekit-backend_SRCS}) + +target_link_libraries(packagekit-backend PRIVATE Discover::Common Qt::Core Qt::Concurrent PK::packagekitqt5 KF5::ConfigGui KF5::KIOCore KF5::KIOGui KF5::Archive AppStreamQt) +install(TARGETS packagekit-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) + +if(TARGET PkgConfig::Markdown) + target_compile_definitions(packagekit-backend PRIVATE -DWITH_MARKDOWN) + target_link_libraries(packagekit-backend PRIVATE PkgConfig::Markdown) +endif() + +#notifier +set (DiscoverPackageKitNotifier_SRCS + PackageKitNotifier.cpp +) +ecm_qt_declare_logging_category(DiscoverPackageKitNotifier_SRCS HEADER libdiscover_backend_debug.h IDENTIFIER LIBDISCOVER_BACKEND_LOG CATEGORY_NAME org.kde.plasma.libdiscover.backend) + +add_library(DiscoverPackageKitNotifier MODULE ${DiscoverPackageKitNotifier_SRCS}) + +target_link_libraries(DiscoverPackageKitNotifier PRIVATE PK::packagekitqt5 Discover::Notifiers KF5::I18n KF5::Notifications KF5::ConfigCore) +set_target_properties(DiscoverPackageKitNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) + +install(TARGETS DiscoverPackageKitNotifier DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover-notifier) + +install(FILES packagekit-backend-categories.xml DESTINATION ${KDE_INSTALL_DATADIR}/libdiscover/categories) + +install( FILES org.kde.discover.packagekit.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) diff --git a/libdiscover/backends/PackageKitBackend/LocalFilePKResource.cpp b/libdiscover/backends/PackageKitBackend/LocalFilePKResource.cpp new file mode 100644 index 0000000..da7a99f --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/LocalFilePKResource.cpp @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "LocalFilePKResource.h" +#include "libdiscover_backend_debug.h" +#include +#include +#include +#include +#include +#include + +LocalFilePKResource::LocalFilePKResource(QUrl path, PackageKitBackend *parent) + : PackageKitResource(path.toString(), path.toString(), parent) + , m_path(std::move(path)) +{ +} + +quint64 LocalFilePKResource::size() +{ + const QFileInfo info(m_path.toLocalFile()); + return info.size(); +} + +QString LocalFilePKResource::name() const +{ + const QFileInfo info(m_path.toLocalFile()); + return info.baseName(); +} + +QString LocalFilePKResource::comment() +{ + return m_path.toLocalFile(); +} + +QString LocalFilePKResource::origin() const +{ + return m_path.toLocalFile(); +} + +void LocalFilePKResource::fetchDetails() +{ + if (!m_details.isEmpty()) + return; + m_details.insert(QStringLiteral("fetching"), true); // we add an entry so it's not re-fetched. + + if (PackageKit::Daemon::roles() & PackageKit::Transaction::RoleGetDetailsLocal) { + PackageKit::Transaction *transaction = PackageKit::Daemon::getDetailsLocal(m_path.toLocalFile()); + connect(transaction, &PackageKit::Transaction::details, this, &LocalFilePKResource::setDetails); + connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitResource::failedFetchingDetails); + } + + if (PackageKit::Daemon::roles() & PackageKit::Transaction::RoleGetFilesLocal) { + PackageKit::Transaction *transaction2 = PackageKit::Daemon::getFilesLocal(m_path.toLocalFile()); + connect(transaction2, &PackageKit::Transaction::errorCode, this, &PackageKitResource::failedFetchingDetails); + connect(transaction2, &PackageKit::Transaction::files, this, [this](const QString & /*pkgid*/, const QStringList &files) { + const auto isFileInstalled = [](const QString &file) { + return QFileInfo::exists(QLatin1Char('/') + file); + }; + const bool allFilesInstalled = std::all_of(files.constBegin(), files.constEnd(), isFileInstalled); + + // PackageKit can't tell us if a package coming from the a file is installed or not, + // so we inspect the files it wants to install and check if they're available on our running system + setState(allFilesInstalled ? Installed : None); + const auto execIdx = kIndexOf(files, [](const QString &file) { + return file.endsWith(QLatin1String(".desktop")) && file.contains(QLatin1String("usr/share/applications")); + }); + if (execIdx >= 0) { + m_exec = files[execIdx]; + + // sometimes aptcc provides paths like usr/share/applications/steam.desktop + if (!m_exec.startsWith(QLatin1Char('/'))) { + m_exec.prepend(QLatin1Char('/')); + } + } else { + qWarning() << "could not find an executable desktop file for" << m_path << "among" << files; + } + }); + } else { + // If we don't get to know the installed files, assume it's not so at least it can be installed + setState(None); + } +} + +void LocalFilePKResource::setDetails(const PackageKit::Details &details) +{ + addPackageId(PackageKit::Transaction::InfoAvailable, details.packageId(), true); + PackageKitResource::setDetails(details); +} + +void LocalFilePKResource::invokeApplication() const +{ + KService::Ptr service = KService::Ptr(new KService(m_exec)); + + if (!service) { + return; + } + + runService(service); +} + +void LocalFilePKResource::setState(AbstractResource::State state) +{ + if (m_state == state) + return; + + m_state = state; + Q_EMIT stateChanged(); +} diff --git a/libdiscover/backends/PackageKitBackend/LocalFilePKResource.h b/libdiscover/backends/PackageKitBackend/LocalFilePKResource.h new file mode 100644 index 0000000..549bf97 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/LocalFilePKResource.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "PackageKitResource.h" + +class LocalFilePKResource : public PackageKitResource +{ + Q_OBJECT +public: + LocalFilePKResource(QUrl path, PackageKitBackend *parent); + + QString name() const override; + QString comment() override; + + AbstractResource::State state() override + { + return m_state; + } + quint64 size() override; + QString origin() const override; + void fetchDetails() override; + bool canExecute() const override + { + return !m_exec.isEmpty(); + } + void invokeApplication() const override; + QString displayOrigin() const override + { + return origin(); + } + + void setDetails(const PackageKit::Details &details); + void setState(AbstractResource::State state); + +private: + AbstractResource::State m_state = AbstractResource::Broken; + QUrl m_path; + QString m_exec; +}; diff --git a/libdiscover/backends/PackageKitBackend/PKResolveTransaction.cpp b/libdiscover/backends/PackageKitBackend/PKResolveTransaction.cpp new file mode 100644 index 0000000..3ed1f61 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PKResolveTransaction.cpp @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PKResolveTransaction.h" +#include "PackageKitBackend.h" +#include + +#include + +PKResolveTransaction::PKResolveTransaction(PackageKitBackend *backend) + : m_backend(backend) +{ + m_floodTimer.setInterval(1000); + m_floodTimer.setSingleShot(true); + connect(&m_floodTimer, &QTimer::timeout, this, &PKResolveTransaction::start); +} + +void PKResolveTransaction::start() +{ + Q_EMIT started(); + + PackageKit::Transaction *tArch = PackageKit::Daemon::resolve(m_packageNames, PackageKit::Transaction::FilterArch); + connect(tArch, &PackageKit::Transaction::package, m_backend, &PackageKitBackend::addPackageArch); + connect(tArch, &PackageKit::Transaction::errorCode, m_backend, &PackageKitBackend::transactionError); + + PackageKit::Transaction *tNotArch = PackageKit::Daemon::resolve(m_packageNames, PackageKit::Transaction::FilterNotArch); + connect(tNotArch, &PackageKit::Transaction::package, m_backend, &PackageKitBackend::addPackageNotArch); + connect(tNotArch, &PackageKit::Transaction::errorCode, m_backend, &PackageKitBackend::transactionError); + + m_transactions = {tArch, tNotArch}; + + for (PackageKit::Transaction *t : qAsConst(m_transactions)) { + connect(t, &PackageKit::Transaction::finished, this, &PKResolveTransaction::transactionFinished); + } +} + +void PKResolveTransaction::transactionFinished(PackageKit::Transaction::Exit exit) +{ + PackageKit::Transaction *t = qobject_cast(sender()); + if (exit != PackageKit::Transaction::ExitSuccess) { + qWarning() << "failed" << exit << t; + } + + m_transactions.removeAll(t); + if (m_transactions.isEmpty()) { + Q_EMIT allFinished(); + deleteLater(); + } +} + +void PKResolveTransaction::addPackageNames(const QStringList &packageNames) +{ + m_packageNames += packageNames; + m_packageNames.removeDuplicates(); + m_floodTimer.start(); +} diff --git a/libdiscover/backends/PackageKitBackend/PKResolveTransaction.h b/libdiscover/backends/PackageKitBackend/PKResolveTransaction.h new file mode 100644 index 0000000..1677cb4 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PKResolveTransaction.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +class PackageKitBackend; + +class PKResolveTransaction : public QObject +{ + Q_OBJECT +public: + PKResolveTransaction(PackageKitBackend *backend); + + void start(); + void addPackageNames(const QStringList &packageNames); + +Q_SIGNALS: + void allFinished(); + void started(); + +private: + void transactionFinished(PackageKit::Transaction::Exit exit); + + QTimer m_floodTimer; + QStringList m_packageNames; + QVector m_transactions; + PackageKitBackend *const m_backend; +}; diff --git a/libdiscover/backends/PackageKitBackend/PKTransaction.cpp b/libdiscover/backends/PackageKitBackend/PKTransaction.cpp new file mode 100644 index 0000000..11ee02e --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PKTransaction.cpp @@ -0,0 +1,339 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PKTransaction.h" +#include "LocalFilePKResource.h" +#include "PackageKitBackend.h" +#include "PackageKitMessages.h" +#include "PackageKitResource.h" +#include "PackageKitUpdater.h" +#include "libdiscover_backend_debug.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include + +PKTransaction::PKTransaction(const QVector &apps, Transaction::Role role) + : Transaction(apps.first(), apps.first(), role) + , m_apps(apps) +{ + Q_ASSERT(!apps.contains(nullptr)); + for (auto r : apps) { + PackageKitResource *res = qobject_cast(r); + m_pkgnames.unite(kToSet(res->allPackageNames())); + } + + QTimer::singleShot(0, this, &PKTransaction::start); +} + +static QStringList packageIds(const QVector &res, std::function func) +{ + QStringList ret; + for (auto r : res) { + ret += func(qobject_cast(r)); + } + ret.removeDuplicates(); + return ret; +} + +void PKTransaction::start() +{ + trigger(PackageKit::Transaction::TransactionFlagSimulate); +} + +void PKTransaction::trigger(PackageKit::Transaction::TransactionFlags flags) +{ + if (m_trans) + m_trans->deleteLater(); + m_newPackageStates.clear(); + + if (m_apps.size() == 1 && qobject_cast(m_apps.at(0))) { + auto app = qobject_cast(m_apps.at(0)); + m_trans = PackageKit::Daemon::installFile(QUrl(app->packageName()).toLocalFile(), flags); + connect(m_trans.data(), &PackageKit::Transaction::finished, this, [this, app](PackageKit::Transaction::Exit status) { + const bool simulate = m_trans->transactionFlags() & PackageKit::Transaction::TransactionFlagSimulate; + if (!simulate && status == PackageKit::Transaction::ExitSuccess) { + app->setState(AbstractResource::Installed); + } + }); + } else + switch (role()) { + case Transaction::ChangeAddonsRole: + case Transaction::InstallRole: { + const QStringList ids = packageIds(m_apps, [](PackageKitResource *r) { + return r->availablePackageId(); + }); + if (ids.isEmpty()) { + // FIXME this state shouldn't exist + qWarning() << "Installing no packages found!"; + for (auto app : m_apps) { + qCDebug(LIBDISCOVER_BACKEND_LOG) << "app" << app << app->state(); + } + + setStatus(Transaction::DoneWithErrorStatus); + return; + } + m_trans = PackageKit::Daemon::installPackages(ids, flags); + } break; + case Transaction::RemoveRole: + // see bug #315063 + m_trans = PackageKit::Daemon::removePackages(packageIds(m_apps, + [](PackageKitResource *r) { + return r->installedPackageId(); + }), + true /*allowDeps*/, + false, + flags); + break; + }; + Q_ASSERT(m_trans); + + // connect(m_trans.data(), &PackageKit::Transaction::statusChanged, this, [this]() { qCDebug(LIBDISCOVER_BACKEND_LOG) << "state..." << + // m_trans->status(); }); + connect(m_trans.data(), &PackageKit::Transaction::package, this, &PKTransaction::packageResolved); + connect(m_trans.data(), &PackageKit::Transaction::finished, this, &PKTransaction::cleanup); + connect(m_trans.data(), &PackageKit::Transaction::errorCode, this, &PKTransaction::errorFound); + connect(m_trans.data(), &PackageKit::Transaction::mediaChangeRequired, this, &PKTransaction::mediaChange); + connect(m_trans.data(), &PackageKit::Transaction::requireRestart, this, &PKTransaction::requireRestart); + connect(m_trans.data(), &PackageKit::Transaction::repoSignatureRequired, this, &PKTransaction::repoSignatureRequired); + connect(m_trans.data(), &PackageKit::Transaction::percentageChanged, this, &PKTransaction::progressChanged); + connect(m_trans.data(), &PackageKit::Transaction::statusChanged, this, &PKTransaction::statusChanged); + connect(m_trans.data(), &PackageKit::Transaction::eulaRequired, this, &PKTransaction::eulaRequired); + connect(m_trans.data(), &PackageKit::Transaction::allowCancelChanged, this, &PKTransaction::cancellableChanged); + connect(m_trans.data(), &PackageKit::Transaction::remainingTimeChanged, this, [this]() { + setRemainingTime(m_trans->remainingTime()); + }); + connect(m_trans.data(), &PackageKit::Transaction::speedChanged, this, [this]() { + setDownloadSpeed(m_trans->speed()); + }); + + setCancellable(m_trans->allowCancel()); +} + +void PKTransaction::statusChanged() +{ + setStatus(m_trans->status() == PackageKit::Transaction::StatusDownload ? Transaction::DownloadingStatus : Transaction::CommittingStatus); + progressChanged(); +} + +void PKTransaction::progressChanged() +{ + auto percent = m_trans->percentage(); + if (percent == 101) { + qWarning() << "percentage cannot be calculated"; + percent = 50; + } + + const auto processedPercentage = percentageWithStatus(m_trans->status(), qBound(0, percent, 100)); + if (processedPercentage >= 0) + setProgress(processedPercentage); +} + +void PKTransaction::cancellableChanged() +{ + setCancellable(m_trans->allowCancel()); +} + +void PKTransaction::cancel() +{ + if (!m_trans) { + setStatus(CancelledStatus); + } else if (m_trans->allowCancel()) { + m_trans->cancel(); + } else { + qWarning() << "trying to cancel a non-cancellable transaction: " << resource()->name(); + } +} + +void PKTransaction::cleanup(PackageKit::Transaction::Exit exit, uint runtime) +{ + Q_UNUSED(runtime) + const bool cancel = !m_proceedFunctions.isEmpty() || exit == PackageKit::Transaction::ExitCancelled; + const bool failed = exit == PackageKit::Transaction::ExitFailed || exit == PackageKit::Transaction::ExitUnknown; + const bool simulate = m_trans->transactionFlags() & PackageKit::Transaction::TransactionFlagSimulate; + + disconnect(m_trans, nullptr, this, nullptr); + m_trans = nullptr; + + const auto backend = qobject_cast(resource()->backend()); + + if (!cancel && !failed && simulate) { + auto packagesToRemove = m_newPackageStates.value(PackageKit::Transaction::InfoRemoving); + QMutableListIterator i(packagesToRemove); + QSet removedResources; + while (i.hasNext()) { + const auto pkgname = PackageKit::Daemon::packageName(i.next()); + removedResources.unite(backend->resourcesByPackageName(pkgname)); + + if (m_pkgnames.contains(pkgname)) { + i.remove(); + } + } + removedResources.subtract(kToSet(m_apps)); + + auto isCritical = [](AbstractResource *r) { + return static_cast(r)->isCritical(); + }; + auto criticals = kFilter>(removedResources, isCritical); + criticals.unite(kFilter>(m_apps, isCritical)); + auto resourceName = [](AbstractResource *a) { + return a->name(); + }; + if (!criticals.isEmpty()) { + const QString msg = i18n( + "This action cannot be completed as it would remove the following software which is critical to the system's operation:" + "
    • %1
    " + "If you believe this is an error, please report it as a bug to the packagers of your distribution.", + resourceName(*criticals.begin())); + Q_EMIT distroErrorMessage(msg); + setStatus(Transaction::DoneWithErrorStatus); + } else if (!packagesToRemove.isEmpty() || !removedResources.isEmpty()) { + QString msg; + const QStringList removedResourcesStr = kTransform(removedResources, resourceName); + msg += QLatin1String("
    • ") + PackageKitResource::joinPackages(packagesToRemove, QLatin1String("
    • "), {}) + QLatin1Char('\n'); + msg += removedResourcesStr.join(QLatin1String("
    • ")); + msg += QStringLiteral("
    "); + + Q_EMIT proceedRequest(i18n("Confirm package removal"), + i18np("This action will also remove the following package:\n%2", + "This action will also remove the following packages:\n%2", + packagesToRemove.count(), + msg)); + } else { + proceed(); + } + return; + } + + this->submitResolve(); + if (failed) + setStatus(Transaction::DoneWithErrorStatus); + else if (cancel) + setStatus(Transaction::CancelledStatus); + else + setStatus(Transaction::DoneStatus); +} + +void PKTransaction::processProceedFunction() +{ + auto t = m_proceedFunctions.takeFirst()(); + connect(t, &PackageKit::Transaction::finished, this, [this](PackageKit::Transaction::Exit status) { + if (status != PackageKit::Transaction::Exit::ExitSuccess) { + qWarning() << "transaction failed" << sender() << status; + cancel(); + return; + } + + if (!m_proceedFunctions.isEmpty()) { + processProceedFunction(); + } else { + start(); + } + }); +} + +void PKTransaction::proceed() +{ + if (!m_proceedFunctions.isEmpty()) { + processProceedFunction(); + } else { + if (m_apps.size() == 1 && qobject_cast(m_apps.at(0))) { + trigger(PackageKit::Transaction::TransactionFlagNone); + } else { + trigger(PackageKit::Transaction::TransactionFlagOnlyTrusted); + } + } +} + +void PKTransaction::packageResolved(PackageKit::Transaction::Info info, const QString &packageId) +{ + m_newPackageStates[info].append(packageId); +} + +void PKTransaction::submitResolve() +{ + const auto backend = qobject_cast(resource()->backend()); + QStringList needResolving; + for (auto it = m_newPackageStates.constBegin(), itEnd = m_newPackageStates.constEnd(); it != itEnd; ++it) { + const auto &itValue = it.value(); + for (const auto &pkgid : itValue) { + const auto resources = backend->resourcesByPackageName(PackageKit::Daemon::packageName(pkgid)); + for (auto res : resources) { + auto r = qobject_cast(res); + r->clearPackageIds(); + Q_EMIT r->stateChanged(); + needResolving << r->allPackageNames(); + } + } + } + needResolving.removeDuplicates(); + backend->resolvePackages(needResolving); +} + +PackageKit::Transaction *PKTransaction::transaction() +{ + return m_trans; +} + +void PKTransaction::eulaRequired(const QString &eulaID, const QString &packageID, const QString &vendor, const QString &licenseAgreement) +{ + const auto handle = handleEula(eulaID, licenseAgreement); + m_proceedFunctions << handle.proceedFunction; + if (handle.request) { + Q_EMIT proceedRequest(i18n("Accept EULA"), + i18n("The package %1 and its vendor %2 require that you accept their license:\n %3", + PackageKit::Daemon::packageName(packageID), + vendor, + licenseAgreement)); + } else { + proceed(); + } +} + +void PKTransaction::errorFound(PackageKit::Transaction::Error err, const QString &error) +{ + if (err == PackageKit::Transaction::ErrorNoLicenseAgreement || err == PackageKit::Transaction::ErrorTransactionCancelled + || err == PackageKit::Transaction::ErrorNotAuthorized) { + return; + } + qWarning() << "PackageKit error:" << err << PackageKitMessages::errorMessage(err, error) << error; + Q_EMIT passiveMessage(PackageKitMessages::errorMessage(err, error)); +} + +void PKTransaction::mediaChange(PackageKit::Transaction::MediaType media, const QString &type, const QString &text) +{ + Q_UNUSED(media) + Q_EMIT passiveMessage(i18n("Media Change of type '%1' is requested.\n%2", type, text)); +} + +void PKTransaction::requireRestart(PackageKit::Transaction::Restart restart, const QString &pkgid) +{ + Q_EMIT passiveMessage(PackageKitMessages::restartMessage(restart, pkgid)); +} + +void PKTransaction::repoSignatureRequired(const QString &packageID, + const QString &repoName, + const QString &keyUrl, + const QString &keyUserid, + const QString &keyId, + const QString &keyFingerprint, + const QString &keyTimestamp, + PackageKit::Transaction::SigType type) +{ + Q_EMIT proceedRequest(i18n("Missing signature for %1 in %2", packageID, repoName), + i18n("Do you trust the following key?\n\nUrl: %1\nUser: %2\nKey: %3\nFingerprint: %4\nTimestamp: %4\n", + keyUrl, + keyUserid, + keyFingerprint, + keyTimestamp)); + + m_proceedFunctions << [type, keyId, packageID]() { + return PackageKit::Daemon::installSignature(type, keyId, packageID); + }; +} diff --git a/libdiscover/backends/PackageKitBackend/PKTransaction.h b/libdiscover/backends/PackageKitBackend/PKTransaction.h new file mode 100644 index 0000000..52abc8e --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PKTransaction.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +class PKTransaction : public Transaction +{ + Q_OBJECT +public: + explicit PKTransaction(const QVector &app, Transaction::Role role); + PackageKit::Transaction *transaction(); + + void cancel() override; + void proceed() override; + +public Q_SLOTS: + void start(); + +private: + void processProceedFunction(); + void statusChanged(); + + void cleanup(PackageKit::Transaction::Exit, uint); + void errorFound(PackageKit::Transaction::Error err, const QString &error); + void mediaChange(PackageKit::Transaction::MediaType media, const QString &type, const QString &text); + void requireRestart(PackageKit::Transaction::Restart restart, const QString &p); + void progressChanged(); + void eulaRequired(const QString &eulaID, const QString &packageID, const QString &vendor, const QString &licenseAgreement); + void cancellableChanged(); + void packageResolved(PackageKit::Transaction::Info info, const QString &packageId); + void submitResolve(); + void repoSignatureRequired(const QString &packageID, + const QString &repoName, + const QString &keyUrl, + const QString &keyUserid, + const QString &keyId, + const QString &keyFingerprint, + const QString &keyTimestamp, + PackageKit::Transaction::SigType type); + + void trigger(PackageKit::Transaction::TransactionFlags flags); + QPointer m_trans; + const QVector m_apps; + QSet m_pkgnames; + QVector> m_proceedFunctions; + + QMap m_newPackageStates; +}; diff --git a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp new file mode 100644 index 0000000..186cb30 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp @@ -0,0 +1,1049 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * SPDX-FileCopyrightText: 2023 Harald Sitter + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PackageKitBackend.h" +#include "AppPackageKitResource.h" +#include "LocalFilePKResource.h" +#include "PKResolveTransaction.h" +#include "PKTransaction.h" +#include "PackageKitSourcesBackend.h" +#include "PackageKitUpdater.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "config-paths.h" +#include "libdiscover_backend_debug.h" +#include "utils.h" +#include + +DISCOVER_BACKEND_PLUGIN(PackageKitBackend) + +bool operator==(const PackageOrAppId &a, const PackageOrAppId &b) +{ + return a.isPackageName == b.isPackageName && a.id == b.id; +} + +uint qHash(const PackageOrAppId &id, uint seed) +{ + return qHash(id.id, seed) ^ qHash(id.isPackageName, seed); +} + +PackageOrAppId makeAppId(const QString &id) +{ + return {id, false}; +} + +PackageOrAppId makePackageId(const QString &id) +{ + return {id, true}; +} + +template +static void setWhenAvailable(const QDBusPendingReply &pending, W func, QObject *parent) +{ + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, parent); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, parent, [func](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + func(reply.value()); + }); +} + +QString PackageKitBackend::locateService(const QString &filename) +{ + return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/") + filename); +} + +Delay::Delay() +{ + m_delay.setSingleShot(true); + m_delay.setInterval(100); + + connect(&m_delay, &QTimer::timeout, this, [this] { + Q_EMIT perform(m_pkgids); + m_pkgids.clear(); + }); +} + +PackageKitBackend::PackageKitBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , m_appdata(new AppStream::Pool) + , m_updater(new PackageKitUpdater(this)) + , m_refresher(nullptr) + , m_isFetching(0) + , m_reviews(AppStreamIntegration::global()->reviews()) +{ + QTimer *t = new QTimer(this); + connect(t, &QTimer::timeout, this, &PackageKitBackend::checkForUpdates); + t->setInterval(60 * 60 * 1000); + t->setSingleShot(false); + t->start(); + + connect(&m_details, &Delay::perform, this, &PackageKitBackend::performDetailsFetch); + connect(&m_details, &Delay::perform, this, [this](const QSet &pkgids) { + PackageKit::Transaction *t = PackageKit::Daemon::getUpdatesDetails(kSetToList(pkgids)); + connect(t, + &PackageKit::Transaction::updateDetail, + this, + [this](const QString &packageID, + const QStringList &updates, + const QStringList &obsoletes, + const QStringList &vendorUrls, + const QStringList &bugzillaUrls, + const QStringList &cveUrls, + PackageKit::Transaction::Restart restart, + const QString &updateText, + const QString &changelog, + PackageKit::Transaction::UpdateState state, + const QDateTime &issued, + const QDateTime &updated) { + const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(packageID)); + for (auto r : resources) { + PackageKitResource *resource = qobject_cast(r); + if (resource->containsPackageId(packageID)) { + resource->updateDetail(packageID, + updates, + obsoletes, + vendorUrls, + bugzillaUrls, + cveUrls, + restart, + updateText, + changelog, + state, + issued, + updated); + } + } + }); + connect(t, &PackageKit::Transaction::errorCode, this, [this, pkgids](PackageKit::Transaction::Error err, const QString &error) { + qWarning() << "error fetching updates:" << err << error; + for (const QString &pkgid : pkgids) { + const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(pkgid)); + for (auto r : resources) { + PackageKitResource *resource = qobject_cast(r); + if (resource->containsPackageId(pkgid)) { + Q_EMIT resource->changelogFetched(QString()); + } + } + } + }); + }); + + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::restartScheduled, m_updater, &PackageKitUpdater::enableNeedsReboot); + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitBackend::checkDaemonRunning); + connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { + m_reviews->emitRatingFetched(this, kTransform>(m_packages.packages, [](AbstractResource *r) { + return r; + })); + }); + + auto proxyWatch = new QFileSystemWatcher(this); + proxyWatch->addPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/kioslaverc")); + connect(proxyWatch, &QFileSystemWatcher::fileChanged, this, [this]() { + KProtocolManager::reparseConfiguration(); + updateProxy(); + }); + + SourcesModel::global()->addSourcesBackend(new PackageKitSourcesBackend(this)); + + reloadPackageList(); + + acquireFetching(true); + setWhenAvailable( + PackageKit::Daemon::getTimeSinceAction(PackageKit::Transaction::RoleRefreshCache), + [this](uint timeSince) { + if (timeSince > 3600) + checkForUpdates(); + else + fetchUpdates(); + acquireFetching(false); + }, + this); + + PackageKit::Daemon::global()->setHints(QStringList() << QStringLiteral("interactive=true") + << QStringLiteral("locale=%1").arg(qEnvironmentVariable("LANG"))); +} + +PackageKitBackend::~PackageKitBackend() +{ + m_threadPool.waitForDone(200); + m_threadPool.clear(); +} + +void PackageKitBackend::updateProxy() +{ + if (PackageKit::Daemon::isRunning()) { + static bool everHad = KProtocolManager::useProxy(); + if (!everHad && !KProtocolManager::useProxy()) + return; + + everHad = KProtocolManager::useProxy(); + PackageKit::Daemon::global()->setProxy(KProtocolManager::proxyFor(QStringLiteral("http")), + KProtocolManager::proxyFor(QStringLiteral("https")), + KProtocolManager::proxyFor(QStringLiteral("ftp")), + KProtocolManager::proxyFor(QStringLiteral("socks")), + {}, + {}); + } +} + +bool PackageKitBackend::isFetching() const +{ + return m_isFetching; +} + +void PackageKitBackend::acquireFetching(bool f) +{ + if (f) + m_isFetching++; + else + m_isFetching--; + + if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) { + Q_EMIT fetchingChanged(); + if (m_isFetching == 0) + Q_EMIT available(); + } + Q_ASSERT(m_isFetching >= 0); +} + +struct DelayedAppStreamLoad { + QVector components; + QHash missingComponents; + bool correct = true; +}; + +static DelayedAppStreamLoad loadAppStream(AppStream::Pool *appdata) +{ + DelayedAppStreamLoad ret; + + ret.correct = appdata->load(); + if (!ret.correct) { + qWarning() << "Could not open the AppStream metadata pool" << appdata->lastError(); + } + + const auto components = appdata->components(); + ret.components.reserve(components.size()); + for (const AppStream::Component &component : components) { + if (component.kind() == AppStream::Component::KindFirmware) + continue; + + const auto pkgNames = component.packageNames(); + if (pkgNames.isEmpty()) { + const auto entries = component.launchable(AppStream::Launchable::KindDesktopId).entries(); + if (component.kind() == AppStream::Component::KindDesktopApp && !entries.isEmpty()) { + const QString file = PackageKitBackend::locateService(entries.first()); + if (!file.isEmpty()) { + ret.missingComponents[file] = component; + } + } + } else { + ret.components << component; + } + } + return ret; +} + +void PackageKitBackend::reloadPackageList() +{ + acquireFetching(true); + if (m_refresher) { + disconnect(m_refresher.data(), &PackageKit::Transaction::finished, this, &PackageKitBackend::reloadPackageList); + } + + m_appdata.reset(new AppStream::Pool); + + auto fw = new QFutureWatcher(this); + connect(fw, &QFutureWatcher::finished, this, [this, fw]() { + const auto data = fw->result(); + fw->deleteLater(); + + if (!data.correct && m_packages.packages.isEmpty()) { + QTimer::singleShot(0, this, [this]() { + Q_EMIT passiveMessage(i18n("Please make sure that Appstream is properly set up on your system")); + }); + } + for (const auto &component : data.components) { + addComponent(component); + } + + if (data.components.isEmpty()) { + qCDebug(LIBDISCOVER_BACKEND_LOG) << "empty appstream db"; + if (PackageKit::Daemon::backendName() == QLatin1String("aptcc") || PackageKit::Daemon::backendName().isEmpty()) { + checkForUpdates(); + } + } + if (!m_appstreamInitialized) { + m_appstreamInitialized = true; + Q_EMIT loadedAppStream(); + } + acquireFetching(false); + + const QList distroComponents = +#if ASQ_CHECK_VERSION(1, 0, 0) + m_appdata->componentsById(AppStream::SystemInfo::currentDistroComponentId()).toList(); +#else + m_appdata->componentsById(AppStream::Utils::currentDistroComponentId()); +#endif + + if (distroComponents.isEmpty()) { +#if ASQ_CHECK_VERSION(1, 0, 0) + qWarning() << "no component found for" << AppStream::SystemInfo::currentDistroComponentId(); +#else + qWarning() << "no component found for" << AppStream::Utils::currentDistroComponentId(); +#endif + } + for (const AppStream::Component &dc : distroComponents) { +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto releases = dc.releasesPlain().entries(); +#else + const auto releases = dc.releases(); +#endif + for (const auto &r : releases) { + int cmp = AppStream::Utils::vercmpSimple(r.version(), AppStreamIntegration::global()->osRelease()->versionId()); + if (cmp == 0) { + // Ignore (likely) empty date_eol entries that are parsed as the UNIX Epoch + if (r.timestampEol().isNull() || r.timestampEol().toSecsSinceEpoch() == 0) { + continue; + } + if (r.timestampEol() < QDateTime::currentDateTime()) { + const QString releaseDate = QLocale().toString(r.timestampEol()); + Q_EMIT inlineMessageChanged( + QSharedPointer::create(InlineMessage::Warning, + QStringLiteral("dialog-warning"), + i18nc("%1 is the date as formatted by the locale", + "Your operating system ended support on %1. Consider upgrading to a supported version.", + releaseDate))); + } + } + } + } + }); + fw->setFuture(QtConcurrent::run(&m_threadPool, &loadAppStream, m_appdata.get())); +} + +AppPackageKitResource *PackageKitBackend::addComponent(const AppStream::Component &component) +{ + Q_ASSERT(isFetching()); + const QStringList pkgNames = component.packageNames(); + Q_ASSERT(!pkgNames.isEmpty()); + + auto &resPos = m_packages.packages[makeAppId(component.id())]; + AppPackageKitResource *res = qobject_cast(resPos); + if (!res) { + res = new AppPackageKitResource(component, pkgNames.at(0), this); + resPos = res; + } else { + res->clearPackageIds(); + } + for (const QString &pkg : pkgNames) { + m_packages.packageToApp[pkg] += component.id(); + } + + const auto componentExtends = component.extends(); + for (const QString &pkg : componentExtends) { + m_packages.extendedBy[pkg] += res; + } + return res; +} + +PKResolveTransaction *PackageKitBackend::resolvePackages(const QStringList &packageNames) +{ + if (packageNames.isEmpty()) { + return nullptr; + } + + if (!m_resolveTransaction) { + m_resolveTransaction = new PKResolveTransaction(this); + connect(m_resolveTransaction, &PKResolveTransaction::allFinished, this, &PackageKitBackend::getPackagesFinished); + connect(m_resolveTransaction, &PKResolveTransaction::started, this, [this] { + m_resolveTransaction = nullptr; + }); + } + + m_resolveTransaction->addPackageNames(packageNames); + return m_resolveTransaction; +} + +void PackageKitBackend::fetchUpdates() +{ + if (m_updater->isProgressing()) + return; + + m_getUpdatesTransaction = PackageKit::Daemon::getUpdates(); + connect(m_getUpdatesTransaction, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesFinished); + connect(m_getUpdatesTransaction, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageToUpdate); + connect(m_getUpdatesTransaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); + connect(m_getUpdatesTransaction, &PackageKit::Transaction::percentageChanged, this, &PackageKitBackend::fetchingUpdatesProgressChanged); + m_updatesPackageId.clear(); + m_hasSecurityUpdates = false; + + m_updater->setProgressing(true); + + Q_EMIT fetchingUpdatesProgressChanged(); +} + +void PackageKitBackend::addPackageArch(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary) +{ + addPackage(info, packageId, summary, true); +} + +void PackageKitBackend::addPackageNotArch(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary) +{ + addPackage(info, packageId, summary, false); +} + +void PackageKitBackend::addPackage(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary, bool arch) +{ + if (PackageKit::Daemon::packageArch(packageId) == QLatin1String("source")) { + // We do not add source packages, they make little sense here. If source is needed, + // we are going to have to consider that in some other way, some other time + // If we do not ignore them here, e.g. openSuse entirely fails at installing applications + return; + } + const QString packageName = PackageKit::Daemon::packageName(packageId); + QSet r = resourcesByPackageName(packageName); + if (r.isEmpty()) { + auto pk = new PackageKitResource(packageName, summary, this); + r = {pk}; + m_packagesToAdd.insert(pk); + } + for (auto res : qAsConst(r)) + static_cast(res)->addPackageId(info, packageId, arch); +} + +void PackageKitBackend::getPackagesFinished() +{ + includePackagesToAdd(); +} + +void PackageKitBackend::includePackagesToAdd() +{ + if (m_packagesToAdd.isEmpty() && m_packagesToDelete.isEmpty()) + return; + + acquireFetching(true); + for (PackageKitResource *res : qAsConst(m_packagesToAdd)) { + m_packages.packages[makePackageId(res->packageName())] = res; + } + for (PackageKitResource *res : qAsConst(m_packagesToDelete)) { + const auto pkgs = m_packages.packageToApp.value(res->packageName(), {res->packageName()}); + for (const auto &pkg : pkgs) { + auto res = m_packages.packages.take(makePackageId(pkg)); + if (res) { + if (AppPackageKitResource *ares = qobject_cast(res)) { + const auto extends = res->extends(); + for (const auto &ext : extends) + m_packages.extendedBy[ext].removeAll(ares); + } + + Q_EMIT resourceRemoved(res); + res->deleteLater(); + } + } + } + m_packagesToAdd.clear(); + m_packagesToDelete.clear(); + acquireFetching(false); +} + +void PackageKitBackend::transactionError(PackageKit::Transaction::Error, const QString &message) +{ + qWarning() << "Transaction error: " << message << sender(); + Q_EMIT passiveMessage(message); +} + +void PackageKitBackend::packageDetails(const PackageKit::Details &details) +{ + const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(details.packageId())); + if (resources.isEmpty()) + qWarning() << "couldn't find package for" << details.packageId(); + + for (AbstractResource *res : resources) { + qobject_cast(res)->setDetails(details); + } +} + +QSet PackageKitBackend::resourcesByPackageName(const QString &name) const +{ + return resourcesByPackageNames>(QStringList{name}); +} + +template +T PackageKitBackend::resourcesByAppNames(const W &appNames) const +{ + T ret; + ret.reserve(appNames.size()); + for (const QString &name : appNames) { + AbstractResource *res = m_packages.packages.value(makeAppId(name)); + if (res) { + ret += res; + } + } + return ret; +} + +template +T PackageKitBackend::resourcesByPackageNames(const W &pkgnames) const +{ + T ret; + ret.reserve(pkgnames.size()); + for (const QString &pkg_name : pkgnames) { + const QStringList app_names = m_packages.packageToApp.value(pkg_name, QStringList()); + if (app_names.isEmpty()) { + AbstractResource *res = m_packages.packages.value(makePackageId(pkg_name)); + if (res) { + ret += res; + } + } else { + for (const QString &app_id : app_names) { + AbstractResource *res = m_packages.packages.value(makeAppId(app_id)); + if (res) { + ret += res; + } + } + } + } + return ret; +} + +void PackageKitBackend::checkForUpdates() +{ + if (PackageKit::Daemon::global()->offline()->updateTriggered()) { + qCDebug(LIBDISCOVER_BACKEND_LOG) << "Won't be checking for updates again, the system needs a reboot to apply the fetched offline updates."; + return; + } + + if (!m_refresher) { + acquireFetching(true); + m_refresher = PackageKit::Daemon::refreshCache(false); + + connect(m_refresher.data(), &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); + connect(m_refresher.data(), &PackageKit::Transaction::finished, this, [this]() { + m_refresher = nullptr; + fetchUpdates(); + acquireFetching(false); + }); + } else { + qWarning() << "already resetting"; + } +} + +QList PackageKitBackend::componentsById(const QString &id) const +{ + Q_ASSERT(m_appstreamInitialized); + auto comps = m_appdata->componentsById(id); + if (comps.isEmpty()) { + comps = m_appdata->componentsByProvided(AppStream::Provided::KindId, id); + } +#if ASQ_CHECK_VERSION(1, 0, 0) + return comps.toList(); +#else + return comps; +#endif +} + +static const auto needsResolveFilter = [](AbstractResource *res) { + return res->state() == AbstractResource::Broken; +}; + +class PKResultsStream : public ResultsStream +{ +private: + PKResultsStream(PackageKitBackend *backend, const QString &name) + : ResultsStream(name) + , backend(backend) + { + } + + PKResultsStream(PackageKitBackend *backend, const QString &name, const QVector &resources) + : ResultsStream(name) + , backend(backend) + { + QTimer::singleShot(0, this, [resources, this]() { + sendResources(resources); + }); + } + +public: + template + [[nodiscard]] static QPointer create(Args&& ...args) + { + return new PKResultsStream(std::forward(args)...); + } + + void sendResources(const QVector &res, bool waitForResolved = false) + { + if (res.isEmpty()) { + finish(); + return; + } + + Q_ASSERT(res.size() == QSet(res.constBegin(), res.constEnd()).size()); + const auto toResolve = kFilter>(res, needsResolveFilter); + if (!toResolve.isEmpty()) { + auto transaction = backend->resolvePackages(kTransform(toResolve, [](AbstractResource *res) { + return res->packageName(); + })); + if (waitForResolved) { + Q_ASSERT(transaction); + connect(transaction, &QObject::destroyed, this, [this, res] { + Q_EMIT resourcesFound(res); + finish(); + }); + return; + } + } + + Q_EMIT resourcesFound(res); + finish(); + } + +private: + PackageKitBackend *const backend; +}; + +ResultsStream *PackageKitBackend::search(const AbstractResourcesBackend::Filters &filter) +{ + if (!filter.resourceUrl.isEmpty()) { + return findResourceByPackageName(filter.resourceUrl); + } else if (!filter.extends.isEmpty()) { + auto stream = PKResultsStream::create(this, QStringLiteral("PackageKitStream-extends")); + auto f = [this, filter, stream] { + if (!stream) { + return; + } + const auto resources = kTransform>(m_packages.extendedBy.value(filter.extends), [](AppPackageKitResource *a) { + return a; + }); + stream->sendResources(resources, filter.state != AbstractResource::Broken); + }; + runWhenInitialized(f, stream); + return stream; + } else if (filter.state == AbstractResource::Upgradeable) { + return new ResultsStream(QStringLiteral("PackageKitStream-upgradeable"), + kTransform>(upgradeablePackages())); // No need for it to be a PKResultsStream + } else if (filter.state == AbstractResource::Installed) { + auto stream = PKResultsStream::create(this, QStringLiteral("PackageKitStream-installed")); + auto f = [this, stream, filter] { + if (!stream) { + return; + } + const auto toResolve = kFilter>(m_packages.packages, needsResolveFilter); + + auto installedAndNameFilter = [filter](AbstractResource *res) { + return res->state() >= AbstractResource::Installed && !qobject_cast(res)->isCritical() + && (res->name().contains(filter.search, Qt::CaseInsensitive) || res->packageName().compare(filter.search, Qt::CaseInsensitive) == 0); + }; + bool furtherSearch = false; + if (!toResolve.isEmpty()) { + resolvePackages(kTransform(toResolve, [](AbstractResource *res) { + return res->packageName(); + })); + connect(m_resolveTransaction, &PKResolveTransaction::allFinished, this, [stream, toResolve, installedAndNameFilter] { + const auto resolved = kFilter>(toResolve, installedAndNameFilter); + if (!resolved.isEmpty()) + Q_EMIT stream->resourcesFound(resolved); + stream->finish(); + }); + furtherSearch = true; + } + + const auto resolved = kFilter>(m_packages.packages, installedAndNameFilter); + if (!resolved.isEmpty()) { + QTimer::singleShot(0, this, [resolved, toResolve, stream]() { + if (!resolved.isEmpty()) + Q_EMIT stream->resourcesFound(resolved); + + if (toResolve.isEmpty()) + stream->finish(); + }); + furtherSearch = true; + } + + if (!furtherSearch) + stream->finish(); + }; + runWhenInitialized(f, stream); + return stream; + } else if (filter.search.isEmpty() && !filter.category) { + auto stream = PKResultsStream::create(this, QStringLiteral("PackageKitStream-all")); + auto f = [this, filter, stream] { + if (!stream) { + return; + } + auto resources = kFilter>(m_packages.packages, [](AbstractResource *res) { + return res->type() != AbstractResource::Technical && !qobject_cast(res)->isCritical() + && !qobject_cast(res)->extendsItself(); + }); + stream->sendResources(resources); + }; + runWhenInitialized(f, stream); + return stream; + } else { + auto stream = PKResultsStream::create(this, QStringLiteral("PackageKitStream-search")); + const auto f = [this, stream, filter]() { + if (!stream) { + return; + } + QList components; + if (!filter.search.isEmpty()) { +#if ASQ_CHECK_VERSION(1, 0, 0) + components = m_appdata->search(filter.search).toList(); +#else + components = m_appdata->search(filter.search); +#endif +#if ASQ_CHECK_VERSION(0, 15, 6) + } else if (filter.category) { + components = AppStreamUtils::componentsByCategories(m_appdata.get(), filter.category, AppStream::Bundle::KindUnknown); +#endif + } else { +#if ASQ_CHECK_VERSION(1, 0, 0) + components = m_appdata->components().toList(); +#else + components = m_appdata->components(); +#endif + } + + const QSet ids = kTransform>(components, [](const AppStream::Component &comp) { + return comp.id(); + }); + if (!ids.isEmpty()) { + const auto resources = kFilter>(resourcesByAppNames>(ids), [](AbstractResource *res) { + return !qobject_cast(res)->extendsItself(); + }); + stream->sendResources(resources, filter.state != AbstractResource::Broken); + } else { + stream->finish(); + } + }; + runWhenInitialized(f, stream); + return stream; + } +} + +void PackageKitBackend::runWhenInitialized(const std::function &f, QObject *stream) +{ + if (!m_appstreamInitialized) { + connect(this, &PackageKitBackend::loadedAppStream, stream, f); + } else { + QTimer::singleShot(0, stream, f); // NOTE `stream` is a child of `this` so this transitively also depends on `this` + } +} + +PKResultsStream *PackageKitBackend::findResourceByPackageName(const QUrl &url) +{ + if (url.isLocalFile()) { + QMimeDatabase db; + const auto mime = db.mimeTypeForUrl(url); + if (mime.inherits(QStringLiteral("application/vnd.debian.binary-package")) // + || mime.inherits(QStringLiteral("application/x-rpm")) // + || mime.inherits(QStringLiteral("application/x-tar")) // + || mime.inherits(QStringLiteral("application/x-zstd-compressed-tar")) // + || mime.inherits(QStringLiteral("application/x-xz-compressed-tar"))) { + return PKResultsStream::create(this, QStringLiteral("PackageKitStream-localpkg"), QVector{new LocalFilePKResource(url, this)}).data(); + } + } else if (url.scheme() == QLatin1String("appstream")) { + const auto appstreamIds = AppStreamUtils::appstreamIds(url); + if (appstreamIds.isEmpty()) + Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); + else { + auto stream = PKResultsStream::create(this, QStringLiteral("PackageKitStream-appstream-url")); + const auto f = [this, appstreamIds, stream]() { + if (!stream) { + return; + } + auto toSend = QSet(); + toSend.reserve(appstreamIds.size()); + for (const auto &appstreamId : appstreamIds) { + const auto comps = componentsById(appstreamId); + if (comps.isEmpty()) { + continue; + } + auto resources = resourcesByComponents>(comps); + for (const auto &r : resources) { + toSend.insert(r); + } + } + stream->sendResources(QVector(toSend.constBegin(), toSend.constEnd())); + }; + runWhenInitialized(f, stream); + return stream; + } + } + return PKResultsStream::create(this, QStringLiteral("PackageKitStream-unknown-url"), QVector{}).data(); +} + +template +T PackageKitBackend::resourcesByComponents(const QList &comps) const +{ + T ret; + ret.reserve(comps.size()); + QSet done; + for (const auto &comp : comps) { + if (comp.packageNames().isEmpty() || done.contains(comp.id())) { + continue; + } + done += comp.id(); + ret << m_packages.packages.value(makeAppId(comp.id())); + Q_ASSERT(ret.constLast()); + } + return ret; +} + +bool PackageKitBackend::hasSecurityUpdates() const +{ + return m_hasSecurityUpdates; +} + +int PackageKitBackend::updatesCount() const +{ + if (PackageKit::Daemon::global()->offline()->updateTriggered()) + return 0; + + int ret = 0; + QSet packages; + const auto toUpgrade = upgradeablePackages(); + for (auto res : toUpgrade) { + const auto packageName = res->packageName(); + if (packages.contains(packageName)) { + continue; + } + packages.insert(packageName); + ret += 1; + } + return ret; +} + +Transaction *PackageKitBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + Transaction *t = nullptr; + if (!addons.addonsToInstall().isEmpty()) { + QVector appsToInstall = resourcesByPackageNames>(addons.addonsToInstall()); + if (!app->isInstalled()) + appsToInstall << app; + t = new PKTransaction(appsToInstall, Transaction::ChangeAddonsRole); + } else if (!app->isInstalled()) + t = installApplication(app); + + if (!addons.addonsToRemove().isEmpty()) { + const auto appsToRemove = resourcesByPackageNames>(addons.addonsToRemove()); + t = new PKTransaction(appsToRemove, Transaction::RemoveRole); + } + + return t; +} + +Transaction *PackageKitBackend::installApplication(AbstractResource *app) +{ + return new PKTransaction({app}, Transaction::InstallRole); +} + +Transaction *PackageKitBackend::removeApplication(AbstractResource *app) +{ + Q_ASSERT(!isFetching()); + if (!qobject_cast(app)) { + Q_EMIT passiveMessage(i18n("Cannot remove '%1'", app->name())); + return nullptr; + } + return new PKTransaction({app}, Transaction::RemoveRole); +} + +QSet PackageKitBackend::upgradeablePackages() const +{ + if (isFetching() || !m_packagesToAdd.isEmpty()) { + return {}; + } + + QSet ret; + ret.reserve(m_updatesPackageId.size()); + for (const QString &pkgid : qAsConst(m_updatesPackageId)) { + const QString pkgname = PackageKit::Daemon::packageName(pkgid); + const auto pkgs = resourcesByPackageName(pkgname); + if (pkgs.isEmpty()) { + qWarning() << "couldn't find resource for" << pkgid; + } + ret.unite(pkgs); + } + return kFilter>(ret, [](AbstractResource *res) { + return !static_cast(res)->extendsItself(); + }); +} + +void PackageKitBackend::addPackageToUpdate(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary) +{ + if (info == PackageKit::Transaction::InfoBlocked) { + return; + } + + if (info == PackageKit::Transaction::InfoRemoving || info == PackageKit::Transaction::InfoObsoleting) { + // Don't try updating packages which need to be removed + return; + } + + if (info == PackageKit::Transaction::InfoSecurity) + m_hasSecurityUpdates = true; + + m_updatesPackageId += packageId; + addPackage(info, packageId, summary, true); +} + +void PackageKitBackend::getUpdatesFinished(PackageKit::Transaction::Exit, uint) +{ + if (!m_updatesPackageId.isEmpty()) { + resolvePackages(kTransform(m_updatesPackageId, [](const QString &pkgid) { + return PackageKit::Daemon::packageName(pkgid); + })); + } + + m_updater->setProgressing(false); + + includePackagesToAdd(); + if (isFetching()) { + auto a = new OneTimeAction( + [this] { + Q_EMIT updatesCountChanged(); + }, + this); + connect(this, &PackageKitBackend::available, a, &OneTimeAction::trigger); + } else + Q_EMIT updatesCountChanged(); +} + +// Copy of Transaction::packageName that doesn't create a copy but just pass a reference +// It's an optimisation as there's a bunch of allocations that happen from packageName +// Having packageName return a QStringRef or a QStringView would fix this issue. +// TODO Qt 6: Have packageName and similars return a QStringView +static QStringView TransactionpackageName(const QString &packageID) +{ + return QStringView(packageID).left(packageID.indexOf(QLatin1Char(';'))); +} + +bool PackageKitBackend::isPackageNameUpgradeable(const PackageKitResource *res) const +{ + const QString name = res->packageName(); + for (const QString &pkgid : m_updatesPackageId) { + if (TransactionpackageName(pkgid) == name) + return true; + } + return false; +} + +QSet PackageKitBackend::upgradeablePackageId(const PackageKitResource *res) const +{ + QSet ids; + const QString name = res->packageName(); + for (const QString &pkgid : m_updatesPackageId) { + if (TransactionpackageName(pkgid) == name) + ids.insert(pkgid); + } + return ids; +} + +void PackageKitBackend::fetchDetails(const QSet &pkgid) +{ + m_details.add(pkgid); +} + +void PackageKitBackend::performDetailsFetch(const QSet &pkgids) +{ + Q_ASSERT(!pkgids.isEmpty()); + const auto ids = pkgids.values(); + + PackageKit::Transaction *transaction = PackageKit::Daemon::getDetails(ids); + connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); + connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); +} + +void PackageKitBackend::checkDaemonRunning() +{ + if (!PackageKit::Daemon::isRunning()) { + qWarning() << "PackageKit stopped running!"; + } else + updateProxy(); +} + +AbstractBackendUpdater *PackageKitBackend::backendUpdater() const +{ + return m_updater; +} + +QVector PackageKitBackend::extendedBy(const QString &id) const +{ + return m_packages.extendedBy[id]; +} + +AbstractReviewsBackend *PackageKitBackend::reviewsBackend() const +{ + return m_reviews.data(); +} + +QString PackageKitBackend::displayName() const +{ + return AppStreamIntegration::global()->osRelease()->prettyName(); +} + +int PackageKitBackend::fetchingUpdatesProgress() const +{ + if (!m_getUpdatesTransaction) + return 0; + + if (m_getUpdatesTransaction->status() == PackageKit::Transaction::StatusWait + || m_getUpdatesTransaction->status() == PackageKit::Transaction::StatusUnknown) { + return m_getUpdatesTransaction->property("lastPercentage").toInt(); + } + int percentage = percentageWithStatus(m_getUpdatesTransaction->status(), m_getUpdatesTransaction->percentage()); + m_getUpdatesTransaction->setProperty("lastPercentage", percentage); + return percentage; +} + +InlineMessage *PackageKitBackend::explainDysfunction() const +{ + const auto error = m_appdata->lastError(); + if (!error.isEmpty()) { + return new InlineMessage(InlineMessage::Error, QStringLiteral("network-disconnect"), error); + } + return AbstractResourcesBackend::explainDysfunction(); +} + +#include "PackageKitBackend.moc" diff --git a/libdiscover/backends/PackageKitBackend/PackageKitBackend.h b/libdiscover/backends/PackageKitBackend/PackageKitBackend.h new file mode 100644 index 0000000..c632fe8 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitBackend.h @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "PackageKitResource.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class AppPackageKitResource; +class PackageKitUpdater; +class OdrsReviewsBackend; +class PKResultsStream; +class PKResolveTransaction; + +/** This is either a package name or an appstream id */ +struct PackageOrAppId { + QString id; + bool isPackageName; +}; +PackageOrAppId makePackageId(const QString &id); +PackageOrAppId makeAppId(const QString &id); + +class Delay : public QObject +{ + Q_OBJECT +public: + Delay(); + void add(const QString &pkgid) + { + if (!m_delay.isActive()) { + m_delay.start(); + } + + m_pkgids << pkgid; + } + void add(const QSet &pkgids) + { + if (!m_delay.isActive()) { + m_delay.start(); + } + + m_pkgids += pkgids; + } + +Q_SIGNALS: + void perform(const QSet &pkgids); + +private: + QTimer m_delay; + QSet m_pkgids; +}; + +class DISCOVERCOMMON_EXPORT PackageKitBackend : public AbstractResourcesBackend +{ + Q_OBJECT +public: + explicit PackageKitBackend(QObject *parent = nullptr); + ~PackageKitBackend() override; + + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + QSet resourcesByPackageName(const QString &name) const; + + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + PKResultsStream *findResourceByPackageName(const QUrl &search); + int updatesCount() const override; + bool hasSecurityUpdates() const override; + + Transaction *installApplication(AbstractResource *app) override; + Transaction *installApplication(AbstractResource *app, const AddonList &addons) override; + Transaction *removeApplication(AbstractResource *app) override; + bool isValid() const override + { + return !QFile::exists(QStringLiteral("/run/ostree-booted")); + } + QSet upgradeablePackages() const; + bool isFetching() const override; + + bool isPackageNameUpgradeable(const PackageKitResource *res) const; + QSet upgradeablePackageId(const PackageKitResource *res) const; + QVector extendedBy(const QString &id) const; + + PKResolveTransaction *resolvePackages(const QStringList &packageNames); + void fetchDetails(const QString &pkgid) + { + m_details.add(pkgid); + } + void fetchDetails(const QSet &pkgid); + + void checkForUpdates() override; + QString displayName() const override; + + bool hasApplications() const override + { + return true; + } + static QString locateService(const QString &filename); + + QList componentsById(const QString &id) const; + void fetchUpdates(); + int fetchingUpdatesProgress() const override; + + InlineMessage *explainDysfunction() const override; + + void addPackageArch(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary); + void addPackageNotArch(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary); + Delay &updateDetails() + { + return m_updateDetails; + } + +public Q_SLOTS: + void reloadPackageList(); + void transactionError(PackageKit::Transaction::Error, const QString &message); + +private Q_SLOTS: + void getPackagesFinished(); + void addPackage(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary, bool arch); + void packageDetails(const PackageKit::Details &details); + void addPackageToUpdate(PackageKit::Transaction::Info, const QString &pkgid, const QString &summary); + void getUpdatesFinished(PackageKit::Transaction::Exit, uint); + +Q_SIGNALS: + void loadedAppStream(); + void available(); + +private: + friend class PackageKitResource; + template + T resourcesByPackageNames(const W &names) const; + + template + T resourcesByAppNames(const W &names) const; + + template + T resourcesByComponents(const QList &names) const; + + void runWhenInitialized(const std::function &f, QObject *stream); + + void checkDaemonRunning(); + void acquireFetching(bool f); + void includePackagesToAdd(); + void performDetailsFetch(const QSet &pkgids); + AppPackageKitResource *addComponent(const AppStream::Component &component); + void updateProxy(); + + QScopedPointer m_appdata; + PackageKitUpdater *m_updater; + QPointer m_refresher; + int m_isFetching; + QSet m_updatesPackageId; + bool m_hasSecurityUpdates = false; + QSet m_packagesToAdd; + QSet m_packagesToDelete; + bool m_appstreamInitialized = false; + + struct { + QHash packages; + QHash packageToApp; + QHash> extendedBy; + } m_packages; + + Delay m_details; + Delay m_updateDetails; + QSharedPointer m_reviews; + QPointer m_getUpdatesTransaction; + QThreadPool m_threadPool; + QPointer m_resolveTransaction; +}; diff --git a/libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp b/libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp new file mode 100644 index 0000000..904c816 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp @@ -0,0 +1,380 @@ +/* + * SPDX-FileCopyrightText: 2012-2014 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PackageKitMessages.h" +#include +#include + +namespace PackageKitMessages +{ +QString errorMessage(PackageKit::Transaction::Error error, const QString &errorMessage) +{ + switch (error) { + case PackageKit::Transaction::ErrorOom: + return i18n("Out of memory"); + case PackageKit::Transaction::ErrorNoNetwork: + return i18n("No network connection available"); + case PackageKit::Transaction::ErrorNotSupported: + return i18n("Operation not supported"); + case PackageKit::Transaction::ErrorInternalError: { + if (errorMessage.isEmpty()) { + return i18n("Internal error"); + } else { + return i18n("Internal error: %1", errorMessage); + } + } + case PackageKit::Transaction::ErrorGpgFailure: + return i18n("GPG failure"); + case PackageKit::Transaction::ErrorPackageIdInvalid: + return i18n("PackageID invalid"); + case PackageKit::Transaction::ErrorPackageNotInstalled: + return i18n("Package not installed"); + case PackageKit::Transaction::ErrorPackageNotFound: + return i18n("Package not found"); + case PackageKit::Transaction::ErrorPackageAlreadyInstalled: + return i18n("Package is already installed"); + case PackageKit::Transaction::ErrorPackageDownloadFailed: + return i18n("Package download failed"); + case PackageKit::Transaction::ErrorGroupNotFound: + return i18n("Package group not found"); + case PackageKit::Transaction::ErrorGroupListInvalid: + return i18n("Package group list invalid"); + case PackageKit::Transaction::ErrorDepResolutionFailed: + return i18n("Dependency resolution failed"); + case PackageKit::Transaction::ErrorFilterInvalid: + return i18n("Filter invalid"); + case PackageKit::Transaction::ErrorCreateThreadFailed: + return i18n("Failed while creating a thread"); + case PackageKit::Transaction::ErrorTransactionError: + return i18n("Transaction failure"); + case PackageKit::Transaction::ErrorTransactionCancelled: + return i18n("Transaction canceled"); + case PackageKit::Transaction::ErrorNoCache: + return i18n("No Cache available"); + case PackageKit::Transaction::ErrorRepoNotFound: + return i18n("Cannot find repository"); + case PackageKit::Transaction::ErrorCannotRemoveSystemPackage: + return i18n("Cannot remove system package"); + case PackageKit::Transaction::ErrorProcessKill: + return i18n("The PackageKit daemon has crashed"); + case PackageKit::Transaction::ErrorFailedInitialization: + return i18n("Initialization failure"); + case PackageKit::Transaction::ErrorFailedFinalise: + return i18n("Failed to finalize transaction"); + case PackageKit::Transaction::ErrorFailedConfigParsing: + return i18n("Config parsing failed"); + case PackageKit::Transaction::ErrorCannotCancel: + return i18n("Cannot cancel transaction"); + case PackageKit::Transaction::ErrorCannotGetLock: + return i18n("Cannot obtain lock"); + case PackageKit::Transaction::ErrorNoPackagesToUpdate: + return i18n("No packages to update"); + case PackageKit::Transaction::ErrorCannotWriteRepoConfig: + return i18n("Cannot write repo config"); + case PackageKit::Transaction::ErrorLocalInstallFailed: + return i18n("Local install failed"); + case PackageKit::Transaction::ErrorBadGpgSignature: + return i18n("Bad GPG signature found"); + case PackageKit::Transaction::ErrorMissingGpgSignature: + return i18n("No GPG signature found"); + case PackageKit::Transaction::ErrorCannotInstallSourcePackage: + return i18n("Cannot install source package"); + case PackageKit::Transaction::ErrorRepoConfigurationError: + return i18n("Repo configuration error"); + case PackageKit::Transaction::ErrorNoLicenseAgreement: + return i18n("No license agreement"); + case PackageKit::Transaction::ErrorFileConflicts: + return i18n("File conflicts found"); + case PackageKit::Transaction::ErrorPackageConflicts: + return i18n("Package conflict found"); + case PackageKit::Transaction::ErrorRepoNotAvailable: + return i18n("Repo not available"); + case PackageKit::Transaction::ErrorInvalidPackageFile: + return i18n("Invalid package file"); + case PackageKit::Transaction::ErrorPackageInstallBlocked: + return i18n("Package install blocked"); + case PackageKit::Transaction::ErrorPackageCorrupt: + return i18n("Corrupt package found"); + case PackageKit::Transaction::ErrorAllPackagesAlreadyInstalled: + return i18n("All packages already installed"); + case PackageKit::Transaction::ErrorFileNotFound: + return i18n("File not found"); + case PackageKit::Transaction::ErrorNoMoreMirrorsToTry: + return i18n("No more mirrors available"); + case PackageKit::Transaction::ErrorNoDistroUpgradeData: + return i18n("No distro upgrade data"); + case PackageKit::Transaction::ErrorIncompatibleArchitecture: + return i18n("Incompatible architecture"); + case PackageKit::Transaction::ErrorNoSpaceOnDevice: + return i18n("No space on device left"); + case PackageKit::Transaction::ErrorMediaChangeRequired: + return i18n("A media change is required"); + case PackageKit::Transaction::ErrorNotAuthorized: + return i18n("You have no authorization to execute this operation"); + case PackageKit::Transaction::ErrorUpdateNotFound: + return i18n("Update not found"); + case PackageKit::Transaction::ErrorCannotInstallRepoUnsigned: + return i18n("Cannot install from unsigned repo"); + case PackageKit::Transaction::ErrorCannotUpdateRepoUnsigned: + return i18n("Cannot update from unsigned repo"); + case PackageKit::Transaction::ErrorCannotGetFilelist: + return i18n("Cannot get file list"); + case PackageKit::Transaction::ErrorCannotGetRequires: + return i18n("Cannot get requires"); + case PackageKit::Transaction::ErrorCannotDisableRepository: + return i18n("Cannot disable repository"); + case PackageKit::Transaction::ErrorRestrictedDownload: + return i18n("Restricted download detected"); + case PackageKit::Transaction::ErrorPackageFailedToConfigure: + return i18n("Package failed to configure"); + case PackageKit::Transaction::ErrorPackageFailedToBuild: + return i18n("Package failed to build"); + case PackageKit::Transaction::ErrorPackageFailedToInstall: + return i18n("Package failed to install"); + case PackageKit::Transaction::ErrorPackageFailedToRemove: + return i18n("Package failed to remove"); + case PackageKit::Transaction::ErrorUpdateFailedDueToRunningProcess: + return i18n("Update failed due to running process"); + case PackageKit::Transaction::ErrorPackageDatabaseChanged: + return i18n("The package database changed"); + case PackageKit::Transaction::ErrorProvideTypeNotSupported: + return i18n("The provided type is not supported"); + case PackageKit::Transaction::ErrorInstallRootInvalid: + return i18n("Install root is invalid"); + case PackageKit::Transaction::ErrorCannotFetchSources: + return i18nc("Failed to sync your Linux distro repositories or other sources of packages", "Cannot fetch sources"); + case PackageKit::Transaction::ErrorCancelledPriority: + return i18n("Canceled priority"); + case PackageKit::Transaction::ErrorUnfinishedTransaction: + return i18n("Unfinished transaction"); + case PackageKit::Transaction::ErrorLockRequired: + return i18n("Lock required"); + case PackageKit::Transaction::ErrorUnknown: + default: { + int idx = PackageKit::Transaction::staticMetaObject.indexOfEnumerator("Error"); + QMetaEnum metaenum = PackageKit::Transaction::staticMetaObject.enumerator(idx); + return i18n("Unknown error %1.", QString::fromLatin1(metaenum.valueToKey(error))); + } + } +} + +QString restartMessage(PackageKit::Transaction::Restart restart, const QString &pkgid) +{ + switch (restart) { + case PackageKit::Transaction::RestartApplication: + return i18n("'%1' was changed and suggests to be restarted.", PackageKit::Daemon::packageName(pkgid)); + case PackageKit::Transaction::RestartSession: + return i18n("A change by '%1' suggests your session to be restarted.", PackageKit::Daemon::packageName(pkgid)); + case PackageKit::Transaction::RestartSecuritySession: + return i18n("'%1' was updated for security reasons, a restart of the session is recommended.", PackageKit::Daemon::packageName(pkgid)); + case PackageKit::Transaction::RestartSecuritySystem: + return i18n("'%1' was updated for security reasons, a restart of the system is recommended.", PackageKit::Daemon::packageName(pkgid)); + case PackageKit::Transaction::RestartSystem: + case PackageKit::Transaction::RestartUnknown: + case PackageKit::Transaction::RestartNone: + default: + return i18n("A change by '%1' suggests your system to be restarted.", PackageKit::Daemon::packageName(pkgid)); + } +} + +QString restartMessage(PackageKit::Transaction::Restart restart) +{ + switch (restart) { + case PackageKit::Transaction::RestartApplication: + return i18n("The application will have to be restarted."); + case PackageKit::Transaction::RestartSession: + return i18n("The session will have to be restarted"); + case PackageKit::Transaction::RestartSystem: + return i18n("The system will have to be restarted."); + case PackageKit::Transaction::RestartSecuritySession: + return i18n("For security, the session will have to be restarted."); + case PackageKit::Transaction::RestartSecuritySystem: + return i18n("For security, the system will have to be restarted."); + case PackageKit::Transaction::RestartUnknown: + case PackageKit::Transaction::RestartNone: + default: + return QString(); + } +} + +QString statusMessage(PackageKit::Transaction::Status status) +{ + switch (status) { + case PackageKit::Transaction::StatusWait: + return i18n("Waiting…"); + case PackageKit::Transaction::StatusRefreshCache: + return i18n("Refreshing Cache…"); + case PackageKit::Transaction::StatusSetup: + return i18n("Setup…"); + case PackageKit::Transaction::StatusRunning: + return i18n("Processing…"); + case PackageKit::Transaction::StatusRemove: + return i18n("Remove…"); + case PackageKit::Transaction::StatusDownload: + return i18n("Downloading…"); + case PackageKit::Transaction::StatusInstall: + return i18n("Installing…"); + case PackageKit::Transaction::StatusUpdate: + return i18n("Updating…"); + case PackageKit::Transaction::StatusCleanup: + return i18n("Cleaning up…"); + // case PackageKit::Transaction::StatusObsolete: + case PackageKit::Transaction::StatusDepResolve: + return i18n("Resolving dependencies…"); + case PackageKit::Transaction::StatusSigCheck: + return i18n("Checking signatures…"); + case PackageKit::Transaction::StatusTestCommit: + return i18n("Test committing…"); + case PackageKit::Transaction::StatusCommit: + return i18n("Committing…"); + // StatusRequest, + case PackageKit::Transaction::StatusFinished: + return i18n("Finished"); + case PackageKit::Transaction::StatusCancel: + return i18n("Canceled"); + case PackageKit::Transaction::StatusWaitingForLock: + return i18n("Waiting for lock…"); + case PackageKit::Transaction::StatusWaitingForAuth: + return i18n("Waiting for authorization…"); + // StatusScanProcessList, + // StatusCheckExecutableFiles, + // StatusCheckLibraries, + case PackageKit::Transaction::StatusCopyFiles: + return i18n("Copying files…"); + case PackageKit::Transaction::StatusUnknown: + default: + return i18n("Unknown Status"); + } +} + +QString statusDetail(PackageKit::Transaction::Status status) +{ + switch (status) { + case PackageKit::Transaction::StatusWait: + return i18n("We are waiting for something."); + case PackageKit::Transaction::StatusSetup: + return i18n("Setting up transaction…"); + case PackageKit::Transaction::StatusRunning: + return i18n("The transaction is currently working…"); + case PackageKit::Transaction::StatusRemove: + return i18n("The transaction is currently removing packages…"); + case PackageKit::Transaction::StatusDownload: + return i18n("The transaction is currently downloading packages…"); + case PackageKit::Transaction::StatusInstall: + return i18n("The transactions is currently installing packages…"); + case PackageKit::Transaction::StatusUpdate: + return i18n("The transaction is currently updating packages…"); + case PackageKit::Transaction::StatusCleanup: + return i18n("The transaction is currently cleaning up…"); + // case PackageKit::Transaction::StatusObsolete, + case PackageKit::Transaction::StatusDepResolve: + return i18n("The transaction is currently resolving the dependencies of the packages it will install…"); + case PackageKit::Transaction::StatusSigCheck: + return i18n("The transaction is currently checking the signatures of the packages…"); + case PackageKit::Transaction::StatusTestCommit: + return i18n("The transaction is currently testing the commit of this set of packages…"); + case PackageKit::Transaction::StatusCommit: + return i18n("The transaction is currently committing its set of packages…"); + // StatusRequest, + case PackageKit::Transaction::StatusFinished: + return i18n("The transaction has finished!"); + case PackageKit::Transaction::StatusCancel: + return i18n("The transaction was canceled"); + case PackageKit::Transaction::StatusWaitingForLock: + return i18n("The transaction is currently waiting for the lock…"); + case PackageKit::Transaction::StatusWaitingForAuth: + return i18n("Waiting for the user to authorize the transaction…"); + // StatusScanProcessList, + // StatusCheckExecutableFiles, + // StatusCheckLibraries, + case PackageKit::Transaction::StatusCopyFiles: + return i18n("The transaction is currently copying files…"); + case PackageKit::Transaction::StatusRefreshCache: + return i18n("Currently refreshing the repository cache…"); + case PackageKit::Transaction::StatusUnknown: + default: { + int idx = PackageKit::Transaction::staticMetaObject.indexOfEnumerator("Status"); + QMetaEnum metaenum = PackageKit::Transaction::staticMetaObject.enumerator(idx); + return i18n("Unknown status %1.", QString::fromLatin1(metaenum.valueToKey(status))); + } + } +} + +QString updateStateMessage(PackageKit::Transaction::UpdateState state) +{ + switch (state) { + case PackageKit::Transaction::UpdateStateUnknown: + return QString(); + case PackageKit::Transaction::UpdateStateStable: + return i18nc("update state", "Stable"); + case PackageKit::Transaction::UpdateStateUnstable: + return i18nc("update state", "Unstable"); + case PackageKit::Transaction::UpdateStateTesting: + return i18nc("update state", "Testing"); + } + return QString(); +} + +QString info(PackageKit::Transaction::Info info) +{ + switch (info) { + case PackageKit::Transaction::InfoUnknown: + return i18n("Unknown"); + case PackageKit::Transaction::InfoInstalled: + return i18n("Installed"); + case PackageKit::Transaction::InfoAvailable: + return i18n("Not Installed"); + case PackageKit::Transaction::InfoLow: + return i18n("Low"); + case PackageKit::Transaction::InfoEnhancement: + return i18n("Enhancement"); + case PackageKit::Transaction::InfoNormal: + return i18n("Normal"); + case PackageKit::Transaction::InfoBugfix: + return i18n("Bugfix"); + case PackageKit::Transaction::InfoImportant: + return i18n("Important"); + case PackageKit::Transaction::InfoSecurity: + return i18n("Security"); + case PackageKit::Transaction::InfoBlocked: + return i18n("Blocked"); + case PackageKit::Transaction::InfoDownloading: + return i18n("Downloading"); + case PackageKit::Transaction::InfoUpdating: + return i18n("Updating"); + case PackageKit::Transaction::InfoInstalling: + return i18n("Installing"); + case PackageKit::Transaction::InfoRemoving: + return i18n("Removing"); + case PackageKit::Transaction::InfoCleanup: + return i18n("Cleanup"); + case PackageKit::Transaction::InfoObsoleting: + return i18n("Obsoleting"); + case PackageKit::Transaction::InfoCollectionInstalled: + return i18n("Collection Installed"); + case PackageKit::Transaction::InfoCollectionAvailable: + return i18n("Collection Available"); + case PackageKit::Transaction::InfoFinished: + return i18n("Finished"); + case PackageKit::Transaction::InfoReinstalling: + return i18n("Reinstalling"); + case PackageKit::Transaction::InfoDowngrading: + return i18n("Downgrading"); + case PackageKit::Transaction::InfoPreparing: + return i18n("Preparing"); + case PackageKit::Transaction::InfoDecompressing: + return i18n("Decompressing"); + case PackageKit::Transaction::InfoUntrusted: + return i18n("Untrusted"); + case PackageKit::Transaction::InfoTrusted: + return i18n("Trusted"); + case PackageKit::Transaction::InfoUnavailable: + return i18n("Unavailable"); + } + return {}; +} +} diff --git a/libdiscover/backends/PackageKitBackend/PackageKitMessages.h b/libdiscover/backends/PackageKitBackend/PackageKitMessages.h new file mode 100644 index 0000000..7e2a477 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitMessages.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2012-2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +namespace PackageKitMessages +{ +QString errorMessage(PackageKit::Transaction::Error error, const QString &errorMessage); +QString restartMessage(PackageKit::Transaction::Restart restart, const QString &p); +QString restartMessage(PackageKit::Transaction::Restart restart); +QString statusMessage(PackageKit::Transaction::Status status); +QString statusDetail(PackageKit::Transaction::Status status); +QString updateStateMessage(PackageKit::Transaction::UpdateState state); +QString info(PackageKit::Transaction::Info info); +} diff --git a/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp new file mode 100644 index 0000000..a5b62b6 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp @@ -0,0 +1,363 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PackageKitNotifier.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libdiscover_backend_debug.h" +#include "pk-offline-private.h" + +using namespace std::chrono_literals; + +PackageKitNotifier::PackageKitNotifier(QObject *parent) + : BackendNotifierModule(parent) + , m_securityUpdates(0) + , m_normalUpdates(0) +{ + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::updatesChanged, this, &PackageKitNotifier::recheckSystemUpdateNeeded); + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::transactionListChanged, this, &PackageKitNotifier::transactionListChanged); + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::restartScheduled, this, &PackageKitNotifier::nowNeedsReboot); + connect(PackageKit::Daemon::global()->offline(), &PackageKit::Offline::changed, this, [this] { + if (PackageKit::Daemon::global()->offline()->updateTriggered()) + nowNeedsReboot(); + }); + + // Check if there's packages after 5' + QTimer::singleShot(5min, this, &PackageKitNotifier::refreshDatabase); + + QTimer *regularCheck = new QTimer(this); + connect(regularCheck, &QTimer::timeout, this, &PackageKitNotifier::refreshDatabase); + + const QString aptconfig = QStandardPaths::findExecutable(QStringLiteral("apt-config")); + if (!aptconfig.isEmpty()) { + checkAptVariable(aptconfig, QLatin1String("Apt::Periodic::Update-Package-Lists"), [regularCheck](const QStringView &value) { + bool ok; + const int days = value.toInt(&ok); + if (!ok || days == 0) { + regularCheck->setInterval(24h); // refresh at least once every day + regularCheck->start(); + if (!value.isEmpty()) + qWarning() << "couldn't understand value for timer:" << value; + } + + // if the setting is not empty, refresh will be carried out by unattended-upgrade + // https://wiki.debian.org/UnattendedUpgrades + }); + } else { + regularCheck->setInterval(24h); // refresh at least once every day + regularCheck->start(); + } + + QTimer::singleShot(3s, this, &PackageKitNotifier::checkOfflineUpdates); + + m_recheckTimer = new QTimer(this); + m_recheckTimer->setInterval(200); + m_recheckTimer->setSingleShot(true); + connect(m_recheckTimer, &QTimer::timeout, this, &PackageKitNotifier::recheckSystemUpdate); + + QFileSystemWatcher *watcher = new QFileSystemWatcher(this); + watcher->addPath(QStringLiteral(PK_OFFLINE_ACTION_FILENAME)); + connect(watcher, &QFileSystemWatcher::fileChanged, this, &PackageKitNotifier::nowNeedsReboot); + + QTimer::singleShot(100, this, [this]() { + if (QFile::exists(QStringLiteral(PK_OFFLINE_ACTION_FILENAME))) + nowNeedsReboot(); + }); +} + +PackageKitNotifier::~PackageKitNotifier() +{ +} + +void PackageKitNotifier::checkOfflineUpdates() +{ + if (!QFile::exists(QStringLiteral(PK_OFFLINE_RESULTS_FILENAME))) { + return; + } + qCDebug(LIBDISCOVER_BACKEND_LOG) << "found offline update results at " << PK_OFFLINE_RESULTS_FILENAME; + + KDesktopFile file(QStringLiteral(PK_OFFLINE_RESULTS_FILENAME)); + KConfigGroup group(&file, PK_OFFLINE_RESULTS_GROUP); + + const bool success = group.readEntry("Success", false); + const QString packagesJoined = group.readEntry("Packages"); + const auto packages = packagesJoined.splitRef(QLatin1Char(',')); + const bool isMobile = QByteArrayList{"1", "true"}.contains(qgetenv("QT_QUICK_CONTROLS_MOBILE")); + const QString errorCode = group.readEntry("ErrorCode"); + static QSet allowedAlreadyInstalled = { + QStringLiteral("package-already-installed"), + QStringLiteral("all-packages-already-installed"), + }; + if (!success && !allowedAlreadyInstalled.contains(errorCode)) { + const QString errorDetails = group.readEntry("ErrorDetails"); + + auto *notification = new KNotification(QStringLiteral("OfflineUpdateFailed"), KNotification::Persistent); + notification->setIconName(QStringLiteral("dialog-error")); + notification->setTitle(i18n("Failed Offline Update")); + notification->setText(i18np("Failed to update %1 package\n%2", "Failed to update %1 packages\n%2", packages.count(), errorDetails)); + notification->setActions(QStringList{i18nc("@action:button", "Open Discover"), i18nc("@action:button", "Repair System")}); + notification->setComponentName(QStringLiteral("discoverabstractnotifier")); + + connect(notification, &KNotification::action1Activated, this, []() { + QProcess::startDetached(QStringLiteral("plasma-discover"), QStringList()); + }); + connect(notification, &KNotification::action2Activated, this, [this]() { + qInfo() << "Repairing system"; + auto trans = PackageKit::Daemon::global()->repairSystem(); + KNotification::event(QStringLiteral("OfflineUpdateRepairStarted"), + i18n("Repairing failed offline update"), + {}, + {}, + KNotification::CloseOnTimeout, + QStringLiteral("discoverabstractnotifier")); + + connect(trans, &PackageKit::Transaction::errorCode, this, [](PackageKit::Transaction::Error /*error*/, const QString &details) { + KNotification::event(QStringLiteral("OfflineUpdateRepairFailed"), + i18n("Repair Failed"), + xi18nc("@info", "%1Please report this error to your distribution.", details), + {}, + KNotification::Persistent, + QStringLiteral("discoverabstractnotifier")); + }); + connect(trans, &PackageKit::Transaction::finished, this, [](PackageKit::Transaction::Exit status, uint runtime) { + qInfo() << "repair finished!" << status << runtime; + if (status == PackageKit::Transaction::ExitSuccess) { + PackageKit::Daemon::global()->offline()->clearResults(); + + KNotification::event(QStringLiteral("OfflineUpdateRepairSuccessful"), + i18n("Repaired Successfully"), + {}, + {}, + KNotification::CloseOnTimeout, + QStringLiteral("discoverabstractnotifier")); + } + }); + + // No matter what happened, clean up the results file if it still exists + // because at this point, there's nothing anyone can do with it + if (QFile::exists(QStringLiteral(PK_OFFLINE_RESULTS_FILENAME))) { + qDebug() << "Removed offline results file"; + PackageKit::Daemon::global()->offline()->clearResults(); + } + }); + + notification->sendEvent(); + } else { + // Apparently on mobile, people are accustomed to seeing notifications + // indicating success when a system update succeeded + if (isMobile) { + KNotification *notification = new KNotification(QStringLiteral("OfflineUpdateSuccessful")); + notification->setIconName(QStringLiteral("system-software-update")); + notification->setTitle(i18n("Offline Updates")); + notification->setText(i18np("Successfully updated %1 package", "Successfully updated %1 packages", packages.count())); + notification->setActions(QStringList{i18nc("@action:button", "Open Discover")}); + notification->setComponentName(QStringLiteral("discoverabstractnotifier")); + + connect(notification, &KNotification::action1Activated, this, []() { + QProcess::startDetached(QStringLiteral("plasma-discover"), QStringList()); + }); + + notification->sendEvent(); + } + PackageKit::Daemon::global()->offline()->clearResults(); + } +} + +void PackageKitNotifier::recheckSystemUpdateNeeded() +{ + static bool first = true; + if (first) { + // PKQt will Q_EMIT these signals when it starts (bug?) and would trigger the system recheck before we've ever checked at all + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::networkStateChanged, this, &PackageKitNotifier::recheckSystemUpdateNeeded); + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitNotifier::recheckSystemUpdateNeeded); + first = false; + } + + if (PackageKit::Daemon::global()->offline()->updateTriggered()) + return; + + m_recheckTimer->start(); +} + +void PackageKitNotifier::recheckSystemUpdate() +{ + if (PackageKit::Daemon::global()->isRunning()) { + PackageKit::Daemon::getUpdates(); + } +} + +void PackageKitNotifier::setupGetUpdatesTransaction(PackageKit::Transaction *trans) +{ + qCDebug(LIBDISCOVER_BACKEND_LOG) << "using..." << trans << trans->tid().path(); + + trans->setProperty("normalUpdates", 0); + trans->setProperty("securityUpdates", 0); + connect(trans, &PackageKit::Transaction::package, this, &PackageKitNotifier::package); + connect(trans, &PackageKit::Transaction::finished, this, &PackageKitNotifier::finished); +} + +void PackageKitNotifier::package(PackageKit::Transaction::Info info, const QString & /*packageID*/, const QString & /*summary*/) +{ + PackageKit::Transaction *trans = qobject_cast(sender()); + + switch (info) { + case PackageKit::Transaction::InfoBlocked: + break; // skip, we ignore blocked updates + case PackageKit::Transaction::InfoSecurity: + trans->setProperty("securityUpdates", trans->property("securityUpdates").toInt() + 1); + break; + default: + trans->setProperty("normalUpdates", trans->property("normalUpdates").toInt() + 1); + break; + } +} + +void PackageKitNotifier::finished(PackageKit::Transaction::Exit /*exit*/, uint) +{ + const PackageKit::Transaction *trans = qobject_cast(sender()); + + const uint normalUpdates = trans->property("normalUpdates").toInt(); + const uint securityUpdates = trans->property("securityUpdates").toInt(); + const bool changed = normalUpdates != m_normalUpdates || securityUpdates != m_securityUpdates; + + m_normalUpdates = normalUpdates; + m_securityUpdates = securityUpdates; + + if (changed) { + Q_EMIT foundUpdates(); + } +} + +bool PackageKitNotifier::hasUpdates() +{ + return m_normalUpdates > 0; +} + +bool PackageKitNotifier::hasSecurityUpdates() +{ + return m_securityUpdates > 0; +} + +void PackageKitNotifier::onDistroUpgrade(PackageKit::Transaction::DistroUpgrade /*type*/, const QString &name, const QString &description) +{ + auto a = new UpgradeAction(name, description, this); + connect(a, &UpgradeAction::triggered, this, [](const QString &name) { + PackageKit::Daemon::upgradeSystem(name, PackageKit::Transaction::UpgradeKindDefault); + }); + Q_EMIT foundUpgradeAction(a); +} + +void PackageKitNotifier::refreshDatabase() +{ + if (auto offline = PackageKit::Daemon::global()->offline(); + offline->updatePrepared() || offline->upgradePrepared() || offline->updateTriggered() || offline->upgradeTriggered()) { + return; + } + + for (const auto &t : m_transactions) { + auto role = t->role(); + if (role == PackageKit::Transaction::RoleUpdatePackages || role == PackageKit::Transaction::RoleUpgradeSystem) { + return; + } + } + + if (!m_refresher) { + m_refresher = PackageKit::Daemon::refreshCache(false); + connect(m_refresher.data(), &PackageKit::Transaction::finished, this, &PackageKitNotifier::recheckSystemUpdateNeeded); + } + + if (!m_distUpgrades && (PackageKit::Daemon::roles() & PackageKit::Transaction::RoleUpgradeSystem)) { + m_distUpgrades = PackageKit::Daemon::getDistroUpgrades(); + connect(m_distUpgrades, &PackageKit::Transaction::distroUpgrade, this, &PackageKitNotifier::onDistroUpgrade); + } +} + +QProcess *PackageKitNotifier::checkAptVariable(const QString &aptconfig, const QLatin1String &varname, const std::function &func) +{ + QProcess *process = new QProcess; + process->start(aptconfig, {QStringLiteral("dump")}); + connect(process, qOverload(&QProcess::finished), this, [func, process, varname](int code) { + if (code != 0) + return; + + QRegularExpression rx(QLatin1Char('^') + varname + QStringLiteral(" \"(.*?)\";?$"), QRegularExpression::CaseInsensitiveOption); + QTextStream stream(process); + QString line; + while (stream.readLineInto(&line)) { + const auto match = rx.match(line); + if (match.hasMatch()) { + func(match.capturedView(1)); + return; + } + } + func({}); + }); + connect(process, qOverload(&QProcess::finished), process, &QObject::deleteLater); + return process; +} + +void PackageKitNotifier::transactionListChanged(const QStringList &tids) +{ + if (PackageKit::Daemon::global()->offline()->updateTriggered()) + return; + + for (const auto &tid : tids) { + if (m_transactions.contains(tid)) + continue; + + auto t = new PackageKit::Transaction(QDBusObjectPath(tid)); + + connect(t, &PackageKit::Transaction::roleChanged, this, [this, t]() { + if (t->role() == PackageKit::Transaction::RoleGetUpdates) { + setupGetUpdatesTransaction(t); + } + }); + connect(t, &PackageKit::Transaction::requireRestart, this, &PackageKitNotifier::onRequireRestart); + connect(t, &PackageKit::Transaction::finished, this, [this, t]() { + auto restart = t->property("requireRestart"); + if (!restart.isNull()) { + auto restartEvent = PackageKit::Transaction::Restart(restart.toInt()); + if (restartEvent >= PackageKit::Transaction::RestartSession) { + nowNeedsReboot(); + } + } + m_transactions.remove(t->tid().path()); + t->deleteLater(); + }); + m_transactions.insert(tid, t); + } +} + +void PackageKitNotifier::nowNeedsReboot() +{ + if (!m_needsReboot) { + m_needsReboot = true; + Q_EMIT needsRebootChanged(); + } +} + +void PackageKitNotifier::onRequireRestart(PackageKit::Transaction::Restart type, const QString &packageID) +{ + PackageKit::Transaction *t = qobject_cast(sender()); + t->setProperty("requireRestart", qMax(t->property("requireRestart").toInt(), type)); + qCDebug(LIBDISCOVER_BACKEND_LOG) << "RESTART" << type << "is required for package" << packageID; +} diff --git a/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h new file mode 100644 index 0000000..f9bcd50 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#pragma once + +#include +#include +#include +#include +#include + +class QTimer; +class QProcess; + +class PackageKitNotifier : public BackendNotifierModule +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.discover.BackendNotifierModule") + Q_INTERFACES(BackendNotifierModule) +public: + explicit PackageKitNotifier(QObject *parent = nullptr); + ~PackageKitNotifier() override; + + bool hasUpdates() override; + bool hasSecurityUpdates() override; + void recheckSystemUpdateNeeded() override; + void refreshDatabase(); + bool needsReboot() const override + { + return m_needsReboot; + } + +private Q_SLOTS: + void package(PackageKit::Transaction::Info info, const QString &packageID, const QString &summary); + void finished(PackageKit::Transaction::Exit exit, uint); + void onRequireRestart(PackageKit::Transaction::Restart type, const QString &packageID); + void transactionListChanged(const QStringList &tids); + void onDistroUpgrade(PackageKit::Transaction::DistroUpgrade type, const QString &name, const QString &description); + +private: + void nowNeedsReboot(); + void recheckSystemUpdate(); + void checkOfflineUpdates(); + void setupGetUpdatesTransaction(PackageKit::Transaction *transaction); + QProcess *checkAptVariable(const QString &aptconfig, const QLatin1String &varname, const std::function &func); + + bool m_needsReboot = false; + uint m_securityUpdates; + uint m_normalUpdates; + QPointer m_refresher; + QPointer m_distUpgrades; + QTimer *m_recheckTimer; + + QHash m_transactions; +}; diff --git a/libdiscover/backends/PackageKitBackend/PackageKitResource.cpp b/libdiscover/backends/PackageKitBackend/PackageKitResource.cpp new file mode 100644 index 0000000..3f6464d --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitResource.cpp @@ -0,0 +1,497 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PackageKitResource.h" +#include "PackageKitBackend.h" +#include "PackageKitMessages.h" +#include "appstream/AppStreamUtils.h" +#include "config-paths.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(WITH_MARKDOWN) +extern "C" { +#include +} +#endif + +const QStringList PackageKitResource::m_objects({QStringLiteral("qrc:/qml/DependenciesButton.qml"), QStringLiteral("qrc:/qml/PackageKitPermissions.qml")}); + +PackageKitResource::PackageKitResource(QString packageName, QString summary, PackageKitBackend *parent) + : AbstractResource(parent) + , m_summary(std::move(summary)) + , m_name(std::move(packageName)) +{ + setObjectName(m_name); +} + +QString PackageKitResource::name() const +{ + return m_name; +} + +QString PackageKitResource::packageName() const +{ + return m_name; +} + +QStringList PackageKitResource::allPackageNames() const +{ + return {m_name}; +} + +QString PackageKitResource::availablePackageId() const +{ + // First we check if it's upgradeable and use this version to display + const QSet pkgids = backend()->upgradeablePackageId(this); + if (!pkgids.isEmpty()) + return *pkgids.constBegin(); + + const auto it = m_packages.constFind(PackageKit::Transaction::InfoAvailable); + if (it != m_packages.constEnd()) + return it->first(); + return installedPackageId(); +} + +QString PackageKitResource::installedPackageId() const +{ + const auto installed = m_packages[PackageKit::Transaction::InfoInstalled]; + return installed.isEmpty() ? QString() : installed.first(); +} + +QString PackageKitResource::comment() +{ + return m_summary; +} + +QString PackageKitResource::longDescription() +{ + fetchDetails(); + return m_details.description(); +} + +QUrl PackageKitResource::homepage() +{ + fetchDetails(); + return QUrl(m_details.url()); +} + +QVariant PackageKitResource::icon() const +{ + return QStringLiteral("applications-other"); +} + +static QMap s_translation = { + {"AGPL", "AGPL-3.0"}, + {"AGPL3", "AGPL-3.0"}, + {"Artistic2.0", "Artistic-2.0"}, + {"Apache", "Apache-2.0"}, + {"APACHE", "Apache-2.0"}, + {"CCPL", "CC0-1.0"}, + {"GPL2", "GPL-2.0"}, + {"GPL3", "GPL-3.0"}, + {"FDL1.2", "GFDL-1.2-only"}, + {"FDL1.3", "GFDL-1.3-only"}, + {"LGPL", "LGPL-2.1"}, + {"LGPL3", "LGPL-3.0"}, + {"MPL", "MPL-1.1"}, + {"MPL2", "MPL-2.0"}, + {"PerlArtistic", "Artistic-1.0-Perl"}, + {"PHP", "PHP-3.01"}, + {"PSF", "Python-2.0"}, + {"RUBY", "Ruby"}, + {"ZPL", "ZPL-2.1"}, +}; + +QJsonArray PackageKitResource::licenses() +{ + fetchDetails(); + + if (!m_details.license().isEmpty()) { + QString id = m_details.license(); + if (!AppStream::SPDX::isLicenseId(id)) { + auto spdxId = AppStream::SPDX::asSpdxId(id); + if (!spdxId.isEmpty()) { + id = spdxId; + } + } + + if (!AppStream::SPDX::isLicenseId(id)) { + id = s_translation.value(id, id); + } + return {AppStreamUtils::license(id)}; + } + + return {QJsonObject{{QStringLiteral("name"), {}}}}; +} + +QList PackageKitResource::addonsInformation() +{ + return QList(); +} + +QString PackageKitResource::availableVersion() const +{ + return PackageKit::Daemon::packageVersion(availablePackageId()); +} + +QString PackageKitResource::installedVersion() const +{ + return PackageKit::Daemon::packageVersion(installedPackageId()); +} + +quint64 PackageKitResource::size() +{ + fetchDetails(); + return m_details.size(); +} + +QString PackageKitResource::origin() const +{ + auto osRelease = AppStreamIntegration::global()->osRelease(); + + if (PackageKit::Daemon::backendName() == QStringLiteral("apt")) { + // Debian and its derivatives have a defined scheme for repository origins that we can parse, to + // guess a better origin name. + QString pkgid = availablePackageId(); + QString dataField = PackageKit::Daemon::packageData(pkgid); + // The "data" field of a package-id may contain a modifier such as "auto:" or "manual:", so + // we will need to strip that in case it exists to extract the actual origin. + // The data field may look like "auto:debian-bookworm-main" or "google_llc-stable-main", + // so we can set the OS name if we see it as origin prefix, and otherwise need to fall back + // to the origin string. + int i = dataField.indexOf(':'); + QString origin = i > 0? dataField.mid(i + 1) : dataField; + if (origin.startsWith(osRelease->id().toLower() + '-')) { + return osRelease->name(); + } else { + return origin.isEmpty()? i18n("Unknown Source") : origin; + } + } + + // PackageKit doesn't give us enough information to be able to distinguish + // between 3rd-party repos (which generally have human-readable names) and + // 1st-party distro repos (which generally name nonsense jargon names) which + // would allow us to substitute the distro name for only the nonsense repos; + // see https://github.com/PackageKit/PackageKit/issues/607 and + // https://bugs.kde.org/show_bug.cgi?id=465204. + // So for now always show the distro name. + return osRelease->name(); +} + +QString PackageKitResource::section() +{ + return QString(); +} + +AbstractResource::State PackageKitResource::state() +{ + if (backend()->isPackageNameUpgradeable(this)) + return Upgradeable; + else if (m_packages.contains(PackageKit::Transaction::InfoInstalled)) + return Installed; + else if (m_packages.contains(PackageKit::Transaction::InfoAvailable)) + return None; + else + return Broken; +} + +void PackageKitResource::addPackageId(PackageKit::Transaction::Info info, const QString &packageId, bool arch) +{ + auto oldState = state(); + if (arch) + m_packages[info].archPkgIds.append(packageId); + else + m_packages[info].nonarchPkgIds.append(packageId); + + if (oldState != state()) + Q_EMIT stateChanged(); + + Q_EMIT versionsChanged(); +} + +QStringList PackageKitResource::categories() +{ + return {QStringLiteral("Unknown")}; +} + +AbstractResource::Type PackageKitResource::type() const +{ + return Technical; +} + +void PackageKitResource::fetchDetails() +{ + const QString pkgid = availablePackageId(); + if (!m_details.isEmpty() || pkgid.isEmpty()) + return; + m_details.insert(QStringLiteral("fetching"), true); // we add an entry so it's not re-fetched. + + backend()->fetchDetails(pkgid); +} + +void PackageKitResource::failedFetchingDetails(PackageKit::Transaction::Error error, const QString &msg) +{ + qWarning() << "error fetching details" << error << msg; +} + +void PackageKitResource::setDependenciesCount(int deps) +{ + if (deps != m_dependenciesCount) { + m_dependenciesCount = deps; + Q_EMIT sizeChanged(); + } +} + +void PackageKitResource::setDetails(const PackageKit::Details &details) +{ + const bool ourDetails = details.packageId() == availablePackageId(); + if (!ourDetails) + return; + + if (m_details != details) { + const auto oldState = state(); + const auto oldSize = m_details.size(); + const auto oldLicense = m_details.license(); + const auto oldDescription = m_details.description(); + m_details = details; + + if (oldState != state()) + Q_EMIT stateChanged(); + + if (!backend()->isFetching()) + Q_EMIT backend()->resourcesChanged(this, {"size", "homepage", "license"}); + + if (oldSize != uint(size())) { + Q_EMIT sizeChanged(); + } + + if (oldLicense != m_details.license()) { + Q_EMIT licensesChanged(); + } + + if (oldDescription != m_details.description()) { + Q_EMIT longDescriptionChanged(); + } + } +} + +void PackageKitResource::fetchChangelog() +{ +} + +void PackageKitResource::fetchUpdateDetails() +{ + const auto pkgid = availablePackageId(); + if (pkgid.isEmpty()) { + auto a = new OneTimeAction( + [this] { + fetchUpdateDetails(); + }, + this); + connect(this, &PackageKitResource::stateChanged, a, &OneTimeAction::trigger); + return; + } + backend()->updateDetails().add(pkgid); +} + +static void addIfNotEmpty(const QString &title, const QString &content, QString &where) +{ + if (!content.isEmpty()) + where += QLatin1String("

    ") + title + QLatin1String(" ") + QString(content).replace(QLatin1Char('\n'), QLatin1String("
    ")) + + QLatin1String("

    "); +} + +QString PackageKitResource::joinPackages(const QStringList &pkgids, const QString &_sep, const QString &shadowPackage) +{ + QStringList ret; + for (const QString &pkgid : pkgids) { + const auto pkgname = PackageKit::Daemon::packageName(pkgid); + if (pkgname == shadowPackage) + ret += PackageKit::Daemon::packageVersion(pkgid); + else + ret += i18nc("package-name (version)", "%1 (%2)", pkgname, PackageKit::Daemon::packageVersion(pkgid)); + } + const QString sep = _sep.isEmpty() ? i18nc("comma separating package names", ", ") : _sep; + return ret.join(sep); +} + +static QStringList urlToLinks(const QStringList &urls) +{ + QStringList ret; + for (const QString &in : urls) + ret += QStringLiteral("%1").arg(in); + return ret; +} + +bool PackageKitResource::containsPackageId(const QString &pkgid) const +{ + return kContains(m_packages, [pkgid](const auto &x) { + return x.archPkgIds.contains(pkgid) || x.nonarchPkgIds.contains(pkgid); + }); +} + +void PackageKitResource::updateDetail(const QString &packageID, + const QStringList & /*updates*/, + const QStringList &obsoletes, + const QStringList &vendorUrls, + const QStringList & /*bugzillaUrls*/, + const QStringList & /*cveUrls*/, + PackageKit::Transaction::Restart restart, + const QString &_updateText, + const QString & /*changelog*/, + PackageKit::Transaction::UpdateState state, + const QDateTime & /*issued*/, + const QDateTime & /*updated*/) +{ +#if defined(WITH_MARKDOWN) + const QByteArray xx = _updateText.toUtf8(); + MMIOT *markdownHandle = mkd_string(xx.constData(), _updateText.size(), 0); + +#ifdef MARKDOWN3 + mkd_flag_t *flags = mkd_flags(); + mkd_set_flag_num(flags, MKD_FENCEDCODE); + mkd_set_flag_num(flags, MKD_GITHUBTAGS); + mkd_set_flag_num(flags, MKD_AUTOLINK); + if (!mkd_compile(markdownHandle, flags)) { +#else + if (!mkd_compile(markdownHandle, MKD_FENCEDCODE | MKD_GITHUBTAGS | MKD_AUTOLINK)) { +#endif + m_changelog = _updateText; + } else { + char *htmlDocument; + const int size = mkd_document(markdownHandle, &htmlDocument); + + m_changelog = QString::fromUtf8(htmlDocument, size); + } + mkd_cleanup(markdownHandle); +#ifdef MARKDOWN3 + mkd_free_flags(flags); +#endif + +#else + m_changelog = _updateText; +#endif + + const auto name = PackageKit::Daemon::packageName(packageID); + + QString info; + addIfNotEmpty(i18n("Obsoletes:"), joinPackages(obsoletes, {}, name), info); + addIfNotEmpty(i18n("Release Notes:"), changelog(), info); + addIfNotEmpty(i18n("Update State:"), PackageKitMessages::updateStateMessage(state), info); + addIfNotEmpty(i18n("Restart:"), PackageKitMessages::restartMessage(restart), info); + + if (!vendorUrls.isEmpty()) + addIfNotEmpty(i18n("Vendor:"), urlToLinks(vendorUrls).join(QLatin1String(", ")), info); + + Q_EMIT changelogFetched(info); +} + +PackageKitBackend *PackageKitResource::backend() const +{ + return qobject_cast(parent()); +} + +QString PackageKitResource::sizeDescription() +{ + if (m_dependenciesCount < 0) { + fetchDetails(); + fetchDependencies(); + } + + if (m_dependenciesCount <= 0) + return AbstractResource::sizeDescription(); + else + return i18np("%2 (plus %1 dependency)", "%2 (plus %1 dependencies)", m_dependenciesCount, AbstractResource::sizeDescription()); +} + +QString PackageKitResource::sourceIcon() const +{ + return QStringLiteral("package-x-generic"); +} + +void PackageKitResource::fetchDependencies() +{ + const auto id = isInstalled() ? installedPackageId() : availablePackageId(); + if (id.isEmpty()) + return; + m_dependenciesCount = 0; + + auto packageDependencies = QSharedPointer::create(); + + auto trans = PackageKit::Daemon::dependsOn(id); + connect(trans, &PackageKit::Transaction::errorCode, this, [this](PackageKit::Transaction::Error, const QString &message) { + qWarning() << "Transaction error: " << message << sender(); + }); + connect(trans, + &PackageKit::Transaction::package, + this, + [packageDependencies](PackageKit::Transaction::Info info, const QString &packageID, const QString &summary) { + (*packageDependencies) + .append(QJsonObject{{QStringLiteral("packageName"), PackageKit::Daemon::packageName(packageID)}, + {QStringLiteral("packageInfo"), PackageKitMessages::info(info)}, + {QStringLiteral("packageDescription"), summary}}); + }); + connect(trans, &PackageKit::Transaction::finished, this, [this, packageDependencies](PackageKit::Transaction::Exit /*status*/) { + std::sort(packageDependencies->begin(), packageDependencies->end(), [](QJsonValue a, QJsonValue b) { + const auto objA = a.toObject(), objB = b.toObject(); + return objA[QLatin1String("packageInfo")].toString() < objB[QLatin1String("packageInfo")].toString() + || (objA[QLatin1String("packageInfo")].toString() == objB[QLatin1String("packageInfo")].toString() + && objA[QLatin1String("packageName")].toString() < objB[QLatin1String("packageName")].toString()); + }); + + Q_EMIT dependenciesFound(*packageDependencies); + setDependenciesCount(packageDependencies->size()); + }); +} + +bool PackageKitResource::extendsItself() const +{ + const auto extendsResources = backend()->resourcesByPackageNames>(extends()); + if (extendsResources.isEmpty()) + return false; + + const auto ourPackageNames = allPackageNames(); + for (auto r : extendsResources) { + PackageKitResource *pkres = qobject_cast(r); + if (pkres->allPackageNames() != ourPackageNames) + return false; + } + return true; +} + +void PackageKitResource::runService(KService::Ptr service) const +{ + auto *job = new KIO::ApplicationLauncherJob(service); + connect(job, &KJob::finished, this, [this, service](KJob *job) { + if (job->error()) { + Q_EMIT backend()->passiveMessage(i18n("Failed to start '%1': %2", service->name(), job->errorString())); + } + }); + + job->start(); +} + +bool PackageKitResource::isCritical() const +{ + return false; +} diff --git a/libdiscover/backends/PackageKitBackend/PackageKitResource.h b/libdiscover/backends/PackageKitBackend/PackageKitResource.h new file mode 100644 index 0000000..d6d3d87 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitResource.h @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +class PackageKitBackend; + +class PackageKitResource : public AbstractResource +{ + Q_OBJECT + Q_PROPERTY(QStringList objects MEMBER m_objects CONSTANT) +public: + explicit PackageKitResource(QString packageName, QString summary, PackageKitBackend *parent); + QString packageName() const override; + QString name() const override; + QString comment() override; + QString longDescription() override; + QUrl homepage() override; + QVariant icon() const override; + QStringList categories() override; + QJsonArray licenses() override; + QString origin() const override; + QString section() override; + AbstractResource::Type type() const override; + quint64 size() override; + void fetchChangelog() override; + void fetchUpdateDetails() override; + + QList addonsInformation() override; + State state() override; + + QString installedVersion() const override; + QString availableVersion() const override; + QString author() const override + { + return {}; + } + virtual QStringList allPackageNames() const; + QString installedPackageId() const; + QString availablePackageId() const; + + void clearPackageIds() + { + m_packages.clear(); + } + + PackageKitBackend *backend() const; + + static QString joinPackages(const QStringList &pkgids, const QString &_sep, const QString &shadowPackageName); + + /** + * Critical packages are those that might render an installation unusable if removed + */ + virtual bool isCritical() const; + + void invokeApplication() const override + { + } + bool canExecute() const override + { + return false; + } + + QString sizeDescription() override; + void setDependenciesCount(int count); + + QString sourceIcon() const override; + + QDate releaseDate() const override + { + return {}; + } + + virtual QString changelog() const + { + return m_changelog; + } + + bool extendsItself() const; + + void runService(KService::Ptr service) const; + bool containsPackageId(const QString &pkgid) const; + +Q_SIGNALS: + void dependenciesFound(const QJsonArray &dependencies); + +public Q_SLOTS: + void addPackageId(PackageKit::Transaction::Info info, const QString &packageId, bool arch); + void setDetails(const PackageKit::Details &details); + + void updateDetail(const QString &packageID, + const QStringList &updates, + const QStringList &obsoletes, + const QStringList &vendorUrls, + const QStringList &bugzillaUrls, + const QStringList &cveUrls, + PackageKit::Transaction::Restart restart, + const QString &updateText, + const QString &changelog, + PackageKit::Transaction::UpdateState state, + const QDateTime &issued, + const QDateTime &updated); + + void failedFetchingDetails(PackageKit::Transaction::Error, const QString &msg); + +protected: + PackageKit::Details m_details; + +private: + void fetchDependencies(); + /** fetches details individually, it's better if done in batch, like for updates */ + virtual void fetchDetails(); + + struct Ids { + QVector archPkgIds; + QVector nonarchPkgIds; + + QString first() const + { + return !archPkgIds.isEmpty() ? archPkgIds.first() : nonarchPkgIds.first(); + } + + bool isEmpty() const + { + return archPkgIds.isEmpty() && nonarchPkgIds.isEmpty(); + } + }; + QMap m_packages; + const QString m_summary; + const QString m_name; + QString m_changelog; + int m_dependenciesCount = -1; + static const QStringList m_objects; +}; diff --git a/libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.cpp b/libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.cpp new file mode 100644 index 0000000..95a631a --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.cpp @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PackageKitSourcesBackend.h" +#include "PackageKitBackend.h" +#include "config-paths.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class PKSourcesModel : public QStandardItemModel +{ +public: + PKSourcesModel(PackageKitSourcesBackend *backend) + : QStandardItemModel(backend) + , m_backend(backend) + { + } + + bool setData(const QModelIndex &index, const QVariant &value, int role) override + { + auto item = itemFromIndex(index); + if (!item) + return false; + + switch (role) { + case Qt::CheckStateRole: { + auto transaction = PackageKit::Daemon::global()->repoEnable(item->data(AbstractSourcesBackend::IdRole).toString(), value.toInt() == Qt::Checked); + connect(transaction, &PackageKit::Transaction::errorCode, m_backend, &PackageKitSourcesBackend::transactionError); + return true; + } + } + item->setData(value, role); + return true; + } + +private: + PackageKitSourcesBackend *m_backend; +}; + +static DiscoverAction *createActionForService(const QString &servicePath, PackageKitSourcesBackend *backend) +{ + DiscoverAction *action = new DiscoverAction(backend); + KDesktopFile parser(servicePath); + action->setIconName(parser.readIcon()); + action->setText(parser.readName()); + action->setToolTip(parser.readComment()); + QObject::connect(action, &DiscoverAction::triggered, action, [backend, servicePath]() { + KService::Ptr service = KService::serviceByStorageId(servicePath); + if (!service) { + qWarning() << "Failed to find service" << servicePath; + return; + } + + auto *job = new KIO::ApplicationLauncherJob(service); + QObject::connect(job, &KJob::finished, backend, [backend, service](KJob *job) { + if (job->error()) { + Q_EMIT backend->passiveMessage(i18n("Failed to start '%1': %2", service->name(), job->errorString())); + } + }); + job->start(); + }); + return action; +} + +PackageKitSourcesBackend::PackageKitSourcesBackend(AbstractResourcesBackend *parent) + : AbstractSourcesBackend(parent) + , m_sources(new PKSourcesModel(this)) +{ + connect(PackageKit::Daemon::global(), &PackageKit::Daemon::repoListChanged, this, &PackageKitSourcesBackend::resetSources); + connect(SourcesModel::global(), &SourcesModel::showingNow, this, &PackageKitSourcesBackend::resetSources); + + // Kubuntu-based + auto addNativeSourcesManager = [this](const QString &file) { + auto service = PackageKitBackend::locateService(file); + if (!service.isEmpty()) + m_actions += QVariant::fromValue(createActionForService(service, this)); + }; + + // New Ubuntu + addNativeSourcesManager(QStringLiteral("software-properties-qt.desktop")); + + // Old Ubuntu + addNativeSourcesManager(QStringLiteral("software-properties-kde.desktop")); + + // OpenSuse + addNativeSourcesManager(QStringLiteral("YaST2/sw_source.desktop")); +} + +QString PackageKitSourcesBackend::idDescription() +{ + return i18n("Repository URL:"); +} + +QStandardItem *PackageKitSourcesBackend::findItemForId(const QString &id) const +{ + for (int i = 0, c = m_sources->rowCount(); i < c; ++i) { + auto it = m_sources->item(i); + if (it->data(AbstractSourcesBackend::IdRole).toString() == id) + return it; + } + return nullptr; +} + +void PackageKitSourcesBackend::addRepositoryDetails(const QString &id, const QString &description, bool enabled) +{ + bool add = false; + QStandardItem *item = findItemForId(id); + + if (!item) { + item = new QStandardItem(description); + if (PackageKit::Daemon::backendName() == QLatin1String("aptcc")) { + QRegularExpression exp(QStringLiteral("^/etc/apt/sources.list.d/(.+?).list:.*")); + + auto matchIt = exp.globalMatch(id); + if (matchIt.hasNext()) { + auto match = matchIt.next(); + item->setData(match.captured(1), Qt::ToolTipRole); + } + } + item->setCheckable(PackageKit::Daemon::roles() & PackageKit::Transaction::RoleRepoEnable); + add = true; + } + item->setData(id, IdRole); + item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); + item->setEnabled(true); + + if (add) + m_sources->appendRow(item); +} + +QAbstractItemModel *PackageKitSourcesBackend::sources() +{ + return m_sources; +} + +bool PackageKitSourcesBackend::addSource(const QString & /*id*/) +{ + return false; +} + +bool PackageKitSourcesBackend::removeSource(const QString &id) +{ + auto transaction = PackageKit::Daemon::global()->repoRemove(id, false); + connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitSourcesBackend::transactionError); + return false; +} + +QVariantList PackageKitSourcesBackend::actions() const +{ + return m_actions; +} + +void PackageKitSourcesBackend::resetSources() +{ + disconnect(SourcesModel::global(), &SourcesModel::showingNow, this, &PackageKitSourcesBackend::resetSources); + for (int i = 0, c = m_sources->rowCount(); i < c; ++i) { + m_sources->item(i, 0)->setEnabled(false); + } + auto transaction = PackageKit::Daemon::global()->getRepoList(); + connect(transaction, &PackageKit::Transaction::repoDetail, this, &PackageKitSourcesBackend::addRepositoryDetails); + connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitSourcesBackend::transactionError); + connect(transaction, &PackageKit::Transaction::finished, this, [this] { + for (int i = 0; i < m_sources->rowCount();) { + if (!m_sources->item(i, 0)->isEnabled()) { + m_sources->removeRow(i); + } else { + ++i; + } + } + }); +} + +void PackageKitSourcesBackend::transactionError(PackageKit::Transaction::Error error, const QString &message) +{ + Q_EMIT passiveMessage(message); + qWarning() << "Transaction error: " << error << message << sender(); +} diff --git a/libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.h b/libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.h new file mode 100644 index 0000000..37b7dd3 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +class QStandardItem; +class PKSourcesModel; + +class PackageKitSourcesBackend : public AbstractSourcesBackend +{ + Q_OBJECT +public: + PackageKitSourcesBackend(AbstractResourcesBackend *parent); + + QString idDescription() override; + + bool supportsAdding() const override + { + return false; + } + bool addSource(const QString &id) override; + bool removeSource(const QString &id) override; + + QAbstractItemModel *sources() override; + QVariantList actions() const override; + + void transactionError(PackageKit::Transaction::Error, const QString &message); + +private: + void resetSources(); + void addRepositoryDetails(const QString &id, const QString &description, bool enabled); + QStandardItem *findItemForId(const QString &id) const; + + PKSourcesModel *m_sources; + QVariantList m_actions; +}; diff --git a/libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp b/libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp new file mode 100644 index 0000000..276fc50 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp @@ -0,0 +1,810 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#include "PackageKitUpdater.h" +#include "PackageKitMessages.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "libdiscover_backend_debug.h" +#include "pk-offline-private.h" +#include "utils.h" + +int percentageWithStatus(PackageKit::Transaction::Status status, uint percentage) +{ + const auto was = percentage; + if (status != PackageKit::Transaction::StatusUnknown) { + static const QMap statuses = { + {PackageKit::Transaction::Status::StatusDownload, 0}, + {PackageKit::Transaction::Status::StatusInstall, 1}, + {PackageKit::Transaction::Status::StatusRemove, 1}, + {PackageKit::Transaction::Status::StatusLoadingCache, 1}, + {PackageKit::Transaction::Status::StatusUpdate, 1}, + }; + const auto idx = statuses.value(status, -1); + if (idx < 0) { + qCDebug(LIBDISCOVER_BACKEND_LOG) << "Status not present" << status << "among" << statuses.keys() << percentage; + return -1; + } + percentage = (idx * 100 + percentage) / 2 /*the maximum in statuses*/; + } + qCDebug(LIBDISCOVER_BACKEND_LOG) << "reporting progress with status:" << status << percentage << was; + return percentage; +} + +static void kRemoveDuplicates(QJsonArray &input, std::function fetchKey) +{ + QSet ret; + for (auto it = input.begin(); it != input.end();) { + const auto key = fetchKey(*it); + if (!ret.contains(key)) { + ret << key; + ++it; + } else { + it = input.erase(it); + } + } +} + +class SystemUpgrade : public AbstractResource +{ + Q_OBJECT +public: + SystemUpgrade(PackageKitBackend *backend) + : AbstractResource(backend) + , m_backend(backend) + , m_updateSizeTimer(new QTimer(this)) + { + connect(m_backend, &AbstractResourcesBackend::resourceRemoved, this, [this](AbstractResource *res) { + m_resources.remove(res); + }); + + m_updateSizeTimer->setInterval(100); + m_updateSizeTimer->setSingleShot(true); + connect(m_updateSizeTimer, &QTimer::timeout, this, &SystemUpgrade::refreshResource); + } + + QString packageName() const override + { + return QStringLiteral("discover-offline-upgrade"); + } + QString name() const override + { + return i18n("System upgrade"); + } + QString comment() override + { + return upgradeText(); + } + QVariant icon() const override + { + return QStringLiteral("system-upgrade"); + } + bool canExecute() const override + { + return false; + } + void invokeApplication() const override + { + } + State state() override + { + return Upgradeable; + } + QStringList categories() override + { + return {}; + } + AbstractResource::Type type() const override + { + return Technical; + } + bool isRemovable() const override + { + return false; + } + + QVector withoutDuplicates() const + { + QVector ret; + QSet donePkgs; + for (auto res : qAsConst(m_resources)) { + PackageKitResource *app = qobject_cast(res); + QString pkgname = app->packageName(); + if (!donePkgs.contains(pkgname)) { + donePkgs.insert(pkgname); + ret += app; + } + } + return ret; + } + + quint64 size() override + { + quint64 ret = 0; + const auto resources = withoutDuplicates(); + for (auto res : resources) { + ret += res->size(); + } + return ret; + } + QJsonArray licenses() override + { + QJsonArray ret; + for (auto res : qAsConst(m_resources)) { + ret += res->licenses(); + } + kRemoveDuplicates(ret, [](const QJsonValueRef &val) -> QString { + return val.toObject()[QLatin1String("name")].toString(); + }); + return ret; + } + QString section() override + { + return {}; + } + QString origin() const override + { + return {}; + } + QString author() const override + { + return {}; + } + QList addonsInformation() override + { + return {}; + } + QString upgradeText() const override + { + return i18np("1 package will be upgraded", "%1 packages will be upgraded", withoutDuplicates().count()); + } + QString longDescription() override + { + QStringList changes; + const auto resources = withoutDuplicates(); + for (auto res : resources) { + const auto changelog = res->changelog(); + if (changelog.isEmpty()) { + changes += i18n("

    %1

    Upgrade to new version %2
    No release notes provided", res->packageName(), res->availableVersion()); + } else { + changes += i18n("

    %1

    Upgrade to new version %2
    Release notes:
    %3
    ", + res->packageName(), + res->availableVersion(), + changelog); + } + } + changes.sort(); + return changes.join(QString()); + } + void fetchChangelog() override + { + for (auto res : qAsConst(m_resources)) { + res->fetchUpdateDetails(); + } + Q_EMIT changelogFetched({}); + } + + QString installedVersion() const override + { + return i18n("Present"); + } + QString availableVersion() const override + { + return i18n("Future"); + } + QString sourceIcon() const override + { + return QStringLiteral("package-x-generic"); + } + QDate releaseDate() const override + { + return {}; + } + + QSet resources() const + { + return m_resources; + } + + QSet allPackageNames() const + { + QSet ret; + for (auto res : qAsConst(m_resources)) { + ret += kToSet(qobject_cast(res)->allPackageNames()); + } + return ret; + } + + void refreshResource() + { + Q_EMIT m_backend->resourcesChanged(this, {"size", "license"}); + Q_EMIT updateSizeChanged(); + } + + void setCandidates(const QSet &candidates) + { + const auto toDisconnect = (m_resources - candidates); + for (auto res : toDisconnect) { + disconnect(res, &AbstractResource::sizeChanged, this, &SystemUpgrade::startIfStopped); + disconnect(res, &AbstractResource::changelogFetched, this, &SystemUpgrade::startIfStopped); + } + + const auto newCandidates = (candidates - m_resources); + m_resources = candidates; + for (auto res : newCandidates) { + connect(res, &AbstractResource::sizeChanged, this, &SystemUpgrade::startIfStopped); + connect(res, &AbstractResource::changelogFetched, this, &SystemUpgrade::startIfStopped); + } + } + + void startIfStopped() + { + if (!m_updateSizeTimer->isActive()) { + m_updateSizeTimer->start(); + } + } + +Q_SIGNALS: + void updateSizeChanged(); + +private: + QSet m_resources; + PackageKitBackend *const m_backend; + QTimer *m_updateSizeTimer; +}; + +PackageKitUpdater::PackageKitUpdater(PackageKitBackend *parent) + : AbstractBackendUpdater(parent) + , m_transaction(nullptr) + , m_backend(parent) + , m_isCancelable(false) + , m_isProgressing(false) + , m_percentage(0) + , m_lastUpdate() + , m_upgrade(new SystemUpgrade(m_backend)) +{ + fetchLastUpdateTime(); +} + +PackageKitUpdater::~PackageKitUpdater() +{ +} + +void PackageKitUpdater::prepare() +{ + if (PackageKit::Daemon::global()->offline()->updateTriggered()) { + m_toUpgrade.clear(); + m_allUpgradeable.clear(); + enableNeedsReboot(); + return; + } + + if (QFile::exists(QStringLiteral(PK_OFFLINE_RESULTS_FILENAME))) { + qDebug() << "Removed offline results file"; + PackageKit::Daemon::global()->offline()->clearResults(); + } + + Q_ASSERT(!m_transaction); + const auto candidates = m_backend->upgradeablePackages(); + if (useOfflineUpdates() && !candidates.isEmpty()) { + m_upgrade->setCandidates(candidates); + + m_toUpgrade = {m_upgrade}; + connect(m_upgrade, &SystemUpgrade::updateSizeChanged, this, &PackageKitUpdater::checkFreeSpace); + } else { + m_toUpgrade = candidates; + } + + checkFreeSpace(); + m_allUpgradeable = m_toUpgrade; +} + +void PackageKitUpdater::checkFreeSpace() +{ + auto j = KIO::fileSystemFreeSpace(QUrl::fromLocalFile("/usr")); + connect(j, &KIO::FileSystemFreeSpaceJob::result, this, [this](KIO::Job * /*job*/, KIO::filesize_t /*size*/, KIO::filesize_t available) { + if (available < updateSize()) { + setErrorMessage(i18nc("@info:status %1 is a formatted disk space string e.g. '240 MiB'", + "Not enough space to perform the update; only %1 of space are available.", + KFormat().formatByteSize(available))); + } + }); +} + +void PackageKitUpdater::setupTransaction(PackageKit::Transaction::TransactionFlags flags) +{ + m_packagesModified.clear(); + auto pkgs = involvedPackages(m_toUpgrade).values(); + pkgs.sort(); + m_transaction = PackageKit::Daemon::updatePackages(pkgs, flags); + m_isCancelable = m_transaction->allowCancel(); + cancellableChanged(); + + connect(m_transaction.data(), &PackageKit::Transaction::finished, this, &PackageKitUpdater::finished); + connect(m_transaction.data(), &PackageKit::Transaction::package, this, &PackageKitUpdater::packageResolved); + connect(m_transaction.data(), &PackageKit::Transaction::errorCode, this, &PackageKitUpdater::errorFound); + connect(m_transaction.data(), &PackageKit::Transaction::mediaChangeRequired, this, &PackageKitUpdater::mediaChange); + connect(m_transaction.data(), &PackageKit::Transaction::eulaRequired, this, &PackageKitUpdater::eulaRequired); + connect(m_transaction.data(), &PackageKit::Transaction::repoSignatureRequired, this, &PackageKitUpdater::repoSignatureRequired); + connect(m_transaction.data(), &PackageKit::Transaction::allowCancelChanged, this, &PackageKitUpdater::cancellableChanged); + connect(m_transaction.data(), &PackageKit::Transaction::percentageChanged, this, &PackageKitUpdater::percentageChanged); + connect(m_transaction.data(), &PackageKit::Transaction::itemProgress, this, &PackageKitUpdater::itemProgress); + connect(m_transaction.data(), &PackageKit::Transaction::speedChanged, this, [this] { + Q_EMIT downloadSpeedChanged(downloadSpeed()); + }); + if (m_toUpgrade.contains(m_upgrade)) { + connect(m_transaction, &PackageKit::Transaction::percentageChanged, this, [this] { + if (m_transaction->status() == PackageKit::Transaction::StatusDownload) { + Q_EMIT resourceProgressed(m_upgrade, m_transaction->percentage(), Downloading); + } + }); + } +} + +QSet PackageKitUpdater::packagesForPackageId(const QSet &pkgids) const +{ + const auto packages = kTransform>(pkgids, [](const QString &pkgid) { + return PackageKit::Daemon::packageName(pkgid); + }); + + QSet ret; + for (AbstractResource *res : qAsConst(m_allUpgradeable)) { + if (auto upgrade = dynamic_cast(res)) { + if (packages.contains(upgrade->allPackageNames())) { + ret += upgrade; + } + continue; + } + + PackageKitResource *pres = qobject_cast(res); + if (packages.contains(kToSet(pres->allPackageNames()))) { + ret.insert(res); + } + } + + return ret; +} + +QSet PackageKitUpdater::involvedPackages(const QSet &packages) const +{ + QSet packageIds; + packageIds.reserve(packages.size()); + for (AbstractResource *res : packages) { + if (SystemUpgrade *upgrade = dynamic_cast(res)) { + packageIds = involvedPackages(upgrade->resources()); + continue; + } + + PackageKitResource *app = qobject_cast(res); + const QSet ids = m_backend->upgradeablePackageId(app); + if (ids.isEmpty()) { + qWarning() << "no upgradeablePackageId for" << app; + continue; + } + + packageIds.unite(ids); + } + return packageIds; +} + +void PackageKitUpdater::processProceedFunction() +{ + auto t = m_proceedFunctions.takeFirst()(); + connect(t, &PackageKit::Transaction::finished, this, [this](PackageKit::Transaction::Exit status) { + if (status != PackageKit::Transaction::Exit::ExitSuccess) { + qWarning() << "transaction failed" << sender() << status; + cancel(); + return; + } + + if (!m_proceedFunctions.isEmpty()) { + processProceedFunction(); + } else { + start(); + } + }); +} + +void PackageKitUpdater::proceed() +{ + if (!m_proceedFunctions.isEmpty()) + processProceedFunction(); + else if (useOfflineUpdates()) + setupTransaction(PackageKit::Transaction::TransactionFlagOnlyTrusted | PackageKit::Transaction::TransactionFlagOnlyDownload); + else + setupTransaction(PackageKit::Transaction::TransactionFlagOnlyTrusted); +} + +bool PackageKitUpdater::useOfflineUpdates() const +{ + return m_useOfflineUpdates || qEnvironmentVariableIntValue("PK_OFFLINE_UPDATE"); +} + +void PackageKitUpdater::setOfflineUpdates(bool use) +{ + m_useOfflineUpdates = use; +} + +void PackageKitUpdater::start() +{ + Q_ASSERT(!isProgressing()); + + setupTransaction(PackageKit::Transaction::TransactionFlagSimulate); + setProgressing(true); + + if (useOfflineUpdates()) { + enableNeedsReboot(); + } +} + +void PackageKitUpdater::finished(PackageKit::Transaction::Exit exit, uint /*time*/) +{ + // qCDebug(LIBDISCOVER_BACKEND_LOG) << "update finished!" << exit << time; + if (!m_proceedFunctions.isEmpty()) + return; + const bool cancel = exit == PackageKit::Transaction::ExitCancelled; + const bool simulate = m_transaction->transactionFlags() & PackageKit::Transaction::TransactionFlagSimulate; + + disconnect(m_transaction, nullptr, this, nullptr); + m_transaction = nullptr; + + if (!cancel && simulate) { + auto toremoveOrig = m_packagesModified.value(PackageKit::Transaction::InfoRemoving); + auto toremove = toremoveOrig; + auto toinstall = QStringList() << m_packagesModified.value(PackageKit::Transaction::InfoInstalling) + << m_packagesModified.value(PackageKit::Transaction::InfoUpdating); + + // some backends will treat upgrades as removal + install, which makes for terrible error messages. + for (auto it = toremove.begin(), itEnd = toremove.end(); it != itEnd;) { + const QString name = PackageKit::Transaction::packageName(*it); + auto itInstall = std::find_if(toinstall.begin(), toinstall.end(), [&](const QString &pkgid) { + return name == PackageKit::Transaction::packageName(pkgid); + }); + if (itInstall != toinstall.end()) { + toinstall.erase(itInstall); + it = toremove.erase(it); + } else { + ++it; + } + }; + + if (PackageKit::Daemon::backendName() == "dnf") { + // Fedora has some packages that it uninstalls then eventually creates on its own. No need to + // notify about these. + toremove = kFilter(toremove, [](const QString &pkgid) { + return !PackageKit::Transaction::packageName(pkgid).startsWith(QLatin1String("kmod")); + }); + } + + if (!toremove.isEmpty()) { + QStringList criticals; + for (const auto &pkgid : std::as_const(toremove)) { + auto res = kFilter>(m_backend->resourcesByPackageName(pkgid), [](AbstractResource *res) { + return static_cast(res)->isCritical(); + }); + criticals << kTransform(res, [](AbstractResource *a) { + return a->name(); + }); + if (!criticals.isEmpty()) { + break; + } + } + + if (!criticals.isEmpty()) { + const QString msg = i18n( + "This update cannot be completed as it would remove the following software which is critical to the system's operation:" + "
    • %1
    " + "If you believe this is an error, please report it as a bug to the packagers of your distribution.", + criticals.constFirst()); + Q_EMIT distroErrorMessage(msg); + } else { + Q_EMIT proceedRequest( + i18n("Packages to remove"), + i18n("The following packages will be removed by the update:
    • %1

    in order to install:
    • %2
    ", + PackageKitResource::joinPackages(toremove, QStringLiteral("
  • "), {}), + PackageKitResource::joinPackages(toinstall, QStringLiteral("
  • "), {}))); + } + } else { + proceed(); + } + return; + } + + setProgressing(false); + m_backend->fetchUpdates(); + fetchLastUpdateTime(); + + if (useOfflineUpdates() && exit == PackageKit::Transaction::ExitSuccess) { + PackageKit::Daemon::global()->offline()->trigger(PackageKit::Offline::ActionReboot); + enableReadyToReboot(); + } +} + +void PackageKitUpdater::cancellableChanged() +{ + if (m_isCancelable != m_transaction->allowCancel()) { + m_isCancelable = m_transaction->allowCancel(); + Q_EMIT cancelableChanged(m_isCancelable); + } +} + +void PackageKitUpdater::percentageChanged() +{ + const auto actualPercentage = percentageWithStatus(m_transaction->status(), m_transaction->percentage()); + if (actualPercentage >= 0 && m_percentage != actualPercentage) { + m_percentage = actualPercentage; + Q_EMIT progressChanged(m_percentage); + } +} + +bool PackageKitUpdater::hasUpdates() const +{ + return m_backend->updatesCount() > 0; +} + +qreal PackageKitUpdater::progress() const +{ + return m_percentage; +} + +void PackageKitUpdater::removeResources(const QList &apps) +{ + const QSet pkgs = involvedPackages(kToSet(apps)); + m_toUpgrade.subtract(packagesForPackageId(pkgs)); +} + +void PackageKitUpdater::addResources(const QList &apps) +{ + const QSet pkgs = involvedPackages(kToSet(apps)); + m_toUpgrade.unite(packagesForPackageId(pkgs)); +} + +QList PackageKitUpdater::toUpdate() const +{ + return m_toUpgrade.values(); +} + +bool PackageKitUpdater::isMarked(AbstractResource *res) const +{ + return m_toUpgrade.contains(res); +} + +QDateTime PackageKitUpdater::lastUpdate() const +{ + return m_lastUpdate; +} + +bool PackageKitUpdater::isCancelable() const +{ + return m_isCancelable; +} + +bool PackageKitUpdater::isProgressing() const +{ + return m_isProgressing; +} + +void PackageKitUpdater::cancel() +{ + if (m_transaction) + m_transaction->cancel(); + else + setProgressing(false); +} + +void PackageKitUpdater::errorFound(PackageKit::Transaction::Error err, const QString &error) +{ + if (err == PackageKit::Transaction::ErrorNoLicenseAgreement || err == PackageKit::Transaction::ErrorTransactionCancelled + || err == PackageKit::Transaction::ErrorNotAuthorized) { + return; + } + QString finalMessage = xi18nc("@info", "%1:%2", PackageKitMessages::errorMessage(err, QString()), error); + Q_EMIT passiveMessage(finalMessage); + qWarning() << "Error happened" << err << error; +} + +void PackageKitUpdater::mediaChange(PackageKit::Transaction::MediaType media, const QString &type, const QString &text) +{ + Q_UNUSED(media) + Q_EMIT passiveMessage(i18n("Media Change of type '%1' is requested.\n%2", type, text)); +} + +EulaHandling handleEula(const QString &eulaID, const QString &licenseAgreement) +{ + KConfigGroup group(KSharedConfig::openConfig(), "EULA"); + auto licenseGroup = group.group(eulaID); + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(licenseAgreement.toUtf8()); + QByteArray hashHex = hash.result().toHex(); + + EulaHandling ret; + ret.request = licenseGroup.readEntry("Hash", QByteArray()) != hashHex; + if (!ret.request) { + ret.proceedFunction = [eulaID] { + return PackageKit::Daemon::acceptEula(eulaID); + }; + } else { + ret.proceedFunction = [eulaID, hashHex] { + KConfigGroup group(KSharedConfig::openConfig(), "EULA"); + KConfigGroup licenseGroup = group.group(eulaID); + licenseGroup.writeEntry("Hash", hashHex); + return PackageKit::Daemon::acceptEula(eulaID); + }; + } + return ret; +} + +void PackageKitUpdater::eulaRequired(const QString &eulaID, const QString &packageID, const QString &vendor, const QString &licenseAgreement) +{ + const auto handle = handleEula(eulaID, licenseAgreement); + m_proceedFunctions << handle.proceedFunction; + if (handle.request) { + Q_EMIT proceedRequest(i18n("Accept EULA"), + i18n("The package %1 and its vendor %2 require that you accept their license:\n %3", + PackageKit::Daemon::packageName(packageID), + vendor, + licenseAgreement)); + } else { + proceed(); + } +} + +void PackageKitUpdater::setProgressing(bool progressing) +{ + if (m_isProgressing != progressing) { + m_isProgressing = progressing; + Q_EMIT progressingChanged(m_isProgressing); + } +} + +void PackageKitUpdater::fetchLastUpdateTime() +{ + QDBusPendingReply transaction = PackageKit::Daemon::global()->getTimeSinceAction(PackageKit::Transaction::RoleGetUpdates); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(transaction, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &PackageKitUpdater::lastUpdateTimeReceived); +} + +void PackageKitUpdater::lastUpdateTimeReceived(QDBusPendingCallWatcher *w) +{ + QDBusPendingReply reply = w->reply(); + if (reply.isError()) { + qWarning() << "Error when fetching the last update time" << reply.error(); + } else { + m_lastUpdate = QDateTime::currentDateTime().addSecs(-int(reply.value())); + } + w->deleteLater(); +} + +AbstractBackendUpdater::State toUpdateState(PackageKit::Transaction::Status t) +{ + switch (t) { + case PackageKit::Transaction::StatusUnknown: + case PackageKit::Transaction::StatusDownload: + return AbstractBackendUpdater::Downloading; + case PackageKit::Transaction::StatusDepResolve: + case PackageKit::Transaction::StatusSigCheck: + case PackageKit::Transaction::StatusTestCommit: + case PackageKit::Transaction::StatusInstall: + case PackageKit::Transaction::StatusCommit: + return AbstractBackendUpdater::Installing; + case PackageKit::Transaction::StatusFinished: + case PackageKit::Transaction::StatusCancel: + return AbstractBackendUpdater::Done; + default: + qCDebug(LIBDISCOVER_BACKEND_LOG) << "unknown packagekit status" << t; + return AbstractBackendUpdater::None; + } + Q_UNREACHABLE(); +} + +void PackageKitUpdater::itemProgress(const QString &itemID, PackageKit::Transaction::Status status, uint percentage) +{ + const auto res = packagesForPackageId({itemID}); + + for (auto r : res) { + Q_EMIT resourceProgressed(r, percentage, toUpdateState(status)); + } +} + +void PackageKitUpdater::fetchChangelog() const +{ + QStringList pkgids; + for (AbstractResource *res : qAsConst(m_allUpgradeable)) { + if (auto upgrade = dynamic_cast(res)) { + upgrade->fetchChangelog(); + } else { + pkgids += static_cast(res)->availablePackageId(); + } + } + Q_ASSERT(!pkgids.isEmpty()); + + PackageKit::Transaction *t = PackageKit::Daemon::getUpdatesDetails(pkgids); + connect(t, &PackageKit::Transaction::updateDetail, this, &PackageKitUpdater::updateDetail); + connect(t, &PackageKit::Transaction::errorCode, this, &PackageKitUpdater::errorFound); +} + +void PackageKitUpdater::updateDetail(const QString &packageID, + const QStringList &updates, + const QStringList &obsoletes, + const QStringList &vendorUrls, + const QStringList &bugzillaUrls, + const QStringList &cveUrls, + PackageKit::Transaction::Restart restart, + const QString &updateText, + const QString &changelog, + PackageKit::Transaction::UpdateState state, + const QDateTime &issued, + const QDateTime &updated) +{ + const auto res = packagesForPackageId({packageID}); + for (auto r : res) { + static_cast(r) + ->updateDetail(packageID, updates, obsoletes, vendorUrls, bugzillaUrls, cveUrls, restart, updateText, changelog, state, issued, updated); + } +} + +void PackageKitUpdater::packageResolved(PackageKit::Transaction::Info info, const QString &packageId) +{ + m_packagesModified[info] << packageId; +} + +void PackageKitUpdater::repoSignatureRequired(const QString &packageID, + const QString &repoName, + const QString &keyUrl, + const QString &keyUserid, + const QString &keyId, + const QString &keyFingerprint, + const QString &keyTimestamp, + PackageKit::Transaction::SigType type) +{ + Q_EMIT proceedRequest(i18n("Missing signature for %1 in %2", packageID, repoName), + i18n("Do you trust the following key?\n\nUrl: %1\nUser: %2\nKey: %3\nFingerprint: %4\nTimestamp: %4\n", + keyUrl, + keyUserid, + keyFingerprint, + keyTimestamp)); + + m_proceedFunctions << [type, keyId, packageID]() { + return PackageKit::Daemon::installSignature(type, keyId, packageID); + }; +} + +double PackageKitUpdater::updateSize() const +{ + double ret = 0.; + QSet donePkgs; + for (AbstractResource *res : m_toUpgrade) { + if (auto upgrade = dynamic_cast(res)) { + ret += upgrade->size(); + continue; + } + + PackageKitResource *app = qobject_cast(res); + QString pkgname = app->packageName(); + if (!donePkgs.contains(pkgname)) { + donePkgs.insert(pkgname); + ret += app->size(); + } + } + return ret; +} + +quint64 PackageKitUpdater::downloadSpeed() const +{ + return m_transaction ? m_transaction->speed() : 0; +} + +#include "PackageKitUpdater.moc" diff --git a/libdiscover/backends/PackageKitBackend/PackageKitUpdater.h b/libdiscover/backends/PackageKitBackend/PackageKitUpdater.h new file mode 100644 index 0000000..3117cf2 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/PackageKitUpdater.h @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2013 Lukas Appelhans + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#pragma once + +#include "PackageKitBackend.h" +#include +#include + +class SystemUpgrade; + +struct EulaHandling { + std::function proceedFunction; + bool request = false; +}; +EulaHandling handleEula(const QString &eulaID, const QString &licenseAgreement); +int percentageWithStatus(PackageKit::Transaction::Status status, uint percentage); + +class PackageKitUpdater : public AbstractBackendUpdater +{ + Q_OBJECT +public: + explicit PackageKitUpdater(PackageKitBackend *parent = nullptr); + ~PackageKitUpdater() override; + + void prepare() override; + void checkFreeSpace(); + + bool hasUpdates() const override; + qreal progress() const override; + + void setProgressing(bool progressing); + + void removeResources(const QList &apps) override; + void addResources(const QList &apps) override; + QList toUpdate() const override; + bool isMarked(AbstractResource *res) const override; + QDateTime lastUpdate() const override; + bool isCancelable() const override; + bool isProgressing() const override; + void fetchChangelog() const override; + double updateSize() const override; + quint64 downloadSpeed() const override; + + void proceed() override; + void setOfflineUpdates(bool use) override; + +public Q_SLOTS: + /// must be implemented if ever isCancelable is true + void cancel() override; + void start() override; + +private Q_SLOTS: + void errorFound(PackageKit::Transaction::Error err, const QString &error); + void mediaChange(PackageKit::Transaction::MediaType media, const QString &type, const QString &text); + void eulaRequired(const QString &eulaID, const QString &packageID, const QString &vendor, const QString &licenseAgreement); + void finished(PackageKit::Transaction::Exit exit, uint); + void cancellableChanged(); + void percentageChanged(); + void updateDetail(const QString &packageID, + const QStringList &updates, + const QStringList &obsoletes, + const QStringList &vendorUrls, + const QStringList &bugzillaUrls, + const QStringList &cveUrls, + PackageKit::Transaction::Restart restart, + const QString &updateText, + const QString &changelog, + PackageKit::Transaction::UpdateState state, + const QDateTime &issued, + const QDateTime &updated); + void packageResolved(PackageKit::Transaction::Info info, const QString &packageId); + void repoSignatureRequired(const QString &packageID, + const QString &repoName, + const QString &keyUrl, + const QString &keyUserid, + const QString &keyId, + const QString &keyFingerprint, + const QString &keyTimestamp, + PackageKit::Transaction::SigType type); + +private: + void processProceedFunction(); + void itemProgress(const QString &itemID, PackageKit::Transaction::Status status, uint percentage); + void fetchLastUpdateTime(); + void lastUpdateTimeReceived(QDBusPendingCallWatcher *w); + void setupTransaction(PackageKit::Transaction::TransactionFlags flags); + bool useOfflineUpdates() const; + + QSet involvedPackages(const QSet &packages) const; + QSet packagesForPackageId(const QSet &packages) const; + + QPointer m_transaction; + PackageKitBackend *const m_backend; + QSet m_toUpgrade; + QSet m_allUpgradeable; + bool m_isCancelable; + bool m_isProgressing; + bool m_useOfflineUpdates = false; + int m_percentage; + QDateTime m_lastUpdate; + QMap m_packagesModified; + QVector> m_proceedFunctions; + + SystemUpgrade *m_upgrade = nullptr; +}; diff --git a/libdiscover/backends/PackageKitBackend/org.kde.discover.packagekit.appdata.xml b/libdiscover/backends/PackageKitBackend/org.kde.discover.packagekit.appdata.xml new file mode 100644 index 0000000..519cf45 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/org.kde.discover.packagekit.appdata.xml @@ -0,0 +1,152 @@ + + + org.kde.discover.packagekit + PackageKit backend + سَند عدّة الحزم + PackageKit modulu + Бекенд на PackageKit + Dorsal del PackageKit + Dorsal de PackageKit + Podpůrná vrstva PackageKit + PackageKit-motor + PackageKit-Backend + PackageKit backend + Motor PackageKit + PackageKiti taustaprogramm + PackageKit bizkarraldekoa + PackageKit-taustaosa + Moteur « PackageKit » + Motor de PackageKit + पैकेजकिट पृष्ठभाग + PackageKit backend + Retro administration de PackageKit (equipamento de pacchetto) + Backend PackageKit + Infrastructura PackageKit + Motore PackageKit + PackageKit -ის უკანაბოლო + PackageKit 백엔드 + PackageKit vidinė pusė + PackageKit ബാക്കെൻഡ് + ပတ်ကေ့ချ်ကစ် အုတ်မြစ်ပရိုဂရမ် + Baksystem for PackageKit + PackageKit-backend + PackageKit-motor + ਪੈਕੇਜਕਿਟ ਬੈਕਐਂਡ + Silnik PackageKit + Infra-estrutura do PackageKit + Infraestrutura PackageKit + Platformă PackageKit + Модуль поддержки PackageKit + Podporný program pre PackageKit + Zaledje PackageKit + Gränssnitt för PackageKit + PackageKit பின்நிலை + Коркардкунандаи PackageKit + PackageKit arka ucu + Модуль PackageKit + xxPackageKit backendxx + PackageKit 后端程序 + PackageKit 後端 + Integrates distribution applications into Discover + يُكامل تطبيقات ”عُدّة الحزم“ في «استكشف» + Distribütor tətbiqlərini Discover-ə inteqrasiya edir + Интегрира приложения на дистрибуцията в Discover + Integra les aplicacions de la distribució al Discover + Integra les aplicacions de la distribució a dins de Discover + Integruje aplikace distribuce do Discover + Integrerer distributionsprogrammer i Discover + Integriert Distributions-Anwendungen in Discover + Integrates distribution applications into Discover + Integra aplicaciones de la distribución en Discover + Distributsiooni rakenduste lõimimine Discoverisse + Banaketaren aplikazioak Dicover-ren integratzen ditu + Yhdistää jakelun sovellukset Discoveriin + Intègre les applications de distribution au sein de Discover + Integra aplicacións da distribución con Discover. + डिस्कवर में वितरण अनुप्रयोगों को एकीकृत करता है। + A disztribúció alkalmazásainak integrálása a Discoverbe + Integrate appicationes de distribution in Discover + Aplikasi distribusi terintegrasi ke dalam Discover + Integra applicationes del distribution con Discover + Integra le applicazioni della distribuzione in Discover + Discover-ში PackageKit-ის ინგეტრაცია + 배포판 앱을 Discover에 통합 + Integruoja platinimo programas į Discover + ഡിസ്‌കവറിലേക്ക് വിതരണ ആപ്ലിക്കേഷനുകൾ സംയോജിപ്പിക്കുന്നു + ဒစ်စထရီဗျူးရှင်း အပ္ပလီကေးရှင်းများကို ဒစ်(စ)ကာဗာနှင့် ပူးပေါင်းဆက်နွယ်ပေးသည် + Integrerer distribusjonsprogrammer i Discover + Integreert distributie-toepassingen in Ontdekken + Integrerer distribusjonsprogram i Discover + ਵੰਡਣ ਵਾਲੀਆਂ ਐਪਲੀਕੇਸ਼ਨਾਂ ਨੂੰ ਡਿਸਕਵਰ ਵਿੱਚ ਜੋੜਦਾ ਹੈ + Integruje aplikacje dystrybucji w Odkrywcy + Integra as aplicações da distribuição no Discover + Integra aplicativos da distribuição no Discover + Integrează aplicațiile distribuției în Descoperă + Добавление поддержки приложений из дистрибутива ОС в центр программ Discover + Integruje aplikácie z distribúcie do aplikácie Discover + V Discover vgradi programe distribucije + Integrerar distributionsprogram i Discover + இயக்குதள செயலிகளை டிஸ்கவருக்குள் ஒருங்கிணைக்கும் + Барномаҳои низоми амалкунандаро ба барномаи Кашфиёт дарунсохт мекунад. + Dağıtım uygulamalarını Keşfet ile bütünleştirir + Інтегрує програми зі сховищ дистрибутива до Discover + xxIntegrates distribution applications into Discoverxx + 为 Discover 提供发行版应用程序的集成功能 + 將發行版的應用程式整合進 Discover 商店 + org.kde.discover.desktop + CC0-1.0 + GPL-2.0+ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + एलिक्स पॉल गोंज़ालेज़ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + အဲလက်ပိုဂွန်ဇလက် + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + ਐਲਿਕਸ ਪੋਲ ਪੋਨਜ਼ਾਵੇਜ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + அலெயிக்சு போல் கொன்ஸாலெசு + Алейкс Пол Гонзалес (Aleix Pol Gonzalez) + Aleix Pol Gonzalez + Aleix Pol Gonzalez + xxAleix Pol Gonzalezxx + Aleix Pol Gonzalez + Aleix Pol Gonzalez + system-software-install + + + + + + + diff --git a/libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml b/libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml new file mode 100644 index 0000000..ff1a922 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml @@ -0,0 +1,586 @@ + + + + + All Applications + applications-all + + + + + Accessibility + AudioVideo + Development + Education + Engineering + Game + Graphics + Network + Office + Science + Settings + System + Utility + + + + + + Accessories + applications-utilities + + + Utility + Accessibility + + + + + + Accessibility + preferences-desktop-accessibility + + + Accessibility + Settings + + + + + + Developer Tools + applications-development + + + Development + + + + + Debugging + tools-report-bug + + + Debugger + + + + + + Graphic Interface Design + + + GUIDesigner + + + + + + IDEs + + + IDE + + + + + + Localization + preferences-desktop-locale + + + Translation + + + + + + Profiling + + + Profiling + + + + + + Web Development + + + WebDevelopment + + + applications-internet + + + + + + Education + applications-education + + + Education + + + + + + Science and Engineering + applications-science + + + Science + Engineering + + + + Astronomy + + + Astronomy + + + + + Biology + + + Biology + + + + + Chemistry + applications-science + + + Chemistry + + + + + Computer Science and Robotics + computer + + + ArtificialIntelligence + ComputerScience + Robotics + + + + + Electronics + audio-card + + + Electronics + + + + + Engineering + applications-engineering + + + Engineering + + + + + Geography + + + Geography + + + + + Geology + + + Geology + Geoscience + + + + + Mathematics + applications-education-mathematics + + + DataVisualization + Math + NumericalAnalysis + + + + + Physics + step + + + Physics + + + + + + + Games + applications-games + + + Game + + + + + Arcade + applications-games-arcade + + + ArcadeGame + + + + + Board Games + applications-games-board + + + BoardGame + + + + + Card Games + applications-games-card + + + CardGame + + + + + Puzzles + applications-games + + + LogicGame + + + + + Role Playing + applications-games + + + RolePlaying + + + + + Simulation + applications-games-strategy + + + Simulation + + + + + Strategy + applications-games-strategy + + + StrategyGame + + + + + Sports + applications-games + + + SportsGame + + + + + Action + applications-games + + + ActionGame + + + + + Emulators + applications-games + + + Emulator + + + + + + + + + Graphics + applications-graphics + + + Graphics + + + + 3D + + + 3DGraphics + + + + + Drawing + draw-freehand + + + VectorGraphics + + Viewer + + + + + + Painting and Editing + draw-brush + + + RasterGraphics + + Viewer + Scanning + + + + + + Photography + image-x-generic + + + Photography + + + + + Publishing + document-export + + + Publishing + + + + + Scanning and OCR + scanner + + + Scanning + OCR + + + + + Viewers + graphics-viewer-document + + + Viewer + + + + + + + + Internet + applications-internet + + + Network + + + + Chat + kopete + + + InstantMessaging + IRCClient + + + + + File Sharing + ktorrent + + + FileTransfer + + + + + Mail + internet-mail + + + Email + + + + + Web Browsers + internet-web-browser + + + WebBrowser + + + + + + + + Multimedia + applications-multimedia + + + AudioVideo + + + + + Audio and Video Editors + edit-cut + + + AudioVideoEditing + + + + + Audio Players + audio-headphones + + + + AudioVideo + Audio + + + Video + AudioVideoEditing + DiscBurning + Music + Sequencer + Mixer + Utility + + + + + + Video Players + emblem-videos-symbolic + + + + AudioVideo + Video + + + Audio + AudioVideoEditing + DiscBurning + Utility + + + + + + CD and DVD + media-optical + + + DiscBurning + + + + + + + + Office + applications-office + + + Office + + + + + + System Settings + preferences-system + + + Settings + System + + + + + Plasma Addons + plasma + + + + + org.kde.plasma.* + + + + + Plasma Widgets + plasma + + + + org.kde.plasma.* + + + + + + + + diff --git a/libdiscover/backends/PackageKitBackend/pk-offline-private.h b/libdiscover/backends/PackageKitBackend/pk-offline-private.h new file mode 100644 index 0000000..6224342 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/pk-offline-private.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * SPDX-FileCopyrightText: 2014 Richard Hughes + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +// NOTE: please don't modify, comes from upstream PackageKit/lib/packagekit-glib2/pk-offline-private.h + +#ifndef PK_OFFLINE_DESTDIR +#define PK_OFFLINE_DESTDIR "" +#endif + +/* the state file for regular offline update */ +#define PK_OFFLINE_PREPARED_FILENAME PK_OFFLINE_DESTDIR "/var/lib/PackageKit/prepared-update" +/* the state file for offline system upgrade */ +#define PK_OFFLINE_PREPARED_UPGRADE_FILENAME PK_OFFLINE_DESTDIR "/var/lib/PackageKit/prepared-upgrade" + +/* the trigger file that systemd uses to start a different boot target */ +#define PK_OFFLINE_TRIGGER_FILENAME PK_OFFLINE_DESTDIR "/system-update" + +/* the keyfile describing the outcome of the latest offline update */ +#define PK_OFFLINE_RESULTS_FILENAME PK_OFFLINE_DESTDIR "/var/lib/PackageKit/offline-update-competed" + +/* the action to take when the offline update has completed, e.g. restart */ +#define PK_OFFLINE_ACTION_FILENAME PK_OFFLINE_DESTDIR "/var/lib/PackageKit/offline-update-action" + +/* the group name for the offline updates results keyfile */ +#define PK_OFFLINE_RESULTS_GROUP "PackageKit Offline Update Results" diff --git a/libdiscover/backends/PackageKitBackend/pkui.qrc b/libdiscover/backends/PackageKitBackend/pkui.qrc new file mode 100644 index 0000000..7c985f4 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/pkui.qrc @@ -0,0 +1,7 @@ + + + + qml/DependenciesButton.qml + qml/PackageKitPermissions.qml + + diff --git a/libdiscover/backends/PackageKitBackend/qml/DependenciesButton.qml b/libdiscover/backends/PackageKitBackend/qml/DependenciesButton.qml new file mode 100644 index 0000000..a87812a --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/qml/DependenciesButton.qml @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import org.kde.kirigami 2.14 as Kirigami + +Kirigami.LinkButton { + text: i18nd("libdiscover", "Show Dependencies…") + + onClicked: overlay.open() + visible: view.model.count > 0 + + Connections { + target: resource + function onDependenciesFound(dependencies) { + view.model.clear() + for (var v in dependencies) { + view.model.append(dependencies[v]) + } + } + } + + Kirigami.OverlaySheet { + id: overlay + + parent: applicationWindow().overlay + + title: i18nd("libdiscover", "Dependencies for package: %1", resource.packageName) + + ListView { + id: view + implicitWidth: Kirigami.Units.gridUnit * 26 + clip: true + model: ListModel {} + // FIXME: Workaround for https://bugs.kde.org/show_bug.cgi?id=435546 + headerPositioning: ListView.OverlayHeader + + section.property: "packageInfo" + section.delegate: Kirigami.ListSectionHeader { + width: view.width + // FIXME: Workaround for https://bugs.kde.org/show_bug.cgi?id=435546 + height: Kirigami.Units.fontMetrics.xHeight * 4 + label: section + } + delegate: Kirigami.BasicListItem { + width: view.width + text: model.packageName + subtitle: model.packageDescription + // No need to offer a hover/selection effect since these list + // items are non-interactive and non-selectable + activeBackgroundColor: "transparent" + activeTextColor: Kirigami.Theme.textColor + } + } + } +} diff --git a/libdiscover/backends/PackageKitBackend/qml/PackageKitPermissions.qml b/libdiscover/backends/PackageKitBackend/qml/PackageKitPermissions.qml new file mode 100644 index 0000000..cf42d20 --- /dev/null +++ b/libdiscover/backends/PackageKitBackend/qml/PackageKitPermissions.qml @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.12 +import QtQml.Models 2.15 +import org.kde.kirigami 2.14 as Kirigami + +ColumnLayout { + Kirigami.Heading { + Layout.fillWidth: true + text: i18ndc("libdiscover", "%1 is the name of the application", "Permissions for %1", resource.name) + level: 2 + type: Kirigami.Heading.Type.Primary + wrapMode: Text.Wrap + } + + Kirigami.BasicListItem { + Layout.fillWidth: true + text: i18nd("libdiscover","Full Access") + subtitle: i18nd("libdiscover", "Can access everything on the system") + icon: "security-medium" + subtitleItem.wrapMode: Text.WordWrap + hoverEnabled: false + } +} diff --git a/libdiscover/backends/RpmOstreeBackend/CMakeLists.txt b/libdiscover/backends/RpmOstreeBackend/CMakeLists.txt new file mode 100644 index 0000000..9effe9c --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/CMakeLists.txt @@ -0,0 +1,16 @@ +set(CMAKE_MODULE_PATH ${ECM_MODULE_DIR}) + +find_file(RpmOstreeDBusInterface org.projectatomic.rpmostree1.xml /usr/share/dbus-1/interfaces/) +qt_add_dbus_interface(RpmOstreeDBusInterface_SRCS ${RpmOstreeDBusInterface} RpmOstreeDBusInterface) + +add_library(rpm-ostree-backend MODULE OstreeFormat.cpp RpmOstreeResource.cpp RpmOstreeBackend.cpp RpmOstreeSourcesBackend.cpp RpmOstreeTransaction.cpp ${RpmOstreeDBusInterface_SRCS}) +target_link_libraries(rpm-ostree-backend PRIVATE Discover::Common Qt::DBus KF5::CoreAddons KF5::I18n PkgConfig::Ostree AppStreamQt) + +install(TARGETS rpm-ostree-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) +install(FILES rpm-ostree-backend-categories.xml DESTINATION ${KDE_INSTALL_DATADIR}/libdiscover/categories) + +add_library(rpm-ostree-notifier MODULE OstreeFormat.cpp RpmOstreeNotifier.cpp) +target_link_libraries(rpm-ostree-notifier Discover::Notifiers PkgConfig::Ostree) +set_target_properties(rpm-ostree-notifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) + +install(TARGETS rpm-ostree-notifier DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover-notifier) diff --git a/libdiscover/backends/RpmOstreeBackend/OstreeFormat.cpp b/libdiscover/backends/RpmOstreeBackend/OstreeFormat.cpp new file mode 100644 index 0000000..664d9b8 --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/OstreeFormat.cpp @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2022 Timothée Ravier + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "OstreeFormat.h" + +OstreeFormat::OstreeFormat(Format format, const QString &source) + : m_format(Format::Unknown) +{ + if (source.isEmpty()) { + return; + } + + switch (format) { + // Using classic ostree image format + // Example: fedora:fedora/38/x86_64/kinoite + case Format::Classic: { + // Get remote and ref from the ostree source + auto split_ref = source.split(':'); + if (split_ref.length() != 2) { + // Unknown or invalid format + return; + } + m_remote = split_ref[0]; + m_branch = split_ref[1]; + // Set the format now that we have a valid remote and ref + m_format = OstreeFormat::Classic; + break; + } + + // Using new OCI container format + // Examples: + // ostree-unverified-image:registry: + // ostree-unverified-image:docker:// + // ostree-unverified-registry: + // ostree-remote-image::registry: + // ostree-remote-image::docker:// + // ostree-remote-registry:: + // ostree-image-signed:registry: + // ostree-image-signed:docker:// + case Format::OCI: { + // All early returns correspond to an unknown or invalid format + QStringList split = source.split(':'); + if ((split.length() < 2) || (split.length() > 5)) { + return; + } + + // First figure out the ostree url format and the transport + if (split.first() == QLatin1String("ostree-image-signed")) { + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + if (!parseTransport(&split)) { + return; + } + } else if (split.first() == QLatin1String("ostree-unverified-image")) { + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + if (!parseTransport(&split)) { + return; + } + } else if (split.first() == QLatin1String("ostree-unverified-registry")) { + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + } else if (split.first() == QLatin1String("ostree-remote-image")) { + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + // Append the ostree remote name + if (split.empty()) { + return; + } + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + if (!parseTransport(&split)) { + return; + } + } else if (split.first() == QLatin1String("ostree-remote-registry")) { + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + // Append the ostree remote name + if (split.empty()) { + return; + } + m_transport = split.first() + QLatin1Char(':'); + split.removeFirst(); + } else { + return; + } + + // Now figure out the repo, image name and optional tag + if (split.empty()) { + return; + } + m_remote = split.first(); + split.removeFirst(); + if (!split.isEmpty()) { + m_branch = split.first(); + } else { + m_branch = QStringLiteral("latest"); + } + + // Set the format now that we have a valid repo and tag + m_format = OstreeFormat::OCI; + break; + } + + // This should never happen and should fail at compilation time as we're using an enum + default: + Q_ASSERT(false); + } +} + +// All transports listed in https://github.com/containers/image/blob/main/docs/containers-transports.5.md +bool OstreeFormat::parseTransport(QStringList *split) +{ + if (split->empty()) { + return false; + } + // Special case for the 'docker://' transport + if (split->first() == QLatin1String("docker")) { + m_transport += "docker://"; + split->removeFirst(); + if (split->empty()) { + return false; + } + if (!split->first().startsWith(QLatin1String("//"))) { + return false; + } + split->first() = split->first().remove(0, 2); + } else if (split->first() == QLatin1String("registry") || split->first() == QLatin1String("oci") || split->first() == QLatin1String("oci-archive") + || split->first() == QLatin1String("containers-storage") || split->first() == QLatin1String("dir")) { + m_transport = split->first() + QLatin1Char(':'); + split->removeFirst(); + } else { + // No known/valid transport found + return false; + } + return true; +} + +bool OstreeFormat::isValid() const +{ + return m_format == Format::Classic || m_format == Format::OCI; +} + +bool OstreeFormat::isClassic() const +{ + return m_format == Format::Classic; +} + +bool OstreeFormat::isOCI() const +{ + return m_format == Format::OCI; +} + +QString OstreeFormat::remote() const +{ + if (m_format != Format::Classic) { + return QStringLiteral("unknown"); + } + return m_remote; +} + +QString OstreeFormat::ref() const +{ + if (m_format != Format::Classic) { + return QStringLiteral("unknown"); + } + return m_branch; +} + +QString OstreeFormat::repo() const +{ + if (m_format != Format::OCI) { + return QStringLiteral("unknown"); + } + return m_remote; +} + +QString OstreeFormat::tag() const +{ + if (m_format != Format::OCI) { + return QStringLiteral("unknown"); + } + return m_branch; +} + +QString OstreeFormat::transport() const +{ + if (m_format != Format::OCI) { + return QStringLiteral("unknown"); + } + return m_transport; +} diff --git a/libdiscover/backends/RpmOstreeBackend/OstreeFormat.h b/libdiscover/backends/RpmOstreeBackend/OstreeFormat.h new file mode 100644 index 0000000..9ff4723 --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/OstreeFormat.h @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2022 Timothée Ravier + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef OSTREEFORMAT_H +#define OSTREEFORMAT_H + +#include +#include + +/* + * Represents the different formats used by ostree to deliver images and updates. + * + * For the classic ostree format, see: + * - https://ostreedev.github.io/ostree/repo/#refs + * + * For the new container format, see: + * - https://github.com/ostreedev/ostree-rs-ext + * - https://coreos.github.io/rpm-ostree/container/ + */ +class OstreeFormat : public QObject +{ + Q_GADGET +public: + /* The format used by ostree to deliver updates */ + enum Format { + // Classic ostree format with a remote (repo), branch and ref logic similar to Git + Classic = 0, + // Container based format (ostree commit encapsulated into a container), with a repo + // and tag logic + OCI, + // Unknown format, used for errors + Unknown, + }; + Q_ENUM(Format) + + OstreeFormat(Format format, const QString &source); + + bool isValid() const; + bool isClassic() const; + bool isOCI() const; + + QString repo() const; + QString tag() const; + + QString transport() const; + QString remote() const; + QString ref() const; + +private: + bool parseTransport(QStringList *); + + /* Store the format used by ostree to pull each deployment */ + Format m_format; + + /* For the classic format: the ostree remote where the image come from + * For the OCI format: The container repo URL */ + QString m_remote; + + /* For the classic format: the ostree ref (branch/name/version) used for the image + * For the OCI format: The container image tag */ + QString m_branch; + + /* Only for the OCI format: The transport method used to get the container. + * Also includes the Ostree remote when used for signature validation */ + QString m_transport; +}; + +#endif diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.cpp b/libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.cpp new file mode 100644 index 0000000..033008e --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.cpp @@ -0,0 +1,618 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "RpmOstreeBackend.h" +#include "RpmOstreeSourcesBackend.h" + +#include "Transaction/TransactionModel.h" + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#include +#include +#else +#include +#include +#include +#endif + +#include +#include +#include + +DISCOVER_BACKEND_PLUGIN(RpmOstreeBackend) + +Q_DECLARE_METATYPE(QList) + +static const QString DBusServiceName = QStringLiteral("org.projectatomic.rpmostree1"); +static const QString SysrootObjectPath = QStringLiteral("/org/projectatomic/rpmostree1/Sysroot"); +static const QString TransactionConnection = QStringLiteral("discover_transaction"); +static const QString DevelopmentVersionName = QStringLiteral("Rawhide"); + +RpmOstreeBackend::RpmOstreeBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , m_currentlyBootedDeployment(nullptr) + , m_transaction(nullptr) + , m_watcher(new QDBusServiceWatcher(this)) + , m_interface(nullptr) + , m_updater(new StandardBackendUpdater(this)) + , m_fetching(false) + , m_appdata(new AppStream::Pool) + , m_developmentEnabled(false) +{ + // Refuse to start on systems not managed by rpm-ostree + if (!this->isValid()) { + qWarning() << "rpm-ostree-backend: Not starting on a system not managed by rpm-ostree"; + return; + } + + // Signal that we're fetching ostree deployments + setFetching(true); + + // Switch to development branches for testing. + // TODO: Create a settings option to set this value. + // m_developmentEnabled = true; + + // List configured remotes and display them in the settings page. + // We can do this early as this does not depend on the rpm-ostree daemon. + SourcesModel::global()->addSourcesBackend(new RpmOstreeSourcesBackend(this)); + + // Register DBus types + qDBusRegisterMetaType>(); + + // Setup watcher for rpm-ostree DBus service + m_watcher->setConnection(QDBusConnection::systemBus()); + m_watcher->addWatchedService(DBusServiceName); + connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, [this](const QString &serviceName, const QString &oldOwner, const QString &newOwner) { + qDebug() << "rpm-ostree-backend: Acting on DBus service owner change"; + if (serviceName != DBusServiceName) { + // This should never happen + qWarning() << "rpm-ostree-backend: Got an unexpected event for service:" << serviceName; + } else if (newOwner.isEmpty()) { + // Re-activate the service if it goes away unexpectedly + m_dbusActivationTimer->start(); + } else if (oldOwner.isEmpty()) { + // This is likely the first activation so let's setup the backend + initializeBackend(); + } else { + // This should never happen + qWarning() << "rpm-ostree-backend: Got an unexpected event for service:" << serviceName << oldOwner << newOwner; + } + }); + + // Setup timer for activation retries + m_dbusActivationTimer = new QTimer(this); + m_dbusActivationTimer->setSingleShot(true); + m_dbusActivationTimer->setInterval(1000); + connect(m_dbusActivationTimer, &QTimer::timeout, [this]() { + QDBusConnection::systemBus().interface()->startService(DBusServiceName); + qDebug() << "rpm-ostree-backend: DBus activating rpm-ostree service"; + }); + + // Look for rpm-ostree's DBus interface among registered services + const auto reply = QDBusConnection::systemBus().interface()->registeredServiceNames(); + if (!reply.isValid()) { + qWarning() << "rpm-ostree-backend: Failed to get the list of registered DBus services"; + return; + } + const auto registeredServices = reply.value(); + if (registeredServices.contains(DBusServiceName)) { + // rpm-ostree daemon is running, let's intialize the backend + initializeBackend(); + } else { + // Activate the rpm-ostreed daemon via DBus service activation + QDBusConnection::systemBus().interface()->startService(DBusServiceName); + qDebug() << "rpm-ostree-backend: DBus activating rpm-ostree service"; + } +} + +void RpmOstreeBackend::initializeBackend() +{ + // If any, remove a previous connection that is now likely invalid + if (m_interface != nullptr) { + delete m_interface; + } + // Connect to the main interface + m_interface = new OrgProjectatomicRpmostree1SysrootInterface(DBusServiceName, SysrootObjectPath, QDBusConnection::systemBus(), this); + if (!m_interface->isValid()) { + qWarning() << "rpm-ostree-backend: Could not connect to rpm-ostree daemon:" << qPrintable(QDBusConnection::systemBus().lastError().message()); + m_dbusActivationTimer->start(); + return; + } + + // Register ourselves as update driver + if (!m_registrered) { + QVariantMap options; + options["id"] = QVariant{QStringLiteral("discover")}; + auto reply = m_interface->RegisterClient(options); + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(reply, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, [this, callWatcher]() { + QDBusPendingReply<> reply = *callWatcher; + callWatcher->deleteLater(); + // Wait and retry if we encounter an error + if (reply.isError()) { + qWarning() << "rpm-ostree-backend: Error registering as client:" << qPrintable(QDBusConnection::systemBus().lastError().message()); + m_dbusActivationTimer->start(); + } else { + // Mark that we are now registered with rpm-ostree and retry + // initializing the backend + m_registrered = true; + initializeBackend(); + } + }); + return; + } + + // Fetch existing deployments + refreshDeployments(); + + // Look for a potentially already in-progress rpm-ostree transaction that + // was started outside of Discover + if (hasExternalTransaction()) { + setFetching(false); + return; + } + + // Start the check for a new version of the current deployment if there is + // no transaction in progress + checkForUpdates(); +} + +void RpmOstreeBackend::refreshDeployments() +{ + // Set fetching in all cases but record the state to decide if we should undo it at the end + bool wasFetching = isFetching(); + setFetching(true); + + // Get the path for the curently booted OS DBus interface. + m_bootedObjectPath = m_interface->booted().path(); + + // Reset the list of deployments + m_currentlyBootedDeployment = nullptr; + m_resources.clear(); + + // Get the list of currently available deployments. This is a DBus property + // and not a method call so we should not need to do it async. + const QList deployments = m_interface->deployments(); + for (QVariantMap d : deployments) { + RpmOstreeResource *deployment = new RpmOstreeResource(d, this); + m_resources << deployment; + if (deployment->isBooted()) { + connect(deployment, &RpmOstreeResource::stateChanged, [this]() { + Q_EMIT updatesCountChanged(); + }); + if (m_currentlyBootedDeployment) { + qWarning() << "rpm-ostree-backend: We already have a booted deployment. This is a bug."; + passiveMessage(i18n("rpm-ostree: Multiple booted deployments found. Please file a bug.")); + return; + } + m_currentlyBootedDeployment = deployment; + } else if (deployment->isPending()) { + // Signal that we have a pending update + m_updater->enableNeedsReboot(); + } + } + + if (!m_currentlyBootedDeployment) { + qWarning() << "rpm-ostree-backend: We have not found the booted deployment. This is a bug."; + passiveMessage(i18n("rpm-ostree: No booted deployment found. Please file a bug.")); + return; + } + + // The number of updates might have changed if we're called after an update + Q_EMIT updatesCountChanged(); + + // Only undo the fetching state if we set it up in this function + if (!wasFetching) { + setFetching(false); + } +} + +void RpmOstreeBackend::transactionStatusChanged(Transaction::Status status) +{ + switch (status) { + case Transaction::Status::DoneStatus: + case Transaction::Status::DoneWithErrorStatus: + case Transaction::Status::CancelledStatus: + m_transaction = nullptr; + setFetching(false); + break; + default: + // Ignore all other status changes + ; + } +} + +void RpmOstreeBackend::setupTransaction(RpmOstreeTransaction::Operation op, QString arg) +{ + m_transaction = new RpmOstreeTransaction(this, m_currentlyBootedDeployment, m_interface, op, arg); + connect(m_transaction, &RpmOstreeTransaction::statusChanged, this, &RpmOstreeBackend::transactionStatusChanged); + connect(m_transaction, &RpmOstreeTransaction::deploymentsUpdated, this, &RpmOstreeBackend::refreshDeployments); + connect(m_transaction, &RpmOstreeTransaction::lookForNextMajorVersion, this, &RpmOstreeBackend::lookForNextMajorVersion); +} + +bool RpmOstreeBackend::hasExternalTransaction() +{ + // Do we already know that we have a transaction in progress? + if (m_transaction) { + qInfo() << "rpm-ostree-backend: A transaction is already in progress"; + return true; + } + + // Is there actualy a transaction in progress we don't know about yet? + const QString transaction = m_interface->activeTransactionPath(); + if (!transaction.isEmpty()) { + qInfo() << "rpm-ostree-backend: Found a transaction in progress"; + // We don't check that m_currentlyBootedDeployment is != nullptr here as we expect + // that the backend is initialized when we're called. + setupTransaction(RpmOstreeTransaction::Unknown); + TransactionModel::global()->addTransaction(m_transaction); + return true; + } + + return false; +} + +void RpmOstreeBackend::checkForUpdates() +{ + if (!m_currentlyBootedDeployment) { + qInfo() << "rpm-ostree-backend: Called checkForUpdates before the backend is done getting deployments"; + return; + } + + // Do not start a transaction if there is already one in-progress (likely externaly started) + if (hasExternalTransaction()) { + qInfo() << "rpm-ostree-backend: Not checking for updates while a transaction is in progress"; + return; + } + + // We're fetching updates + setFetching(true); + + setupTransaction(RpmOstreeTransaction::CheckForUpdate); + connect(m_transaction, &RpmOstreeTransaction::newVersionFound, [this](QString newVersion) { + // Mark that there is a newer version for the current deployment + m_currentlyBootedDeployment->setNewVersion(newVersion); + + // Look for an existing deployment for the new version + QVectorIterator iterator(m_resources); + while (iterator.hasNext()) { + RpmOstreeResource *deployment = iterator.next(); + if (deployment->version() == newVersion) { + qInfo() << "rpm-ostree-backend: Found existing deployment for new version. Skipping."; + // Let the user know that the update is pending a reboot + m_updater->enableNeedsReboot(); + if (m_currentlyBootedDeployment->getNextMajorVersion().isEmpty()) { + Q_EMIT inlineMessageChanged(nullptr); + } else { + Q_EMIT inlineMessageChanged(m_rebootBeforeRebaseMessage); + } + return; + } + } + + // No existing deployment found. Let's offer the update + m_currentlyBootedDeployment->setState(AbstractResource::Upgradeable); + if (m_currentlyBootedDeployment->getNextMajorVersion().isEmpty()) { + Q_EMIT inlineMessageChanged(nullptr); + } else { + Q_EMIT inlineMessageChanged(m_rebootBeforeRebaseMessage); + } + }); + m_transaction->start(); + TransactionModel::global()->addTransaction(m_transaction); +} + +void RpmOstreeBackend::lookForNextMajorVersion() +{ + // Load AppStream metadata + bool res = m_appdata->load(); + if (!res) { + qWarning() << "rpm-ostree-backend: Could not open the AppStream metadata pool" << m_appdata->lastError(); + return; + } + + // Get the DistroComponentId. For Fedora Kinoite, we follow Fedora's + // release schedule so we don't have our own ID. + QString distroId = AppStream::Utils::currentDistroComponentId(); + if (distroId == "org.fedoraproject.kinoite.fedora") { + distroId = "org.fedoraproject.fedora"; + } + + // Look at releases to see if we have a new major version available. +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto distroComponents = m_appdata->componentsById(distroId).toList(); +#else + const auto distroComponents = m_appdata->componentsById(distroId); +#endif + + if (distroComponents.isEmpty()) { + qWarning() << "rpm-ostree-backend: No component found for" << distroId; + return; + } + + QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId(); + QString nextVersion; + + for (const AppStream::Component &dc : distroComponents) { +#if ASQ_CHECK_VERSION(1, 0, 0) + const auto releases = dc.releasesPlain().entries(); +#else + const auto releases = dc.releases(); +#endif + for (const auto &r : releases) { + // Only look at stable releases unless development mode is enabled + if ((r.kind() != AppStream::Release::KindStable) && !m_developmentEnabled) { + continue; + } + + // Let's look at this potentially new verson + QString newVersion = r.version(); + + // Ignore development versions by default for version comparisions. + // With the development mode enabled, we will offer it later if no + // other previous newer major version is found. + if (newVersion == DevelopmentVersionName) { + continue; + } + + if (AppStream::Utils::vercmpSimple(newVersion, currentVersion) > 0) { + if (nextVersion.isEmpty()) { + // No other newer version found yet so let's pick this one + nextVersion = newVersion; + qInfo() << "rpm-ostree-backend: Found new major release:" << nextVersion; + } else if (AppStream::Utils::vercmpSimple(nextVersion, newVersion) > 0) { + // We only offer updating to the very next major release so + // we pick the smallest of all the newest versions + nextVersion = newVersion; + qInfo() << "rpm-ostree-backend: Found a closer new major release:" << nextVersion; + } + } + } + } + + if (nextVersion.isEmpty()) { + if (m_developmentEnabled) { + // If development mode is enabled, always offer Rawhide as an option if + // we are already on the latest major version. + nextVersion = DevelopmentVersionName; + } else { + // No new version found, and not in development mode: we're done here + return; + } + } + + if (!m_currentlyBootedDeployment) { + qInfo() << "rpm-ostree-backend: Called lookForNextMajorVersion before the backend is done getting deployments"; + return; + } + + // Validate that the branch exists for the version to move to and set it for the resource + if (!m_currentlyBootedDeployment->setNewMajorVersion(nextVersion)) { + qWarning() << "rpm-ostree-backend: Found new major release but could not validate it via ostree. File a bug to your distribution."; + return; + } + // Offer the newly found major version to rebase to + foundNewMajorVersion(nextVersion); +} + +void RpmOstreeBackend::foundNewMajorVersion(const QString &newMajorVersion) +{ + qDebug() << "rpm-ostree-backend: Found new release:" << newMajorVersion; + + if (!m_currentlyBootedDeployment) { + qInfo() << "rpm-ostree-backend: Called foundNewMajorVersion before the backend is done getting deployments"; + return; + } + + QString info; + // Message to display when: + // - A new major version is available + // - An update to the current version is available or pending a reboot + info = i18n( + "A new major version of %1 has been released!\n" + "To be able to update to this new version, make sure to apply all pending updates and reboot your system.", + m_currentlyBootedDeployment->packageName()); + m_rebootBeforeRebaseMessage = QSharedPointer::create(InlineMessage::Positive, QStringLiteral("application-x-rpm"), info); + + // Message to display when: + // - A new major version is available + // - No update to the current version are available or pending a reboot + DiscoverAction *rebase = new DiscoverAction(i18n("Upgrade to %1 %2", m_currentlyBootedDeployment->packageName(), newMajorVersion), this); + connect(rebase, &DiscoverAction::triggered, this, &RpmOstreeBackend::rebaseToNewVersion); + info = i18n("A new major version has been released!"); + m_rebaseAvailableMessage = QSharedPointer::create(InlineMessage::Positive, QStringLiteral("application-x-rpm"), info, rebase); + + // Look for an existing deployment for the new major version + QVectorIterator iterator(m_resources); + while (iterator.hasNext()) { + RpmOstreeResource *deployment = iterator.next(); + QString deploymentVersion = deployment->version(); + QStringList deploymentVersionSplit = deploymentVersion.split('.'); + if (!deploymentVersionSplit.empty()) { + deploymentVersion = deploymentVersionSplit.at(0); + } + if (deploymentVersion == newMajorVersion) { + qInfo() << "rpm-ostree-backend: Found existing deployment for new major version"; + m_updater->enableNeedsReboot(); + Q_EMIT inlineMessageChanged(nullptr); + return; + } + } + + // Look for an existing updated deployment or a pending deployment for the + // current version + QString newVersion = m_currentlyBootedDeployment->getNewVersion(); + iterator = QVectorIterator(m_resources); + while (iterator.hasNext()) { + RpmOstreeResource *deployment = iterator.next(); + if ((deployment->version() == newVersion) || deployment->isPending()) { + qInfo() << "rpm-ostree-backend: Found pending or updated deployment for current version"; + m_updater->enableNeedsReboot(); + Q_EMIT inlineMessageChanged(m_rebootBeforeRebaseMessage); + return; + } + } + + // No pending deployment found for the current version. We effectively let + // them upgrade only if they are running the latest version of the current + // release so let's check if there is an update available for the current + // version. + if (m_currentlyBootedDeployment->state() == AbstractResource::Upgradeable) { + qInfo() << "rpm-ostree-backend: Found pending update for current version"; + m_updater->enableNeedsReboot(); + Q_EMIT inlineMessageChanged(m_rebootBeforeRebaseMessage); + return; + } + + // No updates pending or avaiable. We are good to offer the rebase to the + // next major version! + Q_EMIT inlineMessageChanged(m_rebaseAvailableMessage); +} + +int RpmOstreeBackend::updatesCount() const +{ + if (!m_currentlyBootedDeployment) { + // Not yet initialized + return 0; + } + if (m_currentlyBootedDeployment->state() == AbstractResource::Upgradeable) { + return 1; + } + return 0; +} + +bool RpmOstreeBackend::isValid() const +{ + return QFile::exists(QStringLiteral("/run/ostree-booted")); +} + +ResultsStream *RpmOstreeBackend::search(const AbstractResourcesBackend::Filters &filter) +{ + // Skip the search if we're looking into a Category, but not the "Operating System" category + if (filter.category && filter.category->untranslatedName() != QLatin1String("Operating System")) { + return new ResultsStream(QStringLiteral("rpm-ostree-empty"), {}); + } + + // Trim whitespace from beginning and end of the string entered in the search field. + QString keyword = filter.search.trimmed(); + + QVector res; + for (RpmOstreeResource *r : m_resources) { + // Skip if the state does not match the filter + if (r->state() < filter.state) { + continue; + } + // Skip if the search field is not empty and neither the name, description or version matches + if (!keyword.isEmpty()) { + if (!r->name().contains(keyword) && !r->longDescription().contains(keyword) && !r->installedVersion().contains(keyword)) { + continue; + } + } + // Add the ressources to the search filter + res << r; + } + return new ResultsStream(QStringLiteral("rpm-ostree"), res); +} + +Transaction *RpmOstreeBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + Q_UNUSED(addons); + return installApplication(app); +} + +Transaction *RpmOstreeBackend::installApplication(AbstractResource *app) +{ + Q_UNUSED(app); + + if (!m_currentlyBootedDeployment) { + qInfo() << "rpm-ostree-backend: Called installApplication before the backend is done getting deployments"; + return nullptr; + } + + if (m_currentlyBootedDeployment->state() != AbstractResource::Upgradeable) { + return nullptr; + } + + setupTransaction(RpmOstreeTransaction::Update); + m_transaction->start(); + return m_transaction; +} + +Transaction *RpmOstreeBackend::removeApplication(AbstractResource *) +{ + // TODO: Support removing unbooted & unpinned deployments + qWarning() << "rpm-ostree-backend: Unsupported operation:" << __PRETTY_FUNCTION__; + return nullptr; +} + +void RpmOstreeBackend::rebaseToNewVersion() +{ + if (!m_currentlyBootedDeployment) { + qInfo() << "rpm-ostree-backend: Called rebaseToNewVersion before the backend is done getting deployments"; + return; + } + + if (m_currentlyBootedDeployment->state() == AbstractResource::Upgradeable) { + if (m_developmentEnabled) { + qInfo() << "rpm-ostree-backend: You have pending updates for current version. Proceeding anyway."; + passiveMessage(i18n("You have pending updates for the current version. Proceeding anyway.")); + } else { + qInfo() << "rpm-ostree-backend: Refusing to rebase with pending updates for current version"; + passiveMessage(i18n("Please update to the latest version before rebasing to a major version")); + return; + } + } + + const QString ref = m_currentlyBootedDeployment->getNextMajorVersionRef(); + if (ref.isEmpty()) { + qWarning() << "rpm-ostree-backend: Error: Empty ref to rebase to"; + passiveMessage(i18n("Missing remote ref for rebase operation. Please file a bug.")); + return; + } + + // Only start one rebase operation at a time + Q_EMIT inlineMessageChanged(nullptr); + setupTransaction(RpmOstreeTransaction::Rebase, ref); + m_transaction->start(); + TransactionModel::global()->addTransaction(m_transaction); +} + +AbstractBackendUpdater *RpmOstreeBackend::backendUpdater() const +{ + return m_updater; +} + +QString RpmOstreeBackend::displayName() const +{ + return QStringLiteral("rpm-ostree"); +} + +bool RpmOstreeBackend::hasApplications() const +{ + return true; +} + +AbstractReviewsBackend *RpmOstreeBackend::reviewsBackend() const +{ + return nullptr; +} + +bool RpmOstreeBackend::isFetching() const +{ + return m_fetching; +} + +void RpmOstreeBackend::setFetching(bool fetching) +{ + if (m_fetching != fetching) { + m_fetching = fetching; + Q_EMIT fetchingChanged(); + } +} + +#include "RpmOstreeBackend.moc" diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.h b/libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.h new file mode 100644 index 0000000..7ce272d --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeBackend.h @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "RpmOstreeDBusInterface.h" +#include "RpmOstreeResource.h" +#include "RpmOstreeTransaction.h" + +#include +#include + +#ifdef DISCOVER_USE_STABLE_APPSTREAM +#include +#else +#include +#endif + +#include + +class DiscoverAction; + +/* + * This backend currently uses a mix of DBus calls and direct `rpm-ostree` + * command line calls to operate. This is de to the fact that support for direct + * Peer to Peer DBus connections (used for all rpm-ostree transactions) appear + * to not work properly with Qt for an unknown reason. + * + * TODO: Replace code calling the command line by calls via the DBus interface + */ +class RpmOstreeBackend : public AbstractResourcesBackend +{ + Q_OBJECT +public: + explicit RpmOstreeBackend(QObject *parent = nullptr); + + /* Convenience function to set the fetching status and emit the + * corresponding signal */ + void setFetching(bool); + + int updatesCount() const override; + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + + /* Returns true if we are running on an ostree/rpm-ostree managed system */ + bool isValid() const override; + + Transaction *installApplication(AbstractResource *) override; + Transaction *installApplication(AbstractResource *, const AddonList &) override; + Transaction *removeApplication(AbstractResource *) override; + + bool isFetching() const override; + void checkForUpdates() override; + QString displayName() const override; + bool hasApplications() const override; + +public Q_SLOTS: + /* Fetch or refresh the list of deployments */ + void refreshDeployments(); + + /* Look for a new Major version and offer it as an update */ + void lookForNextMajorVersion(); + + /* Rebase to the next major version */ + void rebaseToNewVersion(); + + /* Called when the Transaction changes state. Mostly to cleanup once it's done */ + void transactionStatusChanged(Transaction::Status status); + +private: + /* Once rpm-ostree has effectively stated, registrer ourselves as update + * driver to the rpm-ostreed daemon to make sure that it does not exit while + * we are running and then initialize the rest of the backend. */ + void initializeBackend(); + + /* Check if a transaction has been started outside of Discover */ + bool hasExternalTransaction(); + + /* Helper to setup a Transaction and connect all signals/slots */ + void setupTransaction(RpmOstreeTransaction::Operation op, QString arg = {}); + + /* Called when a new major version is found to setup user facing actions */ + void foundNewMajorVersion(const QString &newMajorVersion); + + /* Set to true once we've successfully registrered with rpm-ostree */ + bool m_registrered; + + /* The list of available deployments */ + QVector m_resources; + + /* The currently booted deployment */ + RpmOstreeResource *m_currentlyBootedDeployment; + + /* The current transaction in progress, if any */ + RpmOstreeTransaction *m_transaction; + + /* DBus path to the currently booted OS DBus interface */ + QString m_bootedObjectPath; + + /* Watcher for rpm-ostree DBus service */ + QDBusServiceWatcher *m_watcher; + + /* Timer for DBus retries */ + QTimer *m_dbusActivationTimer; + + /* Qt bindings to the main rpm-ostree DBus interface */ + OrgProjectatomicRpmostree1SysrootInterface *m_interface; + + /* We're re-using the standard backend updater logic */ + StandardBackendUpdater *m_updater; + + /* Used when refreshing the list of deployments */ + bool m_fetching; + + /* AppStream pool to be able to the distribution major release versions */ + QScopedPointer m_appdata; + + /* Enable development branches and releases. This is mostly usuelful for + * testing and should not be enabled by default. We might add an hidden + * option in the future. + * Behaviour for Fedora: Enable rebasing to Rawhide */ + bool m_developmentEnabled; + + /* Custom message that takes into account major upgrades. To display when: + * - A new major version is available + * - An update to the current version is available or pending a reboot */ + QSharedPointer m_rebootBeforeRebaseMessage; + + /* Custom message that takes into account major upgrades. To display when: + * - A new major version is available + * - No update to the current version are available or pending a reboot */ + QSharedPointer m_rebaseAvailableMessage; +}; diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.cpp b/libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.cpp new file mode 100644 index 0000000..9847058 --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.cpp @@ -0,0 +1,390 @@ +/* + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "RpmOstreeNotifier.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +RpmOstreeNotifier::RpmOstreeNotifier(QObject *parent) + : BackendNotifierModule(parent) + , m_version("") + , m_hasUpdates(false) + , m_needsReboot(false) +{ + // Refuse to run on systems not managed by rpm-ostree + if (!isValid()) { + qWarning() << "rpm-ostree-notifier: Not starting on a system not managed by rpm-ostree"; + return; + } + + // Setup a watcher to trigger a check for reboot when the deployments are changed + // and there is thus likely an new deployment installed following an update. + m_watcher = new QFileSystemWatcher(this); + + // We also setup a timer to avoid triggering a check immediately when a new + // deployment is made available and instead wait a bit to let things settle down. + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + // Wait 10 seconds for all rpm-ostree operations to complete + m_timer->setInterval(10000); + connect(m_timer, &QTimer::timeout, this, &RpmOstreeNotifier::checkForPendingDeployment); + + // Find all ostree managed system installations available. There is usualy only one but + // doing that dynamically here avoids hardcoding a specific value or doing a DBus call. + QDirIterator it(QStringLiteral("/ostree/deploy/"), QDir::AllDirs | QDir::NoDotAndDotDot); + while (it.hasNext()) { + QString path = QStringLiteral("%1/deploy/").arg(it.next()); + m_watcher->addPath(path); + qInfo() << "rpm-ostree-notifier: Looking for new deployments in" << path; + } + connect(m_watcher, &QFileSystemWatcher::directoryChanged, [this]() { + m_timer->start(); + }); + + qInfo() << "rpm-ostree-notifier: Looking for ostree format"; + m_process = new QProcess(this); + m_stdout = QByteArray(); + + // Display stderr + connect(m_process, &QProcess::readyReadStandardError, [this]() { + qWarning() << "rpm-ostree (error):" << m_process->readAllStandardError(); + }); + + // Store stdout to process as JSON + connect(m_process, &QProcess::readyReadStandardOutput, [this]() { + m_stdout += m_process->readAllStandardOutput(); + }); + + // Process command result + connect(m_process, qOverload(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { + m_process->deleteLater(); + m_process = nullptr; + if (exitStatus != QProcess::NormalExit) { + qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments"; + return; + } + if (exitCode != 0) { + // Unexpected error + qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments. Exit code:" << exitCode; + return; + } + + // Parse stdout as JSON and look at the currently booted deployments to figure out + // the format used by ostree + const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); + if (!jsonDocument.isObject()) { + qWarning() << "rpm-ostree-notifier: Could not parse 'rpm-ostree status' output as JSON"; + return; + } + const QJsonArray deployments = jsonDocument.object().value("deployments").toArray(); + if (deployments.isEmpty()) { + qWarning() << "rpm-ostree-notifier: Could not find the deployments in 'rpm-ostree status' JSON output"; + return; + } + bool booted; + for (const QJsonValue &deployment : deployments) { + booted = deployment.toObject()["booted"].toBool(); + if (!booted) { + continue; + } + // Look for "classic" ostree origin format first + QString origin = deployment.toObject()["origin"].toString(); + if (!origin.isEmpty()) { + m_ostreeFormat.reset(new ::OstreeFormat(::OstreeFormat::Format::Classic, origin)); + if (!m_ostreeFormat->isValid()) { + // This should never happen + qWarning() << "rpm-ostree-notifier: Invalid origin for classic ostree format:" << origin; + } + } else { + // Then look for OCI container format + origin = deployment.toObject()["container-image-reference"].toString(); + if (!origin.isEmpty()) { + m_ostreeFormat.reset(new ::OstreeFormat(::OstreeFormat::Format::OCI, origin)); + if (!m_ostreeFormat->isValid()) { + // This should never happen + qWarning() << "rpm-ostree-notifier: Invalid reference for OCI container ostree format:" << origin; + } + } else { + // This should never happen + m_ostreeFormat.reset(new ::OstreeFormat(::OstreeFormat::Format::Unknown, {})); + qWarning() << "rpm-ostree-notifier: Could not find a valid remote ostree format for the booted deployment"; + } + } + // Look for the base-version first. This is the case where we have changes layered + m_version = deployment.toObject()["base-version"].toString(); + if (m_version.isEmpty()) { + // If empty, look for the regular version (no layered changes) + m_version = deployment.toObject()["version"].toString(); + } + } + }); + + m_process->start(QStringLiteral("rpm-ostree"), {QStringLiteral("status"), QStringLiteral("--json")}); + m_process->waitForFinished(); +} + +bool RpmOstreeNotifier::isValid() const +{ + return QFile::exists(QStringLiteral("/run/ostree-booted")); +} + +void RpmOstreeNotifier::recheckSystemUpdateNeeded() +{ + // Refuse to run on systems not managed by rpm-ostree + if (!isValid()) { + qWarning() << "rpm-ostree-notifier: Not starting on a system not managed by rpm-ostree"; + return; + } + + qInfo() << "rpm-ostree-notifier: Checking for system update"; + if (m_ostreeFormat->isClassic()) { + checkSystemUpdateClassic(); + } else if (m_ostreeFormat->isOCI()) { + checkSystemUpdateOCI(); + } +} + +void RpmOstreeNotifier::checkSystemUpdateClassic() +{ + qInfo() << "rpm-ostree-notifier: Checking for system update (classic format)"; + + m_process = new QProcess(this); + m_stdout = QByteArray(); + + // Display stderr + connect(m_process, &QProcess::readyReadStandardError, [this]() { + qWarning() << "rpm-ostree (error):" << m_process->readAllStandardError(); + }); + + // Display and store stdout + connect(m_process, &QProcess::readyReadStandardOutput, [this]() { + QByteArray message = m_process->readAllStandardOutput(); + qInfo() << "rpm-ostree:" << message; + m_stdout += message; + }); + + // Process command result + connect(m_process, qOverload(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { + m_process->deleteLater(); + m_process = nullptr; + if (exitStatus != QProcess::NormalExit) { + qWarning() << "rpm-ostree-notifier: Failed to check for system update"; + return; + } + if (exitCode == 77) { + // rpm-ostree will exit with status 77 when no updates are available + qInfo() << "rpm-ostree-notifier: No updates available"; + return; + } + if (exitCode != 0) { + qWarning() << "rpm-ostree-notifier: Failed to check for system update. Exit code:" << exitCode; + return; + } + + // We have an update available. Let's look if we already have a pending + // deployment for the new version. First, look for the new version + // string in rpm-ostree stdout + QString newVersion, line; + QString output = QString(m_stdout); + QTextStream stream(&output); + while (stream.readLineInto(&line)) { + if (line.contains(QLatin1String("Version: "))) { + newVersion = line; + break; + } + } + + // Could not find the new version in rpm-ostree output. This is unlikely + // to ever happen. + if (newVersion.isEmpty()) { + qInfo() << "rpm-ostree-notifier: Could not find the version for the update available"; + } + + // Process the string to get just the version "number". + newVersion = newVersion.trimmed(); + newVersion.remove(0, QStringLiteral("Version: ").length()); + newVersion.remove(newVersion.size() - QStringLiteral(" (XXXX-XX-XXTXX:XX:XXZ)").length(), newVersion.size() - 1); + qInfo() << "rpm-ostree-notifier: Found new version:" << newVersion; + + // Have we already notified the user about this update? + if (newVersion == m_updateVersion) { + qInfo() << "rpm-ostree-notifier: New version has already been offered. Skipping."; + return; + } + m_updateVersion = newVersion; + + // Look for an existing deployment with this version + checkForPendingDeployment(); + }); + + m_process->start(QStringLiteral("rpm-ostree"), {QStringLiteral("update"), QStringLiteral("--check")}); +} + +void RpmOstreeNotifier::checkSystemUpdateOCI() +{ + qInfo() << "rpm-ostree-notifier: Checking for system update (OCI format)"; + + m_process = new QProcess(this); + m_stdout = QByteArray(); + + // Display stderr + connect(m_process, &QProcess::readyReadStandardError, [this]() { + qWarning() << "skopeo (error):" << m_process->readAllStandardError(); + }); + + // Store stdout to process as JSON + connect(m_process, &QProcess::readyReadStandardOutput, [this]() { + m_stdout += m_process->readAllStandardOutput(); + }); + + // Process command result + connect(m_process, qOverload(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { + m_process->deleteLater(); + m_process = nullptr; + if (exitStatus != QProcess::NormalExit) { + qWarning() << "rpm-ostree-notifier: Failed to check for updates via skopeo"; + return; + } + if (exitCode != 0) { + // Unexpected error + qWarning() << "rpm-ostree-notifier: Failed to check for updates via skopeo. Exit code:" << exitCode; + return; + } + + // Parse stdout as JSON and look at the container image labels for the version + const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); + if (!jsonDocument.isObject()) { + qWarning() << "rpm-ostree-notifier: Could not parse 'rpm-ostree status' output as JSON"; + return; + } + + // Get the version stored in .Labels.version + const QString newVersion = jsonDocument.object().value("Labels").toObject().value("version").toString(); + if (newVersion.isEmpty()) { + qInfo() << "rpm-ostree-notifier: Could not get the version from the container labels"; + return; + } + + QVersionNumber newVersionNumber = QVersionNumber::fromString(newVersion); + QVersionNumber currentVersionNumber = QVersionNumber::fromString(m_version); + if (newVersionNumber <= currentVersionNumber) { + qInfo() << "rpm-ostree-notifier: No new version found"; + return; + } + + // Have we already notified the user about this update? + if (newVersion == m_updateVersion) { + qInfo() << "rpm-ostree-notifier: New version has already been offered. Skipping."; + return; + } + m_updateVersion = newVersion; + + // Look for an existing deployment with this version + checkForPendingDeployment(); + }); + + // This will fail on non-remote transports (oci, oci-archive, containers-storage) but that's + // OK as we can not check for updates in those cases. + m_process->start(QStringLiteral("skopeo"), {QStringLiteral("inspect"), "docker://" + m_ostreeFormat->repo() + ":" + m_ostreeFormat->tag()}); +} + +void RpmOstreeNotifier::checkForPendingDeployment() +{ + qInfo() << "rpm-ostree-notifier: Looking at existing deployments"; + m_process = new QProcess(this); + m_stdout = QByteArray(); + + // Display stderr + connect(m_process, &QProcess::readyReadStandardError, [this]() { + QByteArray message = m_process->readAllStandardError(); + qWarning() << "rpm-ostree (error):" << message; + }); + + // Store stdout to process as JSON + connect(m_process, &QProcess::readyReadStandardOutput, [this]() { + QByteArray message = m_process->readAllStandardOutput(); + m_stdout += message; + }); + + // Process command result + connect(m_process, qOverload(&QProcess::finished), [this](int exitCode, QProcess::ExitStatus exitStatus) { + m_process->deleteLater(); + m_process = nullptr; + if (exitStatus != QProcess::NormalExit) { + qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments"; + return; + } + if (exitCode != 0) { + // Unexpected error + qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments. Exit code:" << exitCode; + return; + } + + // Parse stdout as JSON and look at the deployments for a pending + // deployment for the new version. + const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); + if (!jsonDocument.isObject()) { + qWarning() << "rpm-ostree-notifier: Could not parse 'rpm-ostree status' output as JSON"; + return; + } + const QJsonArray deployments = jsonDocument.object().value("deployments").toArray(); + if (deployments.isEmpty()) { + qWarning() << "rpm-ostree-notifier: Could not find the deployments in 'rpm-ostree status' JSON output"; + return; + } + QString version; + for (const QJsonValue &deployment : deployments) { + version = deployment.toObject()["base-version"].toString(); + if (version.isEmpty()) { + version = deployment.toObject()["version"].toString(); + } + if (version.isEmpty()) { + qInfo() << "rpm-ostree-notifier: Could not read version for deployment:" << deployment; + continue; + } + if (version == m_updateVersion) { + qInfo() << "rpm-ostree-notifier: Found an existing deployment for the update available"; + if (!m_needsReboot) { + qInfo() << "rpm-ostree-notifier: Notifying that a reboot is needed"; + m_needsReboot = true; + Q_EMIT needsRebootChanged(); + } + return; + } + } + + // Reaching here means that no deployment has been found for the new version. + qInfo() << "rpm-ostree-notifier: Notifying that a new update is available"; + m_hasUpdates = true; + Q_EMIT foundUpdates(); + + // TODO: Look for security updates fixed by this new deployment + }); + + m_process->start(QStringLiteral("rpm-ostree"), {QStringLiteral("status"), QStringLiteral("--json")}); +} + +bool RpmOstreeNotifier::hasSecurityUpdates() +{ + return false; +} + +bool RpmOstreeNotifier::needsReboot() const +{ + return m_needsReboot; +} + +bool RpmOstreeNotifier::hasUpdates() +{ + return m_hasUpdates; +} diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.h b/libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.h new file mode 100644 index 0000000..0192e6a --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeNotifier.h @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "OstreeFormat.h" + +#include + +#include +#include +#include +#include + +/* Look for new system updates with rpm-ostree. + * Uses only the rpm-ostree command line to simplify logic for now. + * TODO: Use the DBus interface. + */ +class RpmOstreeNotifier : public BackendNotifierModule +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.discover.BackendNotifierModule") + Q_INTERFACES(BackendNotifierModule) +public: + explicit RpmOstreeNotifier(QObject *parent = nullptr); + + void recheckSystemUpdateNeeded() override; + bool hasSecurityUpdates() override; + bool hasUpdates() override; + bool needsReboot() const override; + +private: + /* Only run this code if we are on an rpm-ostree managed system */ + bool isValid() const; + + /* Called by recheckSystemUpdateNeeded to check for system update when the classic + * ostree format is used. */ + void checkSystemUpdateClassic(); + + /* Called by recheckSystemUpdateNeeded to check for system update when the OCI + * ostree format is used. */ + void checkSystemUpdateOCI(); + + /* Store which format is used for the ostree image */ + QScopedPointer<::OstreeFormat> m_ostreeFormat; + + /* Store the version of the currently booted deployment */ + QString m_version; + + /* Tracks the rpm-ostree command used to check for updates or to look at the + * status. */ + QProcess *m_process; + + /* Store standard output from rpm-ostree command line calls */ + QByteArray m_stdout; + + /* The update version that we've already found in a previous check. Used to + * only notify once about an update for a given version. */ + QString m_updateVersion; + + /* Check if we already have a pending deployment for the version avaialbe + * for update */ + void checkForPendingDeployment(); + + /* Do we have updates available? */ + bool m_hasUpdates; + + /* Do we need to reboot to apply updates? */ + bool m_needsReboot; + + /* Watcher to trigger a reboot check when deployments are modified */ + QFileSystemWatcher *m_watcher; + + /* Timer triggerred by the above watcher to wait for things to settle down before re-doing a deployment check */ + QTimer *m_timer; +}; diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp b/libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp new file mode 100644 index 0000000..91d65ec --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp @@ -0,0 +1,488 @@ +/* + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "RpmOstreeResource.h" +#include "RpmOstreeBackend.h" + +#include + +#include +#include + +#include +#include + +RpmOstreeResource::RpmOstreeResource(const QVariantMap &map, RpmOstreeBackend *parent) + : AbstractResource(parent) + // All available deployments are by definition already installed + , m_state(AbstractResource::Installed) +{ +#ifdef QT_DEBUG + qDebug() << "rpm-ostree-backend: Creating deployments from:"; + QMapIterator iter(map); + while (iter.hasNext()) { + iter.next(); + qDebug() << "rpm-ostree-backend: " << iter.key() << ": " << iter.value(); + } + qDebug() << ""; +#endif + + // Get as much as possible from rpm-ostree + m_osname = map.value(QStringLiteral("osname")).toString(); + + // Look for the base-checksum first. This is the case where we have changes layered + m_checksum = map.value(QStringLiteral("base-checksum")).toString(); + if (m_checksum.isEmpty()) { + // If empty, look for the regular checksum (no layered changes) + m_checksum = map.value(QStringLiteral("checksum")).toString(); + } + + // Look for the base-version first. This is the case where we have changes layered + m_version = map.value(QStringLiteral("base-version")).toString(); + if (m_version.isEmpty()) { + // If empty, look for the regular version (no layered changes) + m_version = map.value(QStringLiteral("version")).toString(); + } + + // Look for the base-timestamp first. This is the case where we have changes layered + auto timestamp = map.value(QStringLiteral("base-timestamp")).toULongLong(); + if (timestamp == 0) { + // If "empty", look for the regular timestamp (no layered changes) + timestamp = map.value(QStringLiteral("timestamp")).toULongLong(); + } + if (timestamp == 0) { + // If it's still empty, set an "empty" date + m_timestamp = QDate(); + } else { + // Otherwise, convert the timestamp to a date + m_timestamp = QDateTime::fromSecsSinceEpoch(timestamp).date(); + } + + m_pinned = map.value(QStringLiteral("pinned")).toBool(); + m_pending = map.value(QStringLiteral("staged")).toBool(); + m_booted = map.value(QStringLiteral("booted")).toBool(); + + if (m_booted) { + // We can directly read the pretty name & variant from os-release + // information if this is the currently booted deployment. + auto osrelease = AppStreamIntegration::global()->osRelease(); + m_name = osrelease->name(); + m_variant = osrelease->variant(); + // Also extract the version if we could not find it earlier + if (m_version.isEmpty()) { + m_version = osrelease->versionId(); + } + } + + // Look for "classic" ostree origin format first + QString origin = map.value(QStringLiteral("origin")).toString(); + if (!origin.isEmpty()) { + m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::Classic, origin)); + if (!m_ostreeFormat->isValid()) { + // This should never happen + qWarning() << "rpm-ostree-backend: Invalid origin for classic ostree format:" << origin; + } + } else { + // Then look for OCI container format + origin = map.value(QStringLiteral("container-image-reference")).toString(); + if (!origin.isEmpty()) { + m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::OCI, origin)); + if (!m_ostreeFormat->isValid()) { + // This should never happen + qWarning() << "rpm-ostree-backend: Invalid reference for OCI container ostree format:" << origin; + } + } else { + // This should never happen + m_ostreeFormat.reset(new OstreeFormat(OstreeFormat::Format::Unknown, {})); + qWarning() << "rpm-ostree-backend: Could not find a valid remote for this deployment:" << m_checksum; + } + } + + // Use ostree as tld for all ostree deployments and differentiate between them with the + // remote/repo, ref/tag and commit.. + // Example: ostree.fedora.fedora-34-x86-64-kinoite.abcd1234567890 + // Example: ostree.quay.io-fedora-ostree-desktops-kinoite.abcd1234567890 + // https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-id-generic + if (m_ostreeFormat->isClassic()) { + m_appstreamid = m_ostreeFormat->remote() + "." + m_ostreeFormat->ref(); + } else if (m_ostreeFormat->isOCI()) { + m_appstreamid = m_ostreeFormat->repo() + "." + m_ostreeFormat->tag(); + } else { + m_appstreamid = QStringLiteral(""); + } + m_appstreamid = QStringLiteral("ostree.") + m_appstreamid.replace('/', '-').replace('_', '-') + '.' + m_checksum; +#ifdef QT_DEBUG + qInfo() << "rpm-ostree-backend: Found deployment:" << m_appstreamid; +#endif + + // Replaced & added packages + m_requested_base_local_replacements = map.value(QStringLiteral("requested-base-local-replacements")).toStringList(); + m_requested_base_removals = map.value(QStringLiteral("requested-base-removals")).toStringList(); + m_requested_local_packages = map.value(QStringLiteral("requested-local-packages")).toStringList(); + m_requested_modules = map.value(QStringLiteral("requested-modules")).toStringList(); + m_requested_packages = map.value(QStringLiteral("requested-packages")).toStringList(); + m_requested_base_local_replacements.sort(); + m_requested_base_removals.sort(); + m_requested_local_packages.sort(); + m_requested_modules.sort(); + m_requested_packages.sort(); + + // TODO: Extract signature information +} + +bool RpmOstreeResource::setNewMajorVersion(const QString &newMajorVersion) +{ + if (!m_ostreeFormat->isValid()) { + // Only operate on valid origin format + return false; + } + + // This check mostly makes sense for the classic Ostree format. Skip most of it for the + // OCI format case: it will fail later if the container tag does not exist. + if (m_ostreeFormat->isOCI()) { + // If we are using the latest tag then it means that we are not following a specific + // major release and thus we don't need to rebase: it will automatically happen once + // the latest tag points to a version build with the new major release. + if (m_ostreeFormat->tag() == QStringLiteral("latest")) { + return false; + } + + // Set the new major version + m_nextMajorVersion = newMajorVersion; + // Replace the current version in the container tag by the new major version to find + // the new tag to rebase to. This assumes that container tag names are lowercase. + QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId(); + m_nextMajorVersionRef = m_ostreeFormat->tag().replace(currentVersion, newMajorVersion.toLower(), Qt::CaseInsensitive); + return true; + } + + // Assume we're using the classic format from now on + if (!m_ostreeFormat->isClassic()) { + // Only operate on valid origin format + return false; + } + + // Fetch the list of refs available on the remote for the deployment + g_autoptr(GFile) path = g_file_new_for_path("/ostree/repo"); + g_autoptr(OstreeRepo) repo = ostree_repo_new(path); + if (repo == NULL) { + qWarning() << "rpm-ostree-backend: Could not find ostree repo:" << path; + return false; + } + + g_autoptr(GError) err = NULL; + gboolean res = ostree_repo_open(repo, NULL, &err); + if (!res) { + qWarning() << "rpm-ostree-backend: Could not open ostree repo:" << path; + return false; + } + + g_autoptr(GHashTable) refs; + QByteArray rem = m_ostreeFormat->remote().toLocal8Bit(); + res = ostree_repo_remote_list_refs(repo, rem.data(), &refs, NULL, &err); + if (!res) { + qWarning() << "rpm-ostree-backend: Could not get the list of refs for ostree repo:" << path; + return false; + } + + // Replace the current version in current branch by the new major version to find the + // new branch. This assumes that ostree branch names are lowercase. + QString currentVersion = AppStreamIntegration::global()->osRelease()->versionId(); + QString newVersionBranch = m_ostreeFormat->ref().replace(currentVersion, newMajorVersion.toLower(), Qt::CaseInsensitive); + + // Iterate over the remote refs to verify that the new verions has a branch available + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init(&iter, refs); + while (g_hash_table_iter_next(&iter, &key, &value)) { + auto ref = QString((char *)key); + if (ref == newVersionBranch) { + m_nextMajorVersion = newMajorVersion; + m_nextMajorVersionRef = newVersionBranch; + return true; + } + } + + // If we reach here, it means that we could not find a matching branch. This + // is unexpected and we should inform the user. + qWarning() << "rpm-ostree-backend: Could not find a remote ref for the new major version in ostree repo"; + return false; +} + +QString RpmOstreeResource::availableVersion() const +{ + return m_newVersion; +} + +QString RpmOstreeResource::version() +{ + return m_version; +} + +void RpmOstreeResource::setNewVersion(const QString &newVersion) +{ + m_newVersion = newVersion; +} + +QString RpmOstreeResource::getNewVersion() const +{ + return m_newVersion; +} + +QString RpmOstreeResource::getNextMajorVersion() const +{ + return m_nextMajorVersion; +} + +QString RpmOstreeResource::getNextMajorVersionRef() const +{ + return m_nextMajorVersionRef; +} + +QString RpmOstreeResource::appstreamId() const +{ + return m_appstreamid; +} + +bool RpmOstreeResource::canExecute() const +{ + return false; +} + +QVariant RpmOstreeResource::icon() const +{ + return QStringLiteral("application-x-rpm"); +} + +QString RpmOstreeResource::installedVersion() const +{ + return m_version; +} + +QUrl RpmOstreeResource::url() const +{ + return QUrl(); +} + +QUrl RpmOstreeResource::donationURL() +{ + return QUrl(); +} + +QUrl RpmOstreeResource::homepage() +{ + return QUrl(AppStreamIntegration::global()->osRelease()->homeUrl()); +} + +QUrl RpmOstreeResource::helpURL() +{ + return QUrl(AppStreamIntegration::global()->osRelease()->documentationUrl()); +} + +QUrl RpmOstreeResource::bugURL() +{ + return QUrl(AppStreamIntegration::global()->osRelease()->bugReportUrl()); +} + +QJsonArray RpmOstreeResource::licenses() +{ + if (m_osname == QStringLiteral("fedora")) { + return {QJsonObject{{QStringLiteral("name"), i18n("GPL and other licenses")}, + {QStringLiteral("url"), QStringLiteral("https://fedoraproject.org/wiki/Legal:Licenses")}}}; + } + return {QJsonObject{{QStringLiteral("name"), i18n("Unknown")}}}; +} + +QString RpmOstreeResource::longDescription() +{ + QString desc; + if (!m_requested_packages.isEmpty()) { + QTextStream(&desc) << i18n("Additional packages: ") << "\n
      "; + for (const QString &package : qAsConst(m_requested_packages)) { + QTextStream(&desc) << "
    • " << package << "
    • \n"; + } + QTextStream(&desc) << "
    \n"; + } + if (!m_requested_modules.isEmpty()) { + QTextStream(&desc) << i18n("Additional modules: ") << "\n
      "; + for (const QString &package : qAsConst(m_requested_modules)) { + QTextStream(&desc) << "
    • " << package << "
    • \n"; + } + QTextStream(&desc) << "
    \n"; + } + if (!m_requested_local_packages.isEmpty()) { + QTextStream(&desc) << i18n("Local packages: ") << "\n
      "; + for (const QString &package : qAsConst(m_requested_local_packages)) { + QTextStream(&desc) << "
    • " << package << "
    • \n"; + } + QTextStream(&desc) << "
    \n"; + } + if (!m_requested_base_local_replacements.isEmpty()) { + QTextStream(&desc) << i18n("Replaced packages:") << "\n
      "; + for (const QString &package : qAsConst(m_requested_base_local_replacements)) { + QTextStream(&desc) << "
    • " << package << "
    • \n"; + } + QTextStream(&desc) << "
    \n"; + } + if (!m_requested_base_removals.isEmpty()) { + QTextStream(&desc) << i18n("Removed packages:") << "\n
      "; + for (const QString &package : qAsConst(m_requested_base_removals)) { + QTextStream(&desc) << "
    • " << package << "
    • \n"; + } + QTextStream(&desc) << "
    \n"; + } + if (m_pinned) { + desc += "
    This version is pinned and won't be automatically removed on updates."; + } + return desc; +} + +QString RpmOstreeResource::name() const +{ + return QStringLiteral("%1 %2").arg(packageName(), m_version); +} + +QString RpmOstreeResource::origin() const +{ + if (m_ostreeFormat->isClassic()) { + if (m_ostreeFormat->remote() == QStringLiteral("fedora")) { + return QStringLiteral("Fedora Project"); + } else { + return m_ostreeFormat->remote(); + } + } else if (m_ostreeFormat->isOCI()) { + return m_ostreeFormat->repo(); + } + return i18n("Unknown"); +} + +QString RpmOstreeResource::packageName() const +{ + if (m_osname == QStringLiteral("fedora")) { + return QStringLiteral("Fedora Kinoite"); + } + return m_osname; +} + +QString RpmOstreeResource::section() +{ + return {}; +} + +AbstractResource::State RpmOstreeResource::state() +{ + return m_state; +} + +QString RpmOstreeResource::author() const +{ + if (m_osname == QStringLiteral("fedora")) { + return QStringLiteral("Fedora Project"); + } + return i18n("Unknown"); +} + +QString RpmOstreeResource::comment() +{ + if (m_booted) { + if (m_pinned) { + return i18n("Currently booted version (pinned)"); + } else { + return i18n("Currently booted version"); + } + } else if (m_pending) { + return i18n("Version that will be used after reboot"); + } else if (m_pinned) { + return i18n("Fallback version (pinned)"); + } + return i18n("Fallback version"); +} + +quint64 RpmOstreeResource::size() +{ + return 0; +} + +QString RpmOstreeResource::sizeDescription() +{ + return QStringLiteral("Unknown"); +} + +QDate RpmOstreeResource::releaseDate() const +{ + return m_timestamp; +} + +void RpmOstreeResource::setState(AbstractResource::State state) +{ + m_state = state; + Q_EMIT stateChanged(); +} + +QString RpmOstreeResource::sourceIcon() const +{ + return QStringLiteral("application-x-rpm"); +} + +QStringList RpmOstreeResource::extends() const +{ + return {}; +} +AbstractResource::Type RpmOstreeResource::type() const +{ + return Technical; +} + +bool RpmOstreeResource::isRemovable() const +{ + // TODO: Add support for pinning, un-pinning and removing a specific + // deployments. Until we have that, we consider all deployments as pinned by + // default (and thus non-removable). + // return !m_booted && !m_pinned; + return false; +} + +QList RpmOstreeResource::addonsInformation() +{ + return QList(); +} + +QStringList RpmOstreeResource::categories() +{ + return {}; +} + +bool RpmOstreeResource::isBooted() +{ + return m_booted; +} + +bool RpmOstreeResource::isPending() +{ + return m_pending; +} + +bool RpmOstreeResource::isClassic() +{ + return m_ostreeFormat->isValid() && m_ostreeFormat->isClassic(); +} + +bool RpmOstreeResource::isOCI() +{ + return m_ostreeFormat->isValid() && m_ostreeFormat->isOCI(); +} + +QString RpmOstreeResource::OCIUrl() +{ + // This will fail on non-remote transports (oci, oci-archive, containers-storage) but that's + // OK as we can not check for updates in those cases. + if (m_ostreeFormat->isValid() && m_ostreeFormat->isOCI()) { + return QLatin1String("docker://") + m_ostreeFormat->repo() + ':' + m_ostreeFormat->tag(); + ; + } + // Should never happen + return {}; +} diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.h b/libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.h new file mode 100644 index 0000000..7177b48 --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.h @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "OstreeFormat.h" + +#include + +class RpmOstreeBackend; +class QAbstractItemModel; + +/* + * Represents an ostree deployment (an installed version of the system) as a + * resource in Discover. + */ +class RpmOstreeResource : public AbstractResource +{ + Q_OBJECT +public: + RpmOstreeResource(const QVariantMap &map, RpmOstreeBackend *parent); + + QString appstreamId() const override; + AbstractResource::State state() override; + QVariant icon() const override; + QString comment() override; + QString name() const override; + Q_SCRIPTABLE QString packageName() const override; + QStringList categories() override; + QJsonArray licenses() override; + QString longDescription() override; + QList addonsInformation() override; + bool isRemovable() const override; + QString availableVersion() const override; + QString installedVersion() const override; + QString origin() const override; + QString section() override; + void fetchScreenshots() override{}; + quint64 size() override; + QString sizeDescription() override; + void fetchChangelog() override{}; + QStringList extends() const override; + AbstractResource::Type type() const override; + QString author() const override; + bool canExecute() const override; + void invokeApplication() const override{}; + QUrl url() const override; + QString sourceIcon() const override; + QUrl homepage() override; + QUrl helpURL() override; + QUrl bugURL() override; + QDate releaseDate() const override; + QUrl donationURL() override; + + void setState(AbstractResource::State); + + /* Get the current version */ + QString version(); + + /* Set the target version for updates */ + void setNewVersion(const QString &newVersion); + + /* Get the target version for updates */ + QString getNewVersion() const; + + /* Validate and set the target major version for rebase */ + bool setNewMajorVersion(const QString &newMajorVersion); + + /* Returns the next major version for the deployment */ + QString getNextMajorVersion() const; + + /* Returns the ostree ref for the next major version for the deployment */ + QString getNextMajorVersionRef() const; + + /* Returns if a given deployment is the currently booted deployment */ + Q_SCRIPTABLE bool isBooted(); + + /* Returns if a given deployment is currently pending */ + Q_SCRIPTABLE bool isPending(); + + /* Returns true only if the deployment is from a classic Ostree format */ + bool isClassic(); + + /* Returns true only if the deployment is from an OCI based format */ + bool isOCI(); + + /* Get the full repo:tag reference, only if it's OCI */ + QString OCIUrl(); + +private: + QString m_name; + QString m_variant; + QString m_osname; + QString m_version; + QDate m_timestamp; + QString m_appstreamid; + bool m_booted; + bool m_pinned; + bool m_pending; + QStringList m_requested_base_local_replacements; + QStringList m_requested_base_removals; + QStringList m_requested_local_packages; + QStringList m_requested_modules; + QStringList m_requested_packages; + QString m_checksum; + + /* Store the format used by ostree to pull each deployment*/ + QScopedPointer m_ostreeFormat; + + AbstractResource::State m_state; + + QString m_newVersion; + QString m_nextMajorVersion; + QString m_nextMajorVersionRef; +}; diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.cpp b/libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.cpp new file mode 100644 index 0000000..03a1bee --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.cpp @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "RpmOstreeSourcesBackend.h" + +#include +#include + +#include +#include + +RpmOstreeSourcesBackend::RpmOstreeSourcesBackend(AbstractResourcesBackend *parent) + : AbstractSourcesBackend(parent) + , m_model(new QStandardItemModel(this)) +{ + g_autoptr(GFile) path = g_file_new_for_path("/ostree/repo"); + g_autoptr(OstreeRepo) repo = ostree_repo_new(path); + if (repo == NULL) { + qInfo() << "rpm-ostree-backend: Could not find ostree repo:" << path; + return; + } + + g_autoptr(GError) err = NULL; + gboolean res = ostree_repo_open(repo, NULL, &err); + if (!res) { + qInfo() << "rpm-ostree-backend: Could not open ostree repo:" << path; + return; + } + + guint remote_count = 0; + char **remotes = ostree_repo_remote_list(repo, &remote_count); + for (guint r = 0; r < remote_count; ++r) { + auto remote = new QStandardItem(QString(remotes[r])); + + char *url = NULL; + res = ostree_repo_remote_get_url(repo, remotes[r], &url, &err); + if (res) { + remote->setData(QString(url), Qt::ToolTipRole); + free(url); + } else { + qWarning() << "rpm-ostree-backend: Could not get the URL for ostree remote:" << remotes[r]; + } + + m_model->appendRow(remote); + } + + for (guint r = 0; r < remote_count; ++r) { + free(remotes[r]); + } + free(remotes); +} + +QAbstractItemModel *RpmOstreeSourcesBackend::sources() +{ + return m_model; +} + +bool RpmOstreeSourcesBackend::addSource(const QString &) +{ + return false; +} + +bool RpmOstreeSourcesBackend::removeSource(const QString &) +{ + return false; +} + +QString RpmOstreeSourcesBackend::idDescription() +{ + return i18n("ostree remotes"); +} + +QVariantList RpmOstreeSourcesBackend::actions() const +{ + return {}; +} + +bool RpmOstreeSourcesBackend::supportsAdding() const +{ + return false; +} + +bool RpmOstreeSourcesBackend::canMoveSources() const +{ + return false; +} diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.h b/libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.h new file mode 100644 index 0000000..99456bb --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeSourcesBackend.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include + +class RpmOstreeSourcesBackend : public AbstractSourcesBackend +{ +public: + explicit RpmOstreeSourcesBackend(AbstractResourcesBackend *parent); + QAbstractItemModel *sources() override; + bool addSource(const QString &) override; + bool removeSource(const QString &) override; + QString idDescription() override; + QVariantList actions() const override; + bool supportsAdding() const override; + bool canMoveSources() const override; + +private: + QStandardItemModel *const m_model; +}; diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.cpp b/libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.cpp new file mode 100644 index 0000000..430649c --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.cpp @@ -0,0 +1,408 @@ +/* + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "RpmOstreeTransaction.h" + +#include + +#include +#include +#include +#include +#include + +static const QString TransactionConnection = QStringLiteral("discover_transaction"); +static const QString DBusServiceName = QStringLiteral("org.projectatomic.rpmostree1"); + +RpmOstreeTransaction::RpmOstreeTransaction(QObject *parent, + AbstractResource *resource, + OrgProjectatomicRpmostree1SysrootInterface *interface, + Operation operation, + QString arg) + : Transaction(parent, resource, Transaction::Role::InstallRole, {}) + , m_timer(nullptr) + , m_operation(operation) + , m_resource((RpmOstreeResource *)resource) + , m_cancelled(false) + , m_interface(interface) +{ + setStatus(Status::SetupStatus); + + // This should never happen. We need a reference to the DBus interface to be + // able to cancel a running transaction. + if (interface == nullptr) { + qWarning() << "rpm-ostree-backend: Error: No DBus interface provided. Please file a bug."; + passiveMessage("rpm-ostree-backend: Error: No DBus interface provided. Please file a bug."); + setStatus(Status::CancelledStatus); + return; + } + + // Make sure we are asking for a supported operation and set up arguments + switch (m_operation) { + case Operation::CheckForUpdate: { + qInfo() << "rpm-ostree-backend: Starting transaction to check for updates"; + if (m_resource->isClassic()) { + m_prog = QStringLiteral("rpm-ostree"); + m_args.append({QStringLiteral("update"), QStringLiteral("--check")}); + } else if (m_resource->isOCI()) { + m_prog = QStringLiteral("skopeo"); + // This will fail on non-remote transports (oci, oci-archive, containers-storage) but + // that's OK as we can not check for updates in those cases. + m_args.append({QStringLiteral("inspect"), m_resource->OCIUrl()}); + } else { + // Should never happen + qWarning() << "rpm-ostree-backend: Error: Can not start a transaction for resource with an invalid format. Please file a bug."; + passiveMessage("rpm-ostree-backend: Error: Can not start a transaction for resource with an invalid format. Please file a bug."); + setStatus(Status::CancelledStatus); + return; + } + break; + } + case Operation::DownloadOnly: + qInfo() << "rpm-ostree-backend: Starting transaction to only download updates"; + m_prog = QStringLiteral("rpm-ostree"); + m_args.append({QStringLiteral("update"), QStringLiteral("--download-only ")}); + break; + case Operation::Update: + qInfo() << "rpm-ostree-backend: Starting transaction to update"; + m_prog = QStringLiteral("rpm-ostree"); + m_args.append({QStringLiteral("update")}); + break; + case Operation::Rebase: + // This should never happen + if (arg.isEmpty()) { + qWarning() << "rpm-ostree-backend: Error: Can not rebase to an empty ref. Please file a bug."; + passiveMessage("rpm-ostree-backend: Error: Can not rebase to an empty ref. Please file a bug."); + setStatus(Status::CancelledStatus); + return; + } + qInfo() << "rpm-ostree-backend: Starting transaction to rebase to:" << arg; + m_prog = QStringLiteral("rpm-ostree"); + m_args.append({QStringLiteral("rebase"), arg}); + break; + case Operation::Unknown: + // This is a transaction started externally to Discover. We'll just + // display it as best as we can. + qInfo() << "rpm-ostree-backend: Creating a transaction for an operation not started by Discover"; + setupExternalTransaction(); + return; + break; + default: + // This should never happen + qWarning() << "rpm-ostree-backend: Error: Unknown operation requested. Please file a bug."; + passiveMessage("rpm-ostree-backend: Error: Unknown operation requested. Please file a bug."); + setStatus(Status::CancelledStatus); + return; + } + + // Directly run the command via a QProcess + m_process = new QProcess(this); + m_process->setProgram(m_prog); + m_process->setArguments(m_args); + + // Store stderr output for later + connect(m_process, &QProcess::readyReadStandardError, [this]() { + QByteArray message = m_process->readAllStandardError(); + qWarning() << (m_prog + " (error):") << message; + m_stderr += message; + }); + + // Store stdout output for later and process it to fake progress + connect(m_process, &QProcess::readyReadStandardOutput, [this]() { + QByteArray message = m_process->readAllStandardOutput(); + qInfo() << (m_prog + ":") << message; + m_stdout += message; + fakeProgress(message); + }); + + // Process the result of the transaction once rpm-ostree is done + connect(m_process, qOverload(&QProcess::finished), this, &RpmOstreeTransaction::processCommand); + + // Wait for the start command to effectively start the transaction so that + // the caller has the time to setup signal/slots connections. +} + +RpmOstreeTransaction::~RpmOstreeTransaction() +{ + delete m_timer; +} + +void RpmOstreeTransaction::start() +{ + // Calling this function only makes sense if we have a QProcess for the + // current transaction. + if (m_process != nullptr) { + m_process->start(); + setStatus(Status::DownloadingStatus); + setProgress(5); + setDownloadSpeed(0); + } +} + +void RpmOstreeTransaction::processCommand(int exitCode, QProcess::ExitStatus exitStatus) +{ + m_process->deleteLater(); + m_process = nullptr; + if (exitStatus != QProcess::NormalExit) { + if (m_cancelled) { + // If the user requested the transaction to be cancelled then we + // don't need to show any error + qWarning() << "rpm-ostree-backend: Transaction cancelled: rpm-ostree " << m_args; + } else { + // The transaction was cancelled unexpectedly so let's display the + // error to the user + qWarning() << "rpm-ostree-backend: Error while calling: rpm-ostree " << m_args; + passiveMessage(i18n("rpm-ostree transaction failed with:") + "\n" + m_stderr); + } + setStatus(Status::CancelledStatus); + return; + } + if (exitCode != 0) { + if ((m_operation == Operation::CheckForUpdate) && (exitCode == 77)) { + // rpm-ostree will exit with status 77 when no updates are available + qInfo() << "rpm-ostree-backend: No updates available"; + // Tell the backend to look for a new major version + Q_EMIT lookForNextMajorVersion(); + setStatus(Status::DoneStatus); + return; + } else if (m_cancelled) { + // If the user requested the transaction to be cancelled then we + // don't need to show any error + qInfo() << "rpm-ostree-backend: Transaction cancelled: rpm-ostree " << m_args; + setStatus(Status::DoneWithErrorStatus); + return; + } else { + // The transaction failed unexpectedly so let's display the error to + // the user + qWarning() << "rpm-ostree-backend: rpm-ostree" << m_args << "returned with an error code:" << exitCode; + passiveMessage(i18n("rpm-ostree transaction failed with:") + "\n" + m_stderr); + setStatus(Status::DoneWithErrorStatus); + return; + } + } + + // The transaction was successful. Let's process the result. + switch (m_operation) { + case Operation::CheckForUpdate: { + if (m_resource->isClassic()) { + // Look for new version in rpm-ostree stdout + QString newVersion, line; + QString output = QString(m_stdout); + QTextStream stream(&output); + while (stream.readLineInto(&line)) { + if (line.contains(QLatin1String("Version: "))) { + newVersion = line; + break; + } + } + // If we found a new version then offer it as an update + if (!newVersion.isEmpty()) { + newVersion = newVersion.trimmed(); + newVersion.remove(0, QStringLiteral("Version: ").length()); + newVersion.remove(newVersion.size() - QStringLiteral(" (XXXX-XX-XXTXX:XX:XXZ)").length(), newVersion.size() - 1); + qInfo() << "rpm-ostree-backend: Found new version:" << newVersion; + Q_EMIT newVersionFound(newVersion); + } + } else if (m_resource->isOCI()) { + // Parse stdout as JSON and look at the container image labels for the version + const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); + if (!jsonDocument.isObject()) { + qWarning() << "rpm-ostree-backend: Could not parse output as JSON:" << m_prog << m_args; + return; + } + + // Get the version stored in .Labels.version + const QString newVersion = jsonDocument.object().value("Labels").toObject().value("version").toString(); + if (newVersion.isEmpty()) { + qInfo() << "rpm-ostree-backend: Could not get the version from the container labels"; + return; + } + + QVersionNumber newVersionNumber = QVersionNumber::fromString(newVersion); + QVersionNumber currentVersionNumber = QVersionNumber::fromString(m_resource->version()); + if (newVersionNumber <= currentVersionNumber) { + qInfo() << "rpm-ostree-backend: No new version found"; + } else { + qInfo() << "rpm-ostree-backend: Found new version:" << newVersion; + Q_EMIT newVersionFound(newVersion); + } + } else { + // Should never happen + qWarning() << "rpm-ostree-backend: Error: Unknown resource format. Please file a bug."; + passiveMessage("rpm-ostree-backend: Error: Unknown resource format. Please file a bug."); + } + + // Always tell the backend to look for a new major version + Q_EMIT lookForNextMajorVersion(); + + break; + } + case Operation::DownloadOnly: + // Nothing to do here after downloading pending updates. + break; + case Operation::Update: + // Refresh ressources (deployments) and update state + Q_EMIT deploymentsUpdated(); + break; + case Operation::Rebase: + // Refresh ressources (deployments) and update state + Q_EMIT deploymentsUpdated(); + // Tell the backend to refresh the new major version message now that + // we've reabsed to the new version + Q_EMIT lookForNextMajorVersion(); + break; + case Operation::Unknown: + default: + // This should never happen + qWarning() << "rpm-ostree-backend: Error: Unknown operation requested. Please file a bug."; + passiveMessage("rpm-ostree-backend: Error: Unknown operation requested. Please file a bug."); + } + setStatus(Status::DoneStatus); +} + +void RpmOstreeTransaction::setupExternalTransaction() +{ + // Create a timer to periodically look for updates on the transaction + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + m_timer->setInterval(2000); + + // Update transaction status + connect(m_timer, &QTimer::timeout, [this]() { + // Is the transaction finished? + qDebug() << "rpm-ostree-backend: External transaction update timer triggered"; + QString transaction = m_interface->activeTransactionPath(); + if (transaction.isEmpty()) { + qInfo() << "rpm-ostree-backend: External transaction finished"; + Q_EMIT deploymentsUpdated(); + setStatus(Status::DoneStatus); + return; + } + + // Read status and fake progress + QStringList transactionInfo = m_interface->activeTransaction(); + if (transactionInfo.length() != 3) { + qInfo() << "rpm-ostree-backend: External transaction:" << transactionInfo; + } else { + qInfo() << "rpm-ostree-backend: External transaction '" << transactionInfo.at(0) << "' requested by '" << transactionInfo.at(1); + } + fakeProgress({}); + + // Restart the timer + m_timer->start(); + }); + + // Setup status, fake progress and start the timer + setStatus(Status::DownloadingStatus); + setProgress(5); + setDownloadSpeed(0); + m_timer->start(); +} + +void RpmOstreeTransaction::fakeProgress(const QByteArray &msg) +{ + QString message = QString(msg); + int progress = this->progress(); + if (message.contains("Receiving metadata objects")) { + progress += 10; + } else if (message.contains("Checking out tree")) { + progress += 5; + } else if (message.contains("Enabled rpm-md repositories:")) { + progress += 1; + } else if (message.contains("Updating metadata for")) { + progress += 1; + } else if (message.contains("rpm-md repo")) { + progress += 1; + } else if (message.contains("Resolving dependencies")) { + progress += 5; + } else if (message.contains("Applying") && (message.contains("overrides") || message.contains("overlays"))) { + progress += 5; + setStatus(Status::CommittingStatus); + } else if (message.contains("Processing packages")) { + progress += 5; + } else if (message.contains("Running pre scripts")) { + progress += 5; + } else if (message.contains("Running post scripts")) { + progress += 5; + } else if (message.contains("Running posttrans scripts")) { + progress += 5; + } else if (message.contains("Writing rpmdb")) { + progress += 5; + } else if (message.contains("Generating initramfs")) { + progress += 10; + } else if (message.contains("Writing OSTree commit")) { + progress += 10; + setCancellable(false); + } else if (message.contains("Staging deployment")) { + progress += 5; + } else if (message.contains("Freed")) { + progress += 1; + } else if (message.contains("Upgraded")) { + progress = 99; + } else { + progress += 1; + } + // As we're faking progress, let's make sure that it stays in expected bounds + setProgress(qBound(1, progress, 99)); +} + +void RpmOstreeTransaction::cancel() +{ + qInfo() << "rpm-ostree-backend: Cancelling current transaction"; + passiveMessage(i18n("Cancelling rpm-ostree transaction. This may take some time. Please wait.")); + + // Cancel directly using the DBus interface to work in all cases whether we + // started the transaction or if it's an externally started one. + QString transaction = m_interface->activeTransactionPath(); + QDBusConnection peerConnection = QDBusConnection::connectToPeer(transaction, TransactionConnection); + OrgProjectatomicRpmostree1TransactionInterface transactionInterface(DBusServiceName, QStringLiteral("/"), peerConnection, this); + auto reply = transactionInterface.Cancel(); + + // Cancelled marker that is used to avoid displaying an error message to the + // user when they asked to cancel a transaction. + m_cancelled = true; + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(reply, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, [callWatcher]() { + callWatcher->deleteLater(); + QDBusConnection::disconnectFromPeer(TransactionConnection); + }); +} + +void RpmOstreeTransaction::proceed() +{ + qInfo() << "rpm-ostree-backend: proceed"; +} + +QString RpmOstreeTransaction::name() const +{ + switch (m_operation) { + case Operation::CheckForUpdate: + return i18n("Checking for a system update"); + break; + case Operation::DownloadOnly: + return i18n("Downloading system update"); + break; + case Operation::Update: + return i18n("Updating the system"); + break; + case Operation::Rebase: + return i18n("Updating to the next major version"); + break; + case Operation::Unknown: + return i18n("Operation in progress (started outside of Discover)"); + break; + default: + break; + } + // This should never happen + return i18n("Unknown transaction type"); +} + +QVariant RpmOstreeTransaction::icon() const +{ + return QStringLiteral("application-x-rpm"); +} diff --git a/libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.h b/libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.h new file mode 100644 index 0000000..1a0459b --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/RpmOstreeTransaction.h @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "RpmOstreeDBusInterface.h" +#include "RpmOstreeResource.h" + +#include + +#include + +/* + * Internal representation of an actual rpm-ostree transaction in progress. + */ +class RpmOstreeTransaction : public Transaction +{ + Q_OBJECT +public: + enum Operation { + /// Only check if an update is available. Note that this is unreliable + /// but good enough for now: + /// Note: --check and --preview may be unreliable. + /// See https://github.com/coreos/rpm-ostree/issues/1579 + CheckForUpdate = 0, + /// Only download update but do not apply it. + DownloadOnly, + /// Download and apply update. + Update, + /// Rebase to a new major version. We do not support rebasing to + /// arbitrary refs. + Rebase, + /// Used to represent transactions that were not started by Discover + Unknown, + }; + Q_ENUM(Operation) + + /* Note: arg is only used to pass a reference for the rebase operation. it + * is ignored in all other cases. */ + RpmOstreeTransaction(QObject *parent, + AbstractResource *resource, + OrgProjectatomicRpmostree1SysrootInterface *interface, + Operation operation, + QString arg = {}); + ~RpmOstreeTransaction(); + + Q_SCRIPTABLE void cancel() override; + Q_SCRIPTABLE void proceed() override; + + QString name() const override; + QVariant icon() const override; + + /* Efectively start the transaction when calling an rpm-ostree command */ + void start(); + +Q_SIGNALS: + /* Emitted when a new version is found and an update can be started */ + void newVersionFound(QString version); + + /* Emitted if no update is found and if the backend should look for a new + * major version */ + void lookForNextMajorVersion(); + + /* Emitted when an operation completed and the backend should refresh the + * listed deployments */ + void deploymentsUpdated(); + +public Q_SLOTS: + /* Process the result of rpm-ostree commands */ + void processCommand(int exitCode, QProcess::ExitStatus exitStatu); + +private: + /* Timer setup for transactions started externally from Discover */ + void setupExternalTransaction(); + + /* We don't have clear progress info, so we're faking it */ + void fakeProgress(const QByteArray &message); + + /* Timer wokaround for Transaction updates when the transaction has not been + * started by Discover */ + QTimer *m_timer; + + /* Operation requested for this transaction */ + Operation m_operation; + + /* The resource (deployment) we're working on */ + RpmOstreeResource *m_resource; + + /* QProcess used to call rpm-ostree */ + QProcess *m_process; + + /* Set when we cancel an in progress transaction */ + bool m_cancelled; + + /* Command line application called to perform the transaction (usually rpm-ostree) */ + QString m_prog; + + /* Arguments passed to the command performing the transaction (usually rpm-ostree) */ + QStringList m_args; + + /* rpm-ostree DBus interface, used to cancel running transactions */ + OrgProjectatomicRpmostree1SysrootInterface *m_interface; + + /* Store standard output from rpm-ostree command line calls */ + QByteArray m_stdout; + + /* Store standard error output from rpm-ostree command line calls */ + QByteArray m_stderr; +}; diff --git a/libdiscover/backends/RpmOstreeBackend/rpm-ostree-backend-categories.xml b/libdiscover/backends/RpmOstreeBackend/rpm-ostree-backend-categories.xml new file mode 100644 index 0000000..e212a85 --- /dev/null +++ b/libdiscover/backends/RpmOstreeBackend/rpm-ostree-backend-categories.xml @@ -0,0 +1,13 @@ + + + + Operating System + application-x-rpm + + + + ostree.* + + + + diff --git a/libdiscover/backends/SnapBackend/CMakeLists.txt b/libdiscover/backends/SnapBackend/CMakeLists.txt new file mode 100644 index 0000000..94c8f0a --- /dev/null +++ b/libdiscover/backends/SnapBackend/CMakeLists.txt @@ -0,0 +1,20 @@ +add_subdirectory(libsnapclient) + +add_library(snap-backend MODULE SnapResource.cpp SnapBackend.cpp SnapTransaction.cpp snapui.qrc) +target_link_libraries(snap-backend Qt::Gui Qt::Core Qt::Concurrent KF5::CoreAddons KF5::ConfigCore Discover::Common Snapd::Core) + +if ("${Snapd_VERSION}" VERSION_GREATER 1.40) + target_compile_definitions(snap-backend PRIVATE -DSNAP_COMMON_IDS -DSNAP_CHANNELS) +endif() +if ("${Snapd_VERSION}" VERSION_GREATER 1.42) + target_compile_definitions(snap-backend PRIVATE -DSNAP_PUBLISHER) +endif() +if ("${Snapd_VERSION}" VERSION_GREATER 1.45) + target_compile_definitions(snap-backend PRIVATE -DSNAP_MEDIA) +endif() +if ("${Snapd_VERSION}" VERSION_GREATER 1.48) + target_compile_definitions(snap-backend PRIVATE -DSNAP_FIND_COMMON_ID -DSNAP_MARKDOWN) +endif() + +install(TARGETS snap-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) +install(FILES org.kde.discover.snap.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) diff --git a/libdiscover/backends/SnapBackend/SnapBackend.cpp b/libdiscover/backends/SnapBackend/SnapBackend.cpp new file mode 100644 index 0000000..8d4c829 --- /dev/null +++ b/libdiscover/backends/SnapBackend/SnapBackend.cpp @@ -0,0 +1,278 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SnapBackend.h" +#include "SnapResource.h" +#include "SnapTransaction.h" +#include "appstream/AppStreamIntegration.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" + +DISCOVER_BACKEND_PLUGIN(SnapBackend) + +class SnapSourcesBackend : public AbstractSourcesBackend +{ +public: + explicit SnapSourcesBackend(AbstractResourcesBackend *parent) + : AbstractSourcesBackend(parent) + , m_model(new QStandardItemModel(this)) + { + auto it = new QStandardItem(i18n("Snap")); + it->setData(QStringLiteral("Snap"), IdRole); + m_model->appendRow(it); + } + + QAbstractItemModel *sources() override + { + return m_model; + } + bool addSource(const QString & /*id*/) override + { + return false; + } + bool removeSource(const QString & /*id*/) override + { + return false; + } + QString idDescription() override + { + return QStringLiteral("Snap"); + } + QVariantList actions() const override + { + return {}; + } + + bool supportsAdding() const override + { + return false; + } + bool canMoveSources() const override + { + return false; + } + +private: + QStandardItemModel *const m_model; +}; + +SnapBackend::SnapBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , m_updater(new StandardBackendUpdater(this)) + , m_reviews(AppStreamIntegration::global()->reviews()) +{ + connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { + m_reviews->emitRatingFetched(this, kTransform>(m_resources.values(), [](AbstractResource *r) { + return r; + })); + }); + + // make sure we populate the installed resources first + refreshStates(); + + SourcesModel::global()->addSourcesBackend(new SnapSourcesBackend(this)); + + m_threadPool.setMaxThreadCount(1); +} + +SnapBackend::~SnapBackend() +{ + Q_EMIT shuttingDown(); + m_threadPool.waitForDone(80000); + m_threadPool.clear(); +} + +int SnapBackend::updatesCount() const +{ + return m_updater->updatesCount(); +} + +static ResultsStream *voidStream() +{ + return new ResultsStream(QStringLiteral("Snap-void"), {}); +} + +ResultsStream *SnapBackend::search(const AbstractResourcesBackend::Filters &filters) +{ + if (!filters.extends.isEmpty()) { + return voidStream(); + } else if (!filters.resourceUrl.isEmpty()) { + return findResourceByPackageName(filters.resourceUrl); + } else if (filters.category && filters.category->isAddons()) { + return voidStream(); + } else if (filters.state >= AbstractResource::Installed || filters.origin == QLatin1String("Snap")) { + std::function &)> f = [filters](const QSharedPointer &s) { + return filters.search.isEmpty() || s->name().contains(filters.search, Qt::CaseInsensitive) + || s->description().contains(filters.search, Qt::CaseInsensitive); + }; + return populateWithFilter(m_client.getSnaps(), f); + } else if (!filters.search.isEmpty()) { + return populate(m_client.find(QSnapdClient::FindFlag::None, filters.search)); + } + return voidStream(); +} + +ResultsStream *SnapBackend::findResourceByPackageName(const QUrl &search) +{ + Q_ASSERT(!search.host().isEmpty() || !AppStreamUtils::appstreamIds(search).isEmpty()); + return search.scheme() == QLatin1String("snap") ? populate(m_client.find(QSnapdClient::MatchName, search.host())) : +#ifdef SNAP_FIND_COMMON_ID + search.scheme() == QLatin1String("appstream") + ? populate(kTransform>(AppStreamUtils::appstreamIds(search), + [this](const QString &id) { + return m_client.find(QSnapdClient::MatchCommonId, id); + })) + : +#endif + voidStream(); +} + +template +ResultsStream *SnapBackend::populate(T *job) +{ + return populate(QVector{job}); +} + +template +ResultsStream *SnapBackend::populate(const QVector &jobs) +{ + std::function &)> acceptAll = [](const QSharedPointer &) { + return true; + }; + return populateJobsWithFilter(jobs, acceptAll); +} + +template +ResultsStream *SnapBackend::populateWithFilter(T *job, std::function &s)> &filter) +{ + return populateJobsWithFilter({job}, filter); +} + +template +ResultsStream *SnapBackend::populateJobsWithFilter(const QVector &jobs, std::function &s)> &filter) +{ + auto stream = new ResultsStream(QStringLiteral("Snap-populate")); + auto future = QtConcurrent::run(&m_threadPool, [this, jobs]() { + for (auto job : jobs) { + connect(this, &SnapBackend::shuttingDown, job, &T::cancel); + job->runSync(); + } + }); + + auto watcher = new QFutureWatcher(this); + watcher->setFuture(future); + connect(watcher, &QFutureWatcher::finished, watcher, &QObject::deleteLater); + connect(watcher, &QFutureWatcher::finished, stream, [this, jobs, filter, stream] { + QVector ret; + for (auto job : jobs) { + job->deleteLater(); + if (job->error()) { + qDebug() << "error:" << job->error() << job->errorString(); + continue; + } + + for (int i = 0, c = job->snapCount(); i < c; ++i) { + QSharedPointer snap(job->snap(i)); + + if (!filter(snap)) + continue; + + const auto snapname = snap->name(); + SnapResource *&res = m_resources[snapname]; + if (!res) { + res = new SnapResource(snap, AbstractResource::None, this); + Q_ASSERT(res->packageName() == snapname); + } else { + res->setSnap(snap); + } + ret += res; + } + } + + if (!ret.isEmpty()) + Q_EMIT stream->resourcesFound(ret); + stream->finish(); + }); + return stream; +} + +void SnapBackend::setFetching(bool fetching) +{ + if (m_fetching != fetching) { + m_fetching = fetching; + Q_EMIT fetchingChanged(); + } else { + qWarning() << "fetching already on state" << fetching; + } +} + +AbstractBackendUpdater *SnapBackend::backendUpdater() const +{ + return m_updater; +} + +AbstractReviewsBackend *SnapBackend::reviewsBackend() const +{ + return m_reviews.data(); +} + +Transaction *SnapBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + Q_ASSERT(addons.isEmpty()); + return installApplication(app); +} + +Transaction *SnapBackend::installApplication(AbstractResource *_app) +{ + auto app = qobject_cast(_app); + return new SnapTransaction(&m_client, app, Transaction::InstallRole, AbstractResource::Installed); +} + +Transaction *SnapBackend::removeApplication(AbstractResource *_app) +{ + auto app = qobject_cast(_app); + return new SnapTransaction(&m_client, app, Transaction::RemoveRole, AbstractResource::None); +} + +QString SnapBackend::displayName() const +{ + return QStringLiteral("Snap"); +} + +void SnapBackend::refreshStates() +{ + auto ret = new StoredResultsStream({populate(m_client.getSnaps())}); + connect(ret, &StoredResultsStream::finishedResources, this, [this](const QVector &resources) { + for (auto res : qAsConst(m_resources)) { + if (resources.contains(res)) + res->setState(AbstractResource::Installed); + else + res->setState(AbstractResource::None); + } + }); +} + +#include "SnapBackend.moc" diff --git a/libdiscover/backends/SnapBackend/SnapBackend.h b/libdiscover/backends/SnapBackend/SnapBackend.h new file mode 100644 index 0000000..8a9b6d3 --- /dev/null +++ b/libdiscover/backends/SnapBackend/SnapBackend.h @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class OdrsReviewsBackend; +class StandardBackendUpdater; +class SnapResource; +class SnapBackend : public AbstractResourcesBackend +{ + Q_OBJECT +public: + explicit SnapBackend(QObject *parent = nullptr); + ~SnapBackend() override; + + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + ResultsStream *findResourceByPackageName(const QUrl &search); + + QString displayName() const override; + int updatesCount() const override; + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + bool isValid() const override + { + return m_valid; + } + + Transaction *installApplication(AbstractResource *app) override; + Transaction *installApplication(AbstractResource *app, const AddonList &addons) override; + Transaction *removeApplication(AbstractResource *app) override; + bool isFetching() const override + { + return m_fetching; + } + void checkForUpdates() override + { + } + bool hasApplications() const override + { + return true; + } + QSnapdClient *client() + { + return &m_client; + } + void refreshStates(); + +Q_SIGNALS: + void shuttingDown(); + +private: + void setFetching(bool fetching); + + template + ResultsStream *populateWithFilter(T *snaps, std::function &)> &filter); + + template + ResultsStream *populateJobsWithFilter(const QVector &snaps, std::function &)> &filter); + + template + ResultsStream *populate(T *snaps); + + template + ResultsStream *populate(const QVector &snaps); + + QHash m_resources; + StandardBackendUpdater *m_updater; + QSharedPointer m_reviews; + + bool m_valid = true; + bool m_fetching = false; + QSnapdClient m_client; + QThreadPool m_threadPool; +}; diff --git a/libdiscover/backends/SnapBackend/SnapResource.cpp b/libdiscover/backends/SnapBackend/SnapResource.cpp new file mode 100644 index 0000000..ec97acd --- /dev/null +++ b/libdiscover/backends/SnapBackend/SnapResource.cpp @@ -0,0 +1,535 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SnapResource.h" +#include "SnapBackend.h" +#include +#include +#include +#include +#include +#include + +#ifdef SNAP_MARKDOWN +#include +#endif + +#include +#include + +QDebug operator<<(QDebug debug, const QSnapdPlug &plug) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "QSnapdPlug("; + debug.nospace() << "name:" << plug.name() << ','; + debug.nospace() << "snap:" << plug.snap() << ','; + debug.nospace() << "label:" << plug.label() << ','; + debug.nospace() << "interface:" << plug.interface() << ','; + // debug.nospace() << "connectionCount:" << plug.connectionSlotCount(); + debug.nospace() << ')'; + return debug; +} + +QDebug operator<<(QDebug debug, const QSnapdSlot &slot) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "QSnapdSlot("; + debug.nospace() << "name:" << slot.name() << ','; + debug.nospace() << "label:" << slot.label() << ','; + debug.nospace() << "snap:" << slot.snap() << ','; + debug.nospace() << "interface:" << slot.interface() << ','; + // debug.nospace() << "connectionCount:" << slot.connectionSlotCount(); + debug.nospace() << ')'; + return debug; +} + +QDebug operator<<(QDebug debug, const QSnapdPlug *plug) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "*" << *plug; + return debug; +} + +QDebug operator<<(QDebug debug, const QSnapdSlot *slot) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "*" << *slot; + return debug; +} + +const QStringList SnapResource::m_objects({QStringLiteral("qrc:/qml/PermissionsButton.qml") +#ifdef SNAP_CHANNELS + , + QStringLiteral("qrc:/qml/ChannelsButton.qml") +#endif +}); + +SnapResource::SnapResource(QSharedPointer snap, AbstractResource::State state, SnapBackend *backend) + : AbstractResource(backend) + , m_state(state) + , m_snap(snap) +{ + setObjectName(snap->name()); +} + +QSnapdClient *SnapResource::client() const +{ + auto backend = qobject_cast(parent()); + return backend->client(); +} + +QString SnapResource::availableVersion() const +{ + return installedVersion(); +} + +QStringList SnapResource::categories() +{ + return {QStringLiteral("Application")}; +} + +QString SnapResource::comment() +{ + return m_snap->summary(); +} + +quint64 SnapResource::size() +{ + // return isInstalled() ? m_snap->installedSize() : m_snap->downloadSize(); + return m_snap->downloadSize(); +} + +QVariant SnapResource::icon() const +{ + if (m_icon.isNull()) { + m_icon = [this]() -> QVariant { + const auto iconPath = m_snap->icon(); + if (iconPath.isEmpty()) + return QStringLiteral("package-x-generic"); + + if (!iconPath.startsWith(QLatin1Char('/'))) + return QUrl(iconPath); + + auto req = client()->getIcon(packageName()); + connect(req, &QSnapdGetIconRequest::complete, this, &SnapResource::gotIcon); + req->runAsync(); + return {}; + }(); + } + return m_icon; +} + +void SnapResource::gotIcon() +{ + auto req = qobject_cast(sender()); + if (req->error()) { + qWarning() << "icon error" << req->errorString(); + return; + } + + auto icon = req->icon(); + + QBuffer buffer; + buffer.setData(icon->data()); + QImageReader reader(&buffer); + + auto theIcon = QVariant::fromValue(reader.read()); + if (theIcon != m_icon) { + m_icon = theIcon; + Q_EMIT iconChanged(); + } +} + +QString SnapResource::installedVersion() const +{ + return m_snap->version(); +} + +QJsonArray SnapResource::licenses() +{ + return AppStreamUtils::licenses(m_snap->license()); +} + +#ifdef SNAP_MARKDOWN +static QString serialize_node(QSnapdMarkdownNode &node); + +static QString serialize_children(QSnapdMarkdownNode &node) +{ + QString result; + for (int i = 0; i < node.childCount(); i++) { + QScopedPointer child(node.child(i)); + result += serialize_node(*child); + } + return result; +} + +static QString serialize_node(QSnapdMarkdownNode &node) +{ + switch (node.type()) { + case QSnapdMarkdownNode::NodeTypeText: + return node.text().toHtmlEscaped(); + + case QSnapdMarkdownNode::NodeTypeParagraph: + return QLatin1String("

    ") + serialize_children(node) + QLatin1String("

    \n"); + + case QSnapdMarkdownNode::NodeTypeUnorderedList: + return QLatin1String("
      \n") + serialize_children(node) + QLatin1String("
    \n"); + + case QSnapdMarkdownNode::NodeTypeListItem: + if (node.childCount() == 0) + return QLatin1String("
  • \n"); + if (node.childCount() == 1) { + QScopedPointer child(node.child(0)); + if (child->type() == QSnapdMarkdownNode::NodeTypeParagraph) + return QLatin1String("
  • ") + serialize_children(*child) + QLatin1String("
  • \n"); + } + return QLatin1String("
  • \n") + serialize_children(node) + QLatin1String("
  • \n"); + + case QSnapdMarkdownNode::NodeTypeCodeBlock: + return QLatin1String("
    ") + serialize_children(node) + QLatin1String("
    \n"); + + case QSnapdMarkdownNode::NodeTypeCodeSpan: + return QLatin1String("") + serialize_children(node) + QLatin1String(""); + + case QSnapdMarkdownNode::NodeTypeEmphasis: + return QLatin1String("") + serialize_children(node) + QLatin1String(""); + + case QSnapdMarkdownNode::NodeTypeStrongEmphasis: + return QLatin1String("") + serialize_children(node) + QLatin1String(""); + + case QSnapdMarkdownNode::NodeTypeUrl: + return serialize_children(node); + + default: + return QString(); + } +} +#endif + +QString SnapResource::longDescription() +{ +#ifdef SNAP_MARKDOWN + QSnapdMarkdownParser parser(QSnapdMarkdownParser::MarkdownVersion0); + QList nodes = parser.parse(m_snap->description()); + QString result; + for (int i = 0; i < nodes.size(); i++) + result += serialize_node(nodes[i]); + return result; +#else + return m_snap->description(); +#endif +} + +QString SnapResource::name() const +{ + return m_snap->title().isEmpty() ? m_snap->name() : m_snap->title(); +} + +QString SnapResource::origin() const +{ + return QStringLiteral("Snap"); +} + +QString SnapResource::packageName() const +{ + return m_snap->name(); +} + +QString SnapResource::section() +{ + return QStringLiteral("snap"); +} + +AbstractResource::State SnapResource::state() +{ + return m_state; +} + +void SnapResource::setState(AbstractResource::State state) +{ + if (m_state != state) { + m_state = state; + Q_EMIT stateChanged(); + } +} + +void SnapResource::fetchChangelog() +{ + QString log; + Q_EMIT changelogFetched(log); +} + +void SnapResource::fetchScreenshots() +{ + Screenshots screenshots; +#ifdef SNAP_MEDIA + for (int i = 0, c = m_snap->mediaCount(); i < c; ++i) { + QScopedPointer media(m_snap->media(i)); + if (media->type() == QLatin1String("screenshot")) + screenshots << QUrl(media->url()); + } +#else + for (int i = 0, c = m_snap->screenshotCount(); i < c; ++i) { + QScopedPointer screenshot(m_snap->screenshot(i)); + screenshots << QUrl(screenshot->url()); + } +#endif + Q_EMIT screenshotsFetched(screenshots); +} + +void SnapResource::invokeApplication() const +{ + QProcess::startDetached(QStringLiteral("snap"), {QStringLiteral("run"), packageName()}); +} + +AbstractResource::Type SnapResource::type() const +{ + return m_snap->snapType() != QLatin1String("app") ? Application : Technical; +} + +void SnapResource::setSnap(const QSharedPointer &snap) +{ + Q_ASSERT(snap->name() == m_snap->name()); + if (m_snap == snap) + return; + + const bool newSize = m_snap->installedSize() != snap->installedSize() || m_snap->downloadSize() != snap->downloadSize(); + m_snap = snap; + if (newSize) + Q_EMIT sizeChanged(); + + Q_EMIT newSnap(); +} + +QDate SnapResource::releaseDate() const +{ + return {}; +} + +class PlugsModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum Roles { + PlugNameRole = Qt::UserRole + 1, + SlotSnapRole, + SlotNameRole, + }; + + PlugsModel(SnapResource *res, SnapBackend *backend, QObject *parent) + : QStandardItemModel(parent) + , m_res(res) + , m_backend(backend) + { + auto roles = roleNames(); + roles.insert(Qt::CheckStateRole, "checked"); + setItemRoleNames(roles); + + auto req = backend->client()->getInterfaces(); + req->runSync(); + + QHash> slotsForInterface; + for (int i = 0; i < req->slotCount(); ++i) { + const auto slot = req->slot(i); + slot->setParent(this); + slotsForInterface[slot->interface()].append(slot); + } + + const auto snap = m_res->snap(); + for (int i = 0; i < req->plugCount(); ++i) { + const QScopedPointer plug(req->plug(i)); + if (plug->snap() == snap->name()) { + if (plug->interface() == QLatin1String("content")) + continue; + + const auto theSlots = slotsForInterface.value(plug->interface()); + for (auto slot : theSlots) { + auto item = new QStandardItem; + if (plug->label().isEmpty()) + item->setText(plug->name()); + else + item->setText(i18n("%1 - %2", plug->name(), plug->label())); + + // qDebug() << "xxx" << plug->name() << plug->label() << plug->interface() << slot->snap() << "slot:" << slot->name() << + // slot->snap() << slot->interface() << slot->label(); + item->setCheckable(true); + item->setCheckState(plug->connectionCount() > 0 ? Qt::Checked : Qt::Unchecked); + item->setData(plug->name(), PlugNameRole); + item->setData(slot->snap(), SlotSnapRole); + item->setData(slot->name(), SlotNameRole); + appendRow(item); + } + } + } + } + +Q_SIGNALS: + void error(InlineMessage *message); + +private: + bool setData(const QModelIndex &index, const QVariant &value, int role) override + { + if (role != Qt::CheckStateRole) + return QStandardItemModel::setData(index, value, role); + + auto item = itemFromIndex(index); + Q_ASSERT(item); + const QString plugName = item->data(PlugNameRole).toString(); + const QString slotSnap = item->data(SlotSnapRole).toString(); + const QString slotName = item->data(SlotNameRole).toString(); + + QSnapdRequest *req; + + const auto snap = m_res->snap(); + if (item->checkState() == Qt::Checked) { + req = m_backend->client()->disconnectInterface(snap->name(), plugName, slotSnap, slotName); + } else { + req = m_backend->client()->connectInterface(snap->name(), plugName, slotSnap, slotName); + } + req->runSync(); + if (req->error()) { + qWarning() << "snapd error" << req->errorString(); + Q_EMIT error(new InlineMessage(InlineMessage::Error, "error", req->errorString())); + } + return req->error() == QSnapdRequest::NoError; + } + + SnapResource *const m_res; + SnapBackend *const m_backend; +}; + +QAbstractItemModel *SnapResource::plugs(QObject *p) +{ + if (!isInstalled()) + return nullptr; + + return new PlugsModel(this, qobject_cast(parent()), p); +} + +QString SnapResource::appstreamId() const +{ + const QStringList ids +#if defined(SNAP_COMMON_IDS) + = m_snap->commonIds() +#endif + ; + return ids.isEmpty() ? QLatin1String("io.snapcraft.") + m_snap->name() + QLatin1Char('-') + m_snap->id() : ids.first(); +} + +QString SnapResource::channel() const +{ +#ifdef SNAP_PUBLISHER + auto req = client()->getSnap(packageName()); +#else + auto req = client()->listOne(packageName()); +#endif + req->runSync(); + return req->error() ? QString() : req->snap()->trackingChannel(); +} + +QString SnapResource::author() const +{ +#ifdef SNAP_PUBLISHER + QString author = m_snap->publisherDisplayName(); + if (m_snap->publisherValidation() == QSnapdEnums::PublisherValidationVerified) { + author += QStringLiteral(" ✅"); + } +#else + QString author; +#endif + + return author; +} + +void SnapResource::setChannel(const QString &channelName) +{ +#ifdef SNAP_CHANNELS + Q_ASSERT(isInstalled()); + auto request = client()->switchChannel(m_snap->name(), channelName); + + const auto currentChannel = channel(); + request->runAsync(); + connect(request, &QSnapdRequest::complete, this, [this, currentChannel]() { + const auto newChannel = channel(); + if (newChannel != currentChannel) { + Q_EMIT channelChanged(newChannel); + } + }); +#endif +} + +void SnapResource::refreshSnap() +{ + auto request = client()->find(QSnapdClient::FindFlag::MatchName, m_snap->name()); + connect(request, &QSnapdRequest::complete, this, [this, request]() { + if (request->error()) { + qWarning() << "error" << request->error() << ": " << request->errorString(); + return; + } + Q_ASSERT(request->snapCount() == 1); + setSnap(QSharedPointer(request->snap(0))); + }); + request->runAsync(); +} + +#ifdef SNAP_CHANNELS +class Channels : public QObject +{ + Q_OBJECT + Q_PROPERTY(QList channels READ channels NOTIFY channelsChanged) + +public: + Channels(SnapResource *res, QObject *parent) + : QObject(parent) + , m_res(res) + { + if (res->snap()->channelCount() == 0) + res->refreshSnap(); + else + refreshChannels(); + + connect(res, &SnapResource::newSnap, this, &Channels::refreshChannels); + } + + void refreshChannels() + { + qDeleteAll(m_channels); + m_channels.clear(); + + auto s = m_res->snap(); + for (int i = 0, c = s->channelCount(); i < c; ++i) { + auto channel = s->channel(i); + channel->setParent(this); + m_channels << channel; + } + Q_EMIT channelsChanged(); + } + + QList channels() const + { + return m_channels; + } + +Q_SIGNALS: + void channelsChanged(); + +private: + QList m_channels; + SnapResource *const m_res; +}; + +#endif + +QObject *SnapResource::channels(QObject *parent) +{ +#ifdef SNAP_CHANNELS + return new Channels(this, parent); +#else + return nullptr; +#endif +} + +#include "SnapResource.moc" diff --git a/libdiscover/backends/SnapBackend/SnapResource.h b/libdiscover/backends/SnapBackend/SnapResource.h new file mode 100644 index 0000000..1dadb75 --- /dev/null +++ b/libdiscover/backends/SnapBackend/SnapResource.h @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +class SnapBackend; +class QAbstractItemModel; +class QSnapdClient; + +class SnapResource : public AbstractResource +{ + Q_OBJECT + Q_PROPERTY(QStringList objects MEMBER m_objects CONSTANT) + Q_PROPERTY(QString channel READ channel WRITE setChannel NOTIFY channelChanged) +public: + explicit SnapResource(QSharedPointer snap, AbstractResource::State state, SnapBackend *parent); + ~SnapResource() override = default; + + QString section() override; + QString origin() const override; + QString longDescription() override; + QString availableVersion() const override; + QString installedVersion() const override; + QJsonArray licenses() override; + quint64 size() override; + QStringList categories() override; + AbstractResource::State state() override; + QVariant icon() const override; + QString comment() override; + QString name() const override; + QString packageName() const override; + AbstractResource::Type type() const override; + bool canExecute() const override + { + return true; + } + void invokeApplication() const override; + void fetchChangelog() override; + void fetchScreenshots() override; + QString author() const override; + QList addonsInformation() override + { + return {}; + } + void setSnap(const QSharedPointer &snap); + + void setState(AbstractResource::State state); + QString sourceIcon() const override + { + return QStringLiteral("snap"); + } + + QDate releaseDate() const override; + + Q_SCRIPTABLE QAbstractItemModel *plugs(QObject *parentC); + Q_SCRIPTABLE QObject *channels(QObject *parent); + QString appstreamId() const override; + + QString channel() const; + void setChannel(const QString &channel); + + QSharedPointer snap() const + { + return m_snap; + } + +Q_SIGNALS: + void channelChanged(const QString &channel); + void newSnap(); + +public: + QSnapdClient *client() const; + void refreshSnap(); + void gotIcon(); + AbstractResource::State m_state; + + QSharedPointer m_snap; + mutable QVariant m_icon; + static const QStringList m_objects; +}; diff --git a/libdiscover/backends/SnapBackend/SnapTransaction.cpp b/libdiscover/backends/SnapBackend/SnapTransaction.cpp new file mode 100644 index 0000000..221ce10 --- /dev/null +++ b/libdiscover/backends/SnapBackend/SnapTransaction.cpp @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SnapTransaction.h" +#include "SnapBackend.h" +#include "SnapResource.h" +#include "libsnapclient/config-paths.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include + +SnapTransaction::SnapTransaction(QSnapdClient *client, SnapResource *app, Role role, AbstractResource::State newState) + : Transaction(app, app, role) + , m_client(client) + , m_app(app) + , m_newState(newState) +{ + if (role == RemoveRole) + setRequest(m_client->remove(app->packageName())); + else + setRequest(m_client->install(app->packageName())); +} + +void SnapTransaction::cancel() +{ + m_request->cancel(); +} + +void SnapTransaction::finishTransaction() +{ + switch (m_request->error()) { + case QSnapdRequest::NoError: + static_cast(m_app->backend())->refreshStates(); + setStatus(DoneStatus); + m_app->setState(m_newState); + break; + case QSnapdRequest::Cancelled: + setStatus(CancelledStatus); + break; + case QSnapdRequest::NeedsClassic: + setStatus(SetupStatus); + if (role() == Transaction::InstallRole) { + Q_EMIT proceedRequest(m_app->name(), + i18n("This Snap application is not compatible with security sandboxing " + "and will have full access to this computer. Install it anyway?")); + return; + } + break; + case QSnapdRequest::AuthDataRequired: { + setStatus(SetupStatus); + QProcess *p = new QProcess; + p->setProgram(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR "/discover/SnapMacaroonDialog")); + p->start(); + + connect(p, qOverload(&QProcess::finished), this, [this, p](int code, QProcess::ExitStatus status) { + p->deleteLater(); + if (code != 0) { + qWarning() << "login failed... code:" << code << status << p->readAll(); + Q_EMIT passiveMessage(m_request->errorString()); + setStatus(DoneWithErrorStatus); + return; + } + const auto doc = QJsonDocument::fromJson(p->readAllStandardOutput()); + const auto result = doc.object(); + + const auto macaroon = result[QStringLiteral("macaroon")].toString(); + const auto discharges = kTransform(result[QStringLiteral("discharges")].toArray(), [](const QJsonValue &val) { + return val.toString(); + }); + static_cast(m_app->backend())->client()->setAuthData(new QSnapdAuthData(macaroon, discharges)); + m_request->runAsync(); + }); + } + return; + default: + qDebug() << "snap error" << m_request << m_request->error() << m_request->errorString(); + Q_EMIT passiveMessage(m_request->errorString()); + setStatus(DoneWithErrorStatus); + break; + } +} + +void SnapTransaction::proceed() +{ + setRequest(m_client->install(QSnapdClient::Classic, m_app->packageName())); +} + +void SnapTransaction::setRequest(QSnapdRequest *req) +{ + m_request.reset(req); + + setCancellable(true); + connect(m_request.data(), &QSnapdRequest::progress, this, &SnapTransaction::progressed); + connect(m_request.data(), &QSnapdRequest::complete, this, &SnapTransaction::finishTransaction); + + setStatus(CommittingStatus); + m_request->runAsync(); +} + +void SnapTransaction::progressed() +{ + const auto change = m_request->change(); + int percentage = 0, count = 0; + + auto status = SetupStatus; + for (int i = 0, c = change->taskCount(); i < c; ++i) { + ++count; + auto task = change->task(i); + if (task->kind() == QLatin1String("download-snap")) { + status = task->status() == QLatin1String("doing") || task->status() == QLatin1String("do") ? DownloadingStatus : CommittingStatus; + } else if (task->kind() == QLatin1String("clear-snap")) { + status = CommittingStatus; + } + percentage += (100 * task->progressDone()) / task->progressTotal(); + } + setProgress(percentage / qMax(count, 1)); + setStatus(status); +} diff --git a/libdiscover/backends/SnapBackend/SnapTransaction.h b/libdiscover/backends/SnapBackend/SnapTransaction.h new file mode 100644 index 0000000..18026ed --- /dev/null +++ b/libdiscover/backends/SnapBackend/SnapTransaction.h @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +class SnapResource; +class QSnapdRequest; +class QSnapdClient; + +class SnapTransaction : public Transaction +{ + Q_OBJECT +public: + SnapTransaction(QSnapdClient *client, SnapResource *app, Role role, AbstractResource::State newState); + + void cancel() override; + void proceed() override; + +private Q_SLOTS: + void finishTransaction(); + +private: + void setRequest(QSnapdRequest *req); + void progressed(); + + QSnapdClient *const m_client; + SnapResource *const m_app; + QScopedPointer m_request; + const AbstractResource::State m_newState; +}; diff --git a/libdiscover/backends/SnapBackend/libsnapclient/CMakeLists.txt b/libdiscover/backends/SnapBackend/libsnapclient/CMakeLists.txt new file mode 100644 index 0000000..4575fc9 --- /dev/null +++ b/libdiscover/backends/SnapBackend/libsnapclient/CMakeLists.txt @@ -0,0 +1,13 @@ +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-paths.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-paths.h) + +ki18n_wrap_ui(SnapMacaroonDialog_SRCS SnapMacaroonDialog.ui) +add_executable(SnapMacaroonDialog SnapMacaroonDialog.cpp ${SnapMacaroonDialog_SRCS}) +target_link_libraries(SnapMacaroonDialog Qt::Network Qt::Widgets KF5::AuthCore KF5::I18n) +install(TARGETS SnapMacaroonDialog DESTINATION ${KDE_INSTALL_LIBEXECDIR}/discover) + +add_executable(libsnap_helper SnapAuthHelper.cpp) +target_link_libraries(libsnap_helper Qt::Network KF5::AuthCore Snapd::Core) +install(TARGETS libsnap_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) + +kauth_install_actions(org.kde.discover.libsnapclient org.kde.discover.libsnapclient.actions) +kauth_install_helper_files(libsnap_helper org.kde.discover.libsnapclient root) diff --git a/libdiscover/backends/SnapBackend/libsnapclient/SnapAuthHelper.cpp b/libdiscover/backends/SnapBackend/libsnapclient/SnapAuthHelper.cpp new file mode 100644 index 0000000..e6d7f30 --- /dev/null +++ b/libdiscover/backends/SnapBackend/libsnapclient/SnapAuthHelper.cpp @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KAuth; + +class SnapAuthHelper : public QObject +{ + Q_OBJECT + QSnapdClient m_client; + +public: + SnapAuthHelper() + { + } + +public Q_SLOTS: + KAuth::ActionReply login(const QVariantMap &args) + { + const QString user = args[QStringLiteral("user")].toString(), pass = args[QStringLiteral("password")].toString(), + otp = args[QStringLiteral("otp")].toString(); + + QScopedPointer req(otp.isEmpty() ? m_client.login(user, pass) : m_client.login(user, pass, otp)); + + req->runSync(); + + ActionReply reply; + bool otpMode = false; + QByteArray replyData; + + if (req->error() == QSnapdRequest::NoError) { + const auto auth = req->authData(); + replyData = QJsonDocument(QJsonObject{ + {QStringLiteral("macaroon"), auth->macaroon()}, + {QStringLiteral("discharges"), QJsonArray::fromStringList(auth->discharges())}, + }) + .toJson(); + + reply = ActionReply::SuccessReply(); + } else { + otpMode = req->error() == QSnapdConnectRequest::TwoFactorRequired; + reply = ActionReply::InvalidActionReply(); + reply.setErrorDescription(req->errorString()); + } + reply.setData({ + {QStringLiteral("reply"), replyData}, + {QStringLiteral("otpMode"), otpMode}, + }); + return reply; + } +}; + +KAUTH_HELPER_MAIN("org.kde.discover.libsnapclient", SnapAuthHelper) + +#include "SnapAuthHelper.moc" diff --git a/libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.cpp b/libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.cpp new file mode 100644 index 0000000..c801e69 --- /dev/null +++ b/libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.cpp @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "ui_SnapMacaroonDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class MacaroonDialog : public QDialog +{ +public: + MacaroonDialog() + : QDialog() + { + m_ui.setupUi(this); + connect(this, &QDialog::accepted, this, &MacaroonDialog::startLogin); + connect(this, &QDialog::rejected, this, []() { + qApp->exit(1); + }); + + setOtpMode(false); + } + + void startLogin() + { + login(m_ui.username->text(), m_ui.password->text(), m_ui.otp->text()); + } + + void login(const QString &username, const QString &password, const QString &otp = {}) + { + KAuth::Action snapAction(QStringLiteral("org.kde.discover.libsnapclient.login")); + snapAction.setHelperId(QStringLiteral("org.kde.discover.libsnapclient")); + snapAction.setArguments({ + {QStringLiteral("user"), username}, + {QStringLiteral("password"), password}, + {QStringLiteral("otp"), otp}, + }); + Q_ASSERT(snapAction.isValid()); + + KAuth::ExecuteJob *reply = snapAction.execute(); + connect(reply, &KAuth::ExecuteJob::result, this, &MacaroonDialog::replied); + reply->start(); + } + + void setOtpMode(bool enabled) + { + m_ui.password->setEnabled(!enabled); + m_ui.otp->setVisible(enabled); + m_ui.otpLabel->setVisible(enabled); + } + + void replied(KJob *job) + { + KAuth::ExecuteJob *reply = static_cast(job); + const QVariantMap replyData = reply->data(); + if (reply->error() == 0) { + QTextStream(stdout) << replyData[QLatin1String("reply")].toString(); + QCoreApplication::instance()->exit(0); + } else { + const QString message = replyData.value(QLatin1String("errorString"), reply->errorString()).toString(); + setOtpMode(replyData[QLatin1String("otpMode")].toBool()); + + m_ui.errorMessage->setText(message); + show(); + } + } + + Ui::SnapMacaroonDialog m_ui; +}; + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); + QPointer dialog = new MacaroonDialog; + dialog->show(); + auto ret = app.exec(); + delete dialog; + return ret; +} diff --git a/libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.ui b/libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.ui new file mode 100644 index 0000000..9038009 --- /dev/null +++ b/libdiscover/backends/SnapBackend/libsnapclient/SnapMacaroonDialog.ui @@ -0,0 +1,146 @@ + + + SnapMacaroonDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + + + Log in to the <a href="https://login.ubuntu.com/">Snap store</a>: + + + + + + + Username: + + + + + + + Username + + + + + + + Password: + + + + + + + QLineEdit::Password + + + Password + + + + + + + Two-Factor: + + + Qt::PlainText + + + + + + + QLineEdit::Password + + + + + + + + + + true + + + Qt::NoTextInteraction + + + + + + + + + Qt::Vertical + + + + 20 + 119 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SnapMacaroonDialog + accept() + + + 254 + 293 + + + 157 + 274 + + + + + buttonBox + rejected() + SnapMacaroonDialog + reject() + + + 322 + 293 + + + 286 + 274 + + + + + diff --git a/libdiscover/backends/SnapBackend/libsnapclient/config-paths.h.cmake b/libdiscover/backends/SnapBackend/libsnapclient/config-paths.h.cmake new file mode 100644 index 0000000..9f26104 --- /dev/null +++ b/libdiscover/backends/SnapBackend/libsnapclient/config-paths.h.cmake @@ -0,0 +1 @@ +#define CMAKE_INSTALL_FULL_LIBEXECDIR "@CMAKE_INSTALL_FULL_LIBEXECDIR@" diff --git a/libdiscover/backends/SnapBackend/libsnapclient/org.kde.discover.libsnapclient.actions b/libdiscover/backends/SnapBackend/libsnapclient/org.kde.discover.libsnapclient.actions new file mode 100644 index 0000000..5a8fafb --- /dev/null +++ b/libdiscover/backends/SnapBackend/libsnapclient/org.kde.discover.libsnapclient.actions @@ -0,0 +1,106 @@ +[org.kde.discover.libsnapclient.login] +Name=Snap login action +Name[ar]=إجراء ولوج سناب +Name[az]=Snap giriş fəaliyyəti +Name[bg]=Действие при Snap влизане +Name[ca]=Accions de connexió de l'Snap +Name[ca@valencia]=Accions de connexió de Snap +Name[cs]=Činnost přihlášení snap +Name[da]=Snap login-handling +Name[de]=Anmeldungsaktion für Snap +Name[el]=Ενέργεια εισόδου snap +Name[en_GB]=Snap login action +Name[es]=Acciones de inicio de sesión en Snap +Name[et]=Snapi sisselogimine +Name[eu]=Snap saioa hasteko ekintza +Name[fi]=Snap-kirjautumistoiminto +Name[fr]=Action d'identification pour Snap +Name[gl]=Acción de identificación de Snap +Name[he]=פעולת התחברות ל־Snap +Name[hi]=स्नैप लॉगिन क्रिया +Name[hsb]=Snap přizjewjenje +Name[hu]=Snap bejelentkezési művelet +Name[ia]=Action de accesso de Snap +Name[id]=Tindakan login snap +Name[ie]=Inregistration snap +Name[it]=Azione di accesso Snap +Name[ja]=スナップログインアクション +Name[ka]=Snap-ის შესვლის ქმედება +Name[ko]=Snap 로그인 동작 +Name[lt]=Snap prisijungimo veiksmas +Name[my]=စနပ် လော့ဂင် လုပ်ဆောင်ချက် +Name[nb]=Snap-innloggingshandling +Name[nl]=Snap login actie +Name[nn]=Snap-innloggingshandling +Name[pa]=ਸਨੈਪ ਲਾਗਇਨ ਕਾਰਵਾਈ +Name[pl]=Działanie logowania Snap +Name[pt]=Acção de autenticação do Snap +Name[pt_BR]=Ação de login do Snap +Name[ro]=Acțiune de autentificare Snap +Name[ru]=Вход в систему приложений из snap-пакетов +Name[sk]=Akcia prihlásenia Snap +Name[sl]=Prijavno dejanje za Snap +Name[sr]=Снапијеве радње пријављивања +Name[sr@ijekavian]=Снапијеве радње пријављивања +Name[sr@ijekavianlatin]=Snappyjeve radnje prijavljivanja +Name[sr@latin]=Snappyjeve radnje prijavljivanja +Name[sv]=Snap inloggningsÃ¥tgärd +Name[tg]=Часпондани амали воридшавӣ +Name[tr]=Snap oturum açma eylemi +Name[uk]=Дія з входу Snap +Name[x-test]=xxSnap login actionxx +Name[zh_CN]=Snap 登录操作 +Name[zh_TW]=Snap 登入動作 +Description=Allows snap front-ends to log in +Description[ar]=يتيح لواجهات سناب الولوج +Description[az]=Snap paket proqramlarına, sistemə daxil olmağa icazə verir +Description[bg]=Позволява приложения за snap да се вписват +Description[ca]=Permet que els frontals de l'Snap es connectin +Description[ca@valencia]=Permet que els frontals de Snap es connecten +Description[cs]=Umožní rozhraní snap přihlásit se +Description[da]=Tillader snap-frontends at logge ind +Description[de]=Ermöglicht die Anmeldung von Snap-Front-Ends +Description[el]=Επιτρέπεται η σύνδεση στα περιβάλλοντα διεπαφής snap +Description[en_GB]=Allows snap front-ends to log in +Description[es]=Permite que las interfaces de Snap inicien sesión +Description[et]=Võimaldab Snapi kasutajaliidestel sisse logida +Description[eu]=Baimendu snap aurrealdekoei saioa hastea +Description[fi]=Sallii Snap-käyttöliittymien kirjautumisen +Description[fr]=Autoriser l'identification via les interfaces « Snap » +Description[gl]=Permite que se identifiquen as interfaces de Snap. +Description[hi]=स्नैप अग्रभाग को लॉगइन करने की अनुमति देता है +Description[hsb]=Dowoli snap-interfejsam so přizjewić +Description[hu]=Lehetővé teszi a snap frontendeknek a bejelentkezést +Description[ia]=Permitte interfacies de snap per authenticar se +Description[id]=Izinkan front-end snap untuk login +Description[ie]=Permisse a interfacies snap inregistrar se +Description[it]=Consenti alle interfacce snap di accedere +Description[ja]=スナップフロントエンドのログインを許可します +Description[ka]=Snap-ის წინაბოლოებისთვის შესვლის უფლების მიცემა +Description[ko]=Snap 프론트엔드 로그인 허용 +Description[lt]=Leidžia snap naudotojo sąsajoms prisijungti +Description[my]=စနပ်၏ အသုံးပြုသူ-ထိတွေ့ပရိုဂရမ်များကို လော့ဂင်ဝင်ခွင့်ပြုမည် +Description[nb]=Tillat Snap-grensesnitt Ã¥ logge inn +Description[nl]=Front-ends van snap toestaan zich aan te melden +Description[nn]=Tillèt Snap-grensesnitt Ã¥ logga inn +Description[pa]=ਲਾਗਇਨ ਲਈ ਸਨੈਪ ਫਰੰਡ-ਐਡ ਦੀ ਇਜਾਜ਼ਤ ਦਿੰਦਾ ਹੈ +Description[pl]=Umożliwia interfejsowi snap logowanie +Description[pt]=Permitir a autenticação da interfaces do 'snap' +Description[pt_BR]=Permite às interfaces do Snap logarem-se +Description[ro]=Permite interfețelor Snap să se autentifice +Description[ru]=Позволяет приложениям из snap-пакетов выполнять вход в систему +Description[sk]=Umožní snap frontendom prihlásenie +Description[sl]=Omogoči prijavo za začelja Snap +Description[sr]=Дозвољава прочељима Снапија да се пријављују +Description[sr@ijekavian]=Дозвољава прочељима Снапија да се пријављују +Description[sr@ijekavianlatin]=Dozvoljava pročeljima Snappyja da se prijavljuju +Description[sr@latin]=Dozvoljava pročeljima Snappyja da se prijavljuju +Description[sv]=Möjliggör för snap-gränssnitt att logga in +Description[tg]=Ба воситаҳои корбарии часпон барои воридшавӣ иҷозат медиҳад +Description[tr]=Snap ön uçlarının oturum açmasına izin verir +Description[uk]=Надає змогу входити до системи оболонкам snap +Description[x-test]=xxAllows snap front-ends to log inxx +Description[zh_CN]=允许 Snap 前端登录 +Description[zh_TW]=允許 snap 前端登入 +Policy=auth_self +Persistence=session diff --git a/libdiscover/backends/SnapBackend/org.kde.discover.snap.appdata.xml b/libdiscover/backends/SnapBackend/org.kde.discover.snap.appdata.xml new file mode 100644 index 0000000..1a5f627 --- /dev/null +++ b/libdiscover/backends/SnapBackend/org.kde.discover.snap.appdata.xml @@ -0,0 +1,150 @@ + + + org.kde.discover.snap + Snap backend + سَند سناب + Snap modulları + Бекенд на Snap + Dorsal de l'Snap + Dorsal de Snap + Podpůrná vrstva Snap + Snap-motor + Snap-Backend + Snap backend + Motor Snap + Snapi taustaprogramm + Snap bizkarraldekoa + Snap-taustaosa + Moteur « Snap » + Motor de Snap + स्नैप पृष्ठभाग + Snap backend + Retro-Administration de Snap + Backend Snap + Infrastructura Snap + Motore Snap + Snap-ის უკანაბოლო + Snap 백엔드 + Snap vidinė pusė + സ്നാപ്പ് ബാക്കെൻഡ് + စနပ် အုတ်မြစ်ပရိုဂရမ် + Baksystem for Snap + Snap-backend + Snap-motor + ਸਨੈਪ ਬੈਕਐਂਡ + Silnik Snap + Infra-estrutura do Snap + Infraestrutura Snap + Platformă Snap + Модуль поддержки формата Snap + Podporný program pre Snap + Zaledje Snap + Gränssnitt för Snap + Коркардкунандаи Часпиш + Snap arka ucu + Модуль Snap + xxSnap backendxx + Snap 后端程序 + Snap 後端 + Integrates Snap applications into Discover + يُكامل تطبيقات ”سناب“ في «استكشف» + Snap tətbiqlərini Discover-ə inteqrasiya edir + Интегрира Snap приложения в Discover + Integra les aplicacions de l'Snap al Discover + Integra les aplicacions de Snap a dins de Discover + Integruje aplikace Snap do Discover + Integrerer Snap-programmer i Discover + Integriert Snap-Anwendungen in Discover + Integrates Snap applications into Discover + Integra aplicaciones Snap en Discover + Snapi rakenduste lõimimine Discoverisse + Snap aplikazioak Discover-ren integratzen ditu + Yhdistää Snap-sovellukset Discoveriin + Intègre les applications « Snap » au sein de Discover + Integra aplicacións de Snap con Discover. + स्नैप अनुप्रयोगों को डिस्कवर में एकीकृत करता है + Snap alkalmazások integrálása a Discoverbe + Integra pplicationes de Snap in Discover + Aplikasi Snap terintegrasi ke dalam Discover + Integra applicationes Snap con Discover + Integra le applicazioni Snap in Discover + Snap-ის ინტეგრაცია Discover-ში + Snap 앱을 Discover에 통합 + Integruoja Snap programas į Discover + സ്നാപ്പ് ആപ്ലിക്കേഷനുകൾ ഡിസ്കവറിലേക്ക് സംയോജിപ്പിക്കുന്നു + စနပ် အပ္ပလီကေးရှင်းများကို ဒစ်(စ)ကာဗာနှင့် ပူးပေါင်းဆက်နွယ်ပေးသည် + Integrerer Snap-programmer i Discover + Integreert Snap-toepassingen in Ontdekken + Integrerer Snap-program i Discover + ਸਨੈਪ ਐਪਲੀਕੇਸ਼ਨਾਂ ਨੂੰ ਡਿਸਕਵਰ ਵਿੱਚ ਜੋੜਦਾ ਹੈ + Integruje aplikacje Snap w Odkrywcy + Integra as aplicações do Snap no Discover + Integra aplicativos Snap no Discover + Integrează aplicații Snap în Discover + Добавление поддержки формата Snap в центр программ Discover + Integruje aplikácie Snap do aplikácie Discover + V Discover vgradi programe Snap + Integrerar Snap-program i Discover + Барномаҳои Часпишро ба барномаи Кашфиёт дарунсохт мекунад + Snap uygulamalarını Keşfet ile bütünleştirir + Інтегрує програми Snap до Discover + xxIntegrates Snap applications into Discoverxx + 为 Discover 提供 Snap 应用程序的集成功能 + 將 Snap 應用程式整合進 Discover 商店 + org.kde.discover.desktop + CC0-1.0 + GPL-2.0+ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + एलिक्स पॉल गोंज़ालेज़ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + အဲလက်ပိုဂွန်ဇလက် + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + ਐਲਿਕਸ ਪੋਲ ਗੋਨਜ਼ਾਵੇਜ + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + Aleix Pol Gonzalez + அலெயிக்சு போல் கொன்ஸாலெசு + Алейкс Пол Гонзалес (Aleix Pol Gonzalez) + Aleix Pol Gonzalez + Aleix Pol Gonzalez + xxAleix Pol Gonzalezxx + Aleix Pol Gonzalez + Aleix Pol Gonzalez + system-software-install + + + + + + + diff --git a/libdiscover/backends/SnapBackend/qml/ChannelsButton.qml b/libdiscover/backends/SnapBackend/qml/ChannelsButton.qml new file mode 100644 index 0000000..1d39598 --- /dev/null +++ b/libdiscover/backends/SnapBackend/qml/ChannelsButton.qml @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.1 +import org.kde.kirigami 2.14 as Kirigami + +Button +{ + id: root + text: i18nd("libdiscover", "Channels…") + + onClicked: overlay.open() + visible: resource.isInstalled /*&& view.count > 0*/ + + Kirigami.OverlaySheet { + id: overlay + parent: applicationWindow().overlay + title: i18nd("libdiscover", "%1 channels", resource.name) + + ListView { + id: view + + model: resource.channels(root).channels + delegate: Kirigami.BasicListItem { + readonly property bool current: resource.channel === modelData.name + label: i18nd("libdiscover", "%1 - %2", modelData.name, modelData.version) + + trailing: Button { + text: i18nd("libdiscover", "Switch") + enabled: !parent.current + onClicked: resource.channel = modelData.name + } + } + } + } +} diff --git a/libdiscover/backends/SnapBackend/qml/PermissionsButton.qml b/libdiscover/backends/SnapBackend/qml/PermissionsButton.qml new file mode 100644 index 0000000..fefee56 --- /dev/null +++ b/libdiscover/backends/SnapBackend/qml/PermissionsButton.qml @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Controls 2.1 +import org.kde.kirigami 2.14 as Kirigami + +Button +{ + id: root + text: i18nd("libdiscover", "Configure permissions…") + + onClicked: overlay.open() + visible: resource.isInstalled && view.count > 0 + + Kirigami.OverlaySheet { + id: overlay + parent: applicationWindow().overlay + title: i18nd("libdiscover", "Permissions for %1", resource.name) + + property QtObject errorMessage: null + + ListView { + id: view + model: resource.plugs(root) + Connections { + target: view.model + function onError(message) { overlay.errorMessage = message } + } + header: DiscoverInlineMessage { + inlineMessage: overlay.errorMessage + } + delegate: CheckDelegate { + id: delegate + width: view.width + text: model.display + checked: model.checked + onToggled: { + model.checked = delegate.checked + } + } + } + } +} diff --git a/libdiscover/backends/SnapBackend/snapui.qrc b/libdiscover/backends/SnapBackend/snapui.qrc new file mode 100644 index 0000000..06921b7 --- /dev/null +++ b/libdiscover/backends/SnapBackend/snapui.qrc @@ -0,0 +1,7 @@ + + + + qml/PermissionsButton.qml + qml/ChannelsButton.qml + + diff --git a/libdiscover/backends/SteamOSBackend/CMakeLists.txt b/libdiscover/backends/SteamOSBackend/CMakeLists.txt new file mode 100644 index 0000000..be09c33 --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/CMakeLists.txt @@ -0,0 +1,23 @@ +set(steamos-backend_SRCS + SteamOSResource.h + SteamOSResource.cpp + SteamOSBackend.h + SteamOSBackend.cpp + SteamOSTransaction.h + SteamOSTransaction.cpp + dbushelpers.h + dbushelpers.cpp +) + +set(atomupd1_xml com.steampowered.Atomupd1.xml) +set_source_files_properties(${atomupd1_xml} PROPERTIES + INCLUDE "dbushelpers.h" + NA_NAMESPACE TRUE +) +qt_add_dbus_interface(steamos-backend_SRCS ${atomupd1_xml} atomupd1) +qt_add_dbus_interface(steamos-backend_SRCS org.freedesktop.DBus.Properties.xml dbusproperties_interface) + +add_library(steamos-backend MODULE ${steamos-backend_SRCS}) +target_link_libraries(steamos-backend Qt::Core Qt::Widgets Qt::DBus KF5::CoreAddons KF5::ConfigCore Discover::Common) + +install(TARGETS steamos-backend DESTINATION ${KDE_INSTALL_PLUGINDIR}/discover) diff --git a/libdiscover/backends/SteamOSBackend/SteamOSBackend.cpp b/libdiscover/backends/SteamOSBackend/SteamOSBackend.cpp new file mode 100644 index 0000000..2430a4e --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/SteamOSBackend.cpp @@ -0,0 +1,230 @@ +/* + * SPDX-FileCopyrightText: 2022 Jeremy Whiting + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SteamOSBackend.h" +#include "SteamOSResource.h" +#include "SteamOSTransaction.h" +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "atomupd1.h" + +DISCOVER_BACKEND_PLUGIN(SteamOSBackend) + +// We expect 2 results, updates and later updates +#define CHECK_UPDATES_RETURN_COUNT 2 +#define ATOMUPD_SERVICE_PATH "/usr/lib/systemd/system/atomupd.service" + +QString SteamOSBackend::service() +{ + return QStringLiteral("com.steampowered.Atomupd1"); +} + +QString SteamOSBackend::path() +{ + return QStringLiteral("/com/steampowered/Atomupd1"); +} + +SteamOSBackend::SteamOSBackend(QObject *parent) + : AbstractResourcesBackend(parent) + , m_updater(new StandardBackendUpdater(this)) + , m_updateVersion() + , m_updateSize(0) + , m_resource(nullptr) +{ + qDBusRegisterMetaType(); + + connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &SteamOSBackend::updatesCountChanged); + + m_interface = new ComSteampoweredAtomupd1Interface(service(), path(), QDBusConnection::systemBus(), this); + + // First try to get the current version, only check valid after that since + // this could wake up the service + m_currentVersion = m_interface->currentVersion(); + m_currentBuildID = m_interface->currentBuildID(); + qDebug() << "steamos-backend: Current version from dbus api: " << m_currentVersion << " and build ID: " << m_currentBuildID; + + // If we got a version property, assume the service is responding and check for updates + if (!m_currentVersion.isEmpty() && !m_currentBuildID.isEmpty()) { + checkForUpdates(); + } else { + qDebug() << "steamos-backend: Unable to query atomupd for SteamOS Updates..."; + // Should never happen, since trying to open the interface above starts + // it, but if this plugin is on non steamos devices we should show something + Q_EMIT passiveMessage(i18n("SteamOS: Unable to query atomupd for SteamOS Updates...")); + } +} + +void SteamOSBackend::hasUpdateChanged(bool hasUpdate) +{ + if (hasUpdate) { + // Create or update resource from m_updateVersion, m_updateBuild + if (!m_resource) { + qDebug() << "steamos-backend: Creating new SteamOSResource with build id: " << m_updateBuild; + m_resource = + new SteamOSResource(m_updateVersion, m_updateBuild, m_updateSize, QStringLiteral("%1 - %2").arg(m_currentVersion).arg(m_currentBuildID), this); + } else { + qDebug() << "steamos-backend: Updating SteamOSResource with new version: " << m_updateVersion << " and new build id: " << m_updateBuild; + m_resource->setVersion(m_updateVersion); + m_resource->setBuild(m_updateBuild); + Q_EMIT m_resource->versionsChanged(); + } + } else { + // Clear or remove any previously created resource + } + + qDebug() << "steamos-backend: Updates count is now " << updatesCount(); +} + +void SteamOSBackend::needRebootChanged() +{ + // Tell gui we need to reboot + m_updater->enableNeedsReboot(); +} + +void SteamOSBackend::acquireFetching(bool f) +{ + if (f) + m_fetching++; + else + m_fetching--; + + if ((!f && m_fetching == 0) || (f && m_fetching == 1)) { + Q_EMIT fetchingChanged(); + } +} + +void SteamOSBackend::checkForUpdates() +{ + if (m_fetching) + return; + + // If there's no dbus object to talk to, just return + // TODO: Maybe show a warning/error? + if (m_currentVersion.isEmpty()) + return; + + acquireFetching(true); + + qDebug() << "steamos-backend-backend::checkForUpdates asking DBus api"; + // We don't send any options for now, dbus api doesn't do anything with them yet anyway + QDBusPendingReply reply = m_interface->CheckForUpdates({}); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &SteamOSBackend::checkForUpdatesFinished); +} + +void SteamOSBackend::checkForUpdatesFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + // qobject_cast(*call); + if (call->isError()) { + Q_EMIT passiveMessage(call->error().message()); + qDebug() << "steamos-backend: CheckForUpdates error: " << call->error().message(); + } else { + // Valid response, parse it + VariantMapMap versions = reply.argumentAt<0>(); + qDebug() << "steamos-backend-backend: Versions available: " << versions; + VariantMapMap laterVersions = reply.argumentAt<1>(); + + if (versions.isEmpty()) { + // No updates + hasUpdateChanged(false); + } else { + m_updateBuild = versions.keys().at(0); + QVariantMap data = versions.value(m_updateBuild); + m_updateVersion = data.value("version").toString(); + m_updateSize = data.value("estimated_size").toUInt(); + qDebug() << "steamos-backend: Data values: " << data.values(); + hasUpdateChanged(true); + } + } + acquireFetching(false); + + call->deleteLater(); +} + +int SteamOSBackend::updatesCount() const +{ + return m_updater->updatesCount(); +} + +// SteamOS never has any searchable packages or anything, so just always +// give a void stream +ResultsStream *SteamOSBackend::search(const AbstractResourcesBackend::Filters &filter) +{ + QVector res; + if (m_resource && m_resource->state() >= filter.state) + res << m_resource; + return new ResultsStream(QLatin1String("SteamOS-stream"), res); +} + +QHash SteamOSBackend::resources() const +{ + return m_resources; +} + +bool SteamOSBackend::isValid() const +{ + return QFile(QStringLiteral(ATOMUPD_SERVICE_PATH)).exists(); +} + +AbstractBackendUpdater *SteamOSBackend::backendUpdater() const +{ + return m_updater; +} + +AbstractReviewsBackend *SteamOSBackend::reviewsBackend() const +{ + return nullptr; +} + +Transaction *SteamOSBackend::installApplication(AbstractResource *app, const AddonList &addons) +{ + Q_UNUSED(addons); + return installApplication(app); +} + +Transaction *SteamOSBackend::installApplication(AbstractResource *app) +{ + SteamOSTransaction *transaction = new SteamOSTransaction(qobject_cast(app), Transaction::InstallRole, m_interface); + connect(transaction, &SteamOSTransaction::needReboot, this, &SteamOSBackend::needRebootChanged); + return transaction; +} + +Transaction *SteamOSBackend::removeApplication(AbstractResource *) +{ + qWarning() << "steamos-backend: Unsupported operation:" << __PRETTY_FUNCTION__; + return nullptr; +} + +bool SteamOSBackend::isFetching() const +{ + return m_fetching > 0; +} + +QString SteamOSBackend::displayName() const +{ + return QStringLiteral("SteamOS"); +} + +#include "SteamOSBackend.moc" diff --git a/libdiscover/backends/SteamOSBackend/SteamOSBackend.h b/libdiscover/backends/SteamOSBackend/SteamOSBackend.h new file mode 100644 index 0000000..e68b95d --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/SteamOSBackend.h @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2022 Jeremy Whiting + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef STEAMOSBACKEND_H +#define STEAMOSBACKEND_H + +#include +#include +#include +#include + +class ComSteampoweredAtomupd1Interface; +class QDBusPendingCallWatcher; +class StandardBackendUpdater; +class SteamOSResource; +class SteamOSBackend : public AbstractResourcesBackend +{ + Q_OBJECT +public: + explicit SteamOSBackend(QObject *parent = nullptr); + + int updatesCount() const override; + AbstractBackendUpdater *backendUpdater() const override; + AbstractReviewsBackend *reviewsBackend() const override; + ResultsStream *search(const AbstractResourcesBackend::Filters &search) override; + QHash resources() const; + bool isValid() const override; + Transaction *installApplication(AbstractResource *app) override; + Transaction *installApplication(AbstractResource *app, const AddonList &addons) override; + Transaction *removeApplication(AbstractResource *app) override; + + bool isFetching() const override; + void checkForUpdates() override; + QString displayName() const override; + + static QString service(); + static QString path(); + +public Q_SLOTS: + void checkForUpdatesFinished(QDBusPendingCallWatcher *call); + +private Q_SLOTS: + void needRebootChanged(); + +private: + void hasUpdateChanged(bool hasUpdate); + + void acquireFetching(bool f); + + QHash m_resources; + StandardBackendUpdater *m_updater; + uint m_fetching = 0; + + QString m_updateVersion; // Next update version, can use once we get from dbus. + QString m_updateBuild; // Next build version. + quint64 m_updateSize; // Estimated size of next update + + QPointer m_resource; // Since we only ever have one, cache it. + + QPointer m_interface; // Interface to atomupd dbus api + QString m_currentVersion; + QString m_currentBuildID; +}; + +#endif // STEAMOSBACKEND_H diff --git a/libdiscover/backends/SteamOSBackend/SteamOSResource.cpp b/libdiscover/backends/SteamOSBackend/SteamOSResource.cpp new file mode 100644 index 0000000..f7d539e --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/SteamOSResource.cpp @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: 2022 Jeremy Whiting + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SteamOSResource.h" +#include +#include +#include +#include + +SteamOSResource::SteamOSResource(const QString &version, const QString &build, quint64 size, const QString ¤tVersion, AbstractResourcesBackend *parent) + : AbstractResource(parent) + , m_name("SteamOS") + , m_build(build) + , m_version(version) + , m_currentVersion(currentVersion) + , m_appstreamId("steamos." + m_build) + , m_state(State::Upgradeable) + , m_addons() + , m_type(AbstractResource::Technical) + , m_size(size) +{ +} + +QString SteamOSResource::appstreamId() const +{ + return m_appstreamId; +} + +QList SteamOSResource::addonsInformation() +{ + return m_addons; +} + +QString SteamOSResource::availableVersion() const +{ + return QStringLiteral("%1 - %2").arg(m_version, m_build); +} + +QStringList SteamOSResource::categories() +{ + return {QStringLiteral("steamos")}; +} + +QString SteamOSResource::comment() +{ + return name(); +} + +quint64 SteamOSResource::size() +{ + return m_size; +} + +QUrl SteamOSResource::homepage() +{ + return QUrl(QStringLiteral("https://store.steampowered.com/")); +} + +QUrl SteamOSResource::helpURL() +{ + return QUrl(QStringLiteral("https://store.steampowered.com/")); +} + +QUrl SteamOSResource::bugURL() +{ + return QUrl(QStringLiteral("https://steamcommunity.com/app/1675200/discussions/")); +} + +QUrl SteamOSResource::donationURL() +{ + return {}; +} + +QUrl SteamOSResource::contributeURL() +{ + return {}; +} + +QVariant SteamOSResource::icon() const +{ + return QStringLiteral("steam"); +} + +QString SteamOSResource::installedVersion() const +{ + return m_currentVersion; +} + +QJsonArray SteamOSResource::licenses() +{ + return {}; +} + +QString SteamOSResource::longDescription() +{ + return {}; +} + +QString SteamOSResource::name() const +{ + return m_name; +} + +QString SteamOSResource::origin() const +{ + return QStringLiteral("SteamOS"); +} + +QString SteamOSResource::packageName() const +{ + return m_name; +} + +bool SteamOSResource::isRemovable() const +{ + return false; +} + +AbstractResource::Type SteamOSResource::type() const +{ + return m_type; +} + +bool SteamOSResource::canExecute() const +{ + return false; +} + +QString SteamOSResource::section() +{ + return QStringLiteral("SteamOS"); +} + +AbstractResource::State SteamOSResource::state() +{ + return m_state; +} + +void SteamOSResource::fetchChangelog() +{ + QString log = longDescription(); + log.replace(QLatin1Char('\n'), QLatin1String("
    ")); + + Q_EMIT changelogFetched(log); +} + +void SteamOSResource::setState(AbstractResource::State state) +{ + if (m_state == state) + return; + + m_state = state; + Q_EMIT stateChanged(); +} + +void SteamOSResource::setSize(quint64 size) +{ + m_size = size; + Q_EMIT sizeChanged(); +} + +QString SteamOSResource::sourceIcon() const +{ + return QStringLiteral("steam"); +} + +QDate SteamOSResource::releaseDate() const +{ + return {}; +} + +void SteamOSResource::setVersion(const QString &version) +{ + m_version = version; +} + +void SteamOSResource::setBuild(const QString &build) +{ + m_build = build; + m_appstreamId = "steamos." + m_build; +} + +QString SteamOSResource::getBuild() const +{ + return m_build; +} + +QUrl SteamOSResource::url() const +{ + return QUrl(QLatin1String("steamos://") + packageName().replace(QLatin1Char(' '), QLatin1Char('.'))); +} + +QString SteamOSResource::author() const +{ + return QStringLiteral("Valve"); +} diff --git a/libdiscover/backends/SteamOSBackend/SteamOSResource.h b/libdiscover/backends/SteamOSBackend/SteamOSResource.h new file mode 100644 index 0000000..14c8a31 --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/SteamOSResource.h @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2022 Jeremy Whiting + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef STEAMOSRESOURCE_H +#define STEAMOSRESOURCE_H + +#include + +class AddonList; +class SteamOSResource : public AbstractResource +{ + Q_OBJECT +public: + explicit SteamOSResource(const QString &version, const QString &build, quint64 size, const QString ¤tVersion, AbstractResourcesBackend *parent); + + QString appstreamId() const override; + QList addonsInformation() override; + QString section() override; + QString origin() const override; + QString longDescription() override; + QString availableVersion() const override; + QString installedVersion() const override; + QJsonArray licenses() override; + quint64 size() override; + QUrl homepage() override; + QUrl helpURL() override; + QUrl bugURL() override; + QUrl donationURL() override; + QUrl contributeURL() override; + QStringList categories() override; + AbstractResource::State state() override; + QVariant icon() const override; + QString comment() override; + QString name() const override; + QString packageName() const override; + bool isRemovable() const override; + AbstractResource::Type type() const override; + bool canExecute() const override; + void invokeApplication() const override{}; + void fetchChangelog() override; + QUrl url() const override; + QString author() const override; + void setState(State state); + void setSize(quint64 size); + + QString sourceIcon() const override; + QDate releaseDate() const override; + + void setVersion(const QString &version); + void setBuild(const QString &build); + QString getBuild() const; + +public: + const QString m_name; + QString m_build; + QString m_version; + QString m_currentVersion; + QString m_appstreamId; + AbstractResource::State m_state; + QList m_addons; + const AbstractResource::Type m_type; + quint64 m_size; +}; + +#endif // STEAMOSRESOURCE_H diff --git a/libdiscover/backends/SteamOSBackend/SteamOSTransaction.cpp b/libdiscover/backends/SteamOSBackend/SteamOSTransaction.cpp new file mode 100644 index 0000000..e2a893c --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/SteamOSTransaction.cpp @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2022 Jeremy Whiting + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SteamOSTransaction.h" +#include "SteamOSResource.h" +#include +#include +#include +#include + +#include "SteamOSBackend.h" +#include "dbusproperties_interface.h" + +SteamOSTransaction::SteamOSTransaction(SteamOSResource *app, Transaction::Role role, ComSteampoweredAtomupd1Interface *interface) + : Transaction(app->backend(), app, role, {}) + , m_app(app) + , m_interface(interface) +{ + setCancellable(true); + setStatus(Status::SetupStatus); + + auto steamosProperties = new OrgFreedesktopDBusPropertiesInterface(SteamOSBackend::service(), SteamOSBackend::path(), QDBusConnection::systemBus(), this); + connect(steamosProperties, + &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, + this, + [this](const QString &interface_name, const QVariantMap &changed_properties, const QStringList &invalidated_properties) { + if (interface_name != SteamOSBackend::service()) { + return; + } + + auto changed = [&](const QString &property) { + return changed_properties.contains(property) || invalidated_properties.contains(property); + }; + if (changed("ProgressPercentage")) { + // Get percentage and pass on to gui + double percent = m_interface->progressPercentage(); + qDebug() << "steamos-backend: Progress percentage: " << percent; + setProgress(qBound(0.0, percent, 100.0)); + } + if (changed("EstimatedCompletionTime")) { + qulonglong timeRemaining = m_interface->estimatedCompletionTime(); + qDebug() << "steamos-backend: Estimated completion time: " << timeRemaining; + setRemainingTime(timeRemaining); + } + if (changed("UpdateStatus")) { + refreshStatus(); + } + }); + + m_interface->StartUpdate(m_app->getBuild()); + refreshStatus(); +} + +void SteamOSTransaction::cancel() +{ + if (!m_interface) { + // This should never happen + qWarning() << "steamos-backend: Error: No DBus interface provided to cancel. Please file a bug."; + return; + } + + m_interface->CancelUpdate(); + + setStatus(CancelledStatus); +} + +void SteamOSTransaction::finishTransaction() +{ + AbstractResource::State newState; + switch (role()) { + case InstallRole: + case ChangeAddonsRole: + newState = AbstractResource::Installed; + Q_EMIT needReboot(); + break; + case RemoveRole: + newState = AbstractResource::None; + break; + } + m_app->setState(newState); + setStatus(DoneStatus); + deleteLater(); +} + +void SteamOSTransaction::refreshStatus() +{ + // Get update state and update our state + uint status = m_interface->updateStatus(); + qDebug() << "steamos-backend: New state: " << status; + + // Status is one of these from the xml definition: + // 0 = IDLE, the update has not been launched yet + // 1 = IN_PROGRESS, the update is currently being applied + // 2 = PAUSED, the update has been paused + // 3 = SUCCESSFUL, the update process successfully completed + // 4 = FAILED, an error occurred during the update + // 5 = CANCELLED, a special case of FAILED where the update attempt has been cancelled + switch (status) { + case 0: // IDLE + break; + case 1: // IN_PROGRESS + setStatus(Status::DownloadingStatus); + break; + case 2: // PAUSED + setStatus(Status::QueuedStatus); + break; + case 3: // SUCCESSFUL + setStatus(Status::DoneStatus); + finishTransaction(); + break; + case 4: // FAILED + setStatus(Status::DoneWithErrorStatus); + finishTransaction(); + break; + case 5: // CANCELLED + setStatus(Status::CancelledStatus); + finishTransaction(); + break; + } +} diff --git a/libdiscover/backends/SteamOSBackend/SteamOSTransaction.h b/libdiscover/backends/SteamOSBackend/SteamOSTransaction.h new file mode 100644 index 0000000..aeeb35d --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/SteamOSTransaction.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2022 Jeremy Whiting + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef STEAMOSTRANSACTION_H +#define STEAMOSTRANSACTION_H + +#include +#include + +#include "atomupd1.h" + +class QTimer; +class SteamOSResource; +class SteamOSTransaction : public Transaction +{ + Q_OBJECT +public: + SteamOSTransaction(SteamOSResource *app, Role role, ComSteampoweredAtomupd1Interface *interface); + + void cancel() override; + +Q_SIGNALS: + void needReboot(); + +private Q_SLOTS: + void refreshStatus(); + +private: + void finishTransaction(); + + SteamOSResource *const m_app; + QPointer m_interface; // Interface to atomupd dbus api +}; + +#endif // STEAMOSTRANSACTION_H diff --git a/libdiscover/backends/SteamOSBackend/com.steampowered.Atomupd1.xml b/libdiscover/backends/SteamOSBackend/com.steampowered.Atomupd1.xml new file mode 100644 index 0000000..71ea0ff --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/com.steampowered.Atomupd1.xml @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libdiscover/backends/SteamOSBackend/dbushelpers.cpp b/libdiscover/backends/SteamOSBackend/dbushelpers.cpp new file mode 100644 index 0000000..ccfb430 --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/dbushelpers.cpp @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2018-2019 Red Hat Inc + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * SPDX-FileCopyrightText: 2018-2019 Jan Grulich + */ + +#include "dbushelpers.h" + +Q_DECLARE_METATYPE(VariantMapMap) diff --git a/libdiscover/backends/SteamOSBackend/dbushelpers.h b/libdiscover/backends/SteamOSBackend/dbushelpers.h new file mode 100644 index 0000000..f9d253a --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/dbushelpers.h @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2018-2019 Red Hat Inc + * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * SPDX-FileCopyrightText: 2018-2019 Jan Grulich + */ + +#pragma once + +#include +#include +#include + +/// a{sa{sv}} +using VariantMapMap = QMap>; diff --git a/libdiscover/backends/SteamOSBackend/org.freedesktop.DBus.Properties.xml b/libdiscover/backends/SteamOSBackend/org.freedesktop.DBus.Properties.xml new file mode 100644 index 0000000..0967d89 --- /dev/null +++ b/libdiscover/backends/SteamOSBackend/org.freedesktop.DBus.Properties.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libdiscover/config-paths.h.cmake b/libdiscover/config-paths.h.cmake new file mode 100644 index 0000000..92f4926 --- /dev/null +++ b/libdiscover/config-paths.h.cmake @@ -0,0 +1 @@ +#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "@CMAKE_INSTALL_FULL_LIBEXECDIR_KF5@" diff --git a/libdiscover/notifiers/BackendNotifierModule.cpp b/libdiscover/notifiers/BackendNotifierModule.cpp new file mode 100644 index 0000000..9ce7bd0 --- /dev/null +++ b/libdiscover/notifiers/BackendNotifierModule.cpp @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "BackendNotifierModule.h" + +BackendNotifierModule::BackendNotifierModule(QObject *parent) + : QObject(parent) +{ +} + +BackendNotifierModule::~BackendNotifierModule() = default; diff --git a/libdiscover/notifiers/BackendNotifierModule.h b/libdiscover/notifiers/BackendNotifierModule.h new file mode 100644 index 0000000..58a46d9 --- /dev/null +++ b/libdiscover/notifiers/BackendNotifierModule.h @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovernotifiers_export.h" +#include + +class DISCOVERNOTIFIERS_EXPORT UpgradeAction : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString description READ description CONSTANT) +public: + UpgradeAction(const QString &name, const QString &description, QObject *parent) + : QObject(parent) + , m_name(name) + , m_description(description) + { + } + + QString name() const + { + return m_name; + } + + QString description() const + { + return m_description; + } + + void trigger() + { + Q_EMIT triggered(m_name); + } + +Q_SIGNALS: + void triggered(const QString &name); + +private: + const QString m_name; + const QString m_description; +}; + +class DISCOVERNOTIFIERS_EXPORT BackendNotifierModule : public QObject +{ + Q_OBJECT +public: + explicit BackendNotifierModule(QObject *parent = nullptr); + ~BackendNotifierModule() override; + + /*** Check for new updates. Emits @see foundUpdates when it finds something. **/ + virtual void recheckSystemUpdateNeeded() = 0; + + /*** @returns count of !security updates only. **/ + virtual bool hasUpdates() = 0; + + /*** @returns count of security updates only. **/ + virtual bool hasSecurityUpdates() = 0; + + /** @returns whether the system changed in a way that needs to be rebooted. */ + virtual bool needsReboot() const = 0; + +Q_SIGNALS: + /** + * This signal is emitted when any new updates are available. + * @see recheckSystemUpdateNeeded + */ + void foundUpdates(); + + /** Notifies that the system needs a reboot. @see needsReboot */ + void needsRebootChanged(); + + /** notifies about an available upgrade */ + void foundUpgradeAction(UpgradeAction *action); +}; + +Q_DECLARE_INTERFACE(BackendNotifierModule, "org.kde.discover.BackendNotifierModule") diff --git a/libdiscover/notifiers/CMakeLists.txt b/libdiscover/notifiers/CMakeLists.txt new file mode 100644 index 0000000..6c16765 --- /dev/null +++ b/libdiscover/notifiers/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(DiscoverNotifiers BackendNotifierModule.cpp BackendNotifierModule.h) +target_link_libraries(DiscoverNotifiers + PUBLIC + Qt::Core +) + +generate_export_header(DiscoverNotifiers) + +target_include_directories(DiscoverNotifiers PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +install(TARGETS DiscoverNotifiers DESTINATION ${KDE_INSTALL_LIBDIR}/plasma-discover) + +add_library(Discover::Notifiers ALIAS DiscoverNotifiers) diff --git a/libdiscover/resources/AbstractBackendUpdater.cpp b/libdiscover/resources/AbstractBackendUpdater.cpp new file mode 100644 index 0000000..7ae9888 --- /dev/null +++ b/libdiscover/resources/AbstractBackendUpdater.cpp @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AbstractBackendUpdater.h" +#include "AbstractResource.h" + +AbstractBackendUpdater::AbstractBackendUpdater(QObject *parent) + : QObject(parent) +{ +} + +void AbstractBackendUpdater::cancel() +{ + Q_ASSERT(isCancelable() && "only call cancel when cancelable"); + Q_ASSERT(false && "if it can be canceled, then ::cancel() must be implemented"); +} + +void AbstractBackendUpdater::fetchChangelog() const +{ + const auto toUpd = toUpdate(); + for (auto res : toUpd) { + res->fetchChangelog(); + } +} + +void AbstractBackendUpdater::enableNeedsReboot() +{ + if (m_needsReboot) + return; + + m_needsReboot = true; + Q_EMIT needsRebootChanged(); +} + +void AbstractBackendUpdater::enableReadyToReboot() +{ + m_readyToReboot = true; +} + +bool AbstractBackendUpdater::needsReboot() const +{ + return m_needsReboot; +} + +bool AbstractBackendUpdater::isReadyToReboot() const +{ + return m_readyToReboot; +} + +void AbstractBackendUpdater::setOfflineUpdates(bool useOfflineUpdates) +{ + Q_UNUSED(useOfflineUpdates); +} + +void AbstractBackendUpdater::setErrorMessage(const QString &errorMessage) +{ + if (errorMessage == m_errorMessage) { + return; + } + m_errorMessage = errorMessage; + Q_EMIT errorMessageChanged(); +} diff --git a/libdiscover/resources/AbstractBackendUpdater.h b/libdiscover/resources/AbstractBackendUpdater.h new file mode 100644 index 0000000..9ab0175 --- /dev/null +++ b/libdiscover/resources/AbstractBackendUpdater.h @@ -0,0 +1,234 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include + +class QDateTime; +class AbstractResource; + +/** + * \class AbstractBackendUpdater AbstractBackendUpdater.h "AbstractBackendUpdater.h" + * + * \brief This is the base class for all abstract classes, which handle system upgrades. + * + * While implementing this is not mandatory for all backends (you can also use the + * StandardBackendUpdater, which just uses the functions in the ResourcesBackend to + * update the packages), it is recommended for many. + * + * Before starting the update, the AbstractBackendUpdater will have to keep a list of + * packages, which are about to be upgraded. First, all packages have to be inserted + * into this list in the \prepare method and they can then be changed by the user through + * the \addResources and \removeResources functions. + * + * When \start is called, the AbstractBackendUpdater should start the update and report its + * progress through the rest of methods outlined in this API documentation. + * + * @see addResources + * @see removeResources + * @see start + * @see prepare + */ +class DISCOVERCOMMON_EXPORT AbstractBackendUpdater : public QObject +{ + Q_OBJECT + Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged) + Q_PROPERTY(bool isCancelable READ isCancelable NOTIFY cancelableChanged) + Q_PROPERTY(bool isProgressing READ isProgressing NOTIFY progressingChanged) + Q_PROPERTY(bool needsReboot READ needsReboot NOTIFY needsRebootChanged) + Q_PROPERTY(quint64 downloadSpeed READ downloadSpeed NOTIFY downloadSpeedChanged) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) +public: + enum State { + None, + Downloading, + Installing, + Done, + }; + Q_ENUM(State) + + /** + * Constructs an AbstractBackendUpdater + */ + explicit AbstractBackendUpdater(QObject *parent = nullptr); + + /** + * This method is called, when Discover switches to the updates view. + * Here the backend should mark all upgradeable packages as to be upgraded. + */ + virtual void prepare() = 0; + + /** + * @returns true if the backend contains packages which can be updated + */ + virtual bool hasUpdates() const = 0; + /** + * @returns the progress of the update in percent + */ + virtual qreal progress() const = 0; + + /** + * This method is used to remove resources from the list of packages + * marked to be upgraded. It will potentially be called before \start. + */ + virtual void removeResources(const QList &apps) = 0; + /** + * This method is used to add resource to the list of packages marked to be upgraded. + * It will potentially be called before \start. + */ + virtual void addResources(const QList &apps) = 0; + + /** + * @returns the list of updateable resources in the system + */ + virtual QList toUpdate() const = 0; + + /** + * @returns the QDateTime when the last update happened + */ + virtual QDateTime lastUpdate() const = 0; + + /** + * @returns whether the updater can currently be canceled or not + * @see cancelableChanged + */ + virtual bool isCancelable() const = 0; + /** + * @returns whether the updater is currently running or not + * this property decides, if there will be progress reporting in the GUI. + * This has to stay true during the whole transaction! + * @see progressingChanged + */ + virtual bool isProgressing() const = 0; + + /** + * @returns whether @p res is marked for update + */ + virtual bool isMarked(AbstractResource *res) const = 0; + + virtual void fetchChangelog() const; + + /** + * @returns the size of all the packages set to update combined + */ + virtual double updateSize() const = 0; + + /** + * @returns the speed at which we are downloading + */ + virtual quint64 downloadSpeed() const = 0; + + void enableNeedsReboot(); + void enableReadyToReboot(); + + bool isReadyToReboot() const; + bool needsReboot() const; + + virtual void setOfflineUpdates(bool useOfflineUpdates); + +public Q_SLOTS: + /** + * If \isCancelable is true during the transaction, this method has + * to be implemented and will potentially be called when the user + * wants to cancel the update. + */ + virtual void cancel(); + /** + * This method starts the update. All packages which are in \toUpdate + * are going to be updated. + * + * From this moment on the AbstractBackendUpdater should continuously update + * the other methods to show its progress. + * + * @see progress + * @see progressChanged + * @see isProgressing + * @see progressingChanged + */ + virtual void start() = 0; + + /** + * Answers a proceed request + */ + virtual void proceed() + { + } + + void setErrorMessage(const QString &errorMessage); + QString errorMessage() const + { + return m_errorMessage; + } + +Q_SIGNALS: + /** + * The AbstractBackendUpdater should Q_EMIT this signal when the progress changed. + * @see progress + */ + void progressChanged(qreal progress); + /** + * The AbstractBackendUpdater should Q_EMIT this signal when the cancelable property changed. + * @see isCancelable + */ + void cancelableChanged(bool cancelable); + /** + * The AbstractBackendUpdater should Q_EMIT this signal when the progressing property changed. + * @see isProgressing + */ + void progressingChanged(bool progressing); + /** + * The AbstractBackendUpdater should Q_EMIT this signal when the status detail changed. + * @see statusDetail + */ + void statusDetailChanged(const QString &msg); + /** + * The AbstractBackendUpdater should Q_EMIT this signal when the status message changed. + * @see statusMessage + */ + void statusMessageChanged(const QString &msg); + /** + * The AbstractBackendUpdater should Q_EMIT this signal when the download speed changed. + * @see downloadSpeed + */ + void downloadSpeedChanged(quint64 downloadSpeed); + + /** + * Provides the @p progress of a specific @p resource in a percentage. + */ + void resourceProgressed(AbstractResource *resource, qreal progress, AbstractBackendUpdater::State state); + + void passiveMessage(const QString &message); + + /** + * Provides a message to be shown to the user + * + * The user gets to acknowledge and proceed or cancel the transaction. + * + * @sa proceed(), cancel() + */ + void proceedRequest(const QString &title, const QString &description); + + /** + * A fatal error was found on distro packaging. Provide a @p message to show + * in a modal dialog that should lead the user towards reporting the problem.. + */ + void distroErrorMessage(const QString &message); + + /** + * emitted when the updater decides it needs to reboot + */ + void needsRebootChanged(); + + /** emitted when we find a new errorMessage to display */ + void errorMessageChanged(); + +private: + bool m_needsReboot = false; + bool m_readyToReboot = false; + QString m_errorMessage; +}; diff --git a/libdiscover/resources/AbstractResource.cpp b/libdiscover/resources/AbstractResource.cpp new file mode 100644 index 0000000..5db52c4 --- /dev/null +++ b/libdiscover/resources/AbstractResource.cpp @@ -0,0 +1,303 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AbstractResource.h" +#include "AbstractResourcesBackend.h" +#include "libdiscover_debug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +AbstractResource::AbstractResource(AbstractResourcesBackend *parent) + : QObject(parent) +{ + connect(this, &AbstractResource::stateChanged, this, &AbstractResource::sizeChanged); + connect(this, &AbstractResource::stateChanged, this, &AbstractResource::versionsChanged); + connect(this, &AbstractResource::stateChanged, this, &AbstractResource::reportNewState); +} + +AbstractResource::~AbstractResource() = default; + +QUrl AbstractResource::homepage() +{ + return QUrl(); +} + +QUrl AbstractResource::helpURL() +{ + return QUrl(); +} + +QUrl AbstractResource::bugURL() +{ + return QUrl(); +} + +QUrl AbstractResource::donationURL() +{ + return QUrl(); +} + +QUrl AbstractResource::contributeURL() +{ + return {}; +} + +void AbstractResource::addMetadata(const QString &key, const QJsonValue &value) +{ + m_metadata.insert(key, value); +} + +QJsonValue AbstractResource::getMetadata(const QString &key) +{ + return m_metadata.value(key); +} + +bool AbstractResource::canUpgrade() +{ + return state() == Upgradeable; +} + +bool AbstractResource::isInstalled() +{ + return state() >= Installed; +} + +void AbstractResource::fetchScreenshots() +{ + Q_EMIT screenshotsFetched({}); +} + +QStringList AbstractResource::mimetypes() const +{ + return QStringList(); +} + +AbstractResourcesBackend *AbstractResource::backend() const +{ + return static_cast(parent()); +} + +QObject *AbstractResource::backendObject() const +{ + return parent(); +} + +QString AbstractResource::status() +{ + switch (state()) { + case Broken: + return i18n("Broken"); + case None: + return i18n("Available"); + case Installed: + return i18n("Installed"); + case Upgradeable: + return i18n("Upgradeable"); + } + return QString(); +} + +QString AbstractResource::sizeDescription() +{ + return KFormat().formatByteSize(size()); +} + +QCollatorSortKey AbstractResource::nameSortKey() +{ + if (!m_collatorKey) { + m_collatorKey.reset(new QCollatorSortKey(QCollator().sortKey(name()))); + } + return *m_collatorKey; +} + +Rating *AbstractResource::rating() const +{ + AbstractReviewsBackend *ratings = backend()->reviewsBackend(); + return ratings ? ratings->ratingForApplication(const_cast(this)) : nullptr; +} + +QVariant AbstractResource::ratingVariant() const +{ + auto instance = rating(); + return instance ? QVariant::fromValue(*instance) : QVariant(); +} + +QStringList AbstractResource::extends() const +{ + return {}; +} + +QString AbstractResource::appstreamId() const +{ + return {}; +} + +void AbstractResource::reportNewState() +{ + if (backend()->isFetching()) + return; + + static const QVector ns = {"state", "status", "canUpgrade", "size", "sizeDescription", "installedVersion", "availableVersion"}; + Q_EMIT backend()->resourcesChanged(this, ns); +} + +static bool shouldFilter(AbstractResource *res, const CategoryFilter &filter) +{ + bool ret = true; + switch (filter.type) { + case CategoryFilter::CategoryNameFilter: + ret = res->categories().contains(std::get(filter.value)); + break; + case CategoryFilter::PkgSectionFilter: + ret = res->section() == std::get(filter.value); + break; + case CategoryFilter::PkgWildcardFilter: { + QString wildcard = std::get(filter.value); + wildcard.remove(QLatin1Char('*')); + ret = res->packageName().contains(wildcard); + } break; + case CategoryFilter::AppstreamIdWildcardFilter: { + QString wildcard = std::get(filter.value); + wildcard.remove(QLatin1Char('*')); + ret = res->appstreamId().contains(wildcard); + } break; + case CategoryFilter::PkgNameFilter: // Only useful in the not filters + ret = res->packageName() == std::get(filter.value); + break; + case CategoryFilter::AndFilter: { + const auto filters = std::get>(filter.value); + ret = std::all_of(filters.begin(), filters.end(), [res](const CategoryFilter &f) { + return shouldFilter(res, f); + }); + break; + } + case CategoryFilter::OrFilter: { + const auto filters = std::get>(filter.value); + ret = std::any_of(filters.begin(), filters.end(), [res](const CategoryFilter &f) { + return shouldFilter(res, f); + }); + break; + } + case CategoryFilter::NotFilter: { + const auto filters = std::get>(filter.value); + ret = !std::any_of(filters.begin(), filters.end(), [res](const CategoryFilter &f) { + return shouldFilter(res, f); + }); + break; + } + } + return ret; +} + +bool AbstractResource::categoryMatches(Category *cat) +{ + return shouldFilter(this, cat->filter()); +} + +static QSet walkCategories(AbstractResource *res, const QVector &cats) +{ + QSet ret; + for (Category *cat : cats) { + if (res->categoryMatches(cat)) { + const auto subcats = walkCategories(res, cat->subCategories()); + if (subcats.isEmpty()) { + ret += cat; + } else { + ret += subcats; + } + } + } + + return ret; +} + +QSet AbstractResource::categoryObjects(const QVector &cats) const +{ + return walkCategories(const_cast(this), cats); +} + +QUrl AbstractResource::url() const +{ + const QString asid = appstreamId(); + return asid.isEmpty() ? QUrl(backend()->name() + QStringLiteral("://") + packageName()) : QUrl(QStringLiteral("appstream://") + asid); +} + +QString AbstractResource::displayOrigin() const +{ + return origin(); +} + +QString AbstractResource::executeLabel() const +{ + return i18n("Launch"); +} + +QString AbstractResource::upgradeText() const +{ + QString installed = installedVersion(), available = availableVersion(); + if (installed == available) { + // Update of the same version; show when old and new are + // the same (common with Flatpak runtimes) + return i18nc("@info 'Refresh' is used as a noun here, and %1 is an app's version number", "Refresh of version %1", available); + } else if (!installed.isEmpty() && !available.isEmpty()) { + // Old and new version numbers + // This thing with \u009C is a fancy feature in QML text handling: + // when the string will be elided, it shows the string after + // the last \u009C. 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 \\u009C", "%1 → %2\u009C%1 → %2\u009C%2", installed, available); + } else { + // Available version only, for when the installed version + // isn't available for some reason + return available; + } +} + +QString AbstractResource::versionString() +{ + const QString version = isInstalled() ? installedVersion() : availableVersion(); + if (version.isEmpty()) { + return {}; + } else { + QLocale l; + const QString releaseString = l.toString(releaseDate(), QLocale::ShortFormat); + if (!releaseString.isEmpty()) { + return i18n("%1, released on %2", version, releaseString); + } else { + return version; + } + } +} + +QString AbstractResource::contentRatingDescription() const +{ + return {}; +} + +AbstractResource::ContentIntensity AbstractResource::contentRatingIntensity() const +{ + return Mild; +} + +QString AbstractResource::contentRatingText() const +{ + return {}; +} + +uint AbstractResource::contentRatingMinimumAge() const +{ + return 0; +} diff --git a/libdiscover/resources/AbstractResource.h b/libdiscover/resources/AbstractResource.h new file mode 100644 index 0000000..773e2a0 --- /dev/null +++ b/libdiscover/resources/AbstractResource.h @@ -0,0 +1,293 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PackageState.h" +#include "discovercommon_export.h" + +class Category; +class Rating; +class AbstractResourcesBackend; + +struct Screenshot { + Screenshot(const QUrl &screenshot) + : thumbnail(screenshot) + , screenshot(screenshot) + { + } + + Screenshot(const QUrl &thumbnail, const QUrl &screenshot, bool isAnimated) + : thumbnail(thumbnail) + , screenshot(screenshot) + , isAnimated(isAnimated) + { + } + + QUrl thumbnail; + QUrl screenshot; + bool isAnimated = false; +}; + +using Screenshots = QVector; + +/** + * \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(QUrl contributeURL READ contributeURL CONSTANT) + Q_PROPERTY(bool canUpgrade READ canUpgrade NOTIFY stateChanged) + Q_PROPERTY(bool isInstalled READ isInstalled NOTIFY stateChanged) + Q_PROPERTY(QJsonArray licenses READ licenses NOTIFY licensesChanged) + Q_PROPERTY(QString longDescription READ longDescription NOTIFY longDescriptionChanged) + Q_PROPERTY(QString origin READ origin CONSTANT) + Q_PROPERTY(QString displayOrigin READ displayOrigin CONSTANT) + Q_PROPERTY(quint64 size READ size NOTIFY sizeChanged) + Q_PROPERTY(QString sizeDescription READ sizeDescription NOTIFY sizeChanged) + Q_PROPERTY(QString installedVersion READ installedVersion NOTIFY versionsChanged) + Q_PROPERTY(QString availableVersion READ availableVersion NOTIFY versionsChanged) + Q_PROPERTY(QString section READ section CONSTANT) + Q_PROPERTY(QStringList mimetypes READ mimetypes CONSTANT) + Q_PROPERTY(QObject *backend READ backendObject CONSTANT) + Q_PROPERTY(QVariant rating READ ratingVariant NOTIFY ratingFetched) + Q_PROPERTY(QString appstreamId READ appstreamId 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 versionsChanged) + Q_PROPERTY(QString upgradeText READ upgradeText NOTIFY versionsChanged) + Q_PROPERTY(bool isRemovable READ isRemovable CONSTANT) + Q_PROPERTY(QString versionString READ versionString NOTIFY versionsChanged) + Q_PROPERTY(QString contentRatingText READ contentRatingText CONSTANT) + Q_PROPERTY(QString contentRatingDescription READ contentRatingDescription CONSTANT) + Q_PROPERTY(ContentIntensity contentRatingIntensity READ contentRatingIntensity CONSTANT) + Q_PROPERTY(uint contentRatingMinimumAge READ contentRatingMinimumAge CONSTANT) +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) + + enum ContentIntensity { + Mild, + Intense, + }; + Q_ENUM(ContentIntensity) + + /** + * 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(); + ///@returns a URL that points to the place where you can contribute to develop the app + virtual QUrl contributeURL(); + + enum Type { + Application, + Addon, + Technical, + }; + Q_ENUM(Type) + virtual Type type() const = 0; + + virtual quint64 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; + virtual 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; + QObject *backendObject() 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; + QVariant ratingVariant() 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; + + virtual QSet alternativeAppstreamIds() const + { + return {}; + } + + virtual QString upgradeText() const; + + /** + * @returns whether the package can ever be removed + */ + virtual bool isRemovable() const + { + return true; + } + + virtual QString versionString(); + + virtual QString contentRatingText() const; + virtual ContentIntensity contentRatingIntensity() const; + virtual QString contentRatingDescription() const; + virtual uint contentRatingMinimumAge() const; + +public Q_SLOTS: + virtual void fetchScreenshots(); + virtual void fetchChangelog() = 0; + virtual void fetchUpdateDetails() + { + fetchChangelog(); + } + +Q_SIGNALS: + void iconChanged(); + void sizeChanged(); + void stateChanged(); + void licensesChanged(); + void ratingFetched(); + void longDescriptionChanged(); + void versionsChanged(); + + /// response to the fetchScreenshots method + void screenshotsFetched(const Screenshots &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) diff --git a/libdiscover/resources/AbstractResourcesBackend.cpp b/libdiscover/resources/AbstractResourcesBackend.cpp new file mode 100644 index 0000000..2b6914d --- /dev/null +++ b/libdiscover/resources/AbstractResourcesBackend.cpp @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AbstractResourcesBackend.h" +#include "Category/Category.h" +#include "libdiscover_debug.h" +#include +#include +#include +#include +#include + +QDebug operator<<(QDebug debug, const AbstractResourcesBackend::Filters &filters) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "Filters("; + if (filters.category) + debug.nospace() << "category: " << filters.category << ','; + if (filters.state) + debug.nospace() << "state: " << filters.state << ','; + if (!filters.mimetype.isEmpty()) + debug.nospace() << "mimetype: " << filters.mimetype << ','; + if (!filters.search.isEmpty()) + debug.nospace() << "search: " << filters.search << ','; + if (!filters.extends.isEmpty()) + debug.nospace() << "extends:" << filters.extends << ','; + if (!filters.origin.isEmpty()) + debug.nospace() << "origin:" << filters.origin << ','; + if (!filters.resourceUrl.isEmpty()) + debug.nospace() << "resourceUrl:" << filters.resourceUrl << ','; + debug.nospace() << ')'; + + return debug; +} + +ResultsStream::ResultsStream(const QString &objectName, const QVector &resources) + : ResultsStream(objectName) +{ + Q_ASSERT(!resources.contains(nullptr)); + QTimer::singleShot(0, this, [resources, this]() { + if (!resources.isEmpty()) + Q_EMIT resourcesFound(resources); + finish(); + }); +} + +ResultsStream::ResultsStream(const QString &objectName) +{ + setObjectName(objectName); + QTimer::singleShot(5000, this, [objectName]() { + qCDebug(LIBDISCOVER_LOG) << "stream took really long" << objectName; + }); +} + +ResultsStream::~ResultsStream() +{ +} + +void ResultsStream::finish() +{ + deleteLater(); +} + +AbstractResourcesBackend::AbstractResourcesBackend(QObject *parent) + : QObject(parent) +{ + QTimer *fetchingChangedTimer = new QTimer(this); + fetchingChangedTimer->setInterval(3000); + fetchingChangedTimer->setSingleShot(true); + connect(fetchingChangedTimer, &QTimer::timeout, this, [this] { + qDebug() << "took really long to fetch" << this; + }); + + connect(this, &AbstractResourcesBackend::fetchingChanged, this, [this, fetchingChangedTimer] { + // Q_ASSERT(isFetching() != fetchingChangedTimer->isActive()); + if (isFetching()) + fetchingChangedTimer->start(); + else + fetchingChangedTimer->stop(); + + Q_EMIT fetchingUpdatesProgressChanged(); + }); +} + +Transaction *AbstractResourcesBackend::installApplication(AbstractResource *app) +{ + return installApplication(app, AddonList()); +} + +void AbstractResourcesBackend::setName(const QString &name) +{ + m_name = name; +} + +QString AbstractResourcesBackend::name() const +{ + return m_name; +} + +void AbstractResourcesBackend::emitRatingsReady() +{ + Q_EMIT allDataChanged({"rating", "ratingPoints", "ratingCount", "sortableRating"}); +} + +bool AbstractResourcesBackend::Filters::shouldFilter(AbstractResource *res) const +{ + Q_ASSERT(res); + + if (!extends.isEmpty() && !res->extends().contains(extends)) { + return false; + } + + if (!origin.isEmpty() && res->origin() != origin) { + return false; + } + + if (filterMinimumState ? (res->state() < state) : (res->state() != state)) { + return false; + } + + if (!mimetype.isEmpty() && !res->mimetypes().contains(mimetype)) { + return false; + } + + return !category || res->categoryMatches(category); +} + +void AbstractResourcesBackend::Filters::filterJustInCase(QVector &input) const +{ + for (auto it = input.begin(); it != input.end();) { + if (shouldFilter(*it)) + ++it; + else + it = input.erase(it); + } +} + +QStringList AbstractResourcesBackend::extends() const +{ + return {}; +} + +int AbstractResourcesBackend::fetchingUpdatesProgress() const +{ + return isFetching() ? 42 : 100; +} + +InlineMessage *AbstractResourcesBackend::explainDysfunction() const +{ + return new InlineMessage(InlineMessage::Error, QStringLiteral("network-disconnect"), i18n("Please verify Internet connectivity")); +} diff --git a/libdiscover/resources/AbstractResourcesBackend.h b/libdiscover/resources/AbstractResourcesBackend.h new file mode 100644 index 0000000..156ad2b --- /dev/null +++ b/libdiscover/resources/AbstractResourcesBackend.h @@ -0,0 +1,306 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +#include "AbstractResource.h" +#include "DiscoverAction.h" +#include "Transaction/AddonList.h" + +#include "discovercommon_export.h" + +class Transaction; +class Category; +class AbstractReviewsBackend; +class AbstractBackendUpdater; + +class DISCOVERCOMMON_EXPORT ResultsStream : public QObject +{ + Q_OBJECT +public: + ResultsStream(const QString &objectName); + + /// assumes all the information is in @p resources + ResultsStream(const QString &objectName, const QVector &resources); + ~ResultsStream() override; + + void finish(); + +Q_SIGNALS: + void resourcesFound(const QVector &resources); + void fetchMore(); +}; + +class DISCOVERCOMMON_EXPORT InlineMessage : public QObject +{ + Q_OBJECT +public: + // Keep in sync with Kirigami's in enums.h + enum InlineMessageType { + Information = 0, + Positive, + Warning, + Error, + }; + Q_ENUM(InlineMessageType) + Q_PROPERTY(InlineMessageType type MEMBER type CONSTANT) + Q_PROPERTY(QString iconName MEMBER iconName CONSTANT) + Q_PROPERTY(QString message MEMBER message CONSTANT) + Q_PROPERTY(QVariantList actions MEMBER actions CONSTANT) + + InlineMessage(InlineMessageType type, const QString &iconName, const QString &message, DiscoverAction *action = nullptr) + : type(type) + , iconName(iconName) + , message(message) + , actions(action ? QVariantList{QVariant::fromValue(action)} : QVariantList()) + { + } + + InlineMessage(InlineMessageType type, const QString &iconName, const QString &message, const QVariantList &actions) + : type(type) + , iconName(iconName) + , message(message) + , actions(actions) + { + } + + InlineMessageType type; + const QString iconName; + const QString message; + const QVariantList actions; +}; + +/** + * \class AbstractResourcesBackend AbstractResourcesBackend.h "AbstractResourcesBackend.h" + * + * \brief This is the base class of all resource backends. + * + * For writing basic new resource backends, we need to implement two classes: this and the + * AbstractResource one. Basic questions on how to build your plugin with those classes + * can be answered by looking at the dummy plugin. + * + * As this is the base class of a backend, we save all the created resources here and also + * accept calls to install and remove applications or to cancel transactions. + * + * To show resources in Discover, we need to initialize all resources we want to show beforehand, + * we should not create resources in the search function. When we reload the resources + * (e.g. when initializing), the backend needs change the fetching property throughout the + * process. + */ +class DISCOVERCOMMON_EXPORT AbstractResourcesBackend : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString displayName READ displayName CONSTANT) + Q_PROPERTY(AbstractReviewsBackend *reviewsBackend READ reviewsBackend CONSTANT) + Q_PROPERTY(int updatesCount READ updatesCount NOTIFY updatesCountChanged) + Q_PROPERTY(int fetchingUpdatesProgress READ fetchingUpdatesProgress NOTIFY fetchingUpdatesProgressChanged) + Q_PROPERTY(bool hasSecurityUpdates READ hasSecurityUpdates NOTIFY updatesCountChanged) + Q_PROPERTY(bool isFetching READ isFetching NOTIFY fetchingChanged) + Q_PROPERTY(bool hasApplications READ hasApplications CONSTANT) +public: + /** + * Constructs an AbstractResourcesBackend + * @param parent the parent of the class (the object will be deleted when the parent gets deleted) + */ + explicit AbstractResourcesBackend(QObject *parent = nullptr); + + /** + * @returns true when the backend is in a valid state, which means it is able to work + * You must return true here if you want the backend to be loaded. + */ + virtual bool isValid() const = 0; + + struct Filters { + Category *category = nullptr; + AbstractResource::State state = AbstractResource::Broken; + QString mimetype; + QString search; + QString extends; + QUrl resourceUrl; + QString origin; + bool allBackends = false; + bool filterMinimumState = true; + AbstractResourcesBackend *backend = nullptr; + + bool isEmpty() const + { + return !category && state == AbstractResource::Broken && mimetype.isEmpty() && search.isEmpty() && extends.isEmpty() && resourceUrl.isEmpty() + && origin.isEmpty(); + } + + bool shouldFilter(AbstractResource *res) const; + void filterJustInCase(QVector &input) const; + }; + + /** + * @returns a stream that will provide elements that match the search + */ + + virtual ResultsStream *search(const Filters &search) = 0; // FIXME: Probably provide a standard implementation?! + + /** + * @returns the reviews backend of this AbstractResourcesBackend (which handles all ratings and reviews of resources) + */ + virtual AbstractReviewsBackend *reviewsBackend() const = 0; // FIXME: Have a standard impl which returns 0? + + /** + * @returns the class which is used by Discover to update the users system, if you are unsure what to do + * just return the StandardBackendUpdater + */ + virtual AbstractBackendUpdater *backendUpdater() const = 0; // FIXME: Standard impl returning the standard updater? + + /** + * @returns the number of resources for which an update is available, it should only count technical packages + */ + virtual int updatesCount() const = 0; // FIXME: Probably provide a standard implementation?! + + /** + * @returns whether either of the updates contains a security fix + */ + virtual bool hasSecurityUpdates() const + { + return false; + } + + /** + * Tells whether the backend is fetching resources + */ + virtual bool isFetching() const = 0; + + /** + * @returns the appstream ids that this backend extends + */ + virtual QStringList extends() const; + + /** @returns the plugin's name */ + QString name() const; + + /** @internal only to be used by the factory */ + void setName(const QString &name); + + virtual QString displayName() const = 0; + + /** + * emits a change for all rating properties + */ + void emitRatingsReady(); + + /** + * @returns the root category tree + */ + virtual QVector category() const + { + return {}; + } + + virtual bool hasApplications() const + { + return false; + } + + virtual int fetchingUpdatesProgress() const; + +public Q_SLOTS: + /** + * This gets called when the backend should install an application. + * The AbstractResourcesBackend should create a Transaction object, is returned and + * will be included in the TransactionModel + * @param app the application to be installed + * @param addons the addons which should be installed with the application + * @returns the Transaction that keeps track of the installation process + */ + virtual Transaction *installApplication(AbstractResource *app, const AddonList &addons) = 0; + + /** + * Overloaded function, which simply does the same, except not installing any addons. + */ + virtual Transaction *installApplication(AbstractResource *app); + + /** + * This gets called when the backend should remove an application. + * Like in the installApplication() method, we'll return the Transaction + * responsible for the removal. + * + * @see installApplication + * @param app the application to be removed + * @returns the Transaction that keeps track of the removal process + */ + virtual Transaction *removeApplication(AbstractResource *app) = 0; + + /** + * Notifies the backend that the user wants the information to be up to date + */ + virtual void checkForUpdates() = 0; + + /** + * Provides a guess why a search might not have offered satisfactory results + */ + Q_SCRIPTABLE virtual InlineMessage *explainDysfunction() const; + +Q_SIGNALS: + /** + * Notify of a change in the backend + */ + void fetchingChanged(); + + /** + * This should be emitted when the number of upgradeable packages changed. + */ + void updatesCountChanged(); + /** + * This should be emitted when all data of the backends resources changed. Internally it will Q_EMIT + * a signal in the model to show the view that all data of a certain backend changed. + */ + void allDataChanged(const QVector &propertyNames); + + /** + * Allows to notify some @p properties in @p resource have changed + */ + void resourcesChanged(AbstractResource *resource, const QVector &properties); + void resourceRemoved(AbstractResource *resource); + + void passiveMessage(const QString &message); + void inlineMessageChanged(const QSharedPointer &inlineMessage); + void fetchingUpdatesProgressChanged(); + +private: + QString m_name; +}; + +DISCOVERCOMMON_EXPORT QDebug operator<<(QDebug dbg, const AbstractResourcesBackend::Filters &filters); + +/** + * @internal Workaround because QPluginLoader enforces 1 instance per plugin + */ +class DISCOVERCOMMON_EXPORT AbstractResourcesBackendFactory : public QObject +{ + Q_OBJECT +public: + virtual QVector newInstance(QObject *parent, const QString &name) const = 0; +}; + +#define DISCOVER_BACKEND_PLUGIN(ClassName) \ + class ClassName##Factory : public AbstractResourcesBackendFactory \ + { \ + Q_OBJECT \ + Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory") \ + Q_INTERFACES(AbstractResourcesBackendFactory) \ + public: \ + QVector newInstance(QObject *parent, const QString &name) const override \ + { \ + auto c = new ClassName(parent); \ + c->setName(name); \ + return {c}; \ + } \ + }; + +Q_DECLARE_INTERFACE(AbstractResourcesBackendFactory, "org.kde.muon.AbstractResourcesBackendFactory") diff --git a/libdiscover/resources/AbstractSourcesBackend.cpp b/libdiscover/resources/AbstractSourcesBackend.cpp new file mode 100644 index 0000000..3e76600 --- /dev/null +++ b/libdiscover/resources/AbstractSourcesBackend.cpp @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "AbstractSourcesBackend.h" +#include "AbstractResourcesBackend.h" +#include + +AbstractSourcesBackend::AbstractSourcesBackend(AbstractResourcesBackend *parent) + : QObject(parent) +{ +} + +AbstractSourcesBackend::~AbstractSourcesBackend() = default; + +AbstractResourcesBackend *AbstractSourcesBackend::resourcesBackend() const +{ + return dynamic_cast(parent()); +} + +bool AbstractSourcesBackend::moveSource(const QString &sourceId, int delta) +{ + Q_UNUSED(sourceId) + Q_UNUSED(delta) + return false; +} + +QString AbstractSourcesBackend::firstSourceId() const +{ + auto m = const_cast(this)->sources(); + return m->index(0, 0).data(AbstractSourcesBackend::IdRole).toString(); +} + +QString AbstractSourcesBackend::lastSourceId() const +{ + auto m = const_cast(this)->sources(); + return m->index(m->rowCount() - 1, 0).data(AbstractSourcesBackend::IdRole).toString(); +} diff --git a/libdiscover/resources/AbstractSourcesBackend.h b/libdiscover/resources/AbstractSourcesBackend.h new file mode 100644 index 0000000..97b3cc7 --- /dev/null +++ b/libdiscover/resources/AbstractSourcesBackend.h @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "DiscoverAction.h" +#include "discovercommon_export.h" +#include + +class QAbstractItemModel; +class AbstractResourcesBackend; + +class DISCOVERCOMMON_EXPORT AbstractSourcesBackend : public QObject +{ + Q_OBJECT + Q_PROPERTY(AbstractResourcesBackend *resourcesBackend READ resourcesBackend CONSTANT) + Q_PROPERTY(QAbstractItemModel *sources READ sources CONSTANT) + Q_PROPERTY(QString idDescription READ idDescription CONSTANT) + Q_PROPERTY(QVariantList actions READ actions CONSTANT) // TODO Make it a QVector again when we depend on newer than Qt 5.12 + Q_PROPERTY(DiscoverAction *inlineAction READ inlineAction CONSTANT) // TODO Make it a QVector again when we depend on newer than Qt 5.12 + Q_PROPERTY(bool supportsAdding READ supportsAdding CONSTANT) + Q_PROPERTY(bool canMoveSources READ canMoveSources CONSTANT) + Q_PROPERTY(bool canFilterSources READ canFilterSources CONSTANT) + Q_PROPERTY(QString firstSourceId READ firstSourceId NOTIFY firstSourceIdChanged) + Q_PROPERTY(QString lastSourceId READ lastSourceId NOTIFY lastSourceIdChanged) +public: + explicit AbstractSourcesBackend(AbstractResourcesBackend *parent); + ~AbstractSourcesBackend() override; + + enum Roles { + IdRole = Qt::UserRole, + LastRole, + }; + Q_ENUM(Roles) + + virtual QString idDescription() = 0; + + Q_SCRIPTABLE virtual bool addSource(const QString &id) = 0; + Q_SCRIPTABLE virtual bool removeSource(const QString &id) = 0; + + virtual QAbstractItemModel *sources() = 0; + virtual QVariantList actions() const = 0; + + virtual bool supportsAdding() const = 0; + virtual DiscoverAction *inlineAction() const + { + return nullptr; + } + + AbstractResourcesBackend *resourcesBackend() const; + + virtual bool canFilterSources() const + { + return false; + } + + virtual bool canMoveSources() const + { + return false; + } + Q_SCRIPTABLE virtual bool moveSource(const QString &sourceId, int delta); + + QString firstSourceId() const; + QString lastSourceId() const; + +public Q_SLOTS: + virtual void cancel() + { + } + + virtual void proceed() + { + } + +Q_SIGNALS: + void firstSourceIdChanged(); + void lastSourceIdChanged(); + void passiveMessage(const QString &message); + void proceedRequest(const QString &title, const QString &description); +}; diff --git a/libdiscover/resources/DiscoverAction.cpp b/libdiscover/resources/DiscoverAction.cpp new file mode 100644 index 0000000..c28e40b --- /dev/null +++ b/libdiscover/resources/DiscoverAction.cpp @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DiscoverAction.h" + +DiscoverAction::DiscoverAction(QObject *parent) + : QObject(parent) +{ +} + +DiscoverAction::DiscoverAction(const QString &icon, const QString &text, QObject *parent) + : QObject(parent) + , m_text(text) + , m_icon(icon) +{ +} + +DiscoverAction::DiscoverAction(const QString &text, QObject *parent) + : QObject(parent) + , m_text(text) +{ +} + +void DiscoverAction::setEnabled(bool enabled) +{ + if (enabled == m_isEnabled) + return; + + m_isEnabled = enabled; + Q_EMIT enabledChanged(enabled); +} + +void DiscoverAction::setVisible(bool visible) +{ + if (visible == m_isVisible) + return; + + m_isVisible = visible; + Q_EMIT visibleChanged(visible); +} + +void DiscoverAction::setIconName(const QString &icon) +{ + if (icon == m_icon) + return; + + m_icon = icon; + Q_EMIT iconNameChanged(icon); +} + +void DiscoverAction::setText(const QString &text) +{ + if (text == m_text) + return; + + m_text = text; + Q_EMIT textChanged(text); +} + +void DiscoverAction::setToolTip(const QString &toolTip) +{ + if (toolTip == m_toolTip) + return; + + m_toolTip = toolTip; + Q_EMIT toolTipChanged(toolTip); +} + +void DiscoverAction::trigger() +{ + Q_EMIT triggered(); +} diff --git a/libdiscover/resources/DiscoverAction.h b/libdiscover/resources/DiscoverAction.h new file mode 100644 index 0000000..a17bf7c --- /dev/null +++ b/libdiscover/resources/DiscoverAction.h @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include + +/** + * An action class that doesn't need QtWidgets + */ +class DISCOVERCOMMON_EXPORT DiscoverAction : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_PROPERTY(QString toolTip READ toolTip WRITE setToolTip NOTIFY toolTipChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) +public: + DiscoverAction(QObject *parent = nullptr); + DiscoverAction(const QString &text, QObject *parent = nullptr); + DiscoverAction(const QString &icon, const QString &text, QObject *parent = nullptr); + + void setText(const QString &text); + void setToolTip(const QString &toolTip); + void setIconName(const QString &iconName); + void setEnabled(bool enabled); + void setVisible(bool enabled); + + bool isVisible() const + { + return m_isVisible; + } + + bool isEnabled() const + { + return m_isEnabled; + } + + QString text() const + { + return m_text; + } + + QString toolTip() const + { + return m_toolTip; + } + + QString iconName() const + { + return m_icon; + } + +public Q_SLOTS: + void trigger(); + +Q_SIGNALS: + void triggered(); + + void textChanged(const QString &text); + void toolTipChanged(const QString &toolTip); + void iconNameChanged(const QString &iconName); + void visibleChanged(bool isVisible); + void enabledChanged(bool isEnabled); + +private: + bool m_isVisible = true; + bool m_isEnabled = true; + QString m_text; + QString m_toolTip; + QString m_icon; +}; diff --git a/libdiscover/resources/PackageState.cpp b/libdiscover/resources/PackageState.cpp new file mode 100644 index 0000000..0631567 --- /dev/null +++ b/libdiscover/resources/PackageState.cpp @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "PackageState.h" +#include "libdiscover_debug.h" + +PackageState::PackageState(const QString &name, const QString &description, bool installed) + : PackageState(name, name, description, installed) +{ +} + +PackageState::PackageState(QString packageName, QString name, QString description, bool installed) + : m_packageName(std::move(packageName)) + , m_name(std::move(name)) + , m_description(std::move(description)) + , m_installed(installed) +{ +} + +PackageState::PackageState(const PackageState &ps) + : m_packageName(ps.m_packageName) + , m_name(ps.m_name) + , m_description(ps.m_description) + , m_installed(ps.m_installed) +{ +} + +QString PackageState::name() const +{ + return m_name; +} + +QString PackageState::description() const +{ + return m_description; +} + +QString PackageState::packageName() const +{ + return m_packageName; +} + +bool PackageState::isInstalled() const +{ + return m_installed; +} + +void PackageState::setInstalled(bool installed) +{ + m_installed = installed; +} + +QDebug operator<<(QDebug debug, const PackageState &state) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "PackageState("; + debug.nospace() << state.name() << ':'; + debug.nospace() << "installed: " << state.isInstalled() << ','; + debug.nospace() << ')'; + return debug; +} diff --git a/libdiscover/resources/PackageState.h b/libdiscover/resources/PackageState.h new file mode 100644 index 0000000..25ab5fd --- /dev/null +++ b/libdiscover/resources/PackageState.h @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include + +/** + * The @class PackageState will be used to expose resources related to an @class AbstractResource. + * + * @see ApplicationAddonsModel + */ +class DISCOVERCOMMON_EXPORT PackageState +{ +public: + PackageState(QString packageName, QString name, QString description, bool installed); + PackageState(const QString &name, const QString &description, bool installed); + PackageState(const PackageState &ps); + PackageState &operator=(const PackageState &other); + + QString packageName() const; + QString name() const; + QString description() const; + bool isInstalled() const; + void setInstalled(bool installed); + +private: + const QString m_packageName; + const QString m_name; + const QString m_description; + bool m_installed; +}; + +DISCOVERCOMMON_EXPORT QDebug operator<<(QDebug dbg, const PackageState &state); diff --git a/libdiscover/resources/ResourcesModel.cpp b/libdiscover/resources/ResourcesModel.cpp new file mode 100644 index 0000000..71c45d5 --- /dev/null +++ b/libdiscover/resources/ResourcesModel.cpp @@ -0,0 +1,441 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "ResourcesModel.h" + +#include "AbstractResource.h" +#include "Category/CategoryModel.h" +#include "Transaction/TransactionModel.h" +#include "libdiscover_debug.h" +#include "resources/AbstractBackendUpdater.h" +#include "resources/AbstractResourcesBackend.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ResourcesModel *ResourcesModel::s_self = nullptr; + +ResourcesModel *ResourcesModel::global() +{ + if (!s_self) { + s_self = new ResourcesModel; + s_self->init(true); + } + return s_self; +} + +ResourcesModel::ResourcesModel(QObject *parent) + : QObject(parent) + , m_isFetching(false) + , m_initializingBackendsCount(0) + , m_currentApplicationBackend(nullptr) + , m_allInitializedEmitter(new QTimer(this)) + , m_updatesCount( + 0, + [this] { + int ret = 0; + for (AbstractResourcesBackend *backend : qAsConst(m_backends)) { + ret += backend->updatesCount(); + } + return ret; + }, + [this](int count) { + Q_EMIT updatesCountChanged(count); + }) + , m_fetchingUpdatesProgress( + 0, + [this] { + if (m_backends.isEmpty()) + return 0; + + int sum = 0; + for (auto backend : qAsConst(m_backends)) { + sum += backend->fetchingUpdatesProgress(); + } + return sum / (int)m_backends.count(); + }, + [this](int progress) { + Q_EMIT fetchingUpdatesProgressChanged(progress); + }) +{ + connect(this, &ResourcesModel::allInitialized, this, &ResourcesModel::slotFetching); + connect(this, &ResourcesModel::backendsChanged, this, &ResourcesModel::initApplicationsBackend); +} + +void ResourcesModel::init(bool load) +{ + Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread()); + + m_allInitializedEmitter->setSingleShot(true); + m_allInitializedEmitter->setInterval(0); + connect(m_allInitializedEmitter, &QTimer::timeout, this, [this]() { + if (m_initializingBackendsCount == 0) { + m_isInitializing = false; + Q_EMIT allInitialized(); + } + }); + + if (load) { + registerAllBackends(); + } + + m_updateAction = new DiscoverAction(this); + m_updateAction->setIconName(QStringLiteral("system-software-update")); + m_updateAction->setText(i18n("Refresh")); + connect(this, &ResourcesModel::fetchingChanged, m_updateAction, [this](bool fetching) { + m_updateAction->setEnabled(!fetching); + m_fetchingUpdatesProgress.reevaluate(); + }); + connect(m_updateAction, &DiscoverAction::triggered, this, &ResourcesModel::checkForUpdates); + + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater); +} + +ResourcesModel::ResourcesModel(const QString &backendName, QObject *parent) + : ResourcesModel(parent) +{ + s_self = this; + registerBackendByName(backendName); + init(false); +} + +ResourcesModel::~ResourcesModel() +{ + s_self = nullptr; + qDeleteAll(m_backends); +} + +void ResourcesModel::addResourcesBackend(AbstractResourcesBackend *backend) +{ + Q_ASSERT(!m_backends.contains(backend)); + if (!backend->isValid()) { + qCWarning(LIBDISCOVER_LOG) << "Discarding invalid backend" << backend->name(); + CategoryModel::global()->blacklistPlugin(backend->name()); + backend->deleteLater(); + return; + } + + m_backends += backend; + if (!backend->isFetching()) { + m_updatesCount.reevaluate(); + } else { + m_initializingBackendsCount++; + } + + connect(backend, &AbstractResourcesBackend::fetchingChanged, this, &ResourcesModel::callerFetchingChanged); + connect(backend, &AbstractResourcesBackend::allDataChanged, this, &ResourcesModel::updateCaller); + connect(backend, &AbstractResourcesBackend::resourcesChanged, this, &ResourcesModel::resourceDataChanged); + connect(backend, &AbstractResourcesBackend::updatesCountChanged, this, [this] { + m_updatesCount.reevaluate(); + }); + connect(backend, &AbstractResourcesBackend::fetchingUpdatesProgressChanged, this, [this] { + m_fetchingUpdatesProgress.reevaluate(); + }); + connect(backend, &AbstractResourcesBackend::resourceRemoved, this, &ResourcesModel::resourceRemoved); + connect(backend, &AbstractResourcesBackend::passiveMessage, this, &ResourcesModel::passiveMessage); + connect(backend, &AbstractResourcesBackend::inlineMessageChanged, this, &ResourcesModel::setInlineMessage); + connect(backend->backendUpdater(), &AbstractBackendUpdater::progressingChanged, this, &ResourcesModel::slotFetching); + if (backend->reviewsBackend()) { + connect(backend->reviewsBackend(), &AbstractReviewsBackend::error, this, &ResourcesModel::passiveMessage, Qt::UniqueConnection); + } + + // In case this is in fact the first backend to be added, and also happens to be + // pre-filled, we still need for the rest of the backends to be added before trying + // to send out the initialized signal. To ensure this happens, schedule it for the + // start of the next run of the event loop. + if (m_initializingBackendsCount == 0) { + m_allInitializedEmitter->start(); + } else { + slotFetching(); + } +} + +void ResourcesModel::callerFetchingChanged() +{ + AbstractResourcesBackend *backend = qobject_cast(sender()); + + if (!backend->isValid()) { + qCWarning(LIBDISCOVER_LOG) << "Discarding invalid backend" << backend->name(); + int idx = m_backends.indexOf(backend); + Q_ASSERT(idx >= 0); + m_backends.removeAt(idx); + Q_EMIT backendsChanged(); + CategoryModel::global()->blacklistPlugin(backend->name()); + backend->deleteLater(); + slotFetching(); + return; + } + + if (backend->isFetching()) { + m_initializingBackendsCount++; + slotFetching(); + } else { + m_initializingBackendsCount--; + if (m_initializingBackendsCount == 0) + m_allInitializedEmitter->start(); + else + slotFetching(); + } +} + +void ResourcesModel::updateCaller(const QVector &properties) +{ + AbstractResourcesBackend *backend = qobject_cast(sender()); + + Q_EMIT backendDataChanged(backend, properties); +} + +QVector ResourcesModel::backends() const +{ + return m_backends; +} + +bool ResourcesModel::hasSecurityUpdates() const +{ + bool ret = false; + + for (AbstractResourcesBackend *backend : qAsConst(m_backends)) { + ret |= backend->hasSecurityUpdates(); + } + + return ret; +} + +void ResourcesModel::installApplication(AbstractResource *app) +{ + TransactionModel::global()->addTransaction(app->backend()->installApplication(app)); +} + +void ResourcesModel::installApplication(AbstractResource *app, const AddonList &addons) +{ + TransactionModel::global()->addTransaction(app->backend()->installApplication(app, addons)); +} + +void ResourcesModel::removeApplication(AbstractResource *app) +{ + TransactionModel::global()->addTransaction(app->backend()->removeApplication(app)); +} + +void ResourcesModel::registerAllBackends() +{ + DiscoverBackendsFactory f; + const auto backends = f.allBackends(); + if (m_initializingBackendsCount == 0 && backends.isEmpty()) { + qCWarning(LIBDISCOVER_LOG) << "Couldn't find any backends"; + m_allInitializedEmitter->start(); + } else { + for (AbstractResourcesBackend *b : backends) { + addResourcesBackend(b); + } + Q_EMIT backendsChanged(); + } +} + +void ResourcesModel::registerBackendByName(const QString &name) +{ + DiscoverBackendsFactory f; + const auto backends = f.backend(name); + for (auto b : backends) + addResourcesBackend(b); + + Q_EMIT backendsChanged(); +} + +bool ResourcesModel::isFetching() const +{ + return m_isFetching; +} + +bool ResourcesModel::isInitializing() const +{ + return m_isInitializing; +} + +void ResourcesModel::slotFetching() +{ + bool newFetching = false; + for (AbstractResourcesBackend *b : qAsConst(m_backends)) { + // isFetching should sort of be enough. However, sometimes the backend itself + // will still be operating on things, which from a model point of view would + // still mean something going on. So, interpret that as fetching as well, for + // the purposes of this property. + if (b->isFetching() || (b->backendUpdater() && b->backendUpdater()->isProgressing())) { + newFetching = true; + break; + } + } + if (newFetching != m_isFetching) { + m_isFetching = newFetching; + Q_EMIT fetchingChanged(m_isFetching); + } +} + +bool ResourcesModel::isBusy() const +{ + return TransactionModel::global()->rowCount() > 0; +} + +bool ResourcesModel::isExtended(const QString &id) +{ + bool ret = true; + for (AbstractResourcesBackend *backend : qAsConst(m_backends)) { + ret = backend->extends().contains(id); + if (ret) + break; + } + return ret; +} + +AggregatedResultsStream::AggregatedResultsStream(const QSet &streams) + : ResultsStream(QStringLiteral("AggregatedResultsStream")) +{ + Q_ASSERT(!streams.contains(nullptr)); + if (streams.isEmpty()) { + qCWarning(LIBDISCOVER_LOG) << "no streams to aggregate!!"; + QTimer::singleShot(0, this, &AggregatedResultsStream::clear); + } + + for (auto stream : streams) { + connect(stream, &ResultsStream::resourcesFound, this, &AggregatedResultsStream::addResults); + connect(stream, &QObject::destroyed, this, &AggregatedResultsStream::streamDestruction); + connect(this, &ResultsStream::fetchMore, stream, &ResultsStream::fetchMore); + m_streams << stream; + } + + m_delayedEmission.setInterval(0); + connect(&m_delayedEmission, &QTimer::timeout, this, &AggregatedResultsStream::emitResults); +} + +AggregatedResultsStream::~AggregatedResultsStream() = default; + +void AggregatedResultsStream::addResults(const QVector &res) +{ + for (auto r : res) + connect(r, &QObject::destroyed, this, &AggregatedResultsStream::resourceDestruction); + + m_results += res; + + m_delayedEmission.start(); +} + +void AggregatedResultsStream::emitResults() +{ + if (!m_results.isEmpty()) { + Q_EMIT resourcesFound(m_results); + m_results.clear(); + } + m_delayedEmission.setInterval(m_delayedEmission.interval() + 100); + m_delayedEmission.stop(); +} + +void AggregatedResultsStream::resourceDestruction(QObject *obj) +{ + m_results.removeAll(qobject_cast(obj)); +} + +void AggregatedResultsStream::streamDestruction(QObject *obj) +{ + m_streams.remove(obj); + clear(); +} + +void AggregatedResultsStream::clear() +{ + if (m_streams.isEmpty()) { + emitResults(); + Q_EMIT finished(); + deleteLater(); + } +} + +AggregatedResultsStream *ResourcesModel::search(const AbstractResourcesBackend::Filters &search) +{ + if (search.isEmpty()) { + return new AggregatedResultsStream({new ResultsStream(QStringLiteral("emptysearch"), {})}); + } + + auto streams = kTransform>(m_backends, [search](AbstractResourcesBackend *backend) { + return backend->search(search); + }); + return new AggregatedResultsStream(streams); +} + +void ResourcesModel::checkForUpdates() +{ + for (auto backend : qAsConst(m_backends)) + backend->checkForUpdates(); +} + +AbstractResourcesBackend *ResourcesModel::currentApplicationBackend() const +{ + return m_currentApplicationBackend; +} + +void ResourcesModel::setCurrentApplicationBackend(AbstractResourcesBackend *backend, bool write) +{ + if (backend != m_currentApplicationBackend) { + if (write) { + KConfigGroup settings(KSharedConfig::openConfig(), "ResourcesModel"); + if (backend) + settings.writeEntry("currentApplicationBackend", backend->name()); + else + settings.deleteEntry("currentApplicationBackend"); + } + + qCDebug(LIBDISCOVER_LOG) << "setting currentApplicationBackend" << backend; + m_currentApplicationBackend = backend; + Q_EMIT currentApplicationBackendChanged(backend); + } +} + +void ResourcesModel::initApplicationsBackend() +{ + const auto name = applicationSourceName(); + + auto idx = kIndexOf(m_backends, [name](AbstractResourcesBackend *b) { + return b->hasApplications() && b->name() == name; + }); + if (idx < 0) { + idx = kIndexOf(m_backends, [](AbstractResourcesBackend *b) { + return b->hasApplications(); + }); + qCDebug(LIBDISCOVER_LOG) << "falling back applications backend to" << idx; + } + setCurrentApplicationBackend(m_backends.value(idx, nullptr), false); +} + +QString ResourcesModel::applicationSourceName() const +{ + KConfigGroup settings(KSharedConfig::openConfig(), "ResourcesModel"); + return settings.readEntry("currentApplicationBackend", QStringLiteral("packagekit-backend")); +} + +QUrl ResourcesModel::distroBugReportUrl() +{ + return QUrl(KOSRelease().bugReportUrl()); +} + +void ResourcesModel::setInlineMessage(const QSharedPointer &inlineMessage) +{ + if (inlineMessage == m_inlineMessage) { + return; + } + + m_inlineMessage = inlineMessage; + Q_EMIT inlineMessageChanged(inlineMessage); +} diff --git a/libdiscover/resources/ResourcesModel.h b/libdiscover/resources/ResourcesModel.h new file mode 100644 index 0000000..b086619 --- /dev/null +++ b/libdiscover/resources/ResourcesModel.h @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +#include "AbstractResourcesBackend.h" +#include "discovercommon_export.h" + +class DiscoverAction; + +class DISCOVERCOMMON_EXPORT AggregatedResultsStream : public ResultsStream +{ + Q_OBJECT +public: + AggregatedResultsStream(const QSet &streams); + ~AggregatedResultsStream(); + + QSet streams() const + { + return m_streams; + } + +Q_SIGNALS: + void finished(); + +private: + void addResults(const QVector &res); + void emitResults(); + void streamDestruction(QObject *obj); + void resourceDestruction(QObject *obj); + void clear(); + + QSet m_streams; + QVector m_results; + QTimer m_delayedEmission; +}; + +template +class EmitWhenChanged +{ +public: + EmitWhenChanged(T initial, const std::function &get, const std::function &emitChanged) + : m_get(get) + , m_emitChanged(emitChanged) + , m_value(initial) + { + } + + void reevaluate() + { + auto newValue = m_get(); + if (newValue != m_value) { + m_value = newValue; + m_emitChanged(m_value); + } + } + + std::function const m_get; + std::function const m_emitChanged; + T m_value; +}; + +class DISCOVERCOMMON_EXPORT ResourcesModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(int updatesCount READ updatesCount NOTIFY updatesCountChanged) + Q_PROPERTY(bool hasSecurityUpdates READ hasSecurityUpdates NOTIFY updatesCountChanged) + Q_PROPERTY(bool isFetching READ isFetching NOTIFY fetchingChanged) + Q_PROPERTY(bool isInitializing READ isInitializing NOTIFY allInitialized) + Q_PROPERTY(AbstractResourcesBackend *currentApplicationBackend READ currentApplicationBackend WRITE setCurrentApplicationBackend NOTIFY + currentApplicationBackendChanged) + Q_PROPERTY(DiscoverAction *updateAction READ updateAction CONSTANT) + Q_PROPERTY(int fetchingUpdatesProgress READ fetchingUpdatesProgress NOTIFY fetchingUpdatesProgressChanged) + Q_PROPERTY(QString applicationSourceName READ applicationSourceName NOTIFY currentApplicationBackendChanged) + Q_PROPERTY(InlineMessage *inlineMessage READ inlineMessage NOTIFY inlineMessageChanged) +public: + /** This constructor should be only used by unit tests. + * @p backendName defines what backend will be loaded when the backend is constructed. + */ + explicit ResourcesModel(const QString &backendName, QObject *parent = nullptr); + static ResourcesModel *global(); + ~ResourcesModel() override; + + QVector backends() const; + int updatesCount() const + { + return m_updatesCount.m_value; + } + bool hasSecurityUpdates() const; + + bool isBusy() const; + bool isFetching() const; + bool isInitializing() const; + + Q_SCRIPTABLE bool isExtended(const QString &id); + + AggregatedResultsStream *search(const AbstractResourcesBackend::Filters &search); + void checkForUpdates(); + + QString applicationSourceName() const; + + void setCurrentApplicationBackend(AbstractResourcesBackend *backend, bool writeConfig = true); + AbstractResourcesBackend *currentApplicationBackend() const; + + DiscoverAction *updateAction() const + { + return m_updateAction; + } + int fetchingUpdatesProgress() const + { + return m_fetchingUpdatesProgress.m_value; + } + + Q_INVOKABLE QUrl distroBugReportUrl(); + + void setInlineMessage(const QSharedPointer &inlineMessage); + InlineMessage *inlineMessage() const + { + return m_inlineMessage.data(); + } + +public Q_SLOTS: + void installApplication(AbstractResource *app, const AddonList &addons); + void installApplication(AbstractResource *app); + void removeApplication(AbstractResource *app); + +Q_SIGNALS: + void fetchingChanged(bool isFetching); + void allInitialized(); + void backendsChanged(); + void updatesCountChanged(int updatesCount); + void backendDataChanged(AbstractResourcesBackend *backend, const QVector &properties); + void resourceDataChanged(AbstractResource *resource, const QVector &properties); + void resourceRemoved(AbstractResource *resource); + void passiveMessage(const QString &message); + void currentApplicationBackendChanged(AbstractResourcesBackend *currentApplicationBackend); + void fetchingUpdatesProgressChanged(int fetchingUpdatesProgress); + void inlineMessageChanged(const QSharedPointer &inlineMessage); + +private Q_SLOTS: + void callerFetchingChanged(); + void updateCaller(const QVector &properties); + void registerAllBackends(); + +private: + ///@p initialize tells if all backends load will be triggered on construction + explicit ResourcesModel(QObject *parent = nullptr); + void init(bool load); + void addResourcesBackend(AbstractResourcesBackend *backend); + void registerBackendByName(const QString &name); + void initApplicationsBackend(); + void slotFetching(); + + bool m_isFetching; + bool m_isInitializing = true; + QVector m_backends; + int m_initializingBackendsCount; + DiscoverAction *m_updateAction = nullptr; + AbstractResourcesBackend *m_currentApplicationBackend; + QTimer *m_allInitializedEmitter; + + EmitWhenChanged m_updatesCount; + EmitWhenChanged m_fetchingUpdatesProgress; + QSharedPointer m_inlineMessage; + + static ResourcesModel *s_self; +}; diff --git a/libdiscover/resources/ResourcesProxyModel.cpp b/libdiscover/resources/ResourcesProxyModel.cpp new file mode 100644 index 0000000..9a8ad5e --- /dev/null +++ b/libdiscover/resources/ResourcesProxyModel.cpp @@ -0,0 +1,739 @@ +/* + * SPDX-FileCopyrightText: 2010 Jonathan Thomas + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "ResourcesProxyModel.h" + +#include "libdiscover_debug.h" +#include +#include +#include + +#include "ResourcesModel.h" +#include +#include +#include +#include + +const QHash ResourcesProxyModel::s_roles = {{NameRole, "name"}, + {IconRole, "icon"}, + {CommentRole, "comment"}, + {StateRole, "state"}, + {RatingRole, "rating"}, + {RatingPointsRole, "ratingPoints"}, + {RatingCountRole, "ratingCount"}, + {SortableRatingRole, "sortableRating"}, + {InstalledRole, "isInstalled"}, + {ApplicationRole, "application"}, + {OriginRole, "origin"}, + {DisplayOriginRole, "displayOrigin"}, + {CanUpgrade, "canUpgrade"}, + {PackageNameRole, "packageName"}, + {CategoryRole, "category"}, + {SectionRole, "section"}, + {MimeTypes, "mimetypes"}, + {LongDescriptionRole, "longDescription"}, + {SourceIconRole, "sourceIcon"}, + {SizeRole, "size"}, + {ReleaseDateRole, "releaseDate"}}; + +ResourcesProxyModel::ResourcesProxyModel(QObject *parent) + : QAbstractListModel(parent) + , m_sortRole(NameRole) + , m_sortOrder(Qt::AscendingOrder) + , m_sortByRelevancy(false) + , m_currentStream(nullptr) +{ + // new QAbstractItemModelTester(this, this); + + connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, this, &ResourcesProxyModel::invalidateFilter); + connect(ResourcesModel::global(), &ResourcesModel::backendDataChanged, this, &ResourcesProxyModel::refreshBackend); + connect(ResourcesModel::global(), &ResourcesModel::resourceDataChanged, this, &ResourcesProxyModel::refreshResource); + connect(ResourcesModel::global(), &ResourcesModel::resourceRemoved, this, &ResourcesProxyModel::removeResource); + + m_countTimer.setInterval(10); + m_countTimer.setSingleShot(true); + connect(&m_countTimer, &QTimer::timeout, this, &ResourcesProxyModel::countChanged); + + connect(this, &QAbstractItemModel::modelReset, &m_countTimer, qOverload<>(&QTimer::start)); + connect(this, &QAbstractItemModel::rowsInserted, &m_countTimer, qOverload<>(&QTimer::start)); + connect(this, &QAbstractItemModel::rowsRemoved, &m_countTimer, qOverload<>(&QTimer::start)); + + connect(this, &ResourcesProxyModel::busyChanged, &m_countTimer, qOverload<>(&QTimer::start)); +} + +void ResourcesProxyModel::componentComplete() +{ + m_setup = true; + invalidateFilter(); +} + +QHash ResourcesProxyModel::roleNames() const +{ + return s_roles; +} + +void ResourcesProxyModel::setSortRole(Roles sortRole) +{ + if (sortRole != m_sortRole) { + Q_ASSERT(roleNames().contains(sortRole)); + + m_sortRole = sortRole; + Q_EMIT sortRoleChanged(sortRole); + invalidateSorting(); + } +} + +void ResourcesProxyModel::setSortOrder(Qt::SortOrder sortOrder) +{ + if (sortOrder != m_sortOrder) { + m_sortOrder = sortOrder; + Q_EMIT sortOrderChanged(sortOrder); + invalidateSorting(); + } +} + +void ResourcesProxyModel::setSearch(const QString &_searchText) +{ + // 1-character searches are painfully slow. >= 2 chars are fine, though + const QString searchText = _searchText.count() <= 1 ? QString() : _searchText; + + const bool diff = searchText != m_filters.search; + + if (diff) { + m_filters.search = searchText; + if (m_sortByRelevancy == searchText.isEmpty()) { + m_sortByRelevancy = !searchText.isEmpty(); + Q_EMIT sortByRelevancyChanged(m_sortByRelevancy); + } + invalidateFilter(); + Q_EMIT searchChanged(m_filters.search); + } +} + +void ResourcesProxyModel::removeDuplicates(QVector &resources) +{ + const auto cab = ResourcesModel::global()->currentApplicationBackend(); + QHash aliases; + QHash::iterator> storedIds; + for (auto it = m_displayedResources.begin(); it != m_displayedResources.end(); ++it) { + const auto appstreamid = (*it)->appstreamId(); + if (appstreamid.isEmpty()) { + continue; + } + auto at = storedIds.find(appstreamid); + if (at == storedIds.end()) { + storedIds[appstreamid] = it; + } else { + qCWarning(LIBDISCOVER_LOG) << "We should have sanitized the displayed resources. There is a bug"; + Q_UNREACHABLE(); + } + + const auto alts = (*it)->alternativeAppstreamIds(); + for (const auto &alias : alts) { + aliases[alias] = appstreamid; + } + } + + QHash::iterator> ids; + for (auto it = resources.begin(); it != resources.end();) { + const auto appstreamid = (*it)->appstreamId(); + if (appstreamid.isEmpty()) { + ++it; + continue; + } + auto at = storedIds.find(appstreamid); + if (at == storedIds.end()) { + auto aliased = aliases.constFind(appstreamid); + if (aliased != aliases.constEnd()) { + at = storedIds.find(aliased.value()); + } + } + + if (at == storedIds.end()) { + const auto alts = (*it)->alternativeAppstreamIds(); + for (const auto &alt : alts) { + at = storedIds.find(alt); + if (at == storedIds.end()) + break; + + auto aliased = aliases.constFind(alt); + if (aliased != aliases.constEnd()) { + at = storedIds.find(aliased.value()); + if (at != storedIds.end()) + break; + } + } + } + if (at == storedIds.end()) { + auto at = ids.find(appstreamid); + if (at == ids.end()) { + auto aliased = aliases.constFind(appstreamid); + if (aliased != aliases.constEnd()) { + at = ids.find(aliased.value()); + } + } + if (at == ids.end()) { + const auto alts = (*it)->alternativeAppstreamIds(); + for (const auto &alt : alts) { + at = ids.find(alt); + if (at != ids.end()) + break; + + auto aliased = aliases.constFind(appstreamid); + if (aliased != aliases.constEnd()) { + at = ids.find(aliased.value()); + if (at != ids.end()) + break; + } + } + } + if (at == ids.end()) { + ids[appstreamid] = it; + const auto alts = (*it)->alternativeAppstreamIds(); + for (const auto &alias : alts) { + aliases[alias] = appstreamid; + } + ++it; + } else { + if ((*it)->backend() == cab && (*it)->backend() != (**at)->backend()) { + qSwap(*it, **at); + } + it = resources.erase(it); + } + } else { + if ((*it)->backend() == cab) { + **at = *it; + auto pos = index(*at - m_displayedResources.begin(), 0); + Q_EMIT dataChanged(pos, pos); + } + it = resources.erase(it); + } + } +} + +void ResourcesProxyModel::addResources(const QVector &_res) +{ + auto res = _res; + m_filters.filterJustInCase(res); + + if (res.isEmpty()) + return; + + if (!m_sortByRelevancy) + std::sort(res.begin(), res.end(), [this](AbstractResource *res, AbstractResource *res2) { + return lessThan(res, res2); + }); + + sortedInsertion(res); + fetchSubcategories(); +} + +void ResourcesProxyModel::invalidateSorting() +{ + if (m_displayedResources.isEmpty()) + return; + + if (!m_sortByRelevancy) { + beginResetModel(); + std::sort(m_displayedResources.begin(), m_displayedResources.end(), [this](AbstractResource *res, AbstractResource *res2) { + return lessThan(res, res2); + }); + endResetModel(); + } +} + +QString ResourcesProxyModel::lastSearch() const +{ + return m_filters.search; +} + +void ResourcesProxyModel::setOriginFilter(const QString &origin) +{ + if (origin == m_filters.origin) + return; + + m_filters.origin = origin; + + invalidateFilter(); +} + +QString ResourcesProxyModel::originFilter() const +{ + return m_filters.origin; +} + +QString ResourcesProxyModel::filteredCategoryName() const +{ + return m_categoryName; +} + +void ResourcesProxyModel::setFilteredCategoryName(const QString &cat) +{ + if (cat == m_categoryName) + return; + + m_categoryName = cat; + + auto category = CategoryModel::global()->findCategoryByName(cat); + if (category) { + setFiltersFromCategory(category); + } else { + qDebug() << "looking up wrong category or too early" << m_categoryName; + auto f = [this, cat] { + auto category = CategoryModel::global()->findCategoryByName(cat); + setFiltersFromCategory(category); + }; + auto one = new OneTimeAction(f, this); + connect(CategoryModel::global(), &CategoryModel::rootCategoriesChanged, one, &OneTimeAction::trigger); + } +} + +void ResourcesProxyModel::setFiltersFromCategory(Category *category) +{ + if (category == m_filters.category) + return; + + m_filters.category = category; + invalidateFilter(); + Q_EMIT categoryChanged(); +} + +void ResourcesProxyModel::fetchSubcategories() +{ + auto cats = kToSet(m_filters.category ? m_filters.category->subCategories() : CategoryModel::global()->rootCategories()); + + const int count = rowCount(); + QSet done; + for (int i = 0; i < count && !cats.isEmpty(); ++i) { + AbstractResource *res = m_displayedResources[i]; + const auto found = res->categoryObjects(kSetToVector(cats)); + done.unite(found); + cats.subtract(found); + } + + const QVariantList ret = kTransform(done, [](Category *cat) { + return QVariant::fromValue(cat); + }); + if (ret != m_subcategories) { + m_subcategories = ret; + Q_EMIT subcategoriesChanged(m_subcategories); + } +} + +QVariantList ResourcesProxyModel::subcategories() const +{ + return m_subcategories; +} + +void ResourcesProxyModel::invalidateFilter() +{ + if (!m_setup || ResourcesModel::global()->backends().isEmpty()) { + return; + } + + if (!m_categoryName.isEmpty() && m_filters.category == nullptr) { + return; + } + + if (m_currentStream) { + qCWarning(LIBDISCOVER_LOG) << "last stream isn't over yet" << m_filters << this; + delete m_currentStream; + } + + m_currentStream = m_filters.backend ? m_filters.backend->search(m_filters) : ResourcesModel::global()->search(m_filters); + Q_EMIT busyChanged(true); + + if (!m_displayedResources.isEmpty()) { + beginResetModel(); + m_displayedResources.clear(); + endResetModel(); + } + + connect(m_currentStream, &ResultsStream::resourcesFound, this, &ResourcesProxyModel::addResources); + connect(m_currentStream, &ResultsStream::destroyed, this, [this]() { + m_currentStream = nullptr; + Q_EMIT busyChanged(false); + }); +} + +int ResourcesProxyModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_displayedResources.count(); +} + +bool ResourcesProxyModel::lessThan(AbstractResource *leftPackage, AbstractResource *rightPackage) const +{ + auto role = m_sortRole; + Qt::SortOrder order = m_sortOrder; + QVariant leftValue; + QVariant rightValue; + // if we're comparing two equal values, we want the model sorted by application name + if (role != NameRole) { + leftValue = roleToValue(leftPackage, role); + rightValue = roleToValue(rightPackage, role); + + if (leftValue == rightValue) { + role = NameRole; + order = Qt::AscendingOrder; + } + } + + bool ret; + if (role == NameRole) { + ret = leftPackage->nameSortKey().compare(rightPackage->nameSortKey()) < 0; + } else if (role == CanUpgrade) { + ret = leftValue.toBool(); + } else { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + ret = leftValue < rightValue; +#else + const auto order = QVariant::compare(leftValue, rightValue); + Q_ASSERT(order != QPartialOrdering::Unordered); + return order == QPartialOrdering::Less; +#endif + } + return ret != (order != Qt::AscendingOrder); +} + +Category *ResourcesProxyModel::filteredCategory() const +{ + return m_filters.category; +} + +void ResourcesProxyModel::setStateFilter(AbstractResource::State s) +{ + if (s != m_filters.state) { + m_filters.state = s; + invalidateFilter(); + Q_EMIT stateFilterChanged(); + } +} + +AbstractResource::State ResourcesProxyModel::stateFilter() const +{ + return m_filters.state; +} + +QString ResourcesProxyModel::mimeTypeFilter() const +{ + return m_filters.mimetype; +} + +void ResourcesProxyModel::setMimeTypeFilter(const QString &mime) +{ + if (m_filters.mimetype != mime) { + m_filters.mimetype = mime; + invalidateFilter(); + } +} + +QString ResourcesProxyModel::extends() const +{ + return m_filters.extends; +} + +void ResourcesProxyModel::setExtends(const QString &extends) +{ + if (m_filters.extends != extends) { + m_filters.extends = extends; + invalidateFilter(); + } +} + +void ResourcesProxyModel::setFilterMinimumState(bool filterMinimumState) +{ + if (filterMinimumState != m_filters.filterMinimumState) { + m_filters.filterMinimumState = filterMinimumState; + invalidateFilter(); + Q_EMIT filterMinimumStateChanged(m_filters.filterMinimumState); + } +} + +bool ResourcesProxyModel::filterMinimumState() const +{ + return m_filters.filterMinimumState; +} + +QUrl ResourcesProxyModel::resourcesUrl() const +{ + return m_filters.resourceUrl; +} + +void ResourcesProxyModel::setResourcesUrl(const QUrl &resourcesUrl) +{ + if (m_filters.resourceUrl != resourcesUrl) { + m_filters.resourceUrl = resourcesUrl; + invalidateFilter(); + } +} + +bool ResourcesProxyModel::allBackends() const +{ + return m_filters.allBackends; +} + +void ResourcesProxyModel::setAllBackends(bool allBackends) +{ + m_filters.allBackends = allBackends; +} + +AbstractResourcesBackend *ResourcesProxyModel::backendFilter() const +{ + return m_filters.backend; +} + +void ResourcesProxyModel::setBackendFilter(AbstractResourcesBackend *filtered) +{ + m_filters.backend = filtered; +} + +QVariant ResourcesProxyModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + AbstractResource *const resource = m_displayedResources[index.row()]; + return roleToValue(resource, role); +} + +QVariant ResourcesProxyModel::roleToValue(AbstractResource *resource, int role) const +{ + switch (role) { + case ApplicationRole: + return QVariant::fromValue(resource); + case RatingPointsRole: + case RatingRole: + case RatingCountRole: + case SortableRatingRole: { + Rating *const rating = resource->rating(); + const int idx = Rating::staticMetaObject.indexOfProperty(roleNames().value(role).constData()); + Q_ASSERT(idx >= 0); + auto prop = Rating::staticMetaObject.property(idx); + if (rating) { + return prop.readOnGadget(rating); + } else { + QVariant val(0); + val.convert(prop.type()); + return val; + } + } + case Qt::DecorationRole: + case Qt::DisplayRole: + case Qt::StatusTipRole: + case Qt::ToolTipRole: + return QVariant(); + default: { + QByteArray roleText = roleNames().value(role); + if (Q_UNLIKELY(roleText.isEmpty())) { + qCDebug(LIBDISCOVER_LOG) << "unsupported role" << role; + return {}; + } + static const QMetaObject *m = &AbstractResource::staticMetaObject; + int propidx = roleText.isEmpty() ? -1 : m->indexOfProperty(roleText.constData()); + + if (Q_UNLIKELY(propidx < 0)) { + qCWarning(LIBDISCOVER_LOG) << "unknown role:" << role << roleText; + return QVariant(); + } else + return m->property(propidx).read(resource); + } + } +} + +bool ResourcesProxyModel::isSorted(const QVector &resources) +{ + auto last = resources.constFirst(); + for (auto it = resources.constBegin() + 1, itEnd = resources.constEnd(); it != itEnd; ++it) { + auto v1 = roleToValue(last, m_sortRole), v2 = roleToValue(*it, m_sortRole); + if (!lessThan(last, *it) && v1 != v2) { + qDebug() << "faulty sort" << last->name() << (*it)->name() << last << (*it); + return false; + } + last = *it; + } + return true; +} + +void ResourcesProxyModel::sortedInsertion(const QVector &_res) +{ + Q_ASSERT(_res.size() == QSet(_res.constBegin(), _res.constEnd()).size()); + + auto resources = _res; + Q_ASSERT(!resources.isEmpty()); + + if (!m_filters.allBackends) { + removeDuplicates(resources); + if (resources.isEmpty()) + return; + } + + if (m_sortByRelevancy || m_displayedResources.isEmpty()) { + // Q_ASSERT(m_sortByRelevancy || isSorted(resources)); + int rows = rowCount(); + beginInsertRows({}, rows, rows + resources.count() - 1); + m_displayedResources += resources; + endInsertRows(); + return; + } + + for (auto resource : qAsConst(resources)) { + const auto finder = [this](AbstractResource *resource, AbstractResource *res) { + return lessThan(resource, res); + }; + const auto it = std::upper_bound(m_displayedResources.constBegin(), m_displayedResources.constEnd(), resource, finder); + const auto newIdx = it == m_displayedResources.constEnd() ? m_displayedResources.count() : (it - m_displayedResources.constBegin()); + + if ((it - 1) != m_displayedResources.constEnd() && *(it - 1) == resource) + continue; + + beginInsertRows({}, newIdx, newIdx); + m_displayedResources.insert(newIdx, resource); + endInsertRows(); + // Q_ASSERT(isSorted(resources)); + } +} + +void ResourcesProxyModel::refreshResource(AbstractResource *resource, const QVector &properties) +{ + const auto residx = m_displayedResources.indexOf(resource); + if (residx < 0) { + return; + } + + if (!m_filters.shouldFilter(resource)) { + beginRemoveRows({}, residx, residx); + m_displayedResources.removeAt(residx); + endRemoveRows(); + return; + } + + const QModelIndex idx = index(residx, 0); + Q_ASSERT(idx.isValid()); + const auto roles = propertiesToRoles(properties); + if (!m_sortByRelevancy && roles.contains(m_sortRole)) { + beginRemoveRows({}, residx, residx); + m_displayedResources.removeAt(residx); + endRemoveRows(); + + sortedInsertion({resource}); + } else + Q_EMIT dataChanged(idx, idx, roles); +} + +void ResourcesProxyModel::removeResource(AbstractResource *resource) +{ + const auto residx = m_displayedResources.indexOf(resource); + if (residx < 0) + return; + beginRemoveRows({}, residx, residx); + m_displayedResources.removeAt(residx); + endRemoveRows(); +} + +void ResourcesProxyModel::refreshBackend(AbstractResourcesBackend *backend, const QVector &properties) +{ + auto roles = propertiesToRoles(properties); + const int count = m_displayedResources.count(); + + bool found = false; + + for (int i = 0; i < count; ++i) { + if (backend != m_displayedResources[i]->backend()) + continue; + + int j = i + 1; + for (; j < count && backend == m_displayedResources[j]->backend(); ++j) { } + + Q_EMIT dataChanged(index(i, 0), index(j - 1, 0), roles); + i = j; + found = true; + } + + if (found && properties.contains(s_roles.value(m_sortRole))) { + invalidateSorting(); + } +} + +QVector ResourcesProxyModel::propertiesToRoles(const QVector &properties) const +{ + QVector roles = kTransform>(properties, [this](const QByteArray &arr) { + return roleNames().key(arr, -1); + }); + roles.removeAll(-1); + return roles; +} + +int ResourcesProxyModel::indexOf(AbstractResource *res) +{ + return m_displayedResources.indexOf(res); +} + +AbstractResource *ResourcesProxyModel::resourceAt(int row) const +{ + return m_displayedResources[row]; +} + +bool ResourcesProxyModel::canFetchMore(const QModelIndex &parent) const +{ + Q_ASSERT(!parent.isValid()); + return m_currentStream; +} + +void ResourcesProxyModel::fetchMore(const QModelIndex &parent) +{ + Q_ASSERT(!parent.isValid()); + if (!m_currentStream) + return; + Q_EMIT m_currentStream->fetchMore(); +} + +bool ResourcesProxyModel::sortByRelevancy() const +{ + return m_sortByRelevancy; +} + +ResourcesCount ResourcesProxyModel::count() const +{ + const int rows = rowCount(); + if (isBusy()) { + // We return an empty string because it's evidently confusing + if (rows == 0) { + return ResourcesCount(); + } + + // We convert rows=1234 into round=1000 + const int round = std::pow(10, std::floor(std::log10(rows))); + if (round >= 1) { + const int roughCount = (rows / round) * round; + const auto string = i18nc("an approximation number, like 3000+", "%1+", roughCount); + return ResourcesCount(roughCount, string); + } + } + return ResourcesCount(rows); +} + +ResourcesCount::ResourcesCount() + : m_valid(false) + , m_number(0) + , m_string() +{ +} + +ResourcesCount::ResourcesCount(int number) + : m_valid(true) + , m_number(number) + , m_string(QString::number(number)) +{ +} + +ResourcesCount::ResourcesCount(int number, const QString &string) + : m_valid(true) + , m_number(number) + , m_string(string) +{ +} diff --git a/libdiscover/resources/ResourcesProxyModel.h b/libdiscover/resources/ResourcesProxyModel.h new file mode 100644 index 0000000..082cb61 --- /dev/null +++ b/libdiscover/resources/ResourcesProxyModel.h @@ -0,0 +1,208 @@ +/* + * SPDX-FileCopyrightText: 2010 Jonathan Thomas + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "AbstractResource.h" +#include "AbstractResourcesBackend.h" +#include "discovercommon_export.h" + +class AggregatedResultsStream; + +/* + * This class encapsulates model's counter. The string property is a + * pre-formatted number possibly with extra symbols for a "rough" appearance. + * If counter is not valid, accessing its number and string is pointless. + */ +class DISCOVERCOMMON_EXPORT ResourcesCount +{ + Q_GADGET + Q_PROPERTY(bool valid MEMBER m_valid CONSTANT FINAL) + Q_PROPERTY(int number MEMBER m_number CONSTANT FINAL) + Q_PROPERTY(QString string MEMBER m_string CONSTANT FINAL) + +public: + explicit ResourcesCount(); + explicit ResourcesCount(int number); + explicit ResourcesCount(int number, const QString &string); + +private: + bool m_valid; + int m_number; + QString m_string; +}; + +Q_DECLARE_METATYPE(ResourcesCount) + +class DISCOVERCOMMON_EXPORT ResourcesProxyModel : public QAbstractListModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(Roles sortRole READ sortRole WRITE setSortRole NOTIFY sortRoleChanged) + Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) + Q_PROPERTY(Category *filteredCategory READ filteredCategory WRITE setFiltersFromCategory NOTIFY categoryChanged) + Q_PROPERTY(QString filteredCategoryName READ filteredCategoryName WRITE setFilteredCategoryName NOTIFY categoryChanged) + Q_PROPERTY(QString originFilter READ originFilter WRITE setOriginFilter) + Q_PROPERTY(AbstractResource::State stateFilter READ stateFilter WRITE setStateFilter NOTIFY stateFilterChanged) + Q_PROPERTY(bool filterMinimumState READ filterMinimumState WRITE setFilterMinimumState NOTIFY filterMinimumStateChanged) + Q_PROPERTY(QString mimeTypeFilter READ mimeTypeFilter WRITE setMimeTypeFilter) + Q_PROPERTY(AbstractResourcesBackend *backendFilter READ backendFilter WRITE setBackendFilter) + Q_PROPERTY(QString search READ lastSearch WRITE setSearch NOTIFY searchChanged) + Q_PROPERTY(QUrl resourcesUrl READ resourcesUrl WRITE setResourcesUrl NOTIFY resourcesUrlChanged) + Q_PROPERTY(QString extending READ extends WRITE setExtends) + Q_PROPERTY(bool allBackends READ allBackends WRITE setAllBackends) + Q_PROPERTY(QVariantList subcategories READ subcategories NOTIFY subcategoriesChanged) + Q_PROPERTY(bool isBusy READ isBusy NOTIFY busyChanged) + Q_PROPERTY(bool sortByRelevancy READ sortByRelevancy NOTIFY sortByRelevancyChanged) + Q_PROPERTY(ResourcesCount count READ count NOTIFY countChanged FINAL) +public: + explicit ResourcesProxyModel(QObject *parent = nullptr); + enum Roles { + NameRole = Qt::UserRole, + IconRole, + CommentRole, + StateRole, + RatingRole, + RatingPointsRole, + RatingCountRole, + SortableRatingRole, + InstalledRole, + ApplicationRole, + OriginRole, + DisplayOriginRole, + CanUpgrade, + PackageNameRole, + CategoryRole, + SectionRole, + MimeTypes, + SizeRole, + LongDescriptionRole, + SourceIconRole, + ReleaseDateRole, + }; + Q_ENUM(Roles) + + QHash roleNames() const override; + + void setSearch(const QString &text); + QString lastSearch() const; + void setOriginFilter(const QString &origin); + QString originFilter() const; + void setFiltersFromCategory(Category *category); + void setStateFilter(AbstractResource::State s); + AbstractResource::State stateFilter() const; + void setSortRole(Roles sortRole); + Roles sortRole() const + { + return m_sortRole; + } + void setSortOrder(Qt::SortOrder sortOrder); + Qt::SortOrder sortOrder() const + { + return m_sortOrder; + } + void setFilterMinimumState(bool filterMinimumState); + bool filterMinimumState() const; + + Category *filteredCategory() const; + QString filteredCategoryName() const; + void setFilteredCategoryName(const QString &cat); + + QString mimeTypeFilter() const; + void setMimeTypeFilter(const QString &mime); + + QString extends() const; + void setExtends(const QString &extends); + + QUrl resourcesUrl() const; + void setResourcesUrl(const QUrl &resourcesUrl); + + bool allBackends() const; + void setAllBackends(bool allBackends); + + AbstractResourcesBackend *backendFilter() const; + void setBackendFilter(AbstractResourcesBackend *filtered); + + QVariantList subcategories() const; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent = {}) const override; + + Q_SCRIPTABLE int indexOf(AbstractResource *res); + Q_SCRIPTABLE AbstractResource *resourceAt(int row) const; + + bool isBusy() const + { + return m_currentStream != nullptr; + } + + bool lessThan(AbstractResource *rl, AbstractResource *rr) const; + Q_SCRIPTABLE void invalidateFilter(); + void invalidateSorting(); + + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + bool sortByRelevancy() const; + + void classBegin() override + { + } + void componentComplete() override; + + ResourcesCount count() const; + +private Q_SLOTS: + void refreshBackend(AbstractResourcesBackend *backend, const QVector &properties); + void refreshResource(AbstractResource *resource, const QVector &properties); + void removeResource(AbstractResource *resource); + +private: + void sortedInsertion(const QVector &res); + QVariant roleToValue(AbstractResource *res, int role) const; + + QVector propertiesToRoles(const QVector &properties) const; + void addResources(const QVector &res); + void fetchSubcategories(); + void removeDuplicates(QVector &newResources); + bool isSorted(const QVector &resources); + + Roles m_sortRole; + Qt::SortOrder m_sortOrder; + + bool m_sortByRelevancy; + bool m_setup = false; + QString m_categoryName; + + AbstractResourcesBackend::Filters m_filters; + QVariantList m_subcategories; + + QVector m_displayedResources; + static const QHash s_roles; + ResultsStream *m_currentStream; + QTimer m_countTimer; + +Q_SIGNALS: + void busyChanged(bool isBusy); + void sortRoleChanged(int sortRole); + void sortOrderChanged(Qt::SortOrder order); + void categoryChanged(); + void stateFilterChanged(); + void searchChanged(const QString &search); + void subcategoriesChanged(const QVariantList &subcategories); + void resourcesUrlChanged(const QUrl &url); + void countChanged(); + void filterMinimumStateChanged(bool filterMinimumState); + void sortByRelevancyChanged(bool sortByRelevancy); +}; diff --git a/libdiscover/resources/ResourcesUpdatesModel.cpp b/libdiscover/resources/ResourcesUpdatesModel.cpp new file mode 100644 index 0000000..a895871 --- /dev/null +++ b/libdiscover/resources/ResourcesUpdatesModel.cpp @@ -0,0 +1,360 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "ResourcesUpdatesModel.h" +#include "AbstractBackendUpdater.h" +#include "AbstractResource.h" +#include "ResourcesModel.h" +#include "libdiscover_debug.h" +#include "utils.h" +#include +#include + +#include +#include +#include +#include +#include + +class UpdateTransaction : public Transaction +{ + Q_OBJECT +public: + UpdateTransaction(ResourcesUpdatesModel * /*parent*/, const QVector &updaters) + : Transaction(nullptr, nullptr, Transaction::InstallRole) + , m_allUpdaters(updaters) + { + bool cancelable = false; + for (auto updater : qAsConst(m_allUpdaters)) { + connect(updater, &AbstractBackendUpdater::progressingChanged, this, &UpdateTransaction::slotProgressingChanged); + connect(updater, &AbstractBackendUpdater::downloadSpeedChanged, this, &UpdateTransaction::slotDownloadSpeedChanged); + connect(updater, &AbstractBackendUpdater::progressChanged, this, &UpdateTransaction::slotUpdateProgress); + connect(updater, &AbstractBackendUpdater::proceedRequest, this, &UpdateTransaction::processProceedRequest); + connect(updater, &AbstractBackendUpdater::distroErrorMessage, this, &UpdateTransaction::distroErrorMessage); + connect(updater, &AbstractBackendUpdater::cancelableChanged, this, [this](bool) { + setCancellable(kContains(m_allUpdaters, [](AbstractBackendUpdater *updater) { + return updater->isCancelable() && updater->isProgressing(); + })); + }); + cancelable |= updater->isCancelable(); + } + setCancellable(cancelable); + } + + void processProceedRequest(const QString &title, const QString &message) + { + m_updatersWaitingForFeedback += qobject_cast(sender()); + Q_EMIT proceedRequest(title, message); + } + + void cancel() override + { + const QVector toCancel = m_updatersWaitingForFeedback.isEmpty() ? m_allUpdaters : m_updatersWaitingForFeedback; + + for (auto updater : toCancel) { + updater->cancel(); + } + } + + void proceed() override + { + m_updatersWaitingForFeedback.takeFirst()->proceed(); + } + + bool isProgressing() const + { + bool progressing = false; + for (AbstractBackendUpdater *upd : qAsConst(m_allUpdaters)) { + progressing |= upd->isProgressing(); + } + return progressing; + } + + void slotProgressingChanged() + { + if (status() > SetupStatus && status() < DoneStatus && !isProgressing()) { + setStatus(Transaction::DoneStatus); + Q_EMIT finished(); + deleteLater(); + } + } + + void slotUpdateProgress() + { + qreal total = 0; + for (AbstractBackendUpdater *updater : qAsConst(m_allUpdaters)) { + total += updater->progress(); + } + setProgress(total / m_allUpdaters.count()); + } + + void slotDownloadSpeedChanged() + { + quint64 total = 0; + for (AbstractBackendUpdater *updater : qAsConst(m_allUpdaters)) { + total += updater->downloadSpeed(); + } + setDownloadSpeed(total); + } + + QVariant icon() const override + { + return QStringLiteral("update-low"); + } + QString name() const override + { + return i18n("Updates"); + } + +Q_SIGNALS: + void finished(); + +private: + QVector m_updatersWaitingForFeedback; + const QVector m_allUpdaters; +}; + +ResourcesUpdatesModel::ResourcesUpdatesModel(QObject *parent) + : QStandardItemModel(parent) + , m_lastIsProgressing(false) + , m_transaction(nullptr) +{ + connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, this, &ResourcesUpdatesModel::init); + + init(); +} + +void ResourcesUpdatesModel::init() +{ + const QVector backends = ResourcesModel::global()->backends(); + m_lastIsProgressing = false; + for (AbstractResourcesBackend *b : backends) { + AbstractBackendUpdater *updater = b->backendUpdater(); + if (updater && !m_updaters.contains(updater)) { + connect(updater, &AbstractBackendUpdater::statusMessageChanged, this, &ResourcesUpdatesModel::message); + connect(updater, &AbstractBackendUpdater::statusDetailChanged, this, &ResourcesUpdatesModel::message); + connect(updater, &AbstractBackendUpdater::downloadSpeedChanged, this, &ResourcesUpdatesModel::downloadSpeedChanged); + connect(updater, &AbstractBackendUpdater::resourceProgressed, this, &ResourcesUpdatesModel::resourceProgressed); + connect(updater, &AbstractBackendUpdater::passiveMessage, this, &ResourcesUpdatesModel::passiveMessage); + connect(updater, &AbstractBackendUpdater::needsRebootChanged, this, &ResourcesUpdatesModel::needsRebootChanged); + connect(updater, &AbstractBackendUpdater::destroyed, this, &ResourcesUpdatesModel::updaterDestroyed); + connect(updater, &AbstractBackendUpdater::errorMessageChanged, this, &ResourcesUpdatesModel::errorMessagesChanged); + m_updaters += updater; + + m_lastIsProgressing |= updater->isProgressing(); + } + } + + // To enable from command line use: + // kwriteconfig5 --file discoverrc --group Software --key UseOfflineUpdates true + auto sharedConfig = KSharedConfig::openConfig(); + KConfigGroup group(sharedConfig, "Software"); + m_offlineUpdates = group.readEntry("UseOfflineUpdates", false); + + KConfigWatcher::Ptr watcher = KConfigWatcher::create(sharedConfig); + connect(watcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { + // Ensure it is for the right file + if (!names.contains("UseOfflineUpdates") || group.name() != "Software") { + return; + } + + if (m_offlineUpdates == group.readEntry("UseOfflineUpdates", false)) { + return; + } + Q_EMIT useUnattendedUpdatesChanged(); + }); + + auto tm = TransactionModel::global(); + const auto transactions = tm->transactions(); + for (auto t : transactions) { + auto updateTransaction = qobject_cast(t); + if (updateTransaction) { + setTransaction(updateTransaction); + } + } + + Q_EMIT errorMessagesChanged(); +} + +void ResourcesUpdatesModel::updaterDestroyed(QObject *obj) +{ + for (auto it = m_updaters.begin(); it != m_updaters.end();) { + if (*it == obj) + it = m_updaters.erase(it); + else + ++it; + } +} + +void ResourcesUpdatesModel::message(const QString &msg) +{ + if (msg.isEmpty()) + return; + + appendRow(new QStandardItem(msg)); +} + +void ResourcesUpdatesModel::prepare() +{ + if (isProgressing()) { + qCWarning(LIBDISCOVER_LOG) << "trying to set up a running instance"; + return; + } + + for (AbstractBackendUpdater *upd : qAsConst(m_updaters)) { + upd->setOfflineUpdates(m_offlineUpdates); + upd->prepare(); + } +} + +void ResourcesUpdatesModel::updateAll() +{ + if (!m_updaters.isEmpty()) { + delete m_transaction; + + const auto updaters = kFilter>(m_updaters, [](AbstractBackendUpdater *u) { + return u->hasUpdates(); + }); + if (updaters.isEmpty()) { + return; + } + + m_transaction = new UpdateTransaction(this, updaters); + m_transaction->setStatus(Transaction::SetupStatus); + setTransaction(m_transaction); + TransactionModel::global()->addTransaction(m_transaction); + for (AbstractBackendUpdater *upd : updaters) { + QMetaObject::invokeMethod(upd, &AbstractBackendUpdater::start, Qt::QueuedConnection); + } + + QMetaObject::invokeMethod( + this, + [this]() { + m_transaction->setStatus(Transaction::CommittingStatus); + m_transaction->slotProgressingChanged(); + }, + Qt::QueuedConnection); + } +} + +bool ResourcesUpdatesModel::isProgressing() const +{ + return m_transaction && m_transaction->status() < Transaction::DoneStatus; +} + +QList ResourcesUpdatesModel::toUpdate() const +{ + QList ret; + for (AbstractBackendUpdater *upd : qAsConst(m_updaters)) { + ret += upd->toUpdate(); + } + return ret; +} + +void ResourcesUpdatesModel::addResources(const QList &resources) +{ + QHash> sortedResources; + for (AbstractResource *res : resources) { + sortedResources[res->backend()] += res; + } + + for (auto it = sortedResources.constBegin(), itEnd = sortedResources.constEnd(); it != itEnd; ++it) { + it.key()->backendUpdater()->addResources(*it); + } +} + +void ResourcesUpdatesModel::removeResources(const QList &resources) +{ + QHash> sortedResources; + for (AbstractResource *res : resources) { + sortedResources[res->backend()] += res; + } + + for (auto it = sortedResources.constBegin(), itEnd = sortedResources.constEnd(); it != itEnd; ++it) { + it.key()->backendUpdater()->removeResources(*it); + } +} + +QDateTime ResourcesUpdatesModel::lastUpdate() const +{ + QDateTime ret; + for (AbstractBackendUpdater *upd : qAsConst(m_updaters)) { + QDateTime current = upd->lastUpdate(); + if (!ret.isValid() || (current.isValid() && current > ret)) { + ret = current; + } + } + return ret; +} + +double ResourcesUpdatesModel::updateSize() const +{ + double ret = 0.; + for (AbstractBackendUpdater *upd : m_updaters) { + ret += std::max(0., upd->updateSize()); + } + return ret; +} + +qint64 ResourcesUpdatesModel::secsToLastUpdate() const +{ + return lastUpdate().secsTo(QDateTime::currentDateTime()); +} + +void ResourcesUpdatesModel::setTransaction(UpdateTransaction *transaction) +{ + m_transaction = transaction; + connect(transaction, &UpdateTransaction::finished, this, &ResourcesUpdatesModel::finished); + connect(transaction, &UpdateTransaction::finished, this, &ResourcesUpdatesModel::progressingChanged); + + Q_EMIT progressingChanged(); +} + +Transaction *ResourcesUpdatesModel::transaction() const +{ + return m_transaction.data(); +} + +bool ResourcesUpdatesModel::needsReboot() const +{ + for (auto upd : m_updaters) { + if (upd->needsReboot()) + return true; + } + return false; +} + +bool ResourcesUpdatesModel::readyToReboot() const +{ + return kContains(m_updaters, [](AbstractBackendUpdater *updater) { + return !updater->needsReboot() || updater->isReadyToReboot(); + }); +} + +bool ResourcesUpdatesModel::useUnattendedUpdates() const +{ + return m_offlineUpdates; +} + +void ResourcesUpdatesModel::setOfflineUpdates(bool offline) +{ + m_offlineUpdates = offline; +} + +QStringList ResourcesUpdatesModel::errorMessages() const +{ + QStringList ret; + for (auto updater : m_updaters) { + const auto error = updater->errorMessage(); + if (!error.isEmpty()) { + ret << error; + } + } + ret.removeDuplicates(); + return ret; +} + +#include "ResourcesUpdatesModel.moc" diff --git a/libdiscover/resources/ResourcesUpdatesModel.h b/libdiscover/resources/ResourcesUpdatesModel.h new file mode 100644 index 0000000..f79ab14 --- /dev/null +++ b/libdiscover/resources/ResourcesUpdatesModel.h @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "discovercommon_export.h" +#include "resources/AbstractBackendUpdater.h" +#include +#include +#include + +class AbstractResource; +class UpdateTransaction; +class Transaction; + +class DISCOVERCOMMON_EXPORT ResourcesUpdatesModel : public QStandardItemModel +{ + Q_OBJECT + Q_PROPERTY(bool isProgressing READ isProgressing NOTIFY progressingChanged) + Q_PROPERTY(QDateTime lastUpdate READ lastUpdate NOTIFY progressingChanged) + Q_PROPERTY(qint64 secsToLastUpdate READ secsToLastUpdate NOTIFY progressingChanged) + Q_PROPERTY(Transaction *transaction READ transaction NOTIFY progressingChanged) + Q_PROPERTY(bool needsReboot READ needsReboot NOTIFY needsRebootChanged) + Q_PROPERTY(bool readyToReboot READ readyToReboot) + Q_PROPERTY(bool useUnattendedUpdates READ useUnattendedUpdates NOTIFY useUnattendedUpdatesChanged) + Q_PROPERTY(QStringList errorMessages READ errorMessages NOTIFY errorMessagesChanged) +public: + explicit ResourcesUpdatesModel(QObject *parent = nullptr); + + Q_SCRIPTABLE void prepare(); + + void setOfflineUpdates(bool offline); + bool isProgressing() const; + QList toUpdate() const; + QDateTime lastUpdate() const; + double updateSize() const; + void addResources(const QList &resources); + void removeResources(const QList &resources); + + qint64 secsToLastUpdate() const; + QVector updaters() const + { + return m_updaters; + } + Transaction *transaction() const; + bool needsReboot() const; + bool readyToReboot() const; + bool useUnattendedUpdates() const; + QStringList errorMessages() const; + +Q_SIGNALS: + void downloadSpeedChanged(); + void progressingChanged(); + void finished(); + void resourceProgressed(AbstractResource *resource, qreal progress, AbstractBackendUpdater::State state); + void passiveMessage(const QString &message); + void needsRebootChanged(); + void useUnattendedUpdatesChanged(); + void fetchingUpdatesProgressChanged(int percent); + void errorMessagesChanged(); + +public Q_SLOTS: + void updateAll(); + +private Q_SLOTS: + void updaterDestroyed(QObject *obj); + void message(const QString &msg); + +private: + void init(); + void setTransaction(UpdateTransaction *transaction); + + QVector m_updaters; + bool m_lastIsProgressing; + bool m_offlineUpdates = false; + QPointer m_transaction; + QStringList m_errorMessages; +}; diff --git a/libdiscover/resources/SourcesModel.cpp b/libdiscover/resources/SourcesModel.cpp new file mode 100644 index 0000000..1e89652 --- /dev/null +++ b/libdiscover/resources/SourcesModel.cpp @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "SourcesModel.h" +#include "libdiscover_debug.h" +#include "resources/AbstractResourcesBackend.h" +#include "resources/AbstractSourcesBackend.h" +#include +#include + +Q_GLOBAL_STATIC(SourcesModel, s_sources) + +const auto DisplayName = "DisplayName"; +const auto SourcesBackendId = "SourcesBackend"; + +SourcesModel::SourcesModel(QObject *parent) + : QConcatenateTablesProxyModel(parent) +{ +} + +SourcesModel::~SourcesModel() = default; + +SourcesModel *SourcesModel::global() +{ + return s_sources; +} + +QHash SourcesModel::roleNames() const +{ + QHash roles = QConcatenateTablesProxyModel::roleNames(); + roles.insert(AbstractSourcesBackend::IdRole, "sourceId"); + roles.insert(Qt::DisplayRole, "display"); + roles.insert(Qt::ToolTipRole, "toolTip"); + roles.insert(Qt::CheckStateRole, "checkState"); + roles.insert(SourceNameRole, "sourceName"); + roles.insert(SourcesBackend, "sourcesBackend"); + roles.insert(ResourcesBackend, "resourcesBackend"); + roles.insert(EnabledRole, "enabled"); + return roles; +} + +void SourcesModel::addSourcesBackend(AbstractSourcesBackend *sources) +{ + auto backend = qobject_cast(sources->parent()); + + auto m = sources->sources(); + m->setProperty(DisplayName, backend->displayName()); + m->setProperty(SourcesBackendId, QVariant::fromValue(sources)); + + // QConcatenateTablesProxyModel will consider empty models as column==0. Empty models + // will have 0 columns so it ends up showing nothing + if (m->rowCount() == 0) { + qWarning() << "adding empty sources model" << m; + auto action = new OneTimeAction( + [this, m] { + addSourceModel(m); + Q_EMIT sourcesChanged(); + }, + this); + connect(m, &QAbstractItemModel::rowsInserted, action, &OneTimeAction::trigger); + } else { + addSourceModel(m); + Q_EMIT sourcesChanged(); + } +} + +const QAbstractItemModel *SourcesModel::modelAt(const QModelIndex &index) const +{ + return mapToSource(index).model(); +} + +QVariant SourcesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + switch (role) { + case SourceNameRole: + return modelAt(index)->property(DisplayName); + case SourcesBackend: + return modelAt(index)->property(SourcesBackendId); + case EnabledRole: + return QVariant(flags(index) & Qt::ItemIsEnabled); + default: + return QConcatenateTablesProxyModel::data(index, role); + } +} + +AbstractSourcesBackend *SourcesModel::sourcesBackendByName(const QString &id) const +{ + for (int i = 0, c = rowCount(); i < c; ++i) { + const auto idx = index(i, 0); + if (idx.data(SourceNameRole) == id) { + return qobject_cast(idx.data(SourcesBackend).value()); + } + } + return nullptr; +} + +QVector SourcesModel::sources() const +{ + QVector sources; + for (int i = 0, c = rowCount(); i < c; ++i) { + const auto idx = index(i, 0); + auto source = qobject_cast(modelAt(idx)->property(SourcesBackendId).value()); + if (!sources.contains(source)) { + sources += source; + } + } + return sources; +} diff --git a/libdiscover/resources/SourcesModel.h b/libdiscover/resources/SourcesModel.h new file mode 100644 index 0000000..8abab3e --- /dev/null +++ b/libdiscover/resources/SourcesModel.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "AbstractSourcesBackend.h" +#include "discovercommon_export.h" +#include +#include +#include + +class DISCOVERCOMMON_EXPORT SourcesModel : public QConcatenateTablesProxyModel +{ + Q_OBJECT + Q_PROPERTY(QVector sources READ sources NOTIFY sourcesChanged) +public: + enum Roles { + SourceNameRole = AbstractSourcesBackend::LastRole, + SourcesBackend, + ResourcesBackend, + EnabledRole, + }; + Q_ENUM(Roles) + + explicit SourcesModel(QObject *parent = nullptr); + ~SourcesModel() override; + + static SourcesModel *global(); + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void addSourcesBackend(AbstractSourcesBackend *sources); + + Q_SCRIPTABLE AbstractSourcesBackend *sourcesBackendByName(const QString &name) const; + QVector sources() const; +Q_SIGNALS: + void sourcesChanged(); + void showingNow(); + +private: + const QAbstractItemModel *modelAt(const QModelIndex &idx) const; +}; diff --git a/libdiscover/resources/StandardBackendUpdater.cpp b/libdiscover/resources/StandardBackendUpdater.cpp new file mode 100644 index 0000000..37a60d1 --- /dev/null +++ b/libdiscover/resources/StandardBackendUpdater.cpp @@ -0,0 +1,290 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "ResourcesModel.h" +#include "libdiscover_debug.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include + +StandardBackendUpdater::StandardBackendUpdater(AbstractResourcesBackend *parent) + : AbstractBackendUpdater(parent) + , m_backend(parent) + , m_settingUp(false) + , m_progress(0) + , m_lastUpdate(QDateTime()) +{ + connect(m_backend, &AbstractResourcesBackend::fetchingChanged, this, &StandardBackendUpdater::refreshUpdateable); + connect(m_backend, &AbstractResourcesBackend::resourcesChanged, this, &StandardBackendUpdater::resourcesChanged); + connect(m_backend, &AbstractResourcesBackend::resourceRemoved, this, [this](AbstractResource *resource) { + if (m_upgradeable.remove(resource)) { + Q_EMIT updatesCountChanged(updatesCount()); + } + m_toUpgrade.remove(resource); + }); + connect(TransactionModel::global(), &TransactionModel::transactionRemoved, this, &StandardBackendUpdater::transactionRemoved); + connect(TransactionModel::global(), &TransactionModel::transactionAdded, this, &StandardBackendUpdater::transactionAdded); + + m_timer.setSingleShot(true); + m_timer.setInterval(10); + connect(&m_timer, &QTimer::timeout, this, &StandardBackendUpdater::refreshUpdateable); +} + +void StandardBackendUpdater::resourcesChanged(AbstractResource *res, const QVector &props) +{ + if (props.contains("state") && (res->state() == AbstractResource::Upgradeable || m_upgradeable.contains(res))) + m_timer.start(); +} + +bool StandardBackendUpdater::hasUpdates() const +{ + return !m_upgradeable.isEmpty(); +} + +void StandardBackendUpdater::start() +{ + m_settingUp = true; + Q_EMIT progressingChanged(true); + setProgress(0); + auto upgradeList = m_toUpgrade.values(); + std::sort(upgradeList.begin(), upgradeList.end(), [](const AbstractResource *a, const AbstractResource *b) { + return a->name() < b->name(); + }); + + const bool couldCancel = m_canCancel; + for (AbstractResource *res : qAsConst(upgradeList)) { + m_pendingResources += res; + auto t = m_backend->installApplication(res); + t->setVisible(false); + t->setProperty("updater", QVariant::fromValue(this)); + connect(t, &Transaction::downloadSpeedChanged, this, [this]() { + Q_EMIT downloadSpeedChanged(downloadSpeed()); + }); + connect(this, &StandardBackendUpdater::cancelTransaction, t, &Transaction::cancel); + TransactionModel::global()->addTransaction(t); + m_canCancel |= t->isCancellable(); + } + if (m_canCancel != couldCancel) { + Q_EMIT cancelableChanged(m_canCancel); + } + m_settingUp = false; + + if (m_pendingResources.isEmpty()) { + cleanup(); + } else { + setProgress(1); + } +} + +void StandardBackendUpdater::cancel() +{ + Q_EMIT cancelTransaction(); +} + +void StandardBackendUpdater::transactionAdded(Transaction *newTransaction) +{ + if (!m_pendingResources.contains(newTransaction->resource())) + return; + + connect(newTransaction, &Transaction::progressChanged, this, &StandardBackendUpdater::transactionProgressChanged); + connect(newTransaction, &Transaction::statusChanged, this, &StandardBackendUpdater::transactionProgressChanged); +} + +AbstractBackendUpdater::State toUpdateState(Transaction *t) +{ + switch (t->status()) { + case Transaction::SetupStatus: + case Transaction::QueuedStatus: + return AbstractBackendUpdater::None; + case Transaction::DownloadingStatus: + return AbstractBackendUpdater::Downloading; + case Transaction::CommittingStatus: + return AbstractBackendUpdater::Installing; + case Transaction::DoneStatus: + case Transaction::DoneWithErrorStatus: + case Transaction::CancelledStatus: + return AbstractBackendUpdater::Done; + } + Q_UNREACHABLE(); +} + +void StandardBackendUpdater::transactionProgressChanged() +{ + Transaction *t = qobject_cast(sender()); + Q_EMIT resourceProgressed(t->resource(), t->progress(), toUpdateState(t)); + + refreshProgress(); +} + +void StandardBackendUpdater::transactionRemoved(Transaction *t) +{ + const bool fromOurBackend = t->resource() && t->resource()->backend() == m_backend; + if (!fromOurBackend) { + return; + } + + const bool found = fromOurBackend && m_pendingResources.remove(t->resource()); + m_anyTransactionFailed |= t->status() != Transaction::DoneStatus; + + if (found && !m_settingUp) { + refreshProgress(); + if (m_pendingResources.isEmpty()) { + cleanup(); + if (needsReboot() && !m_anyTransactionFailed) { + enableReadyToReboot(); + } + } + } + refreshUpdateable(); +} + +void StandardBackendUpdater::refreshProgress() +{ + if (m_toUpgrade.isEmpty()) { + return; + } + + int allProgresses = (m_toUpgrade.size() - m_pendingResources.size()) * 100; + const auto allTransactions = transactions(); + for (auto t : allTransactions) { + allProgresses += t->progress(); + } + setProgress(allProgresses / m_toUpgrade.size()); +} + +void StandardBackendUpdater::refreshUpdateable() +{ + if (m_backend->isFetching() || !m_backend->isValid()) { + return; + } + + if (isProgressing()) { + m_timer.start(1000); + return; + } + + m_settingUp = true; + Q_EMIT progressingChanged(true); + AbstractResourcesBackend::Filters f; + f.state = AbstractResource::Upgradeable; + m_upgradeable.clear(); + auto r = m_backend->search(f); + connect(r, &ResultsStream::resourcesFound, this, [this](const QVector &resources) { + for (auto res : resources) + if (res->state() == AbstractResource::Upgradeable) + m_upgradeable.insert(res); + }); + connect(r, &ResultsStream::destroyed, this, [this]() { + m_settingUp = false; + Q_EMIT updatesCountChanged(updatesCount()); + Q_EMIT progressingChanged(false); + }); +} + +qreal StandardBackendUpdater::progress() const +{ + return m_progress; +} + +void StandardBackendUpdater::setProgress(qreal p) +{ + if (p > m_progress || p < 0) { + m_progress = p; + Q_EMIT progressChanged(p); + } +} + +void StandardBackendUpdater::prepare() +{ + m_lastUpdate = QDateTime::currentDateTime(); + m_toUpgrade = m_upgradeable; +} + +int StandardBackendUpdater::updatesCount() const +{ + return m_upgradeable.count(); +} + +void StandardBackendUpdater::addResources(const QList &apps) +{ + const QSet upgradeableApps = kToSet(apps); + Q_ASSERT(m_upgradeable.contains(upgradeableApps)); + m_toUpgrade += upgradeableApps; +} + +void StandardBackendUpdater::removeResources(const QList &apps) +{ + const QSet upgradeableApps = kToSet(apps); + Q_ASSERT(m_upgradeable.contains(upgradeableApps)); + Q_ASSERT(m_toUpgrade.contains(upgradeableApps)); + m_toUpgrade -= upgradeableApps; +} + +void StandardBackendUpdater::cleanup() +{ + m_lastUpdate = QDateTime::currentDateTime(); + m_toUpgrade.clear(); + + refreshUpdateable(); + Q_EMIT progressingChanged(false); +} + +QList StandardBackendUpdater::toUpdate() const +{ + return m_toUpgrade.values(); +} + +bool StandardBackendUpdater::isMarked(AbstractResource *res) const +{ + return m_toUpgrade.contains(res); +} + +QDateTime StandardBackendUpdater::lastUpdate() const +{ + return m_lastUpdate; +} + +bool StandardBackendUpdater::isCancelable() const +{ + return m_canCancel; +} + +bool StandardBackendUpdater::isProgressing() const +{ + return m_settingUp || !m_pendingResources.isEmpty(); +} + +double StandardBackendUpdater::updateSize() const +{ + double ret = 0.; + for (AbstractResource *res : m_toUpgrade) { + ret += res->size(); + } + return ret; +} + +QVector StandardBackendUpdater::transactions() const +{ + const auto trans = TransactionModel::global()->transactions(); + return kFilter>(trans, [this](Transaction *t) { + return t->property("updater").value() == this; + }); +} + +quint64 StandardBackendUpdater::downloadSpeed() const +{ + quint64 ret = 0; + const auto trans = transactions(); + for (Transaction *t : trans) { + ret += t->downloadSpeed(); + } + return ret; +} diff --git a/libdiscover/resources/StandardBackendUpdater.h b/libdiscover/resources/StandardBackendUpdater.h new file mode 100644 index 0000000..c79860a --- /dev/null +++ b/libdiscover/resources/StandardBackendUpdater.h @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "AbstractResourcesBackend.h" +#include "discovercommon_export.h" +#include +#include +#include +#include + +class AbstractResourcesBackend; + +class DISCOVERCOMMON_EXPORT StandardBackendUpdater : public AbstractBackendUpdater +{ + Q_OBJECT + Q_PROPERTY(int updatesCount READ updatesCount NOTIFY updatesCountChanged) +public: + explicit StandardBackendUpdater(AbstractResourcesBackend *parent = nullptr); + + bool hasUpdates() const override; + qreal progress() const override; + void start() override; + + QList toUpdate() const override; + void addResources(const QList &apps) override; + void removeResources(const QList &apps) override; + void prepare() override; + QDateTime lastUpdate() const override; + bool isCancelable() const override; + bool isProgressing() const override; + bool isMarked(AbstractResource *res) const override; + double updateSize() const override; + void setProgress(qreal p); + int updatesCount() const; + void cancel() override; + quint64 downloadSpeed() const override; + +Q_SIGNALS: + void cancelTransaction(); + void updatesCountChanged(int updatesCount); + +public Q_SLOTS: + void transactionRemoved(Transaction *t); + void cleanup(); + +private: + void resourcesChanged(AbstractResource *res, const QVector &props); + void refreshUpdateable(); + void transactionAdded(Transaction *newTransaction); + void transactionProgressChanged(); + void refreshProgress(); + QVector transactions() const; + + QSet m_toUpgrade; + QSet m_upgradeable; + AbstractResourcesBackend *const m_backend; + QSet m_pendingResources; + bool m_settingUp; + qreal m_progress; + QDateTime m_lastUpdate; + QTimer m_timer; + bool m_canCancel = false; + bool m_anyTransactionFailed = false; +}; diff --git a/libdiscover/resources/StoredResultsStream.cpp b/libdiscover/resources/StoredResultsStream.cpp new file mode 100644 index 0000000..c367b03 --- /dev/null +++ b/libdiscover/resources/StoredResultsStream.cpp @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "StoredResultsStream.h" + +StoredResultsStream::StoredResultsStream(const QSet &streams) + : AggregatedResultsStream(streams) +{ + connect(this, &ResultsStream::resourcesFound, this, [this](const QVector &resources) { + for (auto r : resources) + connect(r, &QObject::destroyed, this, [this, r]() { + m_resources.removeAll(r); + }); + m_resources += resources; + }); + + connect(this, &AggregatedResultsStream::finished, this, [this]() { + Q_EMIT finishedResources(m_resources); + }); +} + +QVector StoredResultsStream::resources() const +{ + return m_resources; +} diff --git a/libdiscover/resources/StoredResultsStream.h b/libdiscover/resources/StoredResultsStream.h new file mode 100644 index 0000000..c2a2d4d --- /dev/null +++ b/libdiscover/resources/StoredResultsStream.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "ResourcesModel.h" + +class DISCOVERCOMMON_EXPORT StoredResultsStream : public AggregatedResultsStream +{ + Q_OBJECT +public: + StoredResultsStream(const QSet &streams); + + QVector resources() const; + +Q_SIGNALS: + void finishedResources(const QVector &resources); + +private: + QVector m_resources; +}; diff --git a/libdiscover/resources/discoverabstractnotifier.notifyrc b/libdiscover/resources/discoverabstractnotifier.notifyrc new file mode 100644 index 0000000..81bc1ee --- /dev/null +++ b/libdiscover/resources/discoverabstractnotifier.notifyrc @@ -0,0 +1,583 @@ +[Global] +IconName=plasmadiscover +DesktopEntry=org.kde.discover +Comment=Discover +Comment[ar]=استكشف +Comment[az]=Discover +Comment[bg]=Discover +Comment[ca]=Discover +Comment[ca@valencia]=Discover +Comment[cs]=Discover +Comment[da]=Discover +Comment[de]=Discover +Comment[el]=Discover +Comment[en_GB]=Discover +Comment[es]=Discover +Comment[et]=Discover +Comment[eu]=Aurkitu +Comment[fi]=Discover +Comment[fr]=Discover +Comment[gl]=Discover. +Comment[he]=Discover +Comment[hi]=डिस्कवर +Comment[hsb]=Discover +Comment[hu]=Discover +Comment[ia]=Discoperi +Comment[id]=Discover +Comment[ie]=Discover +Comment[it]=Discover +Comment[ja]=Discover +Comment[ka]=Discover +Comment[ko]=소프트웨어 둘러보기 +Comment[lt]=Discover +Comment[ml]=കണ്ടെത്തുക +Comment[my]=ဒစ်(စ)ကာဗာ +Comment[nb]=Discover +Comment[nl]=Ontdekken +Comment[nn]=Discover +Comment[pa]=ਡਿਸਕਵਰ +Comment[pl]=Odkrywca +Comment[pt]=Discover +Comment[pt_BR]=Discover +Comment[ro]=Descoperă +Comment[ru]=Центр программ Discover +Comment[sk]=Discover +Comment[sl]=Programsko središče +Comment[sr]=Oткривач +Comment[sr@ijekavian]=Oткривач +Comment[sr@ijekavianlatin]=Otkrivač +Comment[sr@latin]=Otkrivač +Comment[sv]=Upptäck +Comment[ta]=டிஸ்கவர் +Comment[tg]=Кашфиёт +Comment[tr]=Keşfet +Comment[uk]=Discover +Comment[x-test]=xxDiscoverxx +Comment[zh_CN]=Discover +Comment[zh_TW]=Discover + +[Event/Update] +Name=Updates Are Available +Name[ar]=تتوفّر تحديثات +Name[az]=Yenilənmələr mövcuddur +Name[bg]=Налични са актуализации +Name[ca]=Hi ha actualitzacions disponibles +Name[ca@valencia]=Hi ha actualitzacions disponibles +Name[cs]=Jsou dostupné aktualizace +Name[da]=Opdateringer tilgængelige +Name[de]=Es sind Aktualisierungen verfügbar +Name[el]=Υπάρχουν διαθέσιμες ενημερώσεις +Name[en_GB]=Updates Are Available +Name[es]=Existen actualizaciones disponibles +Name[et]=Saadaval on uuendused +Name[eu]=Eguneraketak erabilgarri daude +Name[fi]=Päivityksiä on saatavilla +Name[fr]=Des mises à jour sont disponibles +Name[gl]=Hai actualizacións dispoñíbeis +Name[he]=יש עדכונים זמינים +Name[hi]=अद्यतन उपलब्ध हैं +Name[hsb]=Aktualizowanja steja k dispoziciji +Name[hu]=Frissítések érhetőek el +Name[ia]=Actualisationes es disponibile +Name[id]=Pembaruan Telah Tersedia +Name[ie]=Actualisamentes es disponibil +Name[it]=Sono disponibili aggiornamenti +Name[ja]=更新が利用可能です +Name[ka]=ხელმისაწვდომია განახლებები +Name[ko]=업데이트 사용 가능 +Name[lt]=Yra prieinami atnaujinimai +Name[ml]=അപ്ഡേറ്റുകൾ ലഭ്യമാണ് +Name[my]=အပ်ဒိတ်များရရှိနေပါပြီ +Name[nb]=Oppgraderinger er tilgjengelige +Name[nl]=Updates beschikbaar +Name[nn]=Oppdateringar er tilgjengelege +Name[pa]=ਅੱਪਡੇਟ ਮੌਜੂਦ ਹਨ +Name[pl]=Dostępne są uaktualnienia +Name[pt]=Estão Disponíveis Actualizações +Name[pt_BR]=Atualizações estão disponíveis +Name[ro]=Sunt disponibile actualizări +Name[ru]=Доступны обновления +Name[sk]=Sú dostupné aktualizácie +Name[sl]=Na voljo so posodobitve +Name[sr]=Доступне допуне +Name[sr@ijekavian]=Доступне допуне +Name[sr@ijekavianlatin]=Dostupne dopune +Name[sr@latin]=Dostupne dopune +Name[sv]=Uppdateringar är tillgängliga +Name[ta]=புதுப்பிப்புகள் உள்ளன +Name[tg]=Навсозиҳо дастрасанд +Name[tr]=Güncellemeler Kullanılabilir +Name[uk]=Доступні оновлення +Name[x-test]=xxUpdates Are Availablexx +Name[zh_CN]=系统有可以安装的更新 +Name[zh_TW]=有可用的更新 +Comment=Updates Available +Comment[ar]=تتوفّر تحديثات +Comment[az]=Yenilənmələr mövcuddur +Comment[bg]=Налични актуализации +Comment[ca]=Actualitzacions disponibles +Comment[ca@valencia]=Actualitzacions disponibles +Comment[cs]=Jsou dostupné aktualizace +Comment[da]=Opdateringer tilgængelige +Comment[de]=Aktualisierungen verfügbar +Comment[el]=Διαθέσιμες ενημερώσεις +Comment[en_GB]=Updates Available +Comment[es]=Actualizaciones disponibles +Comment[et]=Saadaval on uuendused +Comment[eu]=Eguneraketak erabilgarri +Comment[fi]=Päivityksiä saatavilla +Comment[fr]=Mises à jour disponibles +Comment[gl]=Hai actualizacións dispoñíbeis. +Comment[he]=עדכונים זמינים +Comment[hi]=अद्यतन उपलब्ध हैं +Comment[hsb]=Aktualizowanja steja k dispoziciji +Comment[hu]=Frissítések érhetőek el +Comment[ia]=Actualisationes disponibile +Comment[id]=Tersedia Pembaruan +Comment[ie]=Actualisamentes disponibil +Comment[it]=Aggiornamenti disponibili +Comment[ja]=更新が利用可能 +Comment[ka]=ხელმისაწვდომია განახლებები +Comment[ko]=업데이트 사용 가능 +Comment[lt]=Prieinami atnaujinimai +Comment[ml]=അപ്ഡേറ്റുകൾ ലഭ്യമാണ് +Comment[my]=အပ်ဒိတ်များရရှိနေပါပြီ +Comment[nb]=Oppgraderinger tilgjengelige +Comment[nl]=Updates beschikbaar +Comment[nn]=Oppdateringar tilgjengelege +Comment[pa]=ਅੱਪਡੇਟ ਮੌਜੂਦ +Comment[pl]=Dostępne uaktualnienia +Comment[pt]=Actualizações Disponíveis +Comment[pt_BR]=Atualizações disponíveis +Comment[ro]=Actualizări disponibile +Comment[ru]=Доступны обновления +Comment[sk]=Dostupné aktualizácie +Comment[sl]=Na voljo so posodobitve +Comment[sr]=Доступне су нове допуне +Comment[sr@ijekavian]=Доступне су нове допуне +Comment[sr@ijekavianlatin]=Dostupne su nove dopune +Comment[sr@latin]=Dostupne su nove dopune +Comment[sv]=Uppdateringar tillgängliga +Comment[ta]=புதுப்பிப்புகள் உள்ளன +Comment[tg]=Навсозиҳо дастрасанд +Comment[tr]=Güncellemeler Kullanılabilir +Comment[uk]=Доступні оновлення +Comment[x-test]=xxUpdates Availablexx +Comment[zh_CN]=系统有可以安装的更新 +Comment[zh_TW]=有可用的更新 +Action=Popup + +[Event/OfflineUpdateSuccessful] +Name=Successful Offline Update +Name[ar]=تحديث ناجح دون اتصال +Name[az]=Uğurlu oflayn yenilənmə +Name[bg]=Успешно офлайн актуализиране +Name[ca]=Actualització amb èxit sense connexió +Name[ca@valencia]=Actualització amb èxit sense connexió +Name[cs]=Úspěšná aktualizace offline +Name[de]=Erfolgreiche Offline-Aktualisierung +Name[en_GB]=Successful Offline Update +Name[es]=Actualización en diferido correcta +Name[eu]=Lerroz-kanpoko eguneratze arrakastatsua +Name[fi]=Käynnistyksen aikainen päivitys onnistui +Name[fr]=La mise à jour en mode déconnecté effectuée avec succès. +Name[gl]=Actualizouse sen Internet +Name[hi]=सफल ऑफ़लाइन अपडेट +Name[hsb]=Wuspěšne aktualizwanje offline +Name[hu]=Sikeres offline frissítés +Name[ia]=Actualisation con successo foras de linea +Name[id]=Pembaruan Luring Berhasil +Name[ie]=Ajornat actualisation successat +Name[it]=Aggiornamento non in linea avvenuto +Name[ja]=オフラインの更新に成功しました +Name[ka]=გათიშული განახლება წარმატებულია +Name[ko]=오프라인 업데이트 성공 +Name[lt]=Sėkmingas autonominis atnaujinimas +Name[ml]=വിജയകരമായ ഓഫ്‌ലൈൻ അപ്‌ഡേറ്റ് +Name[my]=အောင်မြင်သော အော့ဖ်လိုင်း အပ်ဒိတ်တင်မှု +Name[nb]=Frakoblet systemoppdatering er fullført +Name[nl]=Updaten na herstarten geslaagd +Name[nn]=Avlogga systemoppdatering er no fullført +Name[pa]=ਆਫਲਾਈਨ ਅੱਪਡੇਟ ਕਾਮਯਾਬ ਹੈ +Name[pl]=Pomyślnie uaktualniono bez dostępu do sieci +Name[pt]=Actualização Desligada com Sucesso +Name[pt_BR]=Atualização offline com sucesso +Name[ro]=Actualizare offline reușită +Name[ru]=Отложенное обновление выполнено успешно +Name[sk]=ÚspeÅ¡ná offline aktualizácia +Name[sl]=UspeÅ¡na spletna posodobitev +Name[sv]=Lyckat nedkopplad uppdatering +Name[ta]=வெற்றிகரமான அமர்வுக்கு-வெளியான-புதுப்பிப்பு +Name[tr]=Başarılı Çevrimdışı Güncelleme +Name[uk]=Успішне автономне оновлення +Name[x-test]=xxSuccessful Offline Updatexx +Name[zh_CN]=离线更新成功 +Name[zh_TW]=離線更新成功 +Comment=System updated successfully +Comment[ar]=حدث النظام بنجاح +Comment[az]=Sistem uğurla yeniləndi +Comment[bg]=Системата е актуализирана успешно +Comment[ca]=El sistema s'ha actualitzat amb èxit +Comment[ca@valencia]=El sistema s'ha actualitzat amb èxit +Comment[cs]=Aktualizace systému byla úspěšná +Comment[de]=Das System wurde erfolgreich aktualisiert +Comment[en_GB]=System updated successfully +Comment[es]=Sistema actualizado correctamente +Comment[eu]=Sistema eguneratze arrakastatsua +Comment[fi]=Järjestelmäpäivitys onnistui +Comment[fr]=Mise à jour du système effectuée avec succès. +Comment[gl]=Actualizouse o sistema. +Comment[hi]=तंत्र अद्यतन सफलतापूर्ण हो गया +Comment[hsb]=System wuspěšnje aktualizowany +Comment[hu]=Sikeres rendszerfrissítés +Comment[ia]=Systema actualisate con successo +Comment[id]=Sistem berhasil diperbarui +Comment[ie]=Sistema esset actualisat successosimen +Comment[it]=Aggiornamento di sistema avvenuto +Comment[ja]=システムが正常に更新されました +Comment[ka]=სისტემა წარმატებით განახლდა +Comment[ko]=시스템을 업데이트함 +Comment[lt]=Sistema sėkmingai atnaujinta +Comment[ml]=സിസ്റ്റം വിജയകരമായി അപ്‌ഡേറ്റുചെയ്‌തു +Comment[my]=စစ်စတမ်ကို အောင်မြင်စွာ အပ်ဒိတ်တင်ခဲ့သည် +Comment[nb]=Systemoppdatering fullført +Comment[nl]=Systeem bijwerken geslaagd +Comment[nn]=Systemoppdateringa er no fullført +Comment[pa]=ਸਿਸਟਮ ਕਾਮਯਾਬੀ ਨਾਲ ਅੱਪਡੇਟ ਕੀਤਾ +Comment[pl]=Pomyślnie uaktualniono system +Comment[pt]=O sistema foi actualizado com sucesso +Comment[pt_BR]=O sistema foi atualizado com sucesso +Comment[ro]=Sistem actualizat cu succes +Comment[ru]=Обновление системы выполнено успешно +Comment[sk]=Systém bol úspeÅ¡ne aktualizovaný +Comment[sl]=Sistem se je uspeÅ¡no posodobil +Comment[sv]=Systemet uppdaterat med lyckat resultat +Comment[ta]=கணினி வெற்றிகரமாக புதுப்பிக்கப்பட்டுள்ளது +Comment[tr]=Sistem başarıyla güncellendi +Comment[uk]=Систему успішно оновлено +Comment[x-test]=xxSystem updated successfullyxx +Comment[zh_CN]=系统更新成功 +Comment[zh_TW]=系統更新成功 +Urgency=Low +Action=Popup + +[Event/OfflineUpdateFailed] +Name=Failed Offline Update +Name[ar]=فشل تحديث دون اتصال +Name[az]=Uğursuz oflayn yenilənmə +Name[bg]=Неуспешно офлайн актуализиране +Name[ca]=Ha fallat en actualitzar sense connexió +Name[ca@valencia]=No s'ha pogut actualitzar sense connexió +Name[cs]=Selhala aktualizace offline +Name[de]=Offline-Aktualisierung fehlgeschlagen +Name[en_GB]=Failed Offline Update +Name[es]=Actualización en diferido fallida +Name[eu]=Lerroz-kanpoko eguneratzeak huts egin du +Name[fi]=Käynnistyksen aikainen päivitys epäonnistui +Name[fr]=Échec de la mise-à-jour en mode déconnecté +Name[gl]=A actualización sen Internet fallou +Name[hi]=असफल ऑफ़लाइन अपडेट +Name[hsb]=Zwrěšćene aktualizowanje offline +Name[hu]=Sikertelen offline frissítés +Name[ia]=Actualisation foras de linea fallite +Name[id]=Pembaruan Luring Gagal +Name[ie]=Ajornat actualisation ne successat +Name[it]=Aggiornamento non in linea non riuscito +Name[ja]=オフラインの更新に失敗しました +Name[ka]=გათიშული განახლება ვერ მოხერხდა +Name[ko]=오프라인 업데이트 실패 +Name[lt]=Nepavykęs autonominis atnaujinimas +Name[ml]=ഓഫ്‌ലൈൻ അപ്‌ഡേറ്റ് പരാജയപ്പെട്ടു +Name[my]=မအောင်မြင်သော အော့ဖ်လိုင်း အပ်ဒိတ်တင်မှု +Name[nb]=Frakoblet systemoppdatering mislyktes +Name[nl]=Updaten na herstarten niet geslaagd +Name[nn]=Avlogga systemoppdatering var mislukka +Name[pa]=ਆਫਲਾਈਨ ਅੱਪਡੇਟ ਲਈ ਅਸਫ਼ਲ ਹੈ +Name[pl]=Nie udało się uaktualnić bez dostępu do sieci +Name[pt]=Não Foi Possível a Actualização Desligada +Name[pt_BR]=Falha na atualização offline +Name[ro]=Actualizare offline eșuată +Name[ru]=Ошибка автономного обновления +Name[sk]=Offline aktualizácia zlyhala +Name[sl]=Posodobitev brez povezave ni uspela +Name[sv]=Nedkopplad uppdatering misslyckades +Name[ta]=தோல்வியடைந்த அமர்வுக்கு-வெளியான-புதுப்பிப்பு +Name[tr]=Başarısız Çevrimdışı Güncelleme +Name[uk]=Не вдалося виконати автономне оновлення +Name[x-test]=xxFailed Offline Updatexx +Name[zh_CN]=离线更新失败 +Name[zh_TW]=離線更新失敗 +Comment=System update failed +Comment[ar]=فشل تحديث النظام +Comment[az]=Sistem yenilənməsi baş tutmadı +Comment[bg]=Системната актуализация е неуспешна +Comment[ca]=Ha fallat en actualitzar el sistema +Comment[ca@valencia]=No s'ha pogut actualitzar el sistema +Comment[cs]=Aktualizace systému selhala +Comment[de]=Die Aktualisierung des Systems ist fehlgeschlagen +Comment[en_GB]=System update failed +Comment[es]=Actualización del sistema fallida +Comment[eu]=Sistema eguneratzea huts egin du +Comment[fi]=Järjestelmäpäivitys epäonnistui +Comment[fr]=Échec de la mise-à-jour du système en mode déconnecté +Comment[gl]=A actualización do sistema fallou. +Comment[hi]=तंत्र अद्यतन असफल +Comment[hsb]=Aktualizowanje systema so njeje poradźiło +Comment[hu]=Sikertelen rendszerfrissítés +Comment[ia]=Actualisation de systema falleva +Comment[id]=Pembaruan sistem gagal +Comment[ie]=Actualisation del sistema ne successat +Comment[it]=Aggiornamento di sistema non riuscito +Comment[ja]=システムの更新に失敗しました +Comment[ka]=სისტემის განახლების შეცდომა +Comment[ko]=시스템 업데이트 실패 +Comment[lt]=Nepavyko atnaujinti sistemos +Comment[ml]=സിസ്റ്റം അപ്ഡേറ്റ് പരാജയപ്പെട്ടു +Comment[my]=စစ်စတမ်အပ်ဒိတ်တင်မှုမအောင်မြင်ပါ +Comment[nb]=Systemoppdatering mislyktes +Comment[nl]=Systeem bijwerken mislukt +Comment[nn]=Systemoppdateringa var mislukka +Comment[pa]=ਸਿਸਟਮ ਅੱਪਡੇਟ ਅਸਫ਼ਲ ਹੈ +Comment[pl]=Nie udało się uaktualnić systemu +Comment[pt]=Não foi possível actualizar o sistema +Comment[pt_BR]=Falha na atualização do sistema +Comment[ro]=Actualizare sistem eșuată +Comment[ru]=Ошибка во время обновления системы +Comment[sk]=Aktualizácia systému zlyhala +Comment[sl]=Posodobitev ni uspela +Comment[sv]=Systemuppdatering misslyckades +Comment[ta]=கணினியின் புதுப்பிப்பு தோல்வியடைந்தது +Comment[tr]=Sistem güncellemesi başarısız oldu +Comment[uk]=Не вдалося оновити систему +Comment[x-test]=xxSystem update failedxx +Comment[zh_CN]=系统更新失败 +Comment[zh_TW]=系統更新失敗 +Urgency=High +Action=Popup + +[Event/OfflineUpdateRepairStarted] +Name=Repair Started +Name[ar]=بدء الإصلاح +Name[az]=Bərpa edilməyə başladı +Name[bg]=Стартирано е поправяне +Name[ca]=S'ha iniciat la reparació +Name[ca@valencia]=S'ha iniciat la reparació +Name[cs]=Oprava byla zahájena +Name[de]=Reparatur gestartet +Name[en_GB]=Repair Started +Name[es]=Reparación iniciada +Name[eu]=Konponketak hasi da +Name[fi]=Korjaus aloitettu +Name[fr]=Démarrage de la réparation +Name[gl]=Comezou a reparación +Name[ia]=Reparation initiava +Name[id]=Perbaikan Dimulai +Name[ie]=Reparation iniciat +Name[it]=Riparazione avviata +Name[ja]=修復が開始されました +Name[ka]=შეკეთება დაწყებულია +Name[ko]=복구 시작됨 +Name[nb]=Startet reparasjon +Name[nl]=Repareren is gestart +Name[nn]=Starta reparering +Name[pl]=Rozpoczęto naprawianie +Name[pt]=Reparação Iniciada +Name[pt_BR]=Reparo iniciado +Name[ro]=Repararea a început +Name[ru]=Восстановление запущено +Name[sk]=Oprava spustená +Name[sl]=Popravljanje začeto +Name[sv]=Reparation pÃ¥börjad +Name[ta]=பழுதுநீக்கம் துவக்கப்பட்டது +Name[tr]=Onarım Başlatıldı +Name[uk]=Розпочато відновлення +Name[x-test]=xxRepair Startedxx +Name[zh_CN]=修复已开始 +Name[zh_TW]=開始修復 +Action=Popup + +[Event/OfflineUpdateRepairSuccessful] +Name=Repaired Successfully +Name[ar]=نجح الإصلاح +Name[az]=Sistem uğurla bərpa olundu +Name[bg]=Поправянето е успешно +Name[ca]=La reparació ha estat correcta +Name[ca@valencia]=La reparació ha sigut correcta +Name[cs]=Úspěšně opraveno +Name[de]=Erfolgreich repariert +Name[en_GB]=Repaired Successfully +Name[es]=Reparado correctamente +Name[eu]=Konponketa arrakastatsua +Name[fi]=Korjaus onnistui +Name[fr]=La réparation a été réalisée avec succès. +Name[gl]=Completouse a reparación +Name[ia]=Systema reparate con successo +Name[id]=Berhasil Memperbaiki +Name[ie]=Reparation successat +Name[it]=Riparato correttamente +Name[ja]=修復に成功しました +Name[ka]=წარმატებით შეკეთდა +Name[ko]=복구에 성공함 +Name[nb]=Reparering fullført +Name[nl]=Met succes gerepareerd +Name[nn]=Repareringa er ferdig. +Name[pl]=Pomyślnie naprawiono +Name[pt]=Reparado com Sucesso +Name[pt_BR]=Reparado com sucesso +Name[ro]=Reparat cu succes +Name[ru]=Восстановление завершено успешно +Name[sk]=Opravené úspeÅ¡ne +Name[sl]=UspeÅ¡no popravljeno +Name[sv]=Reparation lyckades +Name[ta]=வெற்றிகரமாக சரிசெய்யப்பட்டுள்ளது +Name[tr]=Başarıyla Onarıldı +Name[uk]=Успішно відновлено +Name[x-test]=xxRepaired Successfullyxx +Name[zh_CN]=修复成功 +Name[zh_TW]=修復成功 +Urgency=High +Action=Popup + +[Event/OfflineUpdateRepairFailed] +Name=Repair Failed +Name[ar]=الإصلاح فشل +Name[az]=Bərpa edilə bilmədi +Name[bg]=Неуспешно поправяне +Name[ca]=Ha fallat en reparar +Name[ca@valencia]=No s'ha pogut reparar +Name[cs]=Oprava selhala +Name[de]=Reparatur fehlgeschlagen +Name[en_GB]=Repair Failed +Name[es]=Reparación fallida +Name[eu]=Konponketak huts egin du +Name[fi]=Korjaus epäonnistui +Name[fr]=Réparation impossible +Name[gl]=A reparación fallou +Name[hi]=मरम्मत में विफल +Name[hsb]=Reparowanje so njeje poradźiło +Name[hu]=Sikertelen javítás +Name[ia]=Reparation falleva +Name[id]=Perbaikan Gagal +Name[ie]=Reparation ne successat +Name[it]=Riparazione non riuscita +Name[ja]=修復に失敗しました +Name[ka]=შეკეთების შეცდომა +Name[ko]=복구 실패 +Name[lt]=Pataisymas nepavyko +Name[ml]=അറ്റകുറ്റപ്പണി പരാജയപ്പെട്ടു +Name[my]=ပြန်ပြင်ခြင်း မအောင်မြင်ပါ +Name[nb]=Reparasjon mislyktes +Name[nl]=Herstellen mislukt +Name[nn]=Mislukka reparering +Name[pa]=ਰਿਪੇਅਰ ਫੇਲ੍ਹ ਹੈ +Name[pl]=Nie udało się naprawić +Name[pt]=Reparação sem Sucesso +Name[pt_BR]=Falha no reparo +Name[ro]=Repararea a eșuat +Name[ru]=Ошибка восстановления +Name[sk]=Oprava zlyhala +Name[sl]=NeuspeÅ¡en popravek +Name[sv]=Reparation misslyckades +Name[ta]=சரிசெய்வது தோல்வியடைந்தது +Name[tr]=Onarım Başarısız +Name[uk]=Невдала спроба відновлення +Name[x-test]=xxRepair Failedxx +Name[zh_CN]=修复失败 +Name[zh_TW]=修復失敗 +Comment=Repair failure after offline update +Comment[ar]=فشل الإصلاح بعد التحديث دون اتصال +Comment[az]=Oflayn yenilənmədən sonra bərpa etmək mümkün olmadı +Comment[bg]=Поправяне на повреда след офлайн актуализация +Comment[ca]=Ha fallat en reparar després de l'actualització sense connexió +Comment[ca@valencia]=No s'ha pogut reparar després de l'actualització sense connexió +Comment[cs]=Opravit selhání po aktualizaci offline +Comment[de]=Reparatur nach Offline-Aktualisierung fehlgeschlagen +Comment[en_GB]=Repair failure after offline update +Comment[es]=Reparación fallida tras actualización en diferido +Comment[eu]=Konpondu lerroz-kanpoko eguneraketa ondoko hutsegitea +Comment[fi]=Käynnistyksen aikaisen päivityksen jälkeinen korjaus epäonnistui +Comment[fr]=Réparer la défaillance après une mise à jour en mode déconnecté +Comment[gl]=Fallo de reparación tras a actualización sen Internet. +Comment[hi]=ऑफ़लाइन अद्यतन के बाद मरम्मत विफल हुआ +Comment[hsb]=Reparowanje po aktualizowanju offline njeje so poradźiło +Comment[hu]=Javítási hiba az offline frissítés után +Comment[ia]=Reparation falleva depois actualisation +Comment[id]=Perbaiki kesalahan setelah pembaruan luring +Comment[ie]=Reparation ne successat pos un ajornat actualisation +Comment[it]=Riparazione non riuscita dopo l'aggiornamento non in linea +Comment[ja]=オフラインの更新後の修復に失敗しました +Comment[ka]=გათიშული განახლებების შემდეგ სისტემის აღდგენა +Comment[ko]=오프라인 업데이트 후 복구 실패 +Comment[lt]=Nepavyko pataisyti po autonominio atnaujinimo +Comment[ml]=ഓഫ്‌ലൈൻ അപ്‌ഡേറ്റിന് ശേഷം അറ്റകുറ്റപ്പണി പരാജയപ്പെട്ടു +Comment[my]=မအောင်မြင်သော အော့ဖ်လိုင်း အပ်ဒိတ်တင်မှုကို ပြန်ပြင်မည် +Comment[nb]=Reparasjon mislyktes etter frakoblet systemoppdatering +Comment[nl]=Repareren mislukt na herstart-update +Comment[nn]=Mislukka reparering etter forsøk pÃ¥ avlogga systemoppdatering +Comment[pa]=ਆਫਲਾਈਨ ਅੱਪਡੇਟ ਦੇ ਬਾਅਦ ਅਸਫ਼ਲ ਦੀ ਮੁਰੰਮਤ ਕਰੋ +Comment[pl]=Nie udało się naprawić po uaktualnieniu bez dostępu do sieci +Comment[pt]=Problemas na reparação após uma actualização desligada da rede +Comment[pt_BR]=Falha no reparo após a atualização offline +Comment[ro]=Reparare eșuată după actualizarea offline +Comment[ru]=Ошибка восстановления после установки отложенного обновления +Comment[sk]=Zlyhanie opravy po offline aktualizácii +Comment[sl]=NeuspeÅ¡en popravek po posodobitvi brez povezave +Comment[sv]=Reparera fel efter nedkopplad uppdatering +Comment[ta]=அமர்வுக்கு-வெளியான-புதுப்பிப்பின் தோல்வியை சரிசெய்வது +Comment[tr]=Çevrimdışı güncellemeden sonra onarım hatası +Comment[uk]=Не вдалося відновитися після автономного оновлення +Comment[x-test]=xxRepair failure after offline updatexx +Comment[zh_CN]=离线更新后修复失败 +Comment[zh_TW]=離線更新後修復失敗 +Urgency=High +Action=Popup + +[Event/UpdateResart] +Name=Restart is Required +Name[ar]=إعادة التّشغيل مطلوبة +Name[az]=Yenidən başlatmaq tələb olunur +Name[bg]=Необходимо е рестартиране +Name[ca]=Es requereix reiniciar +Name[ca@valencia]=Es requerix reiniciar +Name[cs]=Je vyžadován restart +Name[de]=Ein Neustart ist erforderlich +Name[en_GB]=Restart is Required +Name[es]=Se necesita un reinicio +Name[eu]=Berrabiatu beharra dago +Name[fi]=Uudellenkäynnistys vaaditaan +Name[fr]=Un redémarrage est nécessaire. +Name[gl]=Hai que reiniciar +Name[hi]=पुनरारंभ करना आवश्यक है। +Name[hsb]=Je trěbne kompjuter znowa starotwać +Name[hu]=Újraindítás szükséges +Name[ia]=Restartar es requirite +Name[id]=Pemulaian ulang Dibutuhkan +Name[ie]=Un restarta es besonat +Name[it]=Il riavvio è richiesto +Name[ja]=再起動が必要です +Name[ka]=საჭიროა რესტარტი +Name[ko]=다시 시작 필요 +Name[lt]=Reikalingas paleidimas iÅ¡ naujo +Name[ml]=പുനരാരംഭിക്കേണ്ടത് ആവശ്യമാണ് +Name[my]=စစ်စတမ်ပြန်စတင်ရန်လိုအပ်သည် +Name[nb]=Du mÃ¥ starte maskinen pÃ¥ nytt +Name[nl]=Een herstart is vereist +Name[nn]=Du mÃ¥ starta maskina pÃ¥ nytt +Name[pa]=ਮੁੜ-ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ +Name[pl]=Wymagane ponowne uruchomienie +Name[pt]=É Necessário Reiniciar +Name[pt_BR]=A reinicialização é necessária +Name[ro]=Repornire necesară +Name[ru]=Требуется перезагрузка +Name[sk]=Vyžaduje sa reÅ¡tart +Name[sl]=Zahtevan je ponovni zagon +Name[sv]=Omstart krävs +Name[ta]=மீள்துவக்க வேண்டும் +Name[tg]=Низом бояд аз нав оғоз карда шавад +Name[tr]=Yeniden Başlatma Gerekiyor +Name[uk]=Потрібне перезавантаження +Name[x-test]=xxRestart is Requiredxx +Name[zh_CN]=需要重启 +Name[zh_TW]=需要重新啟動電腦 +Urgency=High +Action=Popup diff --git a/libdiscover/tests/CMakeLists.txt b/libdiscover/tests/CMakeLists.txt new file mode 100644 index 0000000..682e2cc --- /dev/null +++ b/libdiscover/tests/CMakeLists.txt @@ -0,0 +1 @@ +ecm_add_test(CategoriesTest.cpp TEST_NAME CategoriesTest LINK_LIBRARIES Qt::Test Qt::Gui Discover::Common) diff --git a/libdiscover/tests/CategoriesTest.cpp b/libdiscover/tests/CategoriesTest.cpp new file mode 100644 index 0000000..7449e8f --- /dev/null +++ b/libdiscover/tests/CategoriesTest.cpp @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include +#include +#include +#include + +class CategoriesTest : public QObject +{ + Q_OBJECT +public: + CategoriesTest() + { + } + + QVector populateCategories() + { + const QVector categoryFiles = { + QFINDTESTDATA("../backends/PackageKitBackend/packagekit-backend-categories.xml"), + QFINDTESTDATA("../backends/FlatpakBackend/flatpak-backend-categories.xml"), + QFINDTESTDATA("../backends/DummyBackend/dummy-backend-categories.xml"), + }; + + QVector ret; + CategoriesReader reader; + for (const QString &name : categoryFiles) { + qDebug() << "doing..." << name; + const QVector cats = reader.loadCategoriesPath(name); + + if (ret.isEmpty()) { + ret = cats; + } else { + for (Category *c : cats) + Category::addSubcategory(ret, c); + } + } + std::sort(ret.begin(), ret.end(), Category::categoryLessThan); + return ret; + } + +private Q_SLOTS: + void testReadCategories() + { + auto categories = populateCategories(); + QVERIFY(!categories.isEmpty()); + + for (Category *c : categories) { + if (c->name() != "Dummy Category") + continue; + + auto filter = c->filter(); + QVERIFY(filter.type == CategoryFilter::CategoryNameFilter); + QVERIFY(std::get(filter.value) == "dummy"); + } + } +}; + +QTEST_MAIN(CategoriesTest) + +#include "CategoriesTest.moc" diff --git a/libdiscover/utils.h b/libdiscover/utils.h new file mode 100644 index 0000000..1ca86fb --- /dev/null +++ b/libdiscover/utils.h @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include +#include + +class OneTimeAction : public QObject +{ +public: + // the int argument is only so the compile knows to tell the constructors apart + OneTimeAction(int /*unnecessary*/, const std::function &func, QObject *parent) + : QObject(parent) + , m_function(func) + { + } + + OneTimeAction(const std::function &func, QObject *parent) + : QObject(parent) + , m_function([func] { + func(); + return true; + }) + { + } + + void trigger() + { + if (m_done) + return; + m_done = m_function(); + deleteLater(); + } + +private: + std::function m_function; + bool m_done = false; +}; + +template +static T kTransform(const Q &input, _UnaryOperation op) +{ + T ret; + ret.reserve(input.size()); + for (const auto &v : input) { + ret += op(v); + } + return ret; +} + +template +static T kTransform(const Q &input) +{ + T ret; + ret.reserve(input.size()); + for (const auto &v : input) { + ret += v; + } + return ret; +} + +template +static T kAppend(const Q &input, _UnaryOperation op) +{ + T ret; + ret.reserve(input.size()); + for (const auto &v : input) { + ret.append(op(v)); + } + return ret; +} + +template +static T kFilter(const Q &input, _UnaryOperation op) +{ + T ret; + for (const auto &v : input) { + if (op(v)) + ret += v; + } + return ret; +} + +template +static int kIndexOf(const Q &list, W func) +{ + int i = 0; + for (auto it = list.constBegin(), itEnd = list.constEnd(); it != itEnd; ++it) { + if (func(*it)) + return i; + ++i; + } + return -1; +} + +template +static bool kContains(const Q &list, W func) +{ + return std::any_of(list.begin(), list.end(), func); +} + +template +static bool kContainsValue(const Q &list, W value) +{ + return std::find(list.begin(), list.end(), value) != list.end(); +} + +template +static QVector kSetToVector(const QSet &set) +{ + QVector ret; + ret.reserve(set.size()); + for (auto &x : set) + ret.append(x); + return ret; +} +template +static QList kSetToList(const QSet &set) +{ + QList ret; + ret.reserve(set.size()); + for (auto &x : set) + ret.append(x); + return ret; +} +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +template +static QSet kToSet(const QVector &set) +{ + return QSet(set.begin(), set.end()); +} +#endif +template +static QSet kToSet(const QList &set) +{ + return QSet(set.begin(), set.end()); +} + +class ElapsedDebug : private QElapsedTimer +{ +public: + ElapsedDebug(const QString &name = QStringLiteral("")) + : m_name(name) + { + start(); + } + ~ElapsedDebug() + { + qDebug("elapsed %s: %lld!", m_name.toUtf8().constData(), elapsed()); + } + void step(const QString &step) + { + qDebug("step %s(%s): %lld!", m_name.toUtf8().constData(), qPrintable(step), elapsed()); + } + + QString m_name; +}; + +inline void swap(QJsonValueRef v1, QJsonValueRef v2) +{ + QJsonValue temp(v1); + v1 = QJsonValue(v2); + v2 = temp; +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8834649498ab5c7447441a86e42e45c17d45ef52 GIT binary patch literal 4453 zcmZWtXEYlC7Y%}h+N-rki>lgGsa3nwsM=Jt_Dbv>p=j)ywUgSVYE(6~iP0iLjoQ17 zQN*Y{KHs13&v)Os@1FP0dFR~oe!cVJj107CsMx6h0051yj>fZVMf{f_itCJ3wja0_ zpzmYd=b&qZfgF(6H5jI2=?ee|+5ML!T(M1V*UejgnihU0P$$1Ydml$YU|^t_tCzd) zD|?uu7}Uo(Z~GBD06@R2tD*KhXnLz4^eyMOe{V#2XoypQSyi~p9iRo>3oYckM2-CJ z%y`|HA_?KFEh>W`(;|5!fQ6H3Mwg*ocS&9n-2EvVxDW4V%;@TrM+Iv)9APQOu0_!NTL>GDDHZywI_wvark zSu=T|e0uyTMQ4H$4fdX0SvM72ejx%;3~YV<(V zRS>7@-8W2D6ZBFJxr1C^ucnkt{S&VYgh4ME1y0AM_$#`8orx#DNI_bWj@{aXn(lP$ z4RZ0mW99UHk`5ZJ)+k8( z(tYoVQBnu79eL&!Ft_e44aV@#Qi_VDp`j)Rdrm_@1Cx}Ka%brC%mh=l%!{b~r70g} z)vLah6b5-7GslSIypZ)uZK(&7H;gDhh6wvcip)^`k`P_aA3cg&8}Qwo**SZ0=+`%b zVl8`D*;S^5LPEa)v#Y+hLu;AD4;5-}7#V-7-AVVY8Z^HQbP^~^VLc^ofp%q;*nYg_ zBK;Hf#p{6L#()G`%PZHNKz0xz)kY)m;1vMtvry9RB;CQem`(ZY9=}L4Lbz6Fv(GS? zu$N7rqI$T!|g#&yB{USb;)ffkmTp5WI$o|d=-<1tBrK#u~ zD2{mb24UH~p(ta}QF@cS_(IQQmvX~%wBF&IJ8`>Hm=QJ=_tj;P5t6m2cS@Q;%dGJ) zSxdMpT5io&813h%XhN{2k;rjtJ6sr&omze4_HuJNLC$Ta&Y5KGP`j-Mw$mBA)}6eP z<+E2-1-LoAU(|N_y3f67$YK+J2E9BO^|cRmF!+P}b$-@!xotQ90lqjsHd3z90a#ay zvQr9wij6kD;?OY5C)*8RhN=%=^~PG|N`E5MU{3R_55vxyX7b=53x6DrZS-imV7ZE5 zgyj)R7R)Nr8X;4Xr#aN7J3wpk)iL)ajgMsk)w&N_;hXXI+UvfpOC!<;;B&g7hi;zQ zY+NzWE{1_0v@4DNvSSu*^;n=_;$7(auFl@vboi@dDOjiplj;N8x!<16;Wdfe@?}{8 zQCVVPCK3v$JB*&5qLI0HRuI5T?}3-HDaU)pf}LXymGRC9_@^JTs@?85J50`GqvGL{ zy+wv})q=faTv}fm``7C7X^iKi#$M%=j{I@txTOJ+Q`b()x@|q?5 z@q-yjAMV5s0Hj?}D8p{Mi@QxLEJ`Pp&26}%cvH ziJIr#%=bzqC)C(splq_UL=cAks34%lL^vq|@ zFO0msiWl7KG+&O3xDUw_*zN?{2Jh{RWdf>DL%1JNI?^u0(5_IG)D@-t?nuhC4F%MhHsI+R9Q1}&4`r1|Ug&C_Y-yxr)*+ytj{qJ07KOZstEWAi z^PM%fm6Wq}>O@#!dNJYRf-BSrz~{tfI{$jXv=SUm5Q>eN|Ato502kRmJ7%9FRW|n) z=7fAi=7{8(R#a~-bi$dWihE#4p|9BnH86AJ>m+L9uxlmPNrs*KyI)K*5~s(n{O(0M@<_j^BAH+!-9rG^lfN_a!Np znpTECrcUInXsR|*qtMX=^@{Mk<$X&N2mQt^O^s3E2W1I<)vsj4pI*2|u$=E*y-;^W4yqoW)arZud>ys3s=# z`zlT`QDv%GVPv$D8FMr!*HF!Ek1kUACkO18K}44aq<*S%0d>pXQ`;8~cfE8%?CICK z+>(F6N5&ir?D`twDo6g~+H-irgT=xEwPUJ|qsbytlnQnwBcM)QbtMgQSAJfe@Gdsm zgX)=jdSUYy@x^)D&W706?Mm2{61h5{iv?qYm?kGQf4@`*=^^rw-N0wNaUS8kKvl*Z zYaEF$Tp#)EI!X_*ucoADvufrGrjH7HulgKm!65UF%JK2QvUhp47t@f{GQIwa6>75J z_u?XmQ4cYC{J@%RK3BC*)9PJxSM9+q2srp7>B)^;ZZWR5%BPs^1CY0Q*&rb0Z~xx+ zWTFkO6kGKC^(%_37`!Ky1t$v}`VA{tEs@W3=^UZDqmwCzB#Sf9_W_LXwou%Eeq&r! zIf+zA)Oyf)2o~22DZ>m^D_L^SJ+$F9T`C=!VrKLNI}7Y*0(>6=KYB2IQo7&85B2Ex zACjb{+v+DC%!{PwV04P8pK^(4xT(=Uk-yEk;C8%eA`rx1x&z>(P1zc%|FRnP#1rs# z0Ko>eV3bJ$`9-!{tc)*yb9F(JUO4 zD0==hD!F1H%fBFKXzbNx%4z+hhq&+J3hk4Ixp=-*7wlWKt4v2Eb4&dKB++1!B28Ik zhwD}q(NSx7(8Y9%&M6bs!lFM7KRI+Tj29Xpw>!zMd1h6ps9OmAIPEI^nqVLhuW2ct^5g;ND$ z32XHYcDGJgG|PVO#za)1v~8dsYqjbfW8W!YS7L930^4x)tT%DwEwUL{W`(A;^OxFs z0k{8NO&;hH_1o2G&eh1lg&u&yspLnFr2=tB@i!SL@(m~`Xwhno zG7)qGO=ZI)j>+Fylq(qWE+5=4443A0UYyP30~gK|toF5jsI%@*oU;;ZdZBmLe_DDT zWWW54w1N)fFQAq_Pff2Qs+LA+tnz^MB=gHfmk2LTP~F~w91K4+L)=q+OZYbT;Vyss z;K>fliBuU!i48{v(jIJ70DtPI9s=0Q0lR)(3!_>cwvqaK;PP_1otUmfl8aAb^T{+( zs_r_`th3M+6c&J`Goah>u~gm7M$!N^ua^B(@b9^2f)!Cfjb;1jvu}_0?VBsLx5KJ= zP1bGlNRrCJ*kd5vi2i6JklgnNhj`k*Y;i ze_3~q|3&oBifcl}XiVX=mi&@3j0M?;_wGe%63qXmQ&~R=ah&y*BO8ach3`)S;)&(NC8PUU?b+p6pjhK+N9;to0bN3s*VoLV=^IkuM z`|>8F62%I5>Eq0`NzHk`8w!&dnjO135b)}x%nC{q$*Zvs2^iK1F(}uAPKEH`C^B7C z-&Z=x3~-nAmsp~~T!ZdnOistKBP}YMhJEftS_9E*!+gBNq-Z(1mDMJ1mpr5qom3t` zY&EA#-KROusm(Ak6$CXKB-|DT8{C&i)m5&_OIpEgE+oZ&0Rz5*g!~`woT&B{RbZQn zS6+RX!M}%>_0K0nS0(0Y5bwX`1DJgsC`q-8Gu2yesTGp`b5kXyk~h_YMIhN4w1#&5 zdD$YQ$}jcLjT>Qxza=ycjO@^6=uR@zj(Qma?H_T8(guMw^06i1Ik9i=u?Fj7rfv+2 z-a;pCrm_-0#|Y(?^PDh=gjMoRGm9)RqSJt0H_knJJZD7m7l6K@dWa7)ksRs?x8?Z> z)cfSr2kDZu%e`0v)$_blcpFQkw&)FV?*zjQIXrvY^%0M|_`ype6rX!OIo5tGeDo;g zoZJ|cdnqc(D7GWgF8CqrrFNeapppLhM6i1Je#yNC54bpsY-D_R7Qm5zxtdT{%Rl_` py%Pem8+YZ_Jks_5C`#SE>LI>8RE>DQe*F>w=xQ2he0mIt`VWKmmAn7| literal 0 HcmV?d00001 diff --git a/notifier/BackendNotifierFactory.cpp b/notifier/BackendNotifierFactory.cpp new file mode 100644 index 0000000..aa65952 --- /dev/null +++ b/notifier/BackendNotifierFactory.cpp @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "BackendNotifierFactory.h" +#include +#include +#include +#include +#include + +BackendNotifierFactory::BackendNotifierFactory() = default; + +QList BackendNotifierFactory::allBackends() const +{ + QList ret; + + const auto libraryPaths = QCoreApplication::instance()->libraryPaths(); + for (const QString &path : libraryPaths) { + QDir dir(path + QStringLiteral("/discover-notifier/")); + const auto files = dir.entryList(QDir::Files); + for (const QString &file : files) { + QString fullPath = dir.absoluteFilePath(file); + QPluginLoader loader(fullPath); + loader.load(); + ret += qobject_cast(loader.instance()); + if (ret.last() == nullptr) { + qWarning() << "couldn't load" << fullPath << "because" << loader.errorString(); + ret.removeLast(); + } + } + } + if (ret.isEmpty()) + qWarning() << "couldn't find any notifier backend" << QCoreApplication::instance()->libraryPaths(); + + return ret; +} diff --git a/notifier/BackendNotifierFactory.h b/notifier/BackendNotifierFactory.h new file mode 100644 index 0000000..f0bf1ab --- /dev/null +++ b/notifier/BackendNotifierFactory.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include + +class BackendNotifierModule; + +class BackendNotifierFactory +{ +public: + BackendNotifierFactory(); + + QList allBackends() const; +}; diff --git a/notifier/CMakeLists.txt b/notifier/CMakeLists.txt new file mode 100644 index 0000000..e5ef2db --- /dev/null +++ b/notifier/CMakeLists.txt @@ -0,0 +1,33 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma-discover-notifier\") + +kconfig_add_kcfg_files(notifier_SRCS ../kcm/updatessettings.kcfgc GENERATE_MOC) + +add_executable(DiscoverNotifier + BackendNotifierFactory.cpp + DiscoverNotifier.cpp + NotifierItem.cpp + UnattendedUpdates.cpp + main.cpp + + ${notifier_SRCS} +) + +target_link_libraries(DiscoverNotifier + KF5::Notifications + KF5::I18n + KF5::KIOGui + KF5::Crash + KF5::DBusAddons + KF5::ConfigGui + KF5::IdleTime + + Discover::Notifiers +) + +set_target_properties(DiscoverNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover) +install(TARGETS DiscoverNotifier DESTINATION ${KDE_INSTALL_LIBEXECDIR}) + +set(DesktopExec "${KDE_INSTALL_FULL_LIBEXECDIR}/DiscoverNotifier") +configure_file(org.kde.discover.notifier.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.notifier.desktop) +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.notifier.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.discover.notifier.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) diff --git a/notifier/DiscoverNotifier.cpp b/notifier/DiscoverNotifier.cpp new file mode 100644 index 0000000..56aeb09 --- /dev/null +++ b/notifier/DiscoverNotifier.cpp @@ -0,0 +1,361 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "DiscoverNotifier.h" +#include "BackendNotifierFactory.h" +#include "UnattendedUpdates.h" +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#endif +#include + +#include +#include + +#include "../libdiscover/utils.h" +#include "updatessettings.h" +#include + +using namespace std::chrono_literals; + +DiscoverNotifier::DiscoverNotifier(QObject *parent) + : QObject(parent) +{ + m_settings = new UpdatesSettings(this); + m_settingsWatcher = KConfigWatcher::create(m_settings->sharedConfig()); +#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) + QNetworkInformation::instance()->load(QNetworkInformation::Feature::Reachability | QNetworkInformation::Feature::TransportMedium); + connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &DiscoverNotifier::stateChanged); + connect(QNetworkInformation::instance(), &QNetworkInformation::transportMediumChanged, this, &DiscoverNotifier::stateChanged); +#endif + + refreshUnattended(); + connect(m_settingsWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { + if (group.config()->name() == m_settings->config()->name() && group.name() == "Global" && names.contains("UseUnattendedUpdates")) { + refreshUnattended(); + } + }); + + m_backends = BackendNotifierFactory().allBackends(); + for (BackendNotifierModule *module : qAsConst(m_backends)) { + connect(module, &BackendNotifierModule::foundUpdates, this, &DiscoverNotifier::updateStatusNotifier); + connect(module, &BackendNotifierModule::needsRebootChanged, this, [this]() { + // If we are using offline updates, there is no need to badger the user to + // reboot since it is safe to continue using the system in its current state + if (!m_needsReboot && !m_settings->useUnattendedUpdates()) { + m_needsReboot = true; + showRebootNotification(); + Q_EMIT stateChanged(); + Q_EMIT needsRebootChanged(true); + } + }); + + connect(module, &BackendNotifierModule::foundUpgradeAction, this, &DiscoverNotifier::foundUpgradeAction); + } + connect(&m_timer, &QTimer::timeout, this, &DiscoverNotifier::showUpdatesNotification); + m_timer.setSingleShot(true); + m_timer.setInterval(1s); + updateStatusNotifier(); + + // Only fetch updates after the system is comfortably booted + QTimer::singleShot(0s, this, &DiscoverNotifier::recheckSystemUpdateNeeded); +} + +DiscoverNotifier::~DiscoverNotifier() = default; + +void DiscoverNotifier::showDiscover(const QString &xdgActivationToken) +{ + auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.discover"))); + job->setStartupId(xdgActivationToken.toUtf8()); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + job->start(); + + if (m_updatesAvailableNotification) { + m_updatesAvailableNotification->close(); + } +} + +void DiscoverNotifier::showDiscoverUpdates(const QString &xdgActivationToken) +{ + auto *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-discover"), {QStringLiteral("--mode"), QStringLiteral("update")}); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + job->setDesktopName(QStringLiteral("org.kde.discover")); + job->setStartupId(xdgActivationToken.toUtf8()); + job->start(); + + if (m_updatesAvailableNotification) { + m_updatesAvailableNotification->close(); + } +} + +bool DiscoverNotifier::notifyAboutUpdates() const +{ + if (state() != NormalUpdates && state() != SecurityUpdates) { + // it's not very helpful to notify that everything is in order + return false; + } + + if (m_settings->requiredNotificationInterval() < 0) { + return false; + } + + // To configure to a random value, execute: + // kwriteconfig5 --file PlasmaDiscoverUpdates --group Global --key RequiredNotificationInterval 3600 + const QDateTime earliestNextNotificationTime = m_settings->lastNotificationTime().addSecs(m_settings->requiredNotificationInterval()); + if (earliestNextNotificationTime.isValid() && earliestNextNotificationTime > QDateTime::currentDateTimeUtc()) { + return false; + } + + m_settings->setLastNotificationTime(QDateTime::currentDateTimeUtc()); + m_settings->save(); + + auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.discover"), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.DBus.Peer"), + QStringLiteral("Ping")); + auto call = QDBusConnection::sessionBus().asyncCall(method); + call.waitForFinished(); + if (call.isValid()) { + return false; + } + return true; +} + +void DiscoverNotifier::showUpdatesNotification() +{ + if (m_updatesAvailableNotification) { + m_updatesAvailableNotification->close(); + } + + if (!notifyAboutUpdates()) { + return; + } + + m_updatesAvailableNotification = KNotification::event(QStringLiteral("Update"), + message(), + {}, + iconName(), + nullptr, + KNotification::CloseOnTimeout, + QStringLiteral("discoverabstractnotifier")); + m_updatesAvailableNotification->setHint(QStringLiteral("resident"), true); + const QString name = i18n("View Updates"); + m_updatesAvailableNotification->setDefaultAction(name); + m_updatesAvailableNotification->setActions({name}); + connect(m_updatesAvailableNotification, QOverload::of(&KNotification::activated), this, [this] { + showDiscoverUpdates(m_updatesAvailableNotification->xdgActivationToken()); + }); +} + +void DiscoverNotifier::updateStatusNotifier() +{ + const bool hasSecurityUpdates = kContains(m_backends, [](BackendNotifierModule *module) { + return module->hasSecurityUpdates(); + }); + const bool hasUpdates = hasSecurityUpdates || kContains(m_backends, [](BackendNotifierModule *module) { + return module->hasUpdates(); + }); + + if (m_hasUpdates == hasUpdates && m_hasSecurityUpdates == hasSecurityUpdates) + return; + + m_hasSecurityUpdates = hasSecurityUpdates; + m_hasUpdates = hasUpdates; + + if (state() != NoUpdates) { + m_timer.start(); + } + + Q_EMIT stateChanged(); +} + +// we only want to do unattended updates when on an ethernet or wlan network +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +static bool isConnectionAdequate(const QNetworkConfiguration &network) +{ + return (network.bearerType() == QNetworkConfiguration::BearerEthernet || network.bearerType() == QNetworkConfiguration::BearerWLAN); +} +#elif QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) +static bool isConnectionAdequate() +{ + const auto info = QNetworkInformation::instance(); + if (info->supports(QNetworkInformation::Feature::Metered)) { + return !info->isMetered(); + } else { + const auto transport = info->transportMedium(); + return transport == QNetworkInformation::TransportMedium::Ethernet || transport == QNetworkInformation::TransportMedium::WiFi; + } +} +#endif + +void DiscoverNotifier::refreshUnattended() +{ + m_settings->read(); + + if (!notifyAboutUpdates()) { + return; + } + + const auto enabled = m_settings->useUnattendedUpdates() +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + && m_manager->isOnline() && isConnectionAdequate(m_manager->defaultConfiguration()); +#elif QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) + && QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online && isConnectionAdequate(); +#endif + if (bool(m_unattended) == enabled) + return; + + if (enabled) { + m_unattended = new UnattendedUpdates(this); + } else { + delete m_unattended; + m_unattended = nullptr; + } +} + +DiscoverNotifier::State DiscoverNotifier::state() const +{ + if (m_needsReboot) + return RebootRequired; + else if (m_isBusy) + return Busy; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + else if (m_manager && !m_manager->isOnline()) +#else + else if (QNetworkInformation::instance()->reachability() != QNetworkInformation::Reachability::Online) +#endif + return Offline; + else if (m_hasSecurityUpdates) + return SecurityUpdates; + else if (m_hasUpdates) + return NormalUpdates; + else + return NoUpdates; +} + +QString DiscoverNotifier::iconName() const +{ + switch (state()) { + case SecurityUpdates: + return QStringLiteral("update-high"); + case NormalUpdates: + return QStringLiteral("update-low"); + case NoUpdates: + return QStringLiteral("update-none"); + case RebootRequired: + return QStringLiteral("system-reboot"); + case Offline: + return QStringLiteral("offline"); + case Busy: + return QStringLiteral("state-download"); + } + return QString(); +} + +QString DiscoverNotifier::message() const +{ + switch (state()) { + case SecurityUpdates: + return i18n("Security updates available"); + case NormalUpdates: + return i18n("Updates available"); + case NoUpdates: + return i18n("System up to date"); + case RebootRequired: + return i18n("Computer needs to restart"); + case Offline: + return i18n("Offline"); + case Busy: + return i18n("Applying unattended updates…"); + } + return QString(); +} + +void DiscoverNotifier::recheckSystemUpdateNeeded() +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if (!m_manager) { + m_manager = new QNetworkConfigurationManager(this); + connect(m_manager, &QNetworkConfigurationManager::onlineStateChanged, this, &DiscoverNotifier::stateChanged); + if (!m_manager->isOnline()) { + Q_EMIT stateChanged(); + } + } +#endif + + for (BackendNotifierModule *module : qAsConst(m_backends)) + module->recheckSystemUpdateNeeded(); + + refreshUnattended(); +} + +QStringList DiscoverNotifier::loadedModules() const +{ + QStringList ret; + for (BackendNotifierModule *module : m_backends) + ret += QString::fromLatin1(module->metaObject()->className()); + return ret; +} + +void DiscoverNotifier::showRebootNotification() +{ + KNotification *notification = KNotification::event(QStringLiteral("UpdateRestart"), + i18n("Restart is required"), + i18n("The system needs to be restarted for the updates to take effect."), + QStringLiteral("system-software-update"), + nullptr, + KNotification::Persistent | KNotification::DefaultEvent, + QStringLiteral("discoverabstractnotifier")); + + notification->setActions(QStringList{i18nc("@action:button", "Restart")}); + notification->setDefaultAction(notification->actions().constFirst()); + connect(notification, &KNotification::action1Activated, this, &DiscoverNotifier::reboot); + + notification->sendEvent(); +} + +void DiscoverNotifier::reboot() +{ + auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"), + QStringLiteral("/LogoutPrompt"), + QStringLiteral("org.kde.LogoutPrompt"), + QStringLiteral("promptReboot")); + QDBusConnection::sessionBus().asyncCall(method); +} + +void DiscoverNotifier::foundUpgradeAction(UpgradeAction *action) +{ + KNotification *notification = new KNotification(QStringLiteral("distupgrade-notification"), KNotification::Persistent | KNotification::DefaultEvent); + notification->setIconName(QStringLiteral("system-software-update")); + notification->setActions(QStringList{i18nc("@action:button", "Upgrade")}); + notification->setTitle(i18n("Upgrade available")); + notification->setText(i18n("New version: %1", action->description())); + + connect(notification, &KNotification::action1Activated, this, [action]() { + action->trigger(); + }); + + notification->sendEvent(); +} + +void DiscoverNotifier::setBusy(bool isBusy) +{ + if (isBusy == m_isBusy) + return; + + m_isBusy = isBusy; + Q_EMIT busyChanged(isBusy); + Q_EMIT stateChanged(); +} diff --git a/notifier/DiscoverNotifier.h b/notifier/DiscoverNotifier.h new file mode 100644 index 0000000..d51f362 --- /dev/null +++ b/notifier/DiscoverNotifier.h @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +class KNotification; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +class QNetworkConfigurationManager; +#endif +class UnattendedUpdates; +class UpdatesSettings; + +class DiscoverNotifier : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList modules READ loadedModules CONSTANT) + Q_PROPERTY(QString iconName READ iconName NOTIFY stateChanged) + Q_PROPERTY(QString message READ message NOTIFY stateChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(bool needsReboot READ needsReboot NOTIFY needsRebootChanged) + Q_PROPERTY(bool isBusy READ isBusy NOTIFY busyChanged) +public: + enum State { + NoUpdates, + NormalUpdates, + SecurityUpdates, + Busy, + RebootRequired, + Offline, + }; + Q_ENUM(State) + + explicit DiscoverNotifier(QObject *parent = nullptr); + ~DiscoverNotifier() override; + + State state() const; + QString iconName() const; + QString message() const; + bool hasUpdates() const + { + return m_hasUpdates; + } + bool hasSecurityUpdates() const + { + return m_hasSecurityUpdates; + } + + QStringList loadedModules() const; + bool needsReboot() const + { + return m_needsReboot; + } + + void setBusy(bool isBusy); + bool isBusy() const + { + return m_isBusy; + } + UpdatesSettings *settings() const + { + return m_settings; + } + +public Q_SLOTS: + void recheckSystemUpdateNeeded(); + void showDiscover(const QString &xdgActivationToken); + void showDiscoverUpdates(const QString &xdgActivationToken); + void showUpdatesNotification(); + void reboot(); + void foundUpgradeAction(UpgradeAction *action); + +Q_SIGNALS: + void stateChanged(); + bool needsRebootChanged(bool needsReboot); + void newUpgradeAction(UpgradeAction *action); + bool busyChanged(bool isBusy); + +private: + void showRebootNotification(); + void updateStatusNotifier(); + void refreshUnattended(); + + bool notifyAboutUpdates() const; + + QList m_backends; + QTimer m_timer; + bool m_hasSecurityUpdates = false; + bool m_hasUpdates = false; + bool m_needsReboot = false; + bool m_isBusy = false; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QNetworkConfigurationManager *m_manager = nullptr; +#endif + QPointer m_updatesAvailableNotification; + UnattendedUpdates *m_unattended = nullptr; + KConfigWatcher::Ptr m_settingsWatcher; + UpdatesSettings *m_settings; +}; diff --git a/notifier/NotifierItem.cpp b/notifier/NotifierItem.cpp new file mode 100644 index 0000000..73039e0 --- /dev/null +++ b/notifier/NotifierItem.cpp @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "NotifierItem.h" +#include +#include +#include + +KStatusNotifierItem::ItemStatus sniStatus(DiscoverNotifier::State state) +{ + switch (state) { + case DiscoverNotifier::Offline: + case DiscoverNotifier::NoUpdates: + return KStatusNotifierItem::Passive; + case DiscoverNotifier::Busy: + case DiscoverNotifier::NormalUpdates: + case DiscoverNotifier::SecurityUpdates: + case DiscoverNotifier::RebootRequired: + return KStatusNotifierItem::Active; + } + return KStatusNotifierItem::Active; +} + +NotifierItem::NotifierItem() +{ +} + +void NotifierItem::setupNotifierItem() +{ + Q_ASSERT(!m_item); + m_item = new KStatusNotifierItem(QStringLiteral("org.kde.DiscoverNotifier"), this); + m_item->setTitle(i18n("Updates")); + m_item->setToolTipTitle(i18n("Updates")); + + connect(&m_notifier, &DiscoverNotifier::stateChanged, this, &NotifierItem::refresh); + + connect(m_item, &KStatusNotifierItem::activateRequested, &m_notifier, [this]() { + if (m_notifier.needsReboot()) { + m_notifier.reboot(); + } else { + m_notifier.showDiscoverUpdates(m_item->providedToken()); + } + }); + + QMenu *menu = new QMenu; + connect(m_item, &QObject::destroyed, menu, &QObject::deleteLater); + auto discoverAction = menu->addAction(QIcon::fromTheme(QStringLiteral("plasmadiscover")), i18n("Open Discover…")); + connect(discoverAction, &QAction::triggered, &m_notifier, [this] { + m_notifier.showDiscover(m_item->providedToken()); + }); + + auto updatesAction = menu->addAction(QIcon::fromTheme(QStringLiteral("system-software-update")), i18n("See Updates…")); + connect(updatesAction, &QAction::triggered, &m_notifier, [this] { + m_notifier.showDiscoverUpdates(m_item->providedToken()); + }); + + auto refreshAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Refresh…")); + connect(refreshAction, &QAction::triggered, &m_notifier, &DiscoverNotifier::recheckSystemUpdateNeeded); + + auto f = [this]() { + m_item->setTitle(i18n("Restart to apply installed updates")); + m_item->setToolTipTitle(i18n("Click to restart the device")); + m_item->setIconByName(QStringLiteral("view-refresh")); + }; + if (m_notifier.needsReboot()) + f(); + else + connect(&m_notifier, &DiscoverNotifier::needsRebootChanged, menu, f); + + connect(&m_notifier, &DiscoverNotifier::newUpgradeAction, menu, [menu](UpgradeAction *a) { + QAction *action = new QAction(a->description(), menu); + connect(action, &QAction::triggered, a, &UpgradeAction::trigger); + menu->addAction(action); + }); + m_item->setContextMenu(menu); + refresh(); +} + +void NotifierItem::refresh() +{ + Q_ASSERT(m_item); + m_item->setStatus(sniStatus(m_notifier.state())); + m_item->setIconByName(m_notifier.iconName()); + m_item->setToolTipSubTitle(m_notifier.message()); +} + +void NotifierItem::setVisible(bool visible) +{ + if (visible == m_visible) + return; + m_visible = visible; + + if (m_visible) + setupNotifierItem(); + else + delete m_item; +} diff --git a/notifier/NotifierItem.h b/notifier/NotifierItem.h new file mode 100644 index 0000000..622ec8c --- /dev/null +++ b/notifier/NotifierItem.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "DiscoverNotifier.h" +#include +#include + +class NotifierItem : public QObject +{ + Q_OBJECT +public: + NotifierItem(); + + void setupNotifierItem(); + void refresh(); + + bool isVisible() const + { + return m_visible; + } + void setVisible(bool visible); + +private: + bool m_visible = false; + DiscoverNotifier m_notifier; + QPointer m_item; +}; diff --git a/notifier/UnattendedUpdates.cpp b/notifier/UnattendedUpdates.cpp new file mode 100644 index 0000000..59c63ea --- /dev/null +++ b/notifier/UnattendedUpdates.cpp @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "UnattendedUpdates.h" +#include "DiscoverNotifier.h" +#include "updatessettings.h" +#include +#include +#include +#include +#include + +UnattendedUpdates::UnattendedUpdates(DiscoverNotifier *parent) + : QObject(parent) +{ + connect(parent, &DiscoverNotifier::stateChanged, this, &UnattendedUpdates::checkNewState); + connect(KIdleTime::instance(), QOverload::of(&KIdleTime::timeoutReached), this, &UnattendedUpdates::triggerUpdate); + + checkNewState(); +} + +UnattendedUpdates::~UnattendedUpdates() noexcept +{ + if (m_idleTimeoutId.has_value()) { + KIdleTime::instance()->removeIdleTimeout(m_idleTimeoutId.value()); + } +} + +void UnattendedUpdates::checkNewState() +{ + using namespace std::chrono_literals; + DiscoverNotifier *notifier = static_cast(parent()); + + // Only allow offline updating every 3h. It should keep some peace to our users, especially on rolling distros + const QDateTime updateableTime = notifier->settings()->lastUnattendedTrigger().addSecs(std::chrono::seconds(3h).count()); + if (updateableTime > QDateTime::currentDateTimeUtc()) { + qDebug() << "skipping update, already updated on" << notifier->settings()->lastUnattendedTrigger().toString(); + return; + } + + const bool doTrigger = notifier->hasUpdates() && !notifier->isBusy(); + if (doTrigger && !m_idleTimeoutId.has_value()) { + qDebug() << "waiting for an idle moment"; + // If the system is untouched for 15 minutes, trigger the unattened update + m_idleTimeoutId = KIdleTime::instance()->addIdleTimeout(int(std::chrono::milliseconds(15min).count())); + } else if (!doTrigger && m_idleTimeoutId.has_value()) { + qDebug() << "nothing to do"; + KIdleTime::instance()->removeIdleTimeout(m_idleTimeoutId.value()); + m_idleTimeoutId.reset(); + } +} + +void UnattendedUpdates::triggerUpdate(int timeoutId) +{ + if (!m_idleTimeoutId.has_value() || timeoutId != m_idleTimeoutId.value()) { + return; + } + + KIdleTime::instance()->removeIdleTimeout(m_idleTimeoutId.value()); + m_idleTimeoutId.reset(); + + DiscoverNotifier *notifier = static_cast(parent()); + if (!notifier->hasUpdates() || notifier->isBusy()) { + return; + } + + auto process = new QProcess(this); + connect(process, &QProcess::errorOccurred, this, [](QProcess::ProcessError error) { + qWarning() << "Error running plasma-discover-update" << error; + }); + connect(process, QOverload::of(&QProcess::finished), this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { + qDebug() << "Finished running plasma-discover-update" << exitCode << exitStatus; + DiscoverNotifier *notifier = static_cast(parent()); + process->deleteLater(); + notifier->settings()->setLastUnattendedTrigger(QDateTime::currentDateTimeUtc()); + notifier->settings()->save(); + notifier->setBusy(false); + }); + + notifier->setBusy(true); + process->start(QStringLiteral("plasma-discover-update"), {QStringLiteral("--offline")}); + qInfo() << "started unattended update" << QDateTime::currentDateTimeUtc(); +} diff --git a/notifier/UnattendedUpdates.h b/notifier/UnattendedUpdates.h new file mode 100644 index 0000000..683b41d --- /dev/null +++ b/notifier/UnattendedUpdates.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +#include + +class DiscoverNotifier; + +class UnattendedUpdates : public QObject +{ + Q_OBJECT +public: + UnattendedUpdates(DiscoverNotifier *parent); + ~UnattendedUpdates() override; + +private: + void checkNewState(); + void triggerUpdate(int timeoutId); + + std::optional m_idleTimeoutId; +}; diff --git a/notifier/main.cpp b/notifier/main.cpp new file mode 100644 index 0000000..aa967b1 --- /dev/null +++ b/notifier/main.cpp @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "../DiscoverVersion.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "NotifierItem.h" + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + app.setOrganizationDomain(QStringLiteral("kde.org")); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); +#endif + KCrash::setFlags(KCrash::AutoRestart); + + NotifierItem notifier; + bool hide = false; + KDBusService::StartupOptions startup = {}; + { + KAboutData about(QStringLiteral("discover.notifier"), + i18n("Discover Notifier"), + version, + i18n("System update status notifier"), + KAboutLicense::GPL, + i18n("© 2010-2022 Plasma Development Team")); + about.addAuthor(QStringLiteral("Aleix Pol Gonzalez"), {}, QStringLiteral("aleixpol@kde.org")); + about.setProductName("discover/discover"); + about.setProgramLogo(app.windowIcon()); + about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); + + KAboutData::setApplicationData(about); + + QCommandLineParser parser; + QCommandLineOption replaceOption({QStringLiteral("replace")}, i18n("Replace an existing instance")); + parser.addOption(replaceOption); + QCommandLineOption hideOption({QStringLiteral("hide")}, i18n("Do not show the notifier"), i18n("hidden"), QStringLiteral("false")); + parser.addOption(hideOption); + about.setupCommandLine(&parser); + parser.process(app); + about.processCommandLine(&parser); + + if (parser.isSet(replaceOption)) { + startup |= KDBusService::Replace; + } + + const auto config = KSharedConfig::openConfig(); + KConfigGroup group(config, "Behavior"); + + if (parser.isSet(hideOption)) { + hide = parser.value(hideOption) == QLatin1String("true"); + group.writeEntry("Hide", hide); + config->sync(); + } else { + hide = group.readEntry("Hide", false); + } + } + + KDBusService service(KDBusService::Unique | startup); + notifier.setVisible(!hide); + + return app.exec(); +} diff --git a/notifier/org.kde.discover.notifier.desktop.cmake b/notifier/org.kde.discover.notifier.desktop.cmake new file mode 100644 index 0000000..3a67446 --- /dev/null +++ b/notifier/org.kde.discover.notifier.desktop.cmake @@ -0,0 +1,60 @@ +[Desktop Entry] +Name=Discover +Name[ar]=المستكشف +Name[az]=Discover +Name[bg]=Discover +Name[ca]=Discover +Name[ca@valencia]=Discover +Name[cs]=Discover +Name[da]=Discover +Name[de]=Discover +Name[el]=Discover +Name[en_GB]=Discover +Name[es]=Discover +Name[et]=Discover +Name[eu]=Discover +Name[fi]=Discover +Name[fr]=Discover +Name[gl]=Descubrir +Name[he]=Discover +Name[hi]=डिस्कवर +Name[hu]=Discover +Name[ia]=Discover (Discoperi) +Name[id]=Discover +Name[ie]=Discover +Name[it]=Discover +Name[ja]=Discover +Name[ka]=Discover +Name[ko]=Discover +Name[lt]=Discover +Name[ml]=കണ്ടെത്തുക +Name[my]=ဒစ်(စ)ကာဗာ +Name[nb]=Discover +Name[nl]=Ontdekken +Name[nn]=Discover +Name[pa]=ਡਿਸਕਵਰ +Name[pl]=Odkrywca +Name[pt]=Discover +Name[pt_BR]=Discover +Name[ro]=Discover +Name[ru]=Discover +Name[sk]=Discover +Name[sl]=Discover +Name[sr]=Oткривач +Name[sr@ijekavian]=Oткривач +Name[sr@ijekavianlatin]=Otkrivač +Name[sr@latin]=Otkrivač +Name[sv]=Upptäck +Name[ta]=டிஸ்கவர் +Name[tg]=Кашфиёт +Name[tr]=Keşfet +Name[uk]=Discover +Name[x-test]=xxDiscoverxx +Name[zh_CN]=Discover 软件管理中心 +Name[zh_TW]=Discover +Exec=@DesktopExec@ +Icon=system-software-update +Type=Application +NoDisplay=true +X-KDE-autostart-phase=1 +OnlyShowIn=KDE diff --git a/po/ar/kcm_updates.po b/po/ar/kcm_updates.po new file mode 100644 index 0000000..f124ee5 --- /dev/null +++ b/po/ar/kcm_updates.po @@ -0,0 +1,106 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the discover package. +# +# Zayed Al-Saidi , 2021, 2022. +msgid "" +msgstr "" +"Project-Id-Version: discover\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2022-11-07 00:45+0000\n" +"PO-Revision-Date: 2022-10-12 19:10+0400\n" +"Last-Translator: Zayed Al-Saidi \n" +"Language-Team: ar\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 21.12.3\n" + +#: kcm/package/contents/ui/main.qml:32 +#, kde-format +msgid "Update software:" +msgstr "حدث التطبيقات:" + +#: kcm/package/contents/ui/main.qml:33 +#, kde-format +msgid "Manually" +msgstr "يدويا" + +#: kcm/package/contents/ui/main.qml:43 +#, kde-format +msgid "Automatically" +msgstr "آليا" + +#: kcm/package/contents/ui/main.qml:50 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"Software updates will be downloaded automatically when they become " +"available. Updates for applications will be installed immediately, while " +"system updates will be installed the next time the computer is restarted." +msgstr "" +"ستنزل تحديثات البرامج آليا عندما تصبح متوفرة. وستثبت تحديثات التطبيقات على " +"الفور ، بينما ستثبت تحديثات النظام في المرة التالية التي يعاد فيها تشغيل " +"الحاسوب." + +#: kcm/package/contents/ui/main.qml:61 +#, kde-format +msgctxt "@title:group" +msgid "Update frequency:" +msgstr "تكرار التحديث:" + +#: kcm/package/contents/ui/main.qml:61 +#, kde-format +msgctxt "@title:group" +msgid "Notification frequency:" +msgstr "تكرار التنبيه:" + +#: kcm/package/contents/ui/main.qml:64 +#, kde-format +msgctxt "@item:inlistbox" +msgid "Daily" +msgstr "يوميًّا" + +#: kcm/package/contents/ui/main.qml:65 +#, kde-format +msgctxt "@item:inlistbox" +msgid "Weekly" +msgstr "أسبوعيًّا" + +#: kcm/package/contents/ui/main.qml:66 +#, kde-format +msgctxt "@item:inlistbox" +msgid "Monthly" +msgstr "شهريًّا" + +#: kcm/package/contents/ui/main.qml:67 +#, kde-format +msgctxt "@item:inlistbox" +msgid "Never" +msgstr "البتة" + +#: kcm/package/contents/ui/main.qml:111 +#, kde-format +msgid "Use offline updates:" +msgstr "حدّث أثناء الإقلاع:" + +#: kcm/package/contents/ui/main.qml:124 +#, kde-format +msgid "" +"Offline updates maximize system stability by applying changes while " +"restarting the system. Using this update mode is strongly recommended." +msgstr "" +"تعمل التحديثات أثناء الإقلاع على زيادة استقرار النظام من خلال تطبيق " +"التغييرات أثناء إعادة تشغيل النظام. يوصى بشدة باستخدام وضع التحديث هذا." + +#: kcm/updates.cpp:31 +#, kde-format +msgid "Software Update" +msgstr "تحديث البرمجيّات" + +#: kcm/updates.cpp:33 +#, kde-format +msgid "Configure software update settings" +msgstr "اضبط إعدادات تحديث البرنامج" diff --git a/po/ar/libdiscover.po b/po/ar/libdiscover.po new file mode 100644 index 0000000..debcb59 --- /dev/null +++ b/po/ar/libdiscover.po @@ -0,0 +1,3723 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Safa Alfulaij , 2013, 2015, 2015, 2018. +# Zayed Al-Saidi , 2021, 2022, 2023. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2023-12-13 02:29+0000\n" +"PO-Revision-Date: 2023-02-28 21:04+0400\n" +"Last-Translator: Zayed Al-Saidi \n" +"Language-Team: ar\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 21.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "زايد السعيدي" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "zayed.alsaidi@gmail.com" + +#: libdiscover/appstream/AppStreamUtils.cpp:115 +#, kde-format +msgid "Proprietary" +msgstr "محتكرة" + +#: libdiscover/appstream/AppStreamUtils.cpp:117 +#, kde-format +msgid "Public Domain" +msgstr "ملكية عامة" + +#: libdiscover/appstream/AppStreamUtils.cpp:123 +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:326 +#: libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp:296 +#: libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp:359 +#: libdiscover/backends/RpmOstreeBackend/RpmOstreeResource.cpp:385 +#: libdiscover/UpdateModel/UpdateModel.cpp:117 +#: libdiscover/UpdateModel/UpdateModel.cpp:323 +#, kde-format +msgid "Unknown" +msgstr "مجهول" + +#: libdiscover/appstream/AppStreamUtils.cpp:172 +#: libdiscover/resources/AbstractResource.cpp:278 +#, kde-format +msgid "%1, released on %2" +msgstr "‏‏%1، صدر في %2" + +#: libdiscover/appstream/AppStreamUtils.cpp:214 +#, kde-format +msgctxt "" +"Open Age Ratings Service (https://hughsie.github.io/oars) description of " +"content suitable for everyone" +msgid "All Audiences" +msgstr "مناسب لجميع الأعمار" + +#: libdiscover/appstream/AppStreamUtils.cpp:217 +#, kde-format +msgctxt "" +"Open Age Ratings Service (https://hughsie.github.io/oars) description of " +"content with relatively benign themes only unsuitable for very young " +"children, such as minor cartoon violence or mild profanity" +msgid "Mild Content" +msgstr "غير مناسب للأطفال" + +#: libdiscover/appstream/AppStreamUtils.cpp:220 +#, kde-format +msgctxt "" +"Open Age Ratings Service (https://hughsie.github.io/oars) description of " +"content with some intense themes, such as somewhat realistic violence, " +"references to sexuality, or adult profanity" +msgid "Moderate Content" +msgstr "محتوى خادش" + +#: libdiscover/appstream/AppStreamUtils.cpp:223 +#, kde-format +msgctxt "" +"Open Age Ratings Service (https://hughsie.github.io/oars) description of " +"mature content that could be quite objectionable or unsuitable for young " +"audiences, such as realistic graphic violence, extreme profanity or nudity, " +"or glorification of drug use" +msgid "Intense Content" +msgstr "محتوى خطير" + +#: libdiscover/appstream/OdrsReviewsBackend.cpp:174 +#, kde-format +msgid "Error while fetching reviews: %1" +msgstr "خطأ أثناء جلب المراجعات: ‏%1" + +#: libdiscover/appstream/OdrsReviewsBackend.cpp:226 +#, kde-format +msgid "Error while submitting usefulness: %1" +msgstr "خطأ أثناء إرسال مدى الاستفادة:‏ %1" + +#: libdiscover/appstream/OdrsReviewsBackend.cpp:283 +#, kde-format +msgid "Error while submitting review: %1" +msgstr "خطأ أثناء إرسال المراجعة: %1" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:4 +#, kde-format +msgctxt "Category" +msgid "Dummy Category" +msgstr "فئة دمية" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:11 +#, kde-format +msgctxt "Category" +msgid "dummy" +msgstr "دمية" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:18 +#, kde-format +msgctxt "Category" +msgid "dummy addons" +msgstr "إضافات دمية" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:26 +#, kde-format +msgctxt "Category" +msgid "dummy 1" +msgstr "دمية 1" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:33 +#, kde-format +msgctxt "Category" +msgid "dummy with stuff" +msgstr "دمية بها أشياء" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:40 +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:55 +#, kde-format +msgctxt "Category" +msgid "dummy 2.1" +msgstr "دمية 2Ù«1" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:48 +#, kde-format +msgctxt "Category" +msgid "dummy with quite some stuff" +msgstr "دمية ببعض الأشياء" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:64 +#, kde-format +msgctxt "Category" +msgid "dummy 3" +msgstr "دمية 3" + +#: libdiscover/backends/DummyBackend/dummy-backend-categories.xml:74 +#, kde-format +msgctxt "Category" +msgid "dummy 4" +msgstr "دمية 4" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:5 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:5 +#, kde-format +msgctxt "Category" +msgid "All Applications" +msgstr "كلّ التّطبيقات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:29 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:29 +#, kde-format +msgctxt "Category" +msgid "Accessories" +msgstr "الإكسسوارات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:40 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:40 +#, kde-format +msgctxt "Category" +msgid "Accessibility" +msgstr "الإتاحة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:51 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:51 +#, kde-format +msgctxt "Category" +msgid "Developer Tools" +msgstr "أدوات المطوّر" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:60 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:60 +#, kde-format +msgctxt "Category" +msgid "Debugging" +msgstr "التّنقيح" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:70 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:70 +#, kde-format +msgctxt "Category" +msgid "Graphic Interface Design" +msgstr "تصميم الواجهات الرّسوميّة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:79 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:79 +#, kde-format +msgctxt "Category" +msgid "IDEs" +msgstr "بيئات التّطوير المتكاملة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:88 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:88 +#, kde-format +msgctxt "Category" +msgid "Localization" +msgstr "التّوطين" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:98 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:98 +#, kde-format +msgctxt "Category" +msgid "Profiling" +msgstr "التّحليل البرمجيّ" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:107 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:107 +#, kde-format +msgctxt "Category" +msgid "Web Development" +msgstr "تطوير الوِبّ" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:120 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:119 +#, kde-format +msgctxt "Category" +msgid "Education" +msgstr "التّعليم" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:130 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:129 +#, kde-format +msgctxt "Category" +msgid "Science and Engineering" +msgstr "الهندسة والعلوم" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:139 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:138 +#, kde-format +msgctxt "Category" +msgid "Astronomy" +msgstr "الفلك" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:147 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:146 +#, kde-format +msgctxt "Category" +msgid "Biology" +msgstr "الأحياء" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:155 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:154 +#, kde-format +msgctxt "Category" +msgid "Chemistry" +msgstr "الكيمياء" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:164 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:163 +#, kde-format +msgctxt "Category" +msgid "Computer Science and Robotics" +msgstr "علم الحاسوب والرّوبوتات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:175 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:174 +#, kde-format +msgctxt "Category" +msgid "Electronics" +msgstr "الإلكترونيّات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:184 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:183 +#, kde-format +msgctxt "Category" +msgid "Engineering" +msgstr "الهندسة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:193 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:192 +#, kde-format +msgctxt "Category" +msgid "Geography" +msgstr "الجغرافيا" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:201 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:200 +#, kde-format +msgctxt "Category" +msgid "Geology" +msgstr "الطّبيعة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:210 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:209 +#, kde-format +msgctxt "Category" +msgid "Mathematics" +msgstr "الرّياضيّات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:221 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:220 +#, kde-format +msgctxt "Category" +msgid "Physics" +msgstr "الفيزياء" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:232 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:231 +#, kde-format +msgctxt "Category" +msgid "Games" +msgstr "الألعاب" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:241 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:240 +#, kde-format +msgctxt "Category" +msgid "Arcade" +msgstr "الممرّات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:250 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:249 +#, kde-format +msgctxt "Category" +msgid "Board Games" +msgstr "ألعاب الألواح" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:259 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:258 +#, kde-format +msgctxt "Category" +msgid "Card Games" +msgstr "ألعاب الورق" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:268 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:267 +#, kde-format +msgctxt "Category" +msgid "Puzzles" +msgstr "الأحاجي" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:277 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:276 +#, kde-format +msgctxt "Category" +msgid "Role Playing" +msgstr "ألعاب تقمّص الأدوار" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:286 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:285 +#, kde-format +msgctxt "Category" +msgid "Simulation" +msgstr "المحاكاة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:295 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:294 +#, kde-format +msgctxt "Category" +msgid "Strategy" +msgstr "الاستراتيجيات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:304 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:303 +#, kde-format +msgctxt "Category" +msgid "Sports" +msgstr "الرياضة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:313 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:312 +#, kde-format +msgctxt "Category" +msgid "Action" +msgstr "حركي" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:322 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:321 +#, kde-format +msgctxt "Category" +msgid "Emulators" +msgstr "المحاكاة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:334 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:334 +#, kde-format +msgctxt "Category" +msgid "Graphics" +msgstr "الرّسوميّات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:342 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:342 +#, kde-format +msgctxt "Category" +msgid "3D" +msgstr "ثلاثيّات الأبعاد" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:350 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:350 +#, kde-format +msgctxt "Category" +msgid "Drawing" +msgstr "الرّسم" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:362 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:362 +#, kde-format +msgctxt "Category" +msgid "Painting and Editing" +msgstr "التّلوين والتّحرير" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:375 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:375 +#, kde-format +msgctxt "Category" +msgid "Photography" +msgstr "التّصوير الفوتوغرافيّ" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:384 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:384 +#, kde-format +msgctxt "Category" +msgid "Publishing" +msgstr "النّشر" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:393 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:393 +#, kde-format +msgctxt "Category" +msgid "Scanning and OCR" +msgstr "المسح الضّوئيّ والتّعرّف على الكتابة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:403 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:403 +#, kde-format +msgctxt "Category" +msgid "Viewers" +msgstr "العارضات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:415 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:415 +#, kde-format +msgctxt "Category" +msgid "Internet" +msgstr "الشّابكة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:423 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:423 +#, kde-format +msgctxt "Category" +msgid "Chat" +msgstr "المحادثة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:433 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:433 +#, kde-format +msgctxt "Category" +msgid "File Sharing" +msgstr "مشاركة الملفّات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:442 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:442 +#, kde-format +msgctxt "Category" +msgid "Mail" +msgstr "البريد" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:451 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:451 +#, kde-format +msgctxt "Category" +msgid "Web Browsers" +msgstr "متصفّحات الوِبّ" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:463 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:463 +#, kde-format +msgctxt "Category" +msgid "Multimedia" +msgstr "الوسائط المتعدّدة" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:472 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:472 +#, kde-format +msgctxt "Category" +msgid "Audio and Video Editors" +msgstr "محررات الصوت والفيديو" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:481 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:481 +#, kde-format +msgctxt "Category" +msgid "Audio Players" +msgstr "مشغلات الصوتيات" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:502 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:502 +#, kde-format +msgctxt "Category" +msgid "Video Players" +msgstr "مشغلات الفيديو" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:520 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:520 +#, kde-format +msgctxt "Category" +msgid "CD and DVD" +msgstr "سي دي و دي في دي" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:532 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:532 +#, kde-format +msgctxt "Category" +msgid "Office" +msgstr "المكتب" + +#: libdiscover/backends/FlatpakBackend/flatpak-backend-categories.xml:542 +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:542 +#, kde-format +msgctxt "Category" +msgid "System Settings" +msgstr "إعدادات النّظام" + +#: libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp:132 +#, kde-format +msgctxt "user denotes this as user-scoped flatpak repo" +msgid "%1 (user)" +msgstr "‏%1 (المستخدم)" + +#: libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp:740 +#, kde-format +msgid "Local bundle" +msgstr "حزمة محليّة" + +#: libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp:1709 +#: libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp:789 +#, kde-format +msgid "Malformed appstream url '%1'" +msgstr "مسار appstream ‏’%1‘ مشوّه" + +#: libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp:1813 +#, kde-format +msgid "Failed to add source '%1': %2" +msgstr "فشل في إضافة مصدر '%1': %2" + +#: libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp:1934 +#, kde-format +msgid "There are no Flatpak sources." +msgstr "لا يوجد مصادر للفلات باك" + +#: libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp:1938 +#, kde-format +msgid "Failed to load \"%1\" source" +msgstr "فشل تحميل المصدر '%1'" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:364 +#, kde-kuit-format +msgctxt "@info" +msgid "" +"This application comes from \"%1\" (hosted at %2). Other " +"software in this repository will also be made be available in Discover when " +"the application is installed." +msgstr "" +"يأتي هذا التطبيق من \"%1\" (مستضاف في %2 ). سيتم أيضًا " +"توفير برامج أخرى من هذا المستودع في المستكشف عند تثبيت التطبيق." + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:447 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:455 +#, kde-format +msgid "Retrieving size information" +msgstr "يجلب معلومات الحجم" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:449 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:457 +#, kde-format +msgid "Unknown size" +msgstr "الحجم مجهول" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:451 +#, kde-format +msgctxt "@info app size" +msgid "%1 to download, %2 on disk" +msgstr "‏%1 للتّنزيل، %2 على القرص" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:459 +#, kde-format +msgctxt "@info app size" +msgid "%1 on disk" +msgstr "‏%1 على القرص" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:513 +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:487 +#: libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.cpp:69 +#, kde-format +msgid "Failed to start '%1': %2" +msgstr "فشل بدء '%1': %2" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:756 +#, kde-format +msgid "All Files" +msgstr "كلّ الملفّات" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:758 +#, kde-format +msgid "Home" +msgstr "المنزل" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:760 +#, kde-format +msgid "Downloads" +msgstr "التنزيلات" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:762 +#, kde-format +msgid "Music" +msgstr "الموسيقى" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:810 +#, kde-format +msgid "Network Access" +msgstr "نفاذ الشّبكة" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:811 +#, kde-format +msgid "Can access the internet" +msgstr "يمكنه الوصول إلى الشابكة" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:817 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:958 +#, kde-format +msgid "Session Bus Access" +msgstr "الوصول لناقل الجلسة" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:818 +#, kde-format +msgid "Access is granted to the entire Session Bus" +msgstr "مُنح الوصول إلى ناقل الجلسة بالكامل" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:823 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:969 +#, kde-format +msgid "System Bus Access" +msgstr "الوصول لناقل النظام" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:824 +#, kde-format +msgid "Access is granted to the entire System Bus" +msgstr "مُنح الوصول إلى ناقل النظام بالكامل" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:829 +#, kde-format +msgid "Remote Login Access" +msgstr "الوصول للولوج البعيد" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:830 +#, kde-format +msgid "Can initiate remote login requests using the SSH protocol" +msgstr "يمكنه بدء طلبات الولوج البعيد باستخدام ميفاق SSH" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:834 +#, kde-format +msgid "Smart Card Access" +msgstr "الوصول للبطاقة الذكية" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:835 +#, kde-format +msgid "Can integrate and communicate with smart cards" +msgstr "يمكنه التكامل والتواصل مع البطاقات الذكية" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:839 +#, kde-format +msgid "Printer Access" +msgstr "الوصول إلى الطابعة" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:840 +#, kde-format +msgid "Can integrate and communicate with printers" +msgstr "يمكنه التكامل والتواصل مع الطابعات" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:844 +#, kde-format +msgid "GPG Agent" +msgstr "عميل GPG" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:845 +#, kde-format +msgid "" +"Allows access to the GPG cryptography service, generally used for signing " +"and reading signed documents" +msgstr "" +"يسمح بالوصول إلى خدمة تشفير GPG ، وتستخدم عمومًا لتوقيع المستندات وقراءة " +"الموقع منها." + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:851 +#, kde-format +msgid "Bluetooth Access" +msgstr "الوصول إلى بلوتوث" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:852 +#, kde-format +msgid "Can integrate and communicate with Bluetooth devices" +msgstr "يمكنه التكامل والتواصل مع أجهزة بلوتوث" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:856 +#, kde-format +msgid "Low-Level System Access" +msgstr "الوصول إلى وظائف النظام الدُنيا" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:857 +#, kde-format +msgid "Can make low-level system calls (e.g. ptrace)" +msgstr "يمكنه أن ينادى وظائف النظام الدُنيا (مثل ptrace)" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:863 +#, kde-format +msgid "Device Access" +msgstr "الوصول للجهاز" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:864 +#, kde-format +msgid "Can communicate with and control built-in or connected hardware devices" +msgstr "يمكنه التواصل والتحكم بالعتاد الموصل أو المضمن" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:868 +#, kde-format +msgid "Kernel-based Virtual Machine Access" +msgstr "الوصول إلى الآلات الافتراضية المعتمدة على النواة" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:869 +#, kde-format +msgid "Allows running other operating systems as guests in virtual machines" +msgstr "يسمح بتشغيل أنظمة تشغيل أخرى كضيوف في الآلات الافتراضية" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:893 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:905 +#, kde-format +msgid "%1 (read-only)" +msgstr "‏%1 (للقراءة فقط)" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:896 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:908 +#, kde-format +msgid "%1 (can create files)" +msgstr "‏%1 (يمكنه إنشاء ملفات)" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:899 +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:917 +#, kde-format +msgid "%1 (read & write) " +msgstr "‏%1 (للقراءة و الكتابة) " + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:926 +#, kde-format +msgid "Home Folder Access" +msgstr "الوصول إلى مجلد المنزل" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:929 +#, kde-format +msgid "" +"Can read, write, and create files in the following locations in your home " +"folder without asking permission first: %1" +msgstr "" +"يمكنه قراءة الملفات والكتابة فيها وإنشاؤها في المواقع التالية في مجلد المنزل " +"دون طلب الإذن أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:931 +#, kde-format +msgid "" +"Can read and write files in the following locations in your home folder " +"without asking permission first: %1" +msgstr "" +"يمكنه قراءة الملفات والكتابة فيها في المواقع التالية في مجلد المنزل دون طلب " +"الإذن أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:933 +#, kde-format +msgid "" +"Can read files in the following locations in your home folder without asking " +"permission first: %1" +msgstr "" +"يمكنه قراءة الملفات في المواقع التالية في مجلد المنزل دون طلب الإذن أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:935 +#, kde-format +msgid "" +"Can access files in the following locations in your home folder without " +"asking permission first: %1" +msgstr "" +"يمكنه الوصول إلى الملفات في المواقع التالية في مجلد المنزل دون طلب الإذن " +"أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:941 +#, kde-format +msgid "System Folder Access" +msgstr "الوصول إلى مجلّد النّظام" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:943 +#, kde-format +msgid "" +"Can read, write, and create system files in the following locations without " +"asking permission first: %1" +msgstr "" +"يمكنه قراءة ملفات النظام والكتابة فيها وإنشاؤها في المواقع التالية دون طلب " +"الإذن أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:945 +#, kde-format +msgid "" +"Can read and write system files in the following locations without asking " +"permission first: %1" +msgstr "" +"يمكنه قراءة ملفات النظام والكتابة فيها في المواقع التالية دون طلب الإذن " +"أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:947 +#, kde-format +msgid "" +"Can read system files in the following locations without asking permission " +"first: %1" +msgstr "يمكنه قراءة ملفات النظام في المواقع التالية دون طلب الإذن أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:949 +#, kde-format +msgid "" +"Can access system files in the following locations without asking permission " +"first: %1" +msgstr "" +"يمكنه الوصول إلى ملفات النظام في المواقع التالية دون طلب الإذن أولاً: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:959 +#, kde-format +msgid "" +"Can communicate with other applications and processes in the same desktop " +"session using the following communication protocols: %1" +msgstr "" +"يمكنه التواصل مع التطبيقات والعمليات الأخرى في نفس جلسة سطح المكتب باستخدام " +"ميافيق الاتصال التالية: %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakResource.cpp:971 +#, kde-format +msgid "" +"Can communicate with all applications and system services using the " +"following communication protocols: %1" +msgstr "" +"يمكنه التواصل مع كل التطبيقات وخدمات النظام باستخدام ميافيق الاتصال التالية: " +"%1" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:88 +#, kde-format +msgid "Add Flathub" +msgstr "أضف فلات‌هَب" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:89 +#, kde-format +msgid "Apply Changes" +msgstr "طبّق التغييرات" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:93 +#, kde-format +msgid "" +"Changes to the priority of Flatpak sources must be applied before they will " +"take effect." +msgstr "" +"يجب تطبيق التغييرات على أولوية مصادر فلاتباك قبل أن تصبح سارية المفعول." + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:97 +#, kde-format +msgid "" +"Makes it possible to easily install the applications listed in https://" +"flathub.org" +msgstr "يجعل من الممكن بسهولة تثبيت التطبيقات المدرجة في https://flathub.org " + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:163 +#, kde-format +msgid "Could not add the source %1" +msgstr "تعذّرت إضافة المصدر %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:279 +#, kde-format +msgid "Removing '%1'" +msgstr "يزيل '%1'" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:280 +#, kde-format +msgid "" +"To remove this repository, the following applications must be uninstalled:" +"
    • %1
    " +msgstr "" +"لإزالة هذا المستودع ، يجب إلغاء تثبيت التطبيقات التالية:
    • %1
    " + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:297 +#, kde-format +msgid "Failed to remove %1 remote repository: %2" +msgstr "فشل في إزالة المستودع البعيد %1 : %2" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:301 +#, kde-format +msgid "Could not find %1" +msgstr "لا يمكن إيجاد %1" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:333 +#, kde-format +msgid "%1 (user)" +msgstr "‏%1 (المستخدم)" + +#: libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp:394 +#, kde-format +msgid "Enter a Flatpak repository URI (*.flatpakrepo):" +msgstr "أدخل عنوان مستودع فلاتباك (*.flatpakrepo)" + +#: libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.cpp:30 +#, kde-format +msgid "Adding remote '%1' in %2 from %3" +msgstr "يضيف '%1' البعيد في %2 من %3" + +#: libdiscover/backends/FlatpakBackend/FlatpakTransactionThread.cpp:196 +#, kde-format +msgid "Could not find '%1' in '%2'; please make sure it's available." +msgstr "تعثر العثور على '%1' في '%2'، الرجاء تأكد من توفره." + +#: libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml:20 +#, kde-format +msgctxt "@label %1 is the name of an application" +msgid "" +"This development version of %1 is outdated. Using the stable version is " +"highly recommended." +msgstr "" +"أصبحت هذه الإصدارة التطويرية من %1 قديمة. من المستحسن أن تستعمل الإصدارة " +"المستقرة." + +#: libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml:20 +#, kde-format +msgctxt "@label %1 is the name of an application" +msgid "A more stable version of %1 is available." +msgstr "يتوفّر إصدارة أكثر استقراراً من %1" + +#: libdiscover/backends/FlatpakBackend/qml/FlatpakOldBeta.qml:44 +#, kde-format +msgctxt "@action: button %1 is the name of a Flatpak repo" +msgid "View Stable Version on %1" +msgstr "اعرض الإصدارة المستقرّة على %1" + +#: libdiscover/backends/FlatpakBackend/qml/FlatpakRemoveData.qml:17 +#, kde-format +msgid "%1 is not installed but it still has data present." +msgstr "التطبيق %1 غير مثبت ولكنه يملك بيانات على النظام." + +#: libdiscover/backends/FlatpakBackend/qml/FlatpakRemoveData.qml:31 +#, kde-format +msgid "Delete settings and user data" +msgstr "احذف الإعدادات وبيانات المستخدم" + +#: libdiscover/backends/FlatpakBackend/qml/PermissionsList.qml:20 +#: libdiscover/backends/PackageKitBackend/qml/PackageKitPermissions.qml:16 +#, kde-format +msgctxt "%1 is the name of the application" +msgid "Permissions for %1" +msgstr "صلاحيات لـ %1" + +#: libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp:38 +#, kde-format +msgid "" +"The remote %1 require that you accept their license:\n" +" %2" +msgstr "" +"تطلب %1 البعيدة قبول رخصتهم:\n" +" %2" + +#: libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp:41 +#, kde-format +msgid "Review EULA" +msgstr "راجع رخصة المستخدم النهائي" + +#: libdiscover/backends/FwupdBackend/FwupdSourcesBackend.cpp:137 +#, kde-format +msgid "Could not enable remote %1: %2" +msgstr "تعذّر تمكين %1 البعيد: %2" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:64 +#, kde-format +msgid "" +"Unsupported question:\n" +"%1" +msgstr "" +"سؤال غير مدعوم:\n" +"%1" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:129 +#, kde-format +msgid "Backend %1 took too long to initialize" +msgstr "أخذت الخلفية %1 وقت طويل للبدء" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:191 +#, kde-format +msgid "Applications" +msgstr "التّطبيقات" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:237 +#, kde-format +msgid "Plasma Addons" +msgstr "إضافات بلازما" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:237 +#, kde-format +msgid "Application Addons" +msgstr "إضافات التّطبيقات" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:438 +#, kde-format +msgid "Network error in backend %1: %2" +msgstr "خطأ شبكيّ في الخلفية %1: %2" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:445 +#, kde-format +msgid "" +"Too many requests sent to the server for backend %1. Please try again in a " +"few minutes." +msgstr "" +"العديد من الطلبات أرسلت إلى الخادم لخلفية %1 . رجاء حاول مجددا بعد بضع دقائق." + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:448 +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:454 +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:459 +#, kde-format +msgid "Invalid %1 backend, contact your distributor." +msgstr "خلفية %1 غير صالح، راجع توزيعتك." + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:478 +#, kde-format +msgid "" +"Unable to complete the update of %1. You can try and perform this action " +"through the Get Hot New Stuff dialog, which grants tighter control. The " +"reported error was:\n" +"%2" +msgstr "" +"غير قادر على إكمال تحديث %1. يمكنك محاولة تنفيذ هذا الإجراء من خلال مربع " +"الحوار احصل على أشياء جديدة ، والذي يمنح تحكمًا أكثر إحكامًا. الخطأ الذي تم " +"الإبلاغ عنه هو: \n" +"%2" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:488 +#, kde-format +msgid "Could not fetch screenshot for the entry %1 in backend %2" +msgstr "لم يستطع جلب لقطات للمدخلة %1 في خلفية %2" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:492 +#, kde-format +msgid "Unhandled error in %1 backend. Contact your distributor." +msgstr "خطأ لا يمكن التعامل معه في خلفية %1. راجع توزيعتك." + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:500 +#, kde-format +msgid "%1: %2" +msgstr "‏%1:‏ %2" + +#: libdiscover/backends/KNSBackend/KNSBackend.cpp:638 +#, kde-format +msgid "Wrong KNewStuff URI: %1" +msgstr "معرّف «أشياء جديدة» خطأ: %1" + +#: libdiscover/backends/KNSBackend/KNSResource.cpp:168 +#, kde-format +msgctxt "The name of the KDE Store" +msgid "KDE Store" +msgstr "متجر كِيدِي" + +#: libdiscover/backends/KNSBackend/KNSReviews.cpp:145 +#, kde-format +msgid "Log in information for %1" +msgstr "معلومات ولوج %1" + +#: libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp:237 +#, kde-format +msgid "Cannot launch %1" +msgstr "تعذر بدء %1" + +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:552 +#, kde-format +msgctxt "Category" +msgid "Plasma Addons" +msgstr "إضافات بلازما" + +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:563 +#, kde-format +msgctxt "Category" +msgid "Plasma Widgets" +msgstr "ودجات بلازما" + +#: libdiscover/backends/PackageKitBackend/packagekit-backend-categories.xml:575 +#, kde-format +msgctxt "Category" +msgid "Fonts" +msgstr "الخطوط" + +#: libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp:303 +#, kde-format +msgid "Please make sure that Appstream is properly set up on your system" +msgstr "تحقّق من أنّ Appstream مضبوط كما ينبغي في النّظام" + +#: libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp:355 +#, kde-format +msgctxt "%1 is the date as formatted by the locale" +msgid "" +"Your operating system ended support on %1. Consider upgrading to a supported " +"version." +msgstr "انتهى دعم نظام تشغيلك في %1. ضع في اعتبارك الترقية إلى إصدار مدعوم." + +#: libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp:886 +#, kde-format +msgid "Cannot remove '%1'" +msgstr "لا يمكن إزالة '%1'" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:18 +#, kde-format +msgid "Out of memory" +msgstr "نفذت الذّاكرة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:20 +#, kde-format +msgid "No network connection available" +msgstr "لا اتّصال شبكة متوفّر" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:22 +#, kde-format +msgid "Operation not supported" +msgstr "العمليّة غير مدعومة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:25 +#, kde-format +msgid "Internal error" +msgstr "خطأ داخليّ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:27 +#, kde-format +msgid "Internal error: %1" +msgstr "خطأ داخليّ: %1" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:31 +#, kde-format +msgid "GPG failure" +msgstr "فشل GPG" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:33 +#, kde-format +msgid "PackageID invalid" +msgstr "معرّف الحزمة غير صالح" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:35 +#, kde-format +msgid "Package not installed" +msgstr "الحزمة غير مثبّتة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:37 +#, kde-format +msgid "Package not found" +msgstr "لم يُعثر على الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:39 +#, kde-format +msgid "Package is already installed" +msgstr "الحزمة مثبّتة بالفعل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:41 +#, kde-format +msgid "Package download failed" +msgstr "فشل تنزيل الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:43 +#, kde-format +msgid "Package group not found" +msgstr "لم يُعثر على مجموعة الحزم" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:45 +#, kde-format +msgid "Package group list invalid" +msgstr "قائمة مجموعة الحزم غير صالحة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:47 +#, kde-format +msgid "Dependency resolution failed" +msgstr "فشل حلّ الاعتماديّات" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:49 +#, kde-format +msgid "Filter invalid" +msgstr "المرشّح غير صالح" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:51 +#, kde-format +msgid "Failed while creating a thread" +msgstr "فشل أثناء إنشاء خيط" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:53 +#, kde-format +msgid "Transaction failure" +msgstr "فشلت العمليّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:55 +#, kde-format +msgid "Transaction canceled" +msgstr "أُلغيت العمليّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:57 +#, kde-format +msgid "No Cache available" +msgstr "لا خبيئة متوفّرة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:59 +#, kde-format +msgid "Cannot find repository" +msgstr "تعذّر العثور على المستودع" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:61 +#, kde-format +msgid "Cannot remove system package" +msgstr "لا يمكن إزالة حزمة نظام" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:63 +#, kde-format +msgid "The PackageKit daemon has crashed" +msgstr "انهار عفريت عُدّة الحزم PackageKit" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:65 +#, kde-format +msgid "Initialization failure" +msgstr "خطأ تمهيد" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:67 +#, kde-format +msgid "Failed to finalize transaction" +msgstr "فشل تمهيد العمليّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:69 +#, kde-format +msgid "Config parsing failed" +msgstr "فشل تحليل الضّبط" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:71 +#, kde-format +msgid "Cannot cancel transaction" +msgstr "تعذّر إلغاء العمليّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:73 +#, kde-format +msgid "Cannot obtain lock" +msgstr "تعذّر الحصول على القفل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:75 +#, kde-format +msgid "No packages to update" +msgstr "لا حزم لتحديثها" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:77 +#, kde-format +msgid "Cannot write repo config" +msgstr "تعذّرت كتابة ضبط المستودع" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:79 +#, kde-format +msgid "Local install failed" +msgstr "فشل التّثبيت المحلّيّ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:81 +#, kde-format +msgid "Bad GPG signature found" +msgstr "عُثر على توقيع GPG سيّئ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:83 +#, kde-format +msgid "No GPG signature found" +msgstr "لم يُعثر على توقيع GPG" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:85 +#, kde-format +msgid "Cannot install source package" +msgstr "تعذّر تثبيت الحزمة المصدر" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:87 +#, kde-format +msgid "Repo configuration error" +msgstr "خطأ في ضبط المستودع" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:89 +#, kde-format +msgid "No license agreement" +msgstr "لا اتّفاقيّة ترخيص" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:91 +#, kde-format +msgid "File conflicts found" +msgstr "عُثر على تضارب ملفّات" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:93 +#, kde-format +msgid "Package conflict found" +msgstr "عُثر على تضارب حزم" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:95 +#, kde-format +msgid "Repo not available" +msgstr "المستودع غير متوفّر" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:97 +#, kde-format +msgid "Invalid package file" +msgstr "ملفّ الحزمة غير صالح" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:99 +#, kde-format +msgid "Package install blocked" +msgstr "مُنع تثبيت الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:101 +#, kde-format +msgid "Corrupt package found" +msgstr "عُثر على حزمة معطوبة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:103 +#, kde-format +msgid "All packages already installed" +msgstr "كلّ الحزم مثبّتة بالفعل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:105 +#, kde-format +msgid "File not found" +msgstr "لم يُعثر على الملفّ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:107 +#, kde-format +msgid "No more mirrors available" +msgstr "لا مرايا أكثر متوفّرة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:109 +#, kde-format +msgid "No distro upgrade data" +msgstr "لا بيانات لترقية التّوزيعة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:111 +#, kde-format +msgid "Incompatible architecture" +msgstr "معماريّة غير متوافقة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:113 +#, kde-format +msgid "No space on device left" +msgstr "لم تبقَ مساحة على الجهاز" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:115 +#, kde-format +msgid "A media change is required" +msgstr "تغيير الوسيط مطلوب" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:117 +#, kde-format +msgid "You have no authorization to execute this operation" +msgstr "لا استيثاق لديك لتنفيذ هذه العمليّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:119 +#, kde-format +msgid "Update not found" +msgstr "لم يُعثر على التّحديث" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:121 +#, kde-format +msgid "Cannot install from unsigned repo" +msgstr "لا يمكن التّثبيت من مستودع غير موقّع" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:123 +#, kde-format +msgid "Cannot update from unsigned repo" +msgstr "لا يمكن التّحديث من مستودع غير موقّع" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:125 +#, kde-format +msgid "Cannot get file list" +msgstr "تعذّر الحصول على قائمة الملفّات" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:127 +#, kde-format +msgid "Cannot get requires" +msgstr "تعذّر الحصول على المطلوبات" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:129 +#, kde-format +msgid "Cannot disable repository" +msgstr "تعذّر تعطيل المستودع" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:131 +#, kde-format +msgid "Restricted download detected" +msgstr "اكتُشف تقييد التّنزيل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:133 +#, kde-format +msgid "Package failed to configure" +msgstr "فشل ضبط الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:135 +#, kde-format +msgid "Package failed to build" +msgstr "فشل بناء الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:137 +#, kde-format +msgid "Package failed to install" +msgstr "فشل تثبيت الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:139 +#, kde-format +msgid "Package failed to remove" +msgstr "فشلت إزالة الحزمة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:141 +#, kde-format +msgid "Update failed due to running process" +msgstr "فشل التّحديث بسبب عمليّة تعمل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:143 +#, kde-format +msgid "The package database changed" +msgstr "تغيّرت قاعدة بيانات الحزم" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:145 +#, kde-format +msgid "The provided type is not supported" +msgstr "النّوع الموفّر غير مدعوم" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:147 +#, kde-format +msgid "Install root is invalid" +msgstr "جذر التّثبيت غير صالح" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:149 +#, kde-format +msgctxt "" +"Failed to sync your Linux distro repositories or other sources of packages" +msgid "Cannot fetch sources" +msgstr "تعذّر جلب المصادر" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:151 +#, kde-format +msgid "Canceled priority" +msgstr "أُلغيت الأولويّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:153 +#, kde-format +msgid "Unfinished transaction" +msgstr "عمليّة لم تنته" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:155 +#, kde-format +msgid "Lock required" +msgstr "القفل مطلوب" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:160 +#, kde-format +msgid "Unknown error %1." +msgstr "الخطأ %1 مجهول." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:169 +#, kde-format +msgid "'%1' was changed and suggests to be restarted." +msgstr "تغيّر '%1' وهو يقترح إعادة تشغيله." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:171 +#, kde-format +msgid "A change by '%1' suggests your session to be restarted." +msgstr "يقترح تغيير لِـ'%1' إعادة تشغيل الجلسة." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:173 +#, kde-format +msgid "" +"'%1' was updated for security reasons, a restart of the session is " +"recommended." +msgstr "حُدّث '%1' لأسباب أمنيّة، لذا إعادة تشغيل الجلسة مستحسن." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:175 +#, kde-format +msgid "" +"'%1' was updated for security reasons, a restart of the system is " +"recommended." +msgstr "حُدّث '%1' لأسباب أمنيّة، لذا إعادة تشغيل النّظام مستحسن." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:180 +#, kde-format +msgid "A change by '%1' suggests your system to be restarted." +msgstr "يقترح تغيير لِـ'%1' إعادة تشغيل النّظام." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:188 +#, kde-format +msgid "The application will have to be restarted." +msgstr "يجب أن يُعاد تشغيل التّطبيق." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:190 +#, kde-format +msgid "The session will have to be restarted" +msgstr "يجب أن يُعاد تشغيل الجلسة." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:192 +#, kde-format +msgid "The system will have to be restarted." +msgstr "يجب أن يُعاد تشغيل النّظام." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:194 +#, kde-format +msgid "For security, the session will have to be restarted." +msgstr "للأمان، يجب أن يُعاد تشغيل الجلسة." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:196 +#, kde-format +msgid "For security, the system will have to be restarted." +msgstr "للأمان، يجب أن يُعاد تشغيل النّظام." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:208 +#, kde-format +msgid "Waiting…" +msgstr "ينتظر…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:210 +#, kde-format +msgid "Refreshing Cache…" +msgstr "يحدّث الخبيئة..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:212 +#, kde-format +msgid "Setup…" +msgstr "يعد..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:214 +#, kde-format +msgid "Processing…" +msgstr "يعالج..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:216 +#, kde-format +msgid "Remove…" +msgstr "يزيل..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:218 +#, kde-format +msgid "Downloading…" +msgstr "ينزّل…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:220 +#, kde-format +msgid "Installing…" +msgstr "يثبّت…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:222 +#, kde-format +msgid "Updating…" +msgstr "يحدّث…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:224 +#, kde-format +msgid "Cleaning up…" +msgstr "ينظّف…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:227 +#, kde-format +msgid "Resolving dependencies…" +msgstr "يحلّ الاعتماديّات…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:229 +#, kde-format +msgid "Checking signatures…" +msgstr "يفحص التّواقيع…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:231 +#, kde-format +msgid "Test committing…" +msgstr "يختبر الإيداع..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:233 +#, kde-format +msgid "Committing…" +msgstr "يودع..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:236 +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:362 +#, kde-format +msgid "Finished" +msgstr "انتهى" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:238 +#, kde-format +msgid "Canceled" +msgstr "أُلغي" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:240 +#, kde-format +msgid "Waiting for lock…" +msgstr "ينتظر القفل..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:242 +#, kde-format +msgid "Waiting for authorization…" +msgstr "ينتظر الاستيثاق…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:247 +#, kde-format +msgid "Copying files…" +msgstr "ينسخ الملفّات…" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:250 +#, kde-format +msgid "Unknown Status" +msgstr "حالة مجهولة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:258 +#, kde-format +msgid "We are waiting for something." +msgstr "نحن ننتظر شيئًا." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:260 +#, kde-format +msgid "Setting up transaction…" +msgstr "يعدّ العمليّة..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:262 +#, kde-format +msgid "The transaction is currently working…" +msgstr "العمليّة تعمل حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:264 +#, kde-format +msgid "The transaction is currently removing packages…" +msgstr "العمليّة تزيل الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:266 +#, kde-format +msgid "The transaction is currently downloading packages…" +msgstr "العمليّة تنزّل الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:268 +#, kde-format +msgid "The transactions is currently installing packages…" +msgstr "العمليّات تثبّت الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:270 +#, kde-format +msgid "The transaction is currently updating packages…" +msgstr "العمليّة تحدّث الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:272 +#, kde-format +msgid "The transaction is currently cleaning up…" +msgstr "العمليّة تنظّف حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:275 +#, kde-format +msgid "" +"The transaction is currently resolving the dependencies of the packages it " +"will install…" +msgstr "العمليّة تحلّ اعتماديّات الحزم التي ستثبّتها حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:277 +#, kde-format +msgid "The transaction is currently checking the signatures of the packages…" +msgstr "العمليّة تفحص تواقيع الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:279 +#, kde-format +msgid "" +"The transaction is currently testing the commit of this set of packages…" +msgstr "العمليّة تفحص إيداع مجموعة الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:281 +#, kde-format +msgid "The transaction is currently committing its set of packages…" +msgstr "العمليّة تودع مجموعة الحزم حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:284 +#, kde-format +msgid "The transaction has finished!" +msgstr "انتهت العمليّة!" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:286 +#, kde-format +msgid "The transaction was canceled" +msgstr "أُلغيت العمليّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:288 +#, kde-format +msgid "The transaction is currently waiting for the lock…" +msgstr "العمليّة تعمل حاليًّا للقفل..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:290 +#, kde-format +msgid "Waiting for the user to authorize the transaction…" +msgstr "ينتظر المستخدم لاستيثاق العمليّة..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:295 +#, kde-format +msgid "The transaction is currently copying files…" +msgstr "العمليّة تنسخ الملفّات حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:297 +#, kde-format +msgid "Currently refreshing the repository cache…" +msgstr "ينعش خبيئة المستودع حاليًّا..." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:302 +#, kde-format +msgid "Unknown status %1." +msgstr "الحالة %1 مجهولة." + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:313 +#, kde-format +msgctxt "update state" +msgid "Stable" +msgstr "مستقرّ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:315 +#, kde-format +msgctxt "update state" +msgid "Unstable" +msgstr "غير مستقرّ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:317 +#, kde-format +msgctxt "update state" +msgid "Testing" +msgstr "اختباريّ" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:328 +#: libdiscover/resources/AbstractResource.cpp:103 +#, kde-format +msgid "Installed" +msgstr "مثبّتة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:330 +#, kde-format +msgid "Not Installed" +msgstr "غير مثبّت" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:332 +#, kde-format +msgid "Low" +msgstr "منخفض" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:334 +#, kde-format +msgid "Enhancement" +msgstr "التحسينات" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:336 +#, kde-format +msgid "Normal" +msgstr "عادي" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:338 +#, kde-format +msgid "Bugfix" +msgstr "إصلاح علل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:340 +#, kde-format +msgid "Important" +msgstr "مهمّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:342 +#, kde-format +msgid "Security" +msgstr "أمني" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:344 +#, kde-format +msgid "Blocked" +msgstr "حظر" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:346 +#, kde-format +msgid "Downloading" +msgstr "ينزّل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:348 +#, kde-format +msgid "Updating" +msgstr "يحدّث" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:350 +#, kde-format +msgid "Installing" +msgstr "يثبّت" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:352 +#, kde-format +msgid "Removing" +msgstr "يزيل" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:354 +#, kde-format +msgid "Cleanup" +msgstr "ينظف" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:356 +#, kde-format +msgid "Obsoleting" +msgstr "بائدة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:358 +#, kde-format +msgid "Collection Installed" +msgstr "التجميعة مثبّتة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:360 +#, kde-format +msgid "Collection Available" +msgstr "التجميعة متوفّرة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:364 +#, kde-format +msgid "Reinstalling" +msgstr "يعيد التّثبيت" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:366 +#, kde-format +msgid "Downgrading" +msgstr "يعود إلى النسخة السابقة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:368 +#, kde-format +msgid "Preparing" +msgstr "يحضّر" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:370 +#, kde-format +msgid "Decompressing" +msgstr "يفكّ الضّغط" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:372 +#, kde-format +msgid "Untrusted" +msgstr "غير موثوقة" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:374 +#, kde-format +msgid "Trusted" +msgstr "موثوق" + +#: libdiscover/backends/PackageKitBackend/PackageKitMessages.cpp:376 +#, kde-format +msgid "Unavailable" +msgstr "غير متوفّر" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:115 +#, kde-format +msgid "Failed Offline Update" +msgstr "فشل تحديث دون اتصال" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:116 +#, kde-format +msgid "" +"Failed to update %1 package\n" +"%2" +msgid_plural "" +"Failed to update %1 packages\n" +"%2" +msgstr[0] "" +"تعذّر تحديث الحزمة\n" +"%2" +msgstr[1] "" +"تعذّر تحديث الحزمة\n" +"%2" +msgstr[2] "" +"تعذّر تحديث الحزمتين\n" +"%2" +msgstr[3] "" +"تعذّر تحديث %1 حزم\n" +"%2" +msgstr[4] "" +"تعذّر تحديث %1 حزمة\n" +"%2" +msgstr[5] "" +"تعذّر تحديث %1 حزمة\n" +"%2" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:117 +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:172 +#, kde-format +msgctxt "@action:button" +msgid "Open Discover" +msgstr "افتح المستكشف" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:117 +#, kde-format +msgctxt "@action:button" +msgid "Repair System" +msgstr "إصلاح النّظام" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:127 +#, kde-format +msgid "Repairing failed offline update" +msgstr "فشل إصلاح التحديث دون اتصال" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:135 +#, kde-format +msgid "Repair Failed" +msgstr "الإصلاح فشل" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:136 +#, kde-kuit-format +msgctxt "@info" +msgid "%1Please report this error to your distribution." +msgstr "‏%1الرجاء الإبلاغ عن هذا الخطأ لتوزيعتك." + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:147 +#, kde-format +msgid "Repaired Successfully" +msgstr "نجح الإصلاح" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:170 +#, kde-format +msgid "Offline Updates" +msgstr "التّحديثات غير الشّبكيّة" + +#: libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp:171 +#, kde-format +msgid "Successfully updated %1 package" +msgid_plural "Successfully updated %1 packages" +msgstr[0] "حُدّثت %1 الحزمة بنجاح" +msgstr[1] "حُدّثت حزمة واحدة بنجاح" +msgstr[2] "حُدّثت حزمتين بنجاح" +msgstr[3] "حُدّثت %1 حزم بنجاح" +msgstr[4] "حُدّثت %1 حزمة بنجاح" +msgstr[5] "حُدّثت %1 حزمة بنجاح" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:187 +#, kde-format +msgid "Unknown Source" +msgstr "مصدر مجهول" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:332 +#, kde-format +msgctxt "package-name (version)" +msgid "%1 (%2)" +msgstr "‏%1 (‏%2)" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:334 +#, kde-format +msgctxt "comma separating package names" +msgid ", " +msgstr "، و" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:398 +#, kde-format +msgid "Obsoletes:" +msgstr "البائدة:" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:399 +#, kde-format +msgid "Release Notes:" +msgstr "ملاحظات الإصدارة:" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:400 +#, kde-format +msgid "Update State:" +msgstr "حالة التّحديث:" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:401 +#, kde-format +msgid "Restart:" +msgstr "إعادة التّشغيل" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:404 +#, kde-format +msgid "Vendor:" +msgstr "البائع:" + +#: libdiscover/backends/PackageKitBackend/PackageKitResource.cpp:424 +#, kde-format +msgid "%2 (plus %1 dependency)" +msgid_plural "%2 (plus %1 dependencies)" +msgstr[0] "‏%2 فقط" +msgstr[1] "‏%2 (واعتماديّة واحدة)" +msgstr[2] "‏%2 (واعتماديّتان)" +msgstr[3] "‏%2 (و%1 اعتماديّات)" +msgstr[4] "‏%2 (و%1 اعتماديّة)" +msgstr[5] "‏%2 (و%1 اعتماديّة)" + +#: libdiscover/backends/PackageKitBackend/PackageKitSourcesBackend.cpp:103 +#, kde-format +msgid "Repository URL:" +msgstr "مسار المستودع:" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:85 +#, kde-format +msgid "System upgrade" +msgstr "ترقية النّظام" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:172 +#, kde-format +msgid "1 package will be upgraded" +msgid_plural "%1 packages will be upgraded" +msgstr[0] "لا حزم لتحديثها" +msgstr[1] "حزمة واحد سترقى" +msgstr[2] "حزمتين سترقيان" +msgstr[3] "%1 حزم سترقى" +msgstr[4] "%1 حزمة سترقى" +msgstr[5] "%1 حزمة سترقى" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:181 +#, kde-format +msgid "

    %1

    Upgrade to new version %2
    No release notes provided" +msgstr "

    %1

    سترقى إلى الإصدارة الأحدث %2
    لا يوجد ملاحظات للإصدارة" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:183 +#, kde-format +msgid "" +"

    %1

    Upgrade to new version %2
    Release notes:
    %3" +msgstr "" +"

    %1

    سترقى إلى الإصدارة الأحدث %2
    ملاحظات الإصدارة:
    %3" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:202 +#, kde-format +msgid "Present" +msgstr "موجود" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:206 +#, kde-format +msgid "Future" +msgstr "المستقبل" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:321 +#, kde-format +msgctxt "@info:status %1 is a formatted disk space string e.g. '240 MiB'" +msgid "Not enough space to perform the update; only %1 of space are available." +msgstr "لا يوجد مساحة كافية للتحديث. المتوفر لديك %1." + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:508 +#, kde-format +msgid "" +"This update cannot be completed as it would remove the following software " +"which is critical to the system's operation:
    • %1
    If " +"you believe this is an error, please report it as a bug to the packagers of " +"your distribution." +msgstr "" +"لا يمكن إكمال هذا التحديث لأنه سيؤدي إلى إزالة البرامج التالية التي تعتبر " +"بالغة الأهمية لتشغيل النظام:
    • ‏%1
    إذا كنت تعتقد أن " +"هذا خطأ ، فالرجاء الإبلاغ عنه على أنه خطأ إلى محزمي التطبيقات في توزيعتك." + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:515 +#, kde-format +msgid "Packages to remove" +msgstr "الحزم لإزالتها" + +#: libdiscover/backends/PackageKitBackend/PackageKitUpdater.cpp:516 +#, kde-format +msgid "" +"The following packages will be removed by the update:
    • %1

    in order to install:
    • %2
    " +msgstr "" +"سيُزيل التّحديث الحزم الآتية:
    • %1

    لتثبيت: