diff --git a/discover/qml/DiscoverDrawer.qml b/discover/qml/DiscoverDrawer.qml index 3f26e406..5201dc6e 100644 --- a/discover/qml/DiscoverDrawer.qml +++ b/discover/qml/DiscoverDrawer.qml @@ -1,210 +1,201 @@ /*************************************************************************** * 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 2.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 // FIXME: Dirty workaround for 385992 width: Kirigami.Units.gridUnit * 14 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 (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 Layout.leftMargin: -drawer.leftPadding Layout.rightMargin: -drawer.rightPadding Kirigami.Separator { Layout.fillWidth: true } ProgressView { separatorVisible: false } ActionListItem { action: searchAction } ActionListItem { action: installedAction } ActionListItem { action: sourcesAction } ActionListItem { action: Kirigami.Action { text: i18n("Help") icon.name: "help-feedback" onTriggered: helpMenu.open() } readonly property var p0: Menu { id: helpMenu title: i18n("Help") MenuItem { action: ActionBridge { action: app.action("help_about_app") } } MenuItem { action: ActionBridge { action: app.action("help_report_bug") } } } } 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 ? 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 { if (pageStack.depth>1) pageStack.pop() pageStack.currentIndex = 0 window.leftPage.category = category } } } } 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/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp index f2d246f5..e035b7e5 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp @@ -1,1279 +1,1279 @@ /*************************************************************************** * 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 "FlatpakJobTransaction.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 FlatpakResource::Id 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 { installation, QString::fromUtf8(flatpak_installed_ref_get_origin(ref)), appType, appId, branch, arch }; } FlatpakBackend::FlatpakBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new StandardBackendUpdater(this)) , m_reviews(AppStreamIntegration::global()->reviews()) , m_refreshAppstreamMetadataJobs(0) , m_cancellable(g_cancellable_new()) , m_threadPool(new QThreadPool(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 { loadAppsFromAppstreamData(); m_sources = new FlatpakSourcesBackend(m_installations, this); SourcesModel::global()->addSourcesBackend(m_sources); } connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, &FlatpakBackend::announceRatingsReady); } FlatpakBackend::~FlatpakBackend() { g_cancellable_cancel(m_cancellable); m_threadPool.waitForDone(200); 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 create resource from" << fileUrl.toLocalFile(); Q_EMIT jobFinished(false, nullptr); } } else { qWarning() << "couldn't save" << originalUrl << 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->resourceType() == 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; } for(auto it = m_resources.constBegin(), itEnd = m_resources.constEnd(); it!=itEnd; ++it) { const auto id = it.key(); if (id.type == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) { runtime = *it; break; } } // TODO if runtime wasn't found, create a new one from available info if (!runtime) { qWarning() << "could not find runtime" << runtimeInfo << resource; } 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; } g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef); 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; } const QList components = metadata.components(); if (components.size()) { asComponent = AppStream::Component(components.first()); } else { qWarning() << "Failed to parse appstream metadata"; return nullptr; } } else { qWarning() << "No appstream metadata in bundle"; QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qWarning() << "Failed to get metadata file"; return nullptr; } gsize len = 0; QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len)); 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(); } FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this); gsize len = 0; 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; } 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->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 = nullptr; 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 create install ref file:" << error->message; const auto resources = resourcesByAppstreamName(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); if (!resources.isEmpty()) { return qobject_cast(resources.constFirst()); } 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(); }); + fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource)); } 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); if (!resource->extends().isEmpty()) { m_extends.append(resource->extends()); m_extends.removeDuplicates(); } } 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::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) { Q_ASSERT(m_refreshAppstreamMetadataJobs != 0); 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(); - acquireFetching(true); 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) { loadInstalledApps(); checkForUpdates(); } acquireFetching(false); fw->deleteLater(); }); + acquireFetching(true); + 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(); + })); } 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 res = getAppForInstalledRef(flatpakInstallation, ref); if (res) { res->setState(AbstractResource::Installed); 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; } auto component = metadata.component(); component.setId(name + QLatin1String(".desktop")); FlatpakResource *resource = new FlatpakResource(component, flatpakInstallation, this); resource->setIconPath(pathExports); resource->setState(AbstractResource::Installed); resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(ref))); resource->updateFromRef(FLATPAK_REF(ref)); 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); + connect(fw, &QFutureWatcher::finished, this, [this, installation, fw](){ + auto refs = fw->result(); + onFetchUpdatesFinished(installation, refs); + fw->deleteLater(); + }); fw->setFuture(QtConcurrent::run(&m_threadPool, [installation, this]() -> GPtrArray * { g_autoptr(GError) localError = nullptr; GPtrArray *refs = flatpak_installation_list_installed_refs_for_update(installation, m_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) { if (!updates) { qWarning() << "could not get updates for" << flatpakInstallation; return; } 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) { 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 true; } 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) { g_autoptr(GFile) installationPath = nullptr; if (resource->resourceType() != 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(); }); + fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource)); // 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) { //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(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->resourceType() == 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(); }); + futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchFlatpakSize, flatpakInstallation, resource)); } 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(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 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)) { 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 { return (l->isInstalled() != r->isInstalled()) ? l->isInstalled() : (l->origin() != r->origin()) ? m_sources->originIndex(l->origin()) < m_sources->originIndex(r->origin()) : l < r; } 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.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"), {}); auto stream = new ResultsStream(QStringLiteral("FlatpakStream")); auto f = [this, stream, filter] () { QVector ret; foreach(auto r, m_resources) { if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable) { continue; } if (r->state() < filter.state) continue; if (!filter.extends.isEmpty() && !r->extends().contains(filter.extends)) continue; if (filter.search.isEmpty() || r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)) { ret += r; } } auto f = [this](AbstractResource* l, AbstractResource* r) { return flatpakResourceLessThan(l,r); }; std::sort(ret.begin(), ret.end(), f); if (!ret.isEmpty()) Q_EMIT stream->resourcesFound(ret); stream->finish(); }; if (isFetching()) { connect(this, &FlatpakBackend::initialized, stream, f); } else { QTimer::singleShot(0, this, f); } return stream; } QVector FlatpakBackend::resourcesByAppstreamName(const QString& name) const { QVector resources; foreach(FlatpakResource* res, m_resources) { if (QString::compare(res->appstreamId(), name, Qt::CaseInsensitive)==0 || QString::compare(res->appstreamId(), name + QLatin1String(".desktop"), Qt::CaseInsensitive)==0) resources << res; } 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")) { if (url.host().isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { auto stream = new ResultsStream(QStringLiteral("FlatpakStream")); auto f = [this, stream, url] () { const auto resources = resourcesByAppstreamName(url.host()); 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; } } return new ResultsStream(QStringLiteral("FlatpakStream-packageName-void"), {}); } 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->resourceType() == FlatpakResource::Source) { // Let source backend handle this FlatpakRemote *remote = m_sources->installSource(resource); if (remote) { resource->setState(AbstractResource::Installed); m_refreshAppstreamMetadataJobs++; // 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; } FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::InstallRole); connect(transaction, &FlatpakJobTransaction::statusChanged, [this, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { FlatpakInstallation *installation = resource->installation(); 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->resourceType() == FlatpakResource::Source) { // Let source backend handle this if (m_sources->removeSource(resource->flatpakName())) { resource->setState(AbstractResource::None); } return nullptr; } FlatpakInstallation *installation = resource->installation(); FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::RemoveRole); connect(transaction, &FlatpakJobTransaction::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/resources/ResourcesUpdatesModel.cpp b/libdiscover/resources/ResourcesUpdatesModel.cpp index 79c50c74..77b0ec11 100644 --- a/libdiscover/resources/ResourcesUpdatesModel.cpp +++ b/libdiscover/resources/ResourcesUpdatesModel.cpp @@ -1,294 +1,294 @@ /*************************************************************************** * 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 "ResourcesUpdatesModel.h" #include #include #include "ResourcesModel.h" #include "AbstractBackendUpdater.h" #include "AbstractResource.h" #include "utils.h" #include #include "libdiscover_debug.h" #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; foreach(auto updater, 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::cancelableChanged, this, [this](bool cancelable){ if (cancelable) setCancellable(true); }); 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 { QVector toCancel = m_updatersWaitingForFeedback.isEmpty() ? m_allUpdaters : m_updatersWaitingForFeedback; foreach(auto updater, toCancel) { updater->cancel(); } } void proceed() override { m_updatersWaitingForFeedback.takeFirst()->proceed(); } bool isProgressing() const { bool progressing = false; foreach(AbstractBackendUpdater* upd, 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; foreach(AbstractBackendUpdater* updater, m_allUpdaters) { total += updater->progress(); } setProgress(total / m_allUpdaters.count()); } void slotDownloadSpeedChanged() { quint64 total = 0; foreach(AbstractBackendUpdater* updater, m_allUpdaters) { total += updater->downloadSpeed(); } setDownloadSpeed(total); } QVariant icon() const override { return QStringLiteral("update-low"); } QString name() const override { return i18n("Update"); } 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; foreach(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); m_updaters += updater; m_lastIsProgressing |= updater->isProgressing(); } } auto tm = TransactionModel::global(); foreach(auto t, tm->transactions()) { auto updateTransaction = qobject_cast(t); if (updateTransaction) { setTransaction(updateTransaction); } } } void ResourcesUpdatesModel::updaterDestroyed(QObject* obj) { m_updaters.removeAll(static_cast(obj)); } 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; } foreach(AbstractBackendUpdater* upd, m_updaters) { 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); Q_FOREACH (AbstractBackendUpdater* upd, updaters) { QMetaObject::invokeMethod(upd, "start", Qt::QueuedConnection); } QMetaObject::invokeMethod(this, [this](){ - m_transaction->setStatus(Transaction::QueuedStatus); + 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; foreach(AbstractBackendUpdater* upd, m_updaters) { ret += upd->toUpdate(); } return ret; } void ResourcesUpdatesModel::addResources(const QList& resources) { QHash > sortedResources; foreach(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< AbstractResource* >& resources) { QHash > sortedResources; foreach(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; foreach(AbstractBackendUpdater* upd, 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 += 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; } #include "ResourcesUpdatesModel.moc" diff --git a/notifier/plasmoid/metadata.desktop b/notifier/plasmoid/metadata.desktop index 4aa495ff..6f070bc1 100644 --- a/notifier/plasmoid/metadata.desktop +++ b/notifier/plasmoid/metadata.desktop @@ -1,103 +1,103 @@ [Desktop Entry] Name=Updates Name[ar]=التّحديثات Name[ca]=Actualitzacions Name[ca@valencia]=Actualitzacions Name[cs]=Aktualizace Name[da]=Opdateringer Name[de]=Aktualisierungen Name[el]=Ενημερώσεις Name[en_GB]=Updates Name[es]=Actualizaciones Name[et]=Uuendused Name[eu]=Eguneraketak Name[fi]=Päivitykset Name[fr]=Mises à jour Name[gl]=Actualizacións Name[he]=עדכונים Name[hu]=Frissítések Name[ia]=Actualisationes Name[id]=Update Name[it]=Aggiornamenti Name[ko]=업데이트 Name[nb]=Oppdateringer Name[nl]=Elementen voor bijwerken Name[nn]=Oppdateringar Name[pa]=ਅੱਪਡੇਟ Name[pl]=Uaktualnienia Name[pt]=Actualizações Name[pt_BR]=Atualizações Name[ru]=Обновления Name[sk]=Aktualizácie Name[sl]=Posodobitve Name[sr]=Допуне Name[sr@ijekavian]=Допуне Name[sr@ijekavianlatin]=Dopune Name[sr@latin]=Dopune Name[sv]=Uppdateringar Name[tr]=Güncellemeler Name[uk]=Оновлення Name[x-test]=xxUpdatesxx Name[zh_CN]=更新 Name[zh_TW]=更新 Comment=Helps you keep your system up to date Comment[ar]=يساعدك في إبقاء نظامك محدّثًا Comment[ca]=Us ajuda a mantenir el vostre sistema al dia Comment[ca@valencia]=Vos ajuda a mantindre el vostre sistema al dia Comment[cs]=Umožňuje vám zachovat systém aktuální Comment[da]=Hjælper dig med at holde dit system opdateret Comment[de]=Hilft Ihnen, Ihr System auf dem neusten Stand zu halten Comment[el]=Βοηθά να διατηρήσετε το σύστημά σας ενημερωμένο Comment[en_GB]=Helps you keep your system up to date Comment[es]=Le ayuda a mantener su sistema al día Comment[et]=Aitab hoida süsteemi värske Comment[eu]=Zure sistema egunean mantentzen laguntzen dizu Comment[fi]=Auttaa pitämään järjestelmäsi ajan tasalla Comment[fr]=Vous aide à maintenir le système à jour Comment[gl]=Axúdao a manter o seu sistema actualizado. Comment[he]=עוזר לך לשמור על המערכת מעודכנת Comment[hu]=Segít a rendszere naprakészen tartásában Comment[id]=Membantu kamu menjaga komputermu selalu terupdate Comment[it]=Ti aiuta a mantenere il sistema aggiornato Comment[ko]=시스템을 최신 상태로 유지합니다 Comment[nb]=Hjelper til å holde systemet oppdatert Comment[nl]=Helpt u om uw system bijgewerkt te houden Comment[nn]=Hjelper til å halda systemet oppdatert Comment[pa]=ਤੁਹਾਡੇ ਸਿਸਟਮ ਨੂੰ ਅੱਪਡੇਟ ਰੱਖਣ ਲਈ ਤੁਹਾਡੀ ਮਦਦ ਕਰਦਾ ਹੈ Comment[pl]=Pomaga utrzymać twój system aktualnym Comment[pt]=Ajuda-o a manter o seu sistema actualizado Comment[pt_BR]=Ajuda-o a manter seu sistema atualizado Comment[ru]=Помогает поддерживать систему в актуальном состоянии Comment[sk]=Pomôže vám udržiavať váš systém aktuálny Comment[sl]=Pomaga vam ohraniti posodobljen sistem Comment[sr]=Помаже вам да одржавате систем ажурним Comment[sr@ijekavian]=Помаже вам да одржавате систем ажурним Comment[sr@ijekavianlatin]=Pomaže vam da održavate sistem ažurnim Comment[sr@latin]=Pomaže vam da održavate sistem ažurnim Comment[sv]=Hjälper dig hålla systemet uppdaterat Comment[tr]=Sisteminizin güncel kalmasını sağlar Comment[uk]=Допомагає підтримувати актуальний стан вашої системи Comment[x-test]=xxHelps you keep your system up to datexx Comment[zh_CN]=帮助您保持系统更新 Comment[zh_TW]=協助您的系統更新到最新狀態 Encoding=UTF-8 -Icon=update-none +Icon=system-software-update Type=Service X-KDE-ServiceTypes=Plasma/Applet X-KDE-PluginInfo-Author=The Plasma Team X-KDE-PluginInfo-Email=plasma-devel@kde.org X-KDE-PluginInfo-Name=org.kde.discovernotifier X-KDE-PluginInfo-Version=3.0 X-KDE-PluginInfo-Website=https://www.kde.org/plasma-desktop X-KDE-PluginInfo-Category=Online Services X-KDE-PluginInfo-Depends= X-KDE-PluginInfo-License=GPLv2+ X-KDE-PluginInfo-EnabledByDefault=true X-Plasma-API=declarativeappletscript X-Plasma-MainScript=ui/main.qml X-Plasma-NotificationArea=true X-Plasma-Requires-FileDialog=Unused X-Plasma-Requires-LaunchApp=Required