diff --git a/discover/qml/DiscoverDrawer.qml b/discover/qml/DiscoverDrawer.qml index 0ab22a5a..a3f3db97 100644 --- a/discover/qml/DiscoverDrawer.qml +++ b/discover/qml/DiscoverDrawer.qml @@ -1,179 +1,180 @@ /*************************************************************************** * Copyright © 2015 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.1 import org.kde.discover 2.0 import org.kde.discover.app 1.0 import org.kde.kirigami 2.0 as Kirigami import "navigation.js" as Navigation Kirigami.GlobalDrawer { id: drawer property bool wideScreen: false bannerImageSource: "qrc:/banners/banner.svg" //make the left and bottom margins for search field the same topPadding: drawer.wideScreen ? -toploader.height - leftPadding : 0 bottomPadding: 0 resetMenuOnTriggered: false onBannerClicked: { Navigation.openHome(); if (modal) drawerOpen = false } property string currentSearchText onCurrentSubMenuChanged: { if (currentSubMenu) currentSubMenu.trigger() else if (currentSearchText.length > 0) window.leftPage.category = null else Navigation.openHome() } function suggestSearchText(text) { toploader.item.text = text toploader.item.forceActiveFocus() } topContent: ConditionalLoader { id: toploader condition: drawer.wideScreen Layout.fillWidth: true componentFalse: Item { Layout.minimumHeight: 1 } componentTrue: SearchField { id: searchField visible: window.leftPage && (window.leftPage.searchFor != null || window.leftPage.hasOwnProperty("search")) page: window.leftPage onCurrentSearchTextChanged: { var curr = window.leftPage; if (!curr.hasOwnProperty("search")) { Navigation.clearStack() Navigation.openApplicationList( { search: currentSearchText }) } else { + pageStack.currentIndex = 0 curr.search = currentSearchText; curr.forceActiveFocus() } } } } ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -drawer.leftPadding Layout.rightMargin: -drawer.rightPadding Kirigami.Separator { Layout.fillWidth: true } ProgressView { separatorVisible: false } ActionListItem { action: searchAction } ActionListItem { action: installedAction } ActionListItem { action: settingsAction } ActionListItem { objectName: "updateButton" action: updateAction backgroundColor: ResourcesModel.updatesCount>0 ? "orange" : Kirigami.Theme.viewBackgroundColor } states: [ State { name: "full" when: drawer.wideScreen PropertyChanges { target: drawer; drawerOpen: true } }, State { name: "compact" when: !drawer.wideScreen PropertyChanges { target: drawer; drawerOpen: false } } ] } function rootCategory(cat) { var ret = null while (cat) { ret = cat cat = cat.parent } return ret } Component { id: categoryActionComponent Kirigami.Action { property QtObject category readonly property bool itsMe: window.leftPage && window.leftPage.hasOwnProperty("category") && (window.leftPage.category == category) text: category.name 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 { window.leftPage.category = category pageStack.currentIndex = 0 } } } } function createCategoryActions(categories) { var actions = [] for(var i in categories) { var cat = categories[i]; var catAction = categoryActionComponent.createObject(drawer, {category: cat}); catAction.children = createCategoryActions(cat.subcategories); actions.push(catAction) } return actions; } actions: createCategoryActions(CategoryModel.rootCategories) modal: !drawer.wideScreen handleVisible: !drawer.wideScreen } diff --git a/discover/qml/SearchField.qml b/discover/qml/SearchField.qml index 4db3a3b2..56212316 100644 --- a/discover/qml/SearchField.qml +++ b/discover/qml/SearchField.qml @@ -1,80 +1,82 @@ /*************************************************************************** * Copyright © 2017 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ import QtQuick 2.5 import QtQuick.Controls 1.1 as QQC1 import QtQuick.Controls 2.1 import org.kde.kirigami 2.1 as Kirigami TextField { id: searchField property QtObject page property string currentSearchText placeholderText: (!enabled || !page || page.hasOwnProperty("isHome") || page.title.length === 0) ? i18n("Search...") : i18n("Search in '%1'...", window.leftPage.title) Shortcut { sequence: "Ctrl+F" onActivated: { searchField.forceActiveFocus() searchField.selectAll() } } - onAccepted: currentSearchText = text + onAccepted: { + currentSearchText = text + } hoverEnabled: true ToolTip { delay: Kirigami.Units.longDuration visible: hovered text: searchAction.shortcut } + function clearText() { + searchField.text = "" + searchField.accepted() + } + QQC1.ToolButton { anchors { top: parent.top right: parent.right bottom: parent.bottom margins: Kirigami.Units.smallSpacing } iconName: "edit-clear" visible: searchField.text != "" - onClicked: { - searchField.text = "" - searchField.accepted() - } + onClicked: clearText() } Connections { ignoreUnknownSignals: true target: page - onClearSearch: { - searchField.text = "" - } + onClearSearch: clearText() } Connections { target: applicationWindow() onCurrentTopLevelChanged: { if (applicationWindow().currentTopLevel.length > 0) - searchField.text = "" + clearText() } } } diff --git a/discover/qml/Shadow.qml b/discover/qml/Shadow.qml index b74bbfa4..1d0548c2 100644 --- a/discover/qml/Shadow.qml +++ b/discover/qml/Shadow.qml @@ -1,48 +1,48 @@ /* * Copyright 2018 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.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: Qt.rgba(1, 1, 1, 1) + color: Theme.backgroundColor } GradientStop { position: 0.3 color: Qt.rgba(0, 0, 0, 0.1) } GradientStop { position: 1.0 color: "transparent" } } } diff --git a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp index d6140bdf..802c9aa7 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp @@ -1,1236 +1,1227 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "FlatpakBackend.h" #include "FlatpakFetchDataJob.h" #include "FlatpakResource.h" #include "FlatpakSourcesBackend.h" #include "FlatpakTransaction.h" #include #include #include #include #include #include #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) static QString idForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref) { const FlatpakResource::ResourceType appType = flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_APP ? FlatpakResource::DesktopApp : FlatpakResource::Runtime; const QString name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); const QString appId = appType == FlatpakResource::DesktopApp ? QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + QStringLiteral(".desktop") : name; 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 QStringLiteral("%1/%2/%3/%4/%5/%6").arg(FlatpakResource::installationPath(installation), QLatin1String("flatpak"), QString::fromUtf8(flatpak_installed_ref_get_origin(ref)), FlatpakResource::typeAsString(appType), appId, branch); } FlatpakBackend::FlatpakBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new StandardBackendUpdater(this)) , m_reviews(AppStreamIntegration::global()->reviews()) , m_fetching(false) , m_refreshAppstreamMetadataJobs(0) , m_threadPool(new QThreadPool(this)) { g_autoptr(GError) error = nullptr; m_cancellable = g_cancellable_new(); connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged); // Load flatpak installation if (!setupFlatpakInstallations(&error)) { qWarning() << "Failed to setup flatpak installations:" << error->message; } else { loadAppsFromAppstreamData(); m_sources = new FlatpakSourcesBackend(m_installations, this); SourcesModel::global()->addSourcesBackend(m_sources); } connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, &FlatpakBackend::announceRatingsReady); } FlatpakBackend::~FlatpakBackend() { m_threadPool.clear(); for(auto inst : m_installations) g_object_unref(inst); g_object_unref(m_cancellable); } bool FlatpakBackend::isValid() const { return m_sources && !m_installations.isEmpty(); } void FlatpakBackend::announceRatingsReady() { emitRatingsReady(); const auto ids = m_reviews->appstreamIds().toSet(); foreach(AbstractResource* res, m_resources) { if (ids.contains(res->appstreamId())) { res->ratingFetched(); } } } class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager { Q_OBJECT public: FlatpakFetchRemoteResourceJob(const QUrl &url, FlatpakBackend *backend) : QNetworkAccessManager(backend) , m_backend(backend) , m_url(url) { } void start() { auto replyGet = get(QNetworkRequest(m_url)); connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] { const QUrl originalUrl = replyGet->request().url(); if (replyGet->error() != QNetworkReply::NoError) { qWarning() << "couldn't download" << originalUrl << replyGet->errorString(); Q_EMIT jobFinished(false, nullptr); return; } const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1Char('/') + originalUrl.fileName()); auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll()); connect(replyPut, &QNetworkReply::finished, this, [this, originalUrl, fileUrl, replyPut]() { if (replyPut->error() == QNetworkReply::NoError) { auto res = m_backend->resourceForFile(fileUrl); if (res) { FlatpakResource *resource = qobject_cast(res); resource->setResourceFile(originalUrl); Q_EMIT jobFinished(true, resource); } else { qWarning() << "couldn't download" << originalUrl << "into" << fileUrl << replyPut->errorString(); Q_EMIT jobFinished(false, nullptr); } } }); }); } Q_SIGNALS: void jobFinished(bool success, FlatpakResource *resource); private: FlatpakBackend *m_backend; 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(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) const { FlatpakInstalledRef *ref = nullptr; g_autoptr(GError) localError = nullptr; if (!flatpakInstallation) { return ref; } const auto type = resource->type() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME; return flatpak_installation_get_installed_ref(flatpakInstallation, type, resource->flatpakName().toUtf8().constData(), resource->arch().toUtf8().constData(), resource->branch().toUtf8().constData(), m_cancellable, &localError); } FlatpakResource * FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *flatpakInstallation, FlatpakInstalledRef *ref) const { return m_resources.value(idForInstalledRef(flatpakInstallation, ref)); } FlatpakResource * FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const { FlatpakResource *runtime = nullptr; const auto runtimeInfo = resource->runtime().split(QLatin1Char('/')); if (runtimeInfo.count() != 3) { return runtime; } const QString runtimeId = QStringLiteral("runtime/") + runtimeInfo.at(0) + QLatin1Char('/') + runtimeInfo.at(2); for(auto it = m_resources.constBegin(), itEnd = m_resources.constEnd(); it!=itEnd; ++it) { if (it.key().endsWith(runtimeId)) { runtime = *it; break; } } // TODO if runtime wasn't found, create a new one from available info return runtime; } FlatpakResource * FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url) { 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 nullptr; } appstreamGz = flatpak_bundle_ref_get_appstream(bundleRef); if (appstreamGz) { 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 nullptr; } streamData = g_converter_input_stream_new (streamGz, G_CONVERTER (decompressor)); appstream = g_input_stream_read_bytes (streamData, 0x100000, m_cancellable, &localError); if (!appstream) { qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message; return nullptr; } gsize len = 0; gconstpointer data = g_bytes_get_data(appstream, &len); AppStream::Metadata metadata; metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); 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 nullptr; } QList components = metadata.components(); if (components.size()) { asComponent = AppStream::Component(components.first()); } else { qWarning() << "Failed to parse appstream metadata"; return nullptr; } } else { asComponent = AppStream::Component(); qWarning() << "No appstream metadata in bundle"; } gsize len = 0; g_autoptr(GBytes) iconData = nullptr; g_autoptr(GBytes) metadata = nullptr; FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this); metadata = flatpak_bundle_ref_get_metadata(bundleRef); QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len)); if (!updateAppMetadata(resource, metadataContent)) { delete resource; qWarning() << "Failed to update metadata from app bundle"; return nullptr; } iconData = flatpak_bundle_ref_get_icon(bundleRef, 128); if (!iconData) { iconData = flatpak_bundle_ref_get_icon(bundleRef, 64); } if (iconData) { gsize len = 0; QPixmap pixmap; char * data = (char *)g_bytes_get_data(iconData, &len); QByteArray icon = QByteArray(data, len); pixmap.loadFromData(icon, "PNG"); resource->setBundledIcon(pixmap); } const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(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(QStringLiteral("flatpak")); resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin); resource->setResourceFile(url); resource->setState(FlatpakResource::None); resource->setType(FlatpakResource::DesktopApp); addResource(resource); return resource; } FlatpakResource * FlatpakBackend::addAppFromFlatpakRef(const QUrl &url) { QSettings settings(url.toLocalFile(), QSettings::NativeFormat); const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString(); g_autoptr(GError) error = NULL; g_autoptr(FlatpakRemoteRef) remoteRef = nullptr; { QFile f(url.toLocalFile()); if (!f.open(QFile::ReadOnly | QFile::Text)) { return nullptr; } QByteArray contents = f.readAll(); g_autoptr(GBytes) bytes = g_bytes_new (contents.data(), contents.size()); remoteRef = flatpak_installation_install_ref_file (preferredInstallation(), bytes, m_cancellable, &error); if (!remoteRef) { qWarning() << "Failed to install ref file:" << error->message; return nullptr; } } const auto remoteName = flatpak_remote_ref_get_remote_name(remoteRef); auto ref = FLATPAK_REF(remoteRef); 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(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); 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(QStringLiteral("flatpakref")); resource->setOrigin(QString::fromUtf8(remoteName)); resource->updateFromRef(ref); QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString()); if (!runtimeUrl.isEmpty()) { auto installation = preferredInstallation(); // We need to fetch metadata to find information about required runtime auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource)); connect(fw, &QFutureWatcher::finished, this, [this, installation, resource, fw, runtimeUrl]() { 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(installation, resource, metadata); } else { updateAppMetadata(resource, metadata); auto runtime = getRuntimeForApp(resource); if (!runtime || (runtime && !runtime->isInstalled())) { FlatpakFetchRemoteResourceJob *fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, this); connect(fetchRemoteResource, &FlatpakFetchRemoteResourceJob::jobFinished, this, [this, resource] (bool success, FlatpakResource *repoResource) { if (success) { installApplication(repoResource); } addResource(resource); }); fetchRemoteResource->start(); return; } else { addResource(resource); } } fw->deleteLater(); }); } else { addResource(resource); } return resource; } FlatpakResource * FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url) { 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 nullptr; } if (gpgKey.startsWith(QStringLiteral("http://")) || gpgKey.startsWith(QStringLiteral("https://"))) { return nullptr; } 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 Ref/Name")).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); auto 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); } return resource; } void FlatpakBackend::addResource(FlatpakResource *resource) { // Update app with all possible information we have if (!parseMetadataFromAppBundle(resource)) { qWarning() << "Failed to parse metadata from app bundle for" << resource->name(); } auto installation = resource->installation(); updateAppState(installation, resource); // This will update also metadata (required runtime) updateAppSize(installation, resource); m_resources.insert(resource->uniqueId(), resource); } bool FlatpakBackend::compareAppFlatpakRef(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource, FlatpakInstalledRef *ref) const { return resource->uniqueId() == idForInstalledRef(flatpakInstallation, ref); } class FlatpakSource { public: FlatpakSource(FlatpakRemote* remote) : m_remote(remote) {} bool isEnabled() const { return !flatpak_remote_get_disabled(m_remote); } QString appstreamDir() const { 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 {}; } return QString::fromUtf8(g_file_get_path(appstreamDir)); } QString name() const { return QString::fromUtf8(flatpak_remote_get_name(m_remote)); } private: FlatpakRemote* m_remote; }; void FlatpakBackend::finishInitialization() { loadInstalledApps(); checkForUpdates(); } void FlatpakBackend::loadAppsFromAppstreamData() { for (auto installation : qAsConst(m_installations)) { // Load applications from appstream metadata if (!loadAppsFromAppstreamData(installation)) { qWarning() << "Failed to load packages from appstream data from installation" << installation; } } } bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation) { Q_ASSERT(flatpakInstallation); GPtrArray *remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, nullptr); if (!remotes) { return false; } m_refreshAppstreamMetadataJobs += remotes->len; for (uint i = 0; i < remotes->len; i++) { FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, nullptr); QFileInfo fileInfo = QFileInfo(QString::fromUtf8(g_file_get_path(fileTimestamp))); // Refresh appstream metadata in case they have never been refreshed or the cache is older than 6 hours if (!fileInfo.exists() || fileInfo.lastModified().toUTC().secsTo(QDateTime::currentDateTimeUtc()) > 21600) { refreshAppstreamMetadata(flatpakInstallation, remote); } else { integrateRemote(flatpakInstallation, remote); } } return true; } void FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote) { m_refreshAppstreamMetadataJobs--; FlatpakSource source(remote); if (!source.isEnabled() || flatpak_remote_get_noenumerate(remote)) { return; } const QString appstreamDirPath = source.appstreamDir(); const QString appstreamIconsPath = source.appstreamDir() + QLatin1String("/icons/"); const QString appDirFileName = appstreamDirPath + QLatin1String("/appstream.xml.gz"); if (!QFile::exists(appDirFileName)) { qWarning() << "No" << appDirFileName << "appstream metadata found for" << source.name(); return; } auto fw = new QFutureWatcher>(this); fw->setFuture(QtConcurrent::run(&m_threadPool, [appDirFileName]() -> QList { AppStream::Metadata metadata; metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); AppStream::Metadata::MetadataError error = metadata.parseFile(appDirFileName, AppStream::Metadata::FormatKindXml); if (error != AppStream::Metadata::MetadataErrorNoError) { qWarning() << "Failed to parse appstream metadata: " << error; return {}; } return metadata.components(); })); const auto sourceName = source.name(); connect(fw, &QFutureWatcher>::finished, this, [this, fw, flatpakInstallation, appstreamIconsPath, sourceName]() { const auto components = fw->result(); foreach (const AppStream::Component& appstreamComponent, components) { FlatpakResource *resource = new FlatpakResource(appstreamComponent, flatpakInstallation, this); resource->setIconPath(appstreamIconsPath); resource->setOrigin(sourceName); addResource(resource); } if (!m_refreshAppstreamMetadataJobs) { finishInitialization(); } fw->deleteLater(); }); } void FlatpakBackend::loadInstalledApps() { for (auto installation : qAsConst(m_installations)) { // Load installed applications and update existing resources with info from installed application if (!loadInstalledApps(installation)) { qWarning() << "Failed to load installed packages from installation" << installation; } } } bool FlatpakBackend::loadInstalledApps(FlatpakInstallation *flatpakInstallation) { Q_ASSERT(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 updates:" << localError->message; return false; } const QString pathExports = FlatpakResource::installationPath(flatpakInstallation) + QLatin1String("/exports/"); const QString pathApps = pathExports + QLatin1String("share/applications/"); for (uint i = 0; i < refs->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); if (flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_RUNTIME) { continue; } const auto name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); 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) { qWarning() << "Failed to parse appstream metadata: " << error << fnDesktop; continue; } - AppStream::Component appstreamComponent(metadata.component()); + const AppStream::Component appstreamComponent(metadata.component()); + + const auto res = getAppForInstalledRef(flatpakInstallation, ref); + if (res) { + res->setState(AbstractResource::Installed); + continue; + } + FlatpakResource *resource = new FlatpakResource(appstreamComponent, flatpakInstallation, this); resource->setIconPath(pathExports); resource->setState(AbstractResource::Installed); resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(ref))); resource->updateFromRef(FLATPAK_REF(ref)); - // Go through apps we already know about from appstream metadata - bool resourceExists = false; - foreach (FlatpakResource *res, m_resources) { - // Compare the only information we have - if (res->appstreamId() == QStringLiteral("%1.desktop").arg(resource->appstreamId()) && res->name() == resource->name()) { - resourceExists = true; - res->setState(resource->state()); - break; - } - } - - if (!resourceExists) { - addResource(resource); - } else { - emit resourceRemoved(resource); - resource->deleteLater(); - } + addResource(resource); } return true; } void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation) { g_autoptr(GError) localError = nullptr; g_autoptr(GPtrArray) refs = nullptr; refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing 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(flatpakInstallation, resource); } } } void FlatpakBackend::loadRemoteUpdates(FlatpakInstallation* installation) { auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, [installation]() -> GPtrArray * { g_autoptr(GCancellable) cancellable = g_cancellable_new(); g_autoptr(GError) localError = nullptr; 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; } return refs; })); connect(fw, &QFutureWatcher::finished, this, [this, installation, fw](){ auto refs = fw->result(); onFetchUpdatesFinished(installation, refs); fw->deleteLater(); }); } void FlatpakBackend::onFetchUpdatesFinished(FlatpakInstallation *flatpakInstallation, GPtrArray *updates) { g_autoptr(GPtrArray) fetchedUpdates = updates; for (uint i = 0; i < fetchedUpdates->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(fetchedUpdates, i)); FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); if (resource) { resource->setState(AbstractResource::Upgradeable); updateAppSize(flatpakInstallation, resource); } } } bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource) { g_autoptr(FlatpakRef) ref = nullptr; g_autoptr(GError) localError = nullptr; AppStream::Bundle bundle = resource->appstreamComponent().bundle(AppStream::Bundle::KindFlatpak); // Get arch/branch/commit/name from FlatpakRef if (!bundle.isEmpty()) { ref = flatpak_ref_parse(bundle.id().toUtf8().constData(), &localError); if (!ref) { qWarning() << "Failed to parse" << bundle.id() << localError->message; return false; } else { resource->updateFromRef(ref); } } return true; } class FlatpakRefreshAppstreamMetadataJob : public QThread { Q_OBJECT public: FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote) : QThread() , m_cancellable(g_cancellable_new()) , m_installation(installation) , m_remote(remote) { connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater); } ~FlatpakRefreshAppstreamMetadataJob() { g_object_unref(m_cancellable); } void cancel() { g_cancellable_cancel(m_cancellable); } void run() override { 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 qWarning() << "Failed to refresh appstream metadata for " << flatpak_remote_get_name(m_remote) << ": " << (localError ? localError->message : ""); Q_EMIT jobRefreshAppstreamMetadataFailed(); } else { Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote); } } Q_SIGNALS: void jobRefreshAppstreamMetadataFailed(); void jobRefreshAppstreamMetadataFinished(FlatpakInstallation *installation, FlatpakRemote *remote); private: GCancellable *m_cancellable; FlatpakInstallation *m_installation; FlatpakRemote *m_remote; }; void FlatpakBackend::refreshAppstreamMetadata(FlatpakInstallation *installation, FlatpakRemote *remote) { FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFailed, this, [this] () { m_refreshAppstreamMetadataJobs--; }); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote); job->start(); } bool FlatpakBackend::setupFlatpakInstallations(GError **error) { 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++) { m_installations << FLATPAK_INSTALLATION(g_ptr_array_index(installations, i)); } 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(FlatpakInstallation* flatpakInstallation, FlatpakResource *resource) { QByteArray metadataContent; g_autoptr(GFile) installationPath = nullptr; if (resource->type() != FlatpakResource::DesktopApp) { return true; } installationPath = flatpak_installation_get_path(flatpakInstallation); const QString path = QString::fromUtf8(g_file_get_path(installationPath)) + QStringLiteral("/app/%1/%2/%3/active/metadata").arg(resource->flatpakName()).arg(resource->arch()).arg(resource->branch()); if (QFile::exists(path)) { return updateAppMetadata(resource, path); } else { auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource)); connect(fw, &QFutureWatcher::finished, this, [this, flatpakInstallation, resource, fw]() { const auto metadata = fw->result(); if (!metadata.isEmpty()) onFetchMetadataFinished(flatpakInstallation, resource, metadata); fw->deleteLater(); }); // Return false to indicate we cannot continue (right now used only in updateAppSize()) return false; } } void FlatpakBackend::onFetchMetadataFinished(FlatpakInstallation *flatpakInstallation, 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 betwen these calls updateAppSizeFromRemote(flatpakInstallation, 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) { // Save the content to temporary file QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qWarning() << "Failed to get metadata file"; return false; } tempFile.write(data); tempFile.close(); updateAppMetadata(resource, tempFile.fileName()); tempFile.remove(); return true; } bool FlatpakBackend::updateAppSize(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { // Check if the size is already set, we should also distiguish 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(flatpakInstallation, resource)) { return false; } } return updateAppSizeFromRemote(flatpakInstallation, resource); } bool FlatpakBackend::updateAppSizeFromRemote(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { // Calculate the runtime size if (resource->state() == AbstractResource::None && resource->type() == FlatpakResource::DesktopApp) { auto runtime = getRuntimeForApp(resource); if (runtime) { // Re-check runtime state if case a new one was created updateAppState(flatpakInstallation, runtime); if (!runtime->isInstalled()) { if (!updateAppSize(flatpakInstallation, 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(flatpakInstallation, 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->origin().isEmpty()) { qWarning() << "Failed to get size of" << resource->name() << " because of missing origin"; return false; } auto futureWatcher = new QFutureWatcher(this); futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchFlatpakSize, flatpakInstallation, resource)); connect(futureWatcher, &QFutureWatcher::finished, this, [this, resource, futureWatcher]() { auto value = futureWatcher->result(); if (value.valid) { onFetchSizeFinished(resource, value.downloadSize, value.installedSize); } else { resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed); resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed); } futureWatcher->deleteLater(); }); } return true; } void FlatpakBackend::onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize) { FlatpakResource *runtime = nullptr; if (resource->state() == AbstractResource::None && resource->type() == FlatpakResource::DesktopApp) { runtime = getRuntimeForApp(resource); } if (runtime && !runtime->isInstalled()) { resource->setDownloadSize(runtime->downloadSize() + downloadSize); resource->setInstalledSize(installedSize); } else { resource->setDownloadSize(downloadSize); resource->setInstalledSize(installedSize); } } void FlatpakBackend::updateAppState(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { FlatpakInstalledRef *ref = getInstalledRefForApp(flatpakInstallation, 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 actuall still available resource->setState(AbstractResource::None); } } void FlatpakBackend::setFetching(bool fetching) { if (m_fetching != fetching) { m_fetching = fetching; emit fetchingChanged(); } } int FlatpakBackend::updatesCount() const { return m_updater->updatesCount(); } ResultsStream * FlatpakBackend::search(const AbstractResourcesBackend::Filters &filter) { if (filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpakrepo")) || filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpakref"))) { auto stream = new ResultsStream(QStringLiteral("FlatpakStream-http-")+filter.resourceUrl.fileName()); FlatpakFetchRemoteResourceJob *fetchResourceJob = new FlatpakFetchRemoteResourceJob(filter.resourceUrl, this); connect(fetchResourceJob, &FlatpakFetchRemoteResourceJob::jobFinished, this, [fetchResourceJob, stream] (bool success, FlatpakResource *resource) { if (success) { stream->resourcesFound({resource}); } stream->finish(); fetchResourceJob->deleteLater(); }); fetchResourceJob->start(); return stream; } else if (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("appstream")) return new ResultsStream(QStringLiteral("FlatpakStream-void"), {}); QVector ret; foreach(AbstractResource* r, m_resources) { if (r->isTechnical() && filter.state != AbstractResource::Upgradeable) { continue; } if (!filter.resourceUrl.isEmpty() && filter.resourceUrl.host().compare(r->appstreamId(), Qt::CaseInsensitive) != 0) continue; 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; } } return new ResultsStream(QStringLiteral("FlatpakStream"), ret); } ResultsStream * FlatpakBackend::findResourceByPackageName(const QUrl &url) { QVector resources; if (url.scheme() == QLatin1String("appstream")) { if (url.host().isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { foreach(FlatpakResource* res, m_resources) { if (QString::compare(res->appstreamId(), url.host(), Qt::CaseInsensitive)==0) resources << res; } } } return new ResultsStream(QStringLiteral("FlatpakStream"), resources); } AbstractBackendUpdater * FlatpakBackend::backendUpdater() const { return m_updater; } AbstractReviewsBackend * FlatpakBackend::reviewsBackend() const { return m_reviews.data(); } Transaction* FlatpakBackend::installApplication(AbstractResource *app, const AddonList &addons) { Q_UNUSED(addons); FlatpakResource *resource = qobject_cast(app); if (resource->type() == FlatpakResource::Source) { // Let source backend handle this FlatpakRemote *remote = m_sources->installSource(resource); if (remote) { resource->setState(AbstractResource::Installed); // Make sure we update appstream metadata first // FIXME we have to let flatpak to return the remote as the one created by FlatpakSourcesBackend will not have appstream directory refreshAppstreamMetadata(preferredInstallation(), flatpak_installation_get_remote_by_name(preferredInstallation(), flatpak_remote_get_name(remote), nullptr, nullptr)); } return nullptr; } FlatpakTransaction *transaction = nullptr; FlatpakInstallation *installation = resource->installation(); if (resource->propertyState(FlatpakResource::RequiredRuntime) == FlatpakResource::NotKnownYet && resource->type() == FlatpakResource::DesktopApp) { transaction = new FlatpakTransaction(resource, Transaction::InstallRole, true); connect(resource, &FlatpakResource::propertyStateChanged, [resource, transaction, this] (FlatpakResource::PropertyKind kind, FlatpakResource::PropertyState state) { if (kind != FlatpakResource::RequiredRuntime) { return; } if (state == FlatpakResource::AlreadyKnown) { FlatpakResource *runtime = getRuntimeForApp(resource); if (runtime && !runtime->isInstalled()) { transaction->setRuntime(runtime); } } transaction->start(); }); } else { FlatpakResource *runtime = getRuntimeForApp(resource); if (runtime && !runtime->isInstalled()) { transaction = new FlatpakTransaction(resource, runtime, Transaction::InstallRole); } else { transaction = new FlatpakTransaction(resource, Transaction::InstallRole); } } connect(transaction, &FlatpakTransaction::statusChanged, [this, installation, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { updateAppState(installation, resource); } }); return transaction; } Transaction* FlatpakBackend::installApplication(AbstractResource *app) { return installApplication(app, {}); } Transaction* FlatpakBackend::removeApplication(AbstractResource *app) { FlatpakResource *resource = qobject_cast(app); if (resource->type() == FlatpakResource::Source) { // Let source backend handle this if (m_sources->removeSource(resource->flatpakName())) { resource->setState(AbstractResource::None); } return nullptr; } FlatpakInstallation *installation = resource->installation(); FlatpakTransaction *transaction = new FlatpakTransaction(resource, Transaction::RemoveRole); connect(transaction, &FlatpakTransaction::statusChanged, [this, installation, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { updateAppSize(installation, resource); } }); return transaction; } void FlatpakBackend::checkForUpdates() { for (auto installation : qAsConst(m_installations)) { // Load local updates, comparing current and latest commit loadLocalUpdates(installation); // Load updates from remote repositories loadRemoteUpdates(installation); } } AbstractResource * FlatpakBackend::resourceForFile(const QUrl &url) { if (!url.isLocalFile()) { return nullptr; } FlatpakResource *resource = nullptr; if (url.path().endsWith(QLatin1String(".flatpak"))) { resource = addAppFromFlatpakBundle(url); } else if (url.path().endsWith(QLatin1String(".flatpakref"))) { resource = addAppFromFlatpakRef(url); } else if (url.path().endsWith(QLatin1String(".flatpakrepo"))) { resource = addSourceFromFlatpakRepo(url); } return resource; } QString FlatpakBackend::displayName() const { return QStringLiteral("Flatpak"); } #include "FlatpakBackend.moc" diff --git a/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp b/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp index 4d87a63c..9353cc3b 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakResource.cpp @@ -1,545 +1,547 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "FlatpakResource.h" #include "FlatpakBackend.h" #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)).arg(icon.url().fileName()); } FlatpakResource::FlatpakResource(const AppStream::Component &component, FlatpakInstallation* installation, FlatpakBackend *parent) : AbstractResource(parent) , m_appdata(component) , m_downloadSize(0) , m_installedSize(0) , m_propertyStates({{DownloadSize, NotKnownYet}, {InstalledSize, NotKnownYet},{RequiredRuntime, NotKnownYet}}) , m_installation(installation) , m_state(AbstractResource::None) , m_type(FlatpakResource::DesktopApp) { + setObjectName(component.id()); + // Start fetching remote icons during initialization const auto icons = m_appdata.icons(); if (!icons.isEmpty()) { foreach (const AppStream::Icon &icon, icons) { if (icon.kind() == AppStream::Icon::KindRemote) { 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")); QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, [this, icon, fileName, manager] (QNetworkReply *reply) { if (reply->error() == QNetworkReply::NoError) { QByteArray iconData = reply->readAll(); QFile file(fileName); if (file.open(QIODevice::WriteOnly)) { file.write(iconData); } file.close(); Q_EMIT iconChanged(); } manager->deleteLater(); }); manager->get(QNetworkRequest(icon.url())); } } } } } AppStream::Component FlatpakResource::appstreamComponent() const { return m_appdata; } QList FlatpakResource::addonsInformation() { return m_addons; } QString FlatpakResource::availableVersion() const { QString theBranch = branch(); if (theBranch.isEmpty()) { theBranch = i18n("Unknown"); } if (!m_appdata.releases().isEmpty()) { auto release = m_appdata.releases().constFirst(); return i18n("%1 (%2)", release.version(), theBranch); } return theBranch; } QString FlatpakResource::appstreamId() const { return m_appdata.id(); } QString FlatpakResource::arch() const { return m_arch; } QString FlatpakResource::branch() const { return m_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 ? FlatpakResource::DesktopApp : FlatpakResource::Runtime); } 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; } int 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 foreach(const AppStream::Icon &icon, icons) { QStringList stock; switch (icon.kind()) { case AppStream::Icon::KindLocal: case AppStream::Icon::KindCached: { const QString path = m_iconPath + icon.url().path(); if (QFileInfo::exists(path)) { ret.addFile(path, icon.size()); } else { const QString altPath = m_iconPath + QStringLiteral("%1x%2/").arg(icon.size().width()).arg(icon.size().height()) + icon.url().path(); if (QFileInfo::exists(altPath)) { ret.addFile(altPath, icon.size()); } } } break; case AppStream::Icon::KindStock: stock += icon.name(); 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(stock.value(0), QIcon::fromTheme(QStringLiteral("package-x-generic"))); } } return ret; } QString FlatpakResource::installedVersion() const { // TODO check if there is actually version available QString version = branch(); if (version.isEmpty()) { version = i18n("Unknown"); } return version; } int FlatpakResource::installedSize() const { return m_installedSize; } bool FlatpakResource::isTechnical() const { return m_type == FlatpakResource::Runtime; } 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); } QString 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_appdata.id(); } return m_flatpakName; } QString FlatpakResource::license() { return m_appdata.projectLicense(); } QString FlatpakResource::longDescription() { return m_appdata.description(); } QString FlatpakResource::name() { QString name = m_appdata.name(); if (name.isEmpty()) { name = m_appdata.id(); } if (name.startsWith(QLatin1String("(Nightly) "))) { return name.mid(10); } return name; } QString FlatpakResource::origin() const { return m_origin; } QString FlatpakResource::packageName() const { return flatpakName(); } 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(); } int 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) { 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) { 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::type() const { return m_type; } QString FlatpakResource::typeAsString() const { switch (m_type) { case FlatpakResource::DesktopApp: case FlatpakResource::Source: return QLatin1String("app"); case FlatpakResource::Runtime: return QLatin1String("runtime"); default: return QLatin1String("app"); } } QString FlatpakResource::uniqueId() const { // Build uniqueId return QString::fromUtf8("%1/%2/%3/%4/%5/%6").arg(installationPath()) .arg(QLatin1String("flatpak")) .arg(origin()) .arg(typeAsString()) .arg(m_appdata.id()) .arg(branch()); } void FlatpakResource::invokeApplication() const { g_autoptr(GCancellable) cancellable = g_cancellable_new(); g_autoptr(GError) localError = nullptr; if (!flatpak_installation_launch(m_installation, flatpakName().toUtf8().constData(), arch().toUtf8().constData(), branch().toUtf8().constData(), nullptr, cancellable, &localError)) { qWarning() << "Failed to launch " << m_appdata.name() << ": " << localError->message; } } void FlatpakResource::fetchChangelog() { QString log = longDescription(); log.replace(QLatin1Char('\n'), QLatin1String("
")); emit changelogFetched(log); } void FlatpakResource::fetchScreenshots() { QList thumbnails, screenshots; Q_FOREACH (const AppStream::Screenshot &s, m_appdata.screenshots()) { const QUrl thumbnail = AppStreamUtils::imageOfKind(s.images(), AppStream::Image::KindThumbnail); const QUrl plain = AppStreamUtils::imageOfKind(s.images(), AppStream::Image::KindSource); if (plain.isEmpty()) qWarning() << "invalid screenshot for" << name(); screenshots << plain; thumbnails << (thumbnail.isEmpty() ? plain : thumbnail); } Q_EMIT screenshotsFetched(thumbnails, screenshots); } void FlatpakResource::setArch(const QString &arch) { m_arch = arch; } void FlatpakResource::setBranch(const QString &branch) { m_branch = branch; } void FlatpakResource::setBundledIcon(const QPixmap &pixmap) { m_bundledIcon = pixmap; } void FlatpakResource::setCommit(const QString &commit) { m_commit = commit; } void FlatpakResource::setDownloadSize(int size) { m_downloadSize = size; setPropertyState(DownloadSize, AlreadyKnown); Q_EMIT sizeChanged(); } void FlatpakResource::setFlatpakFileType(const QString &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(int size) { m_installedSize = size; setPropertyState(InstalledSize, AlreadyKnown); Q_EMIT sizeChanged(); } void FlatpakResource::setOrigin(const QString &origin) { m_origin = origin; } 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) { if (m_state != state) { m_state = state; 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); return QString::fromUtf8(g_file_get_path(path)); } QUrl FlatpakResource::url() const { return m_resourceFile.isEmpty() ? QUrl(QStringLiteral("appstream://") + appstreamId()) : m_resourceFile; } diff --git a/libdiscover/backends/FlatpakBackend/FlatpakResource.h b/libdiscover/backends/FlatpakBackend/FlatpakResource.h index 7dcb43e7..98b29bce 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakResource.h +++ b/libdiscover/backends/FlatpakBackend/FlatpakResource.h @@ -1,160 +1,160 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef FLATPAKRESOURCE_H #define FLATPAKRESOURCE_H #include extern "C" { #include } #include #include class AddonList; class FlatpakBackend; class FlatpakResource : public AbstractResource { Q_OBJECT public: explicit FlatpakResource(const AppStream::Component &component, FlatpakInstallation* installation, FlatpakBackend *parent); enum PropertyKind { DownloadSize = 0, InstalledSize, RequiredRuntime }; enum PropertyState { NotKnownYet = 0, AlreadyKnown, UnknownOrFailed, }; enum ResourceType { DesktopApp = 0, Runtime, Source }; 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; int downloadSize() const; QVariant icon() const override; QString installedVersion() const override; int installedSize() const; bool isTechnical() const override; QUrl homepage() override; QUrl helpURL() override; QUrl bugURL() override; QUrl donationURL() override; QString flatpakFileType() const; QString flatpakName() const; QString license() override; QString longDescription() override; QString name() override; QString origin() const override; QString packageName() const override; PropertyState propertyState(PropertyKind kind) const; QUrl resourceFile() const; QString runtime() const; QString section() override; int size() override; QString sizeDescription() override; AbstractResource::State state() override; ResourceType type() const; QString typeAsString() const; QString uniqueId() const; QUrl url() const override; FlatpakInstallation* installation() const { return m_installation; } void invokeApplication() const override; void fetchChangelog() override; void fetchScreenshots() override; void setArch(const QString &arch); void setBranch(const QString &branch); void setBundledIcon(const QPixmap &pixmap); void setCommit(const QString &commit); void setDownloadSize(int size); void setIconPath(const QString &path); void setInstalledSize(int size); void setFlatpakFileType(const QString &fileType); void setFlatpakName(const QString &name); void setOrigin(const QString &origin); void setPropertyState(PropertyKind kind, PropertyState state); void setResourceFile(const QUrl &url); void setRuntime(const QString &runtime); void setState(State state); void setType(ResourceType type); // void setAddons(const AddonList& addons); // void setAddonInstalled(const QString& addon, bool installed); void updateFromRef(FlatpakRef* ref); Q_SIGNALS: void propertyStateChanged(PropertyKind kind, PropertyState state); public: QList m_addons; - AppStream::Component m_appdata; + const AppStream::Component m_appdata; FlatpakRefKind m_flatpakRefKind; QString m_arch; QString m_branch; QPixmap m_bundledIcon; QString m_commit; int m_downloadSize; QString m_flatpakFileType; QString m_flatpakName; QString m_iconPath; int m_installedSize; QString m_origin; QHash m_propertyStates; QUrl m_resourceFile; QString m_runtime; FlatpakInstallation* const m_installation; AbstractResource::State m_state; ResourceType m_type; }; #endif // FLATPAKRESOURCE_H diff --git a/libdiscover/backends/KNSBackend/KNSBackend.cpp b/libdiscover/backends/KNSBackend/KNSBackend.cpp index 2a14dc2a..d0395c97 100644 --- a/libdiscover/backends/KNSBackend/KNSBackend.cpp +++ b/libdiscover/backends/KNSBackend/KNSBackend.cpp @@ -1,416 +1,417 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ // Qt includes #include #include #include #include #include #include // Attica includes #include #include // KDE includes #include #include #include #include #include #include // DiscoverCommon includes #include "Transaction/Transaction.h" #include "Transaction/TransactionModel.h" #include "Category/Category.h" // Own includes #include "KNSBackend.h" #include "KNSResource.h" #include "KNSReviews.h" #include #include "utils.h" class KNSBackendFactory : public AbstractResourcesBackendFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory") Q_INTERFACES(AbstractResourcesBackendFactory) public: KNSBackendFactory() { connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question* q) { qWarning() << q->question() << q->questionType(); q->setResponse(KNSCore::Question::InvalidResponse); }); } QVector newInstance(QObject* parent, const QString &/*name*/) const override { QVector ret; for (const QString &path: QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation)) { QDirIterator dirIt(path, {QStringLiteral("*.knsrc")}, QDir::Files); for(; dirIt.hasNext(); ) { dirIt.next(); auto bk = new KNSBackend(parent, QStringLiteral("plasma"), dirIt.filePath()); ret += bk; } } return ret; } }; Q_DECLARE_METATYPE(KNSCore::EntryInternal) KNSBackend::KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc) : AbstractResourcesBackend(parent) , m_fetching(false) , m_isValid(true) , m_reviews(new KNSReviews(this)) , m_name(knsrc) , m_iconName(iconName) , m_updater(new StandardBackendUpdater(this)) { const QString fileName = QFileInfo(m_name).fileName(); setName(fileName); setObjectName(knsrc); const KConfig conf(m_name); if (!conf.hasGroup("KNewStuff3")) { markInvalid(QStringLiteral("Config group not found! Check your KNS3 installation.")); return; } m_categories = QStringList{ fileName }; const KConfigGroup group = conf.group("KNewStuff3"); m_extends = group.readEntry("Extends", QStringList()); m_reviews->setProviderUrl(QUrl(group.readEntry("ProvidersUrl", QString()))); setFetching(true); m_engine = new KNSCore::Engine(this); m_engine->init(m_name); #if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=36 m_engine->setPageSize(100); #endif // Setting setFetching to false when we get an error ensures we don't end up in an eternally-fetching state connect(m_engine, &KNSCore::Engine::signalError, this, [this](const QString &_error) { QString error = _error; if(error == QLatin1Literal("All categories are missing")) { markInvalid(error); error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); } m_responsePending = false; Q_EMIT searchFinished(); Q_EMIT availableForQueries(); this->setFetching(false); qWarning() << "kns error" << objectName() << error; passiveMessage(i18n("%1: %2", name(), error)); }); connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection); connect(m_engine, &KNSCore::Engine::signalEntryChanged, this, &KNSBackend::statusChanged, Qt::QueuedConnection); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::statusChanged); connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled); const QVector> filters = { {CategoryFilter, fileName } }; const QSet backendName = { name() }; m_displayName = group.readEntry("Name", QString()); if (m_displayName.isEmpty()) { m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.'))); m_displayName[0] = m_displayName[0].toUpper(); } static const QSet knsrcPlasma = { QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"), QStringLiteral("wallpaper.knsrc"), QStringLiteral("xcursor.knsrc"), QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), QStringLiteral("cgctheme.knsrc"), //GTK integration QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), QStringLiteral("kwinscripts.knsrc"), //KWin QStringLiteral("comic.knsrc"), QStringLiteral("colorschemes.knsrc"), QStringLiteral("emoticons.knsrc"), QStringLiteral("plymouth.knsrc"), QStringLiteral("sddmtheme.knsrc") }; auto actualCategory = new Category(m_displayName, QStringLiteral("plasma"), filters, backendName, {}, QUrl(), true); const auto topLevelName = knsrcPlasma.contains(fileName)? i18n("Plasma Addons") : i18n("Application Addons"); const QUrl decoration(knsrcPlasma.contains(fileName)? QStringLiteral("https://c2.staticflickr.com/4/3148/3042248532_20bd2e38f4_b.jpg") : QStringLiteral("https://c2.staticflickr.com/8/7067/6847903539_d9324dcd19_o.jpg")); auto addonsCategory = new Category(topLevelName, QStringLiteral("plasma"), filters, backendName, {actualCategory}, decoration, true); m_rootCategories = { addonsCategory }; } KNSBackend::~KNSBackend() = default; void KNSBackend::markInvalid(const QString &message) { qWarning() << "invalid kns backend!" << m_name << "because:" << message; m_isValid = false; setFetching(false); } void KNSBackend::fetchInstalled() { auto search = new OneTimeAction([this]() { Q_EMIT startingSearch(); m_onePage = true; m_responsePending = true; m_engine->checkForInstalled(); }, this); if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection); } else { search->trigger(); } } void KNSBackend::setFetching(bool f) { if(m_fetching!=f) { m_fetching = f; emit fetchingChanged(); } } 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) { r = new KNSResource(entry, m_categories, this); m_resourcesByName.insert(entry.uniqueId(), r); } else { r->setEntry(entry); } return r; } void KNSBackend::receivedEntries(const KNSCore::EntryInternal::List& entries) { m_responsePending = false; const auto resources = kTransform>(entries, [this](const KNSCore::EntryInternal& entry){ return resourceForEntry(entry); }); if (!resources.isEmpty()) { Q_EMIT receivedResources(resources); } if(resources.isEmpty()) { Q_EMIT searchFinished(); Q_EMIT availableForQueries(); setFetching(false); return; } // qDebug() << "received" << objectName() << this << m_resourcesByName.count(); if (!m_responsePending && !m_onePage) { // We _have_ to set this first. If we do not, we may run into a situation where the // data request will conclude immediately, causing m_responsePending to remain true // for perpetuity as the slots will be called before the function returns. m_responsePending = true; m_engine->requestMoreData(); } else { Q_EMIT availableForQueries(); + setFetching(false); } } void KNSBackend::statusChanged(const KNSCore::EntryInternal& entry) { resourceForEntry(entry); } class KNSTransaction : public Transaction { public: KNSTransaction(QObject* parent, KNSResource* res, Transaction::Role role) : Transaction(parent, res, role) , m_id(res->entry().uniqueId()) { setCancellable(false); auto manager = res->knsBackend()->engine(); connect(manager, &KNSCore::Engine::signalEntryChanged, this, &KNSTransaction::anEntryChanged); TransactionModel::global()->addTransaction(this); } void anEntryChanged(const KNSCore::EntryInternal& entry) { if (entry.uniqueId() == m_id) { switch (entry.status()) { case KNS3::Entry::Invalid: qWarning() << "invalid status for" << entry.uniqueId() << entry.status(); break; case KNS3::Entry::Installing: case KNS3::Entry::Updating: setStatus(CommittingStatus); break; case KNS3::Entry::Downloadable: case KNS3::Entry::Installed: case KNS3::Entry::Deleted: case KNS3::Entry::Updateable: if (status() != DoneStatus) { setStatus(DoneStatus); } break; } } } void cancel() override {} private: const QString m_id; }; Transaction* KNSBackend::removeApplication(AbstractResource* app) { auto res = qobject_cast(app); auto t = new KNSTransaction(this, res, Transaction::RemoveRole); m_engine->uninstall(res->entry()); return t; } Transaction* KNSBackend::installApplication(AbstractResource* app) { auto res = qobject_cast(app); m_engine->install(res->entry()); 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) { QVector ret; foreach(AbstractResource* r, m_resourcesByName) { if(r->state()>=filter.state && (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive))) ret += r; } return new ResultsStream(QStringLiteral("KNS-installed"), ret); } else if (filter.category && filter.category->matchesCategoryName(m_categories.constFirst())) { return searchStream(filter.search); } else if (!filter.category && !filter.search.isEmpty()) { return searchStream(filter.search); } return voidStream(); } ResultsStream* KNSBackend::searchStream(const QString &searchText) { Q_EMIT startingSearch(); auto stream = new ResultsStream(QStringLiteral("KNS-search-")+name()); auto start = [this, stream, searchText]() { // No need to explicitly launch a search, setting the search term already does that for us m_engine->setSearchTerm(searchText); m_onePage = false; m_responsePending = true; connect(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 { start(); } return stream; } ResultsStream * KNSBackend::findResourceByPackageName(const QUrl& search) { if (search.scheme() != QLatin1String("kns") || search.host() != name()) return voidStream(); const auto pathParts = search.path().split(QLatin1Char('/'), QString::SkipEmptyParts); if (pathParts.size() != 2) { 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(QStringLiteral("KNS-byname-")+entryid); auto start = [this, entryid, stream, providerid]() { m_responsePending = true; m_engine->fetchEntryById(entryid); m_onePage = false; connect(m_engine, &KNSCore::Engine::signalError, stream, &ResultsStream::finish); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, stream, [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry) { if (entry.uniqueId() == entryid && providerid == QUrl(entry.providerId()).host()) { stream->resourcesFound({resourceForEntry(entry)}); } m_responsePending = false; QTimer::singleShot(0, this, &KNSBackend::availableForQueries); stream->finish(); }); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start); } else { start(); } return stream; } bool KNSBackend::isFetching() const { return m_fetching; } AbstractBackendUpdater* KNSBackend::backendUpdater() const { return m_updater; } QString KNSBackend::displayName() const { return QStringLiteral("KNewStuff"); } #include "KNSBackend.moc" diff --git a/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp b/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp index d72198c2..0b8bc166 100644 --- a/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp +++ b/libdiscover/backends/KNSBackend/tests/KNSBackendTest.cpp @@ -1,159 +1,162 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "KNSBackendTest.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTEST_MAIN( KNSBackendTest ) KNSBackendTest::KNSBackendTest(QObject* parent) : QObject(parent) , m_r(nullptr) { QStandardPaths::setTestModeEnabled(true); ResourcesModel* model = new ResourcesModel(QLatin1String("kns-backend"), this); Q_ASSERT(!model->backends().isEmpty()); auto findTestBackend = [](AbstractResourcesBackend* backend) { return backend->name() == QLatin1String("discover_ktexteditor_codesnippets_core.knsrc"); }; m_backend = kFilter>(model->backends(), findTestBackend).at(0); if (!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) { Q_ASSERT(stream); Q_ASSERT(stream->objectName() != QLatin1String("KNS-void")); QSignalSpy spyResources(stream, &ResultsStream::destroyed); QVector resources; connect(stream, &ResultsStream::resourcesFound, this, [&resources](const QVector& res) { resources += res; }); Q_ASSERT(spyResources.wait(10000)); Q_ASSERT(!resources.isEmpty()); 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); foreach(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()); foreach(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() { const QUrl url(QStringLiteral("kns://") + m_backend->name() + QStringLiteral("/api.kde-look.org/1136471")); auto resources = getResources(m_backend->findResourceByPackageName(url)); const QVector res = kTransform>(resources, [](AbstractResource* res){ return res->url(); }); QCOMPARE(res.count(), 1); QCOMPARE(url, res.constFirst()); AbstractResourcesBackend::Filters f; f.resourceUrl = url; const QVector res2 = kTransform>(getResources(m_backend->search(f)), [](AbstractResource* res){ return res->url(); }); QCOMPARE(res, res2); 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()); }