Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit 8280fa0c authored by Aleix Pol Gonzalez's avatar Aleix Pol Gonzalez 🐧

Don't make requests if we didn't manage to connect

Summary:
Use QtConcurrent::run instead of QThread. QThread will issue the thread
immediately and that will overload the operating system giving "too many files
opened errors". QtConcurrent will do the appropriate balancing for us.

Test Plan: Doesn't crash on me anymore

Reviewers: jgrulich

Reviewed By: jgrulich

Subscribers: plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D7785
parent 335b4ded
......@@ -6,20 +6,19 @@ set(flatpak-backend_SRCS
FlatpakResource.cpp
FlatpakBackend.cpp
FlatpakFetchDataJob.cpp
FlatpakFetchUpdatesJob.cpp
FlatpakSourcesBackend.cpp
FlatpakTransaction.cpp
FlatpakTransactionJob.cpp
)
add_library(flatpak-backend MODULE ${flatpak-backend_SRCS})
target_link_libraries(flatpak-backend Qt5::Core Qt5::Widgets KF5::CoreAddons KF5::ConfigCore Discover::Common AppStreamQt ${FLATPAK_LIBRARIES})
target_link_libraries(flatpak-backend Qt5::Core Qt5::Widgets Qt5::Concurrent KF5::CoreAddons KF5::ConfigCore Discover::Common AppStreamQt ${FLATPAK_LIBRARIES})
install(TARGETS flatpak-backend DESTINATION ${PLUGIN_INSTALL_DIR}/discover)
install(FILES flatpak-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories)
add_library(FlatpakNotifier MODULE FlatpakNotifier.cpp FlatpakFetchUpdatesJob.cpp)
target_link_libraries(FlatpakNotifier Discover::Notifiers ${FLATPAK_LIBRARIES})
add_library(FlatpakNotifier MODULE FlatpakNotifier.cpp)
target_link_libraries(FlatpakNotifier Discover::Notifiers Qt5::Concurrent ${FLATPAK_LIBRARIES})
set_target_properties(FlatpakNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover)
install(TARGETS FlatpakNotifier DESTINATION ${PLUGIN_INSTALL_DIR}/discover-notifier)
......
......@@ -21,7 +21,6 @@
#include "FlatpakBackend.h"
#include "FlatpakFetchDataJob.h"
#include "FlatpakFetchUpdatesJob.h"
#include "FlatpakResource.h"
#include "FlatpakSourcesBackend.h"
#include "FlatpakTransaction.h"
......@@ -44,9 +43,11 @@
#include <KSharedConfig>
#include <QAction>
#include <QtConcurrentRun>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFutureWatcher>
#include <QMessageBox>
#include <QSettings>
#include <QThread>
......@@ -64,6 +65,7 @@ FlatpakBackend::FlatpakBackend(QObject* parent)
, m_updater(new StandardBackendUpdater(this))
, m_reviews(AppStreamIntegration::global()->reviews())
, m_fetching(false)
, m_threadPool(new QThreadPool(this))
{
g_autoptr(GError) error = nullptr;
m_cancellable = g_cancellable_new();
......@@ -87,6 +89,7 @@ FlatpakBackend::FlatpakBackend(QObject* parent)
FlatpakBackend::~FlatpakBackend()
{
m_threadPool.clear();
for(auto inst : m_installations)
g_object_unref(inst);
......@@ -411,34 +414,35 @@ FlatpakResource * FlatpakBackend::addAppFromFlatpakRef(const QUrl &url)
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
FlatpakFetchDataJob *job = new FlatpakFetchDataJob(preferredInstallation(), resource, FlatpakFetchDataJob::FetchMetadata);
connect(job, &FlatpakFetchDataJob::finished, job, &FlatpakFetchDataJob::deleteLater);
connect(job, &FlatpakFetchDataJob::jobFetchMetadataFailed, this, [this, resource] {
auto fw = new QFutureWatcher<QByteArray>(this);
fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource));
connect(fw, &QFutureWatcher<QByteArray>::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
addResource(resource);
});
connect(job, &FlatpakFetchDataJob::jobFetchMetadataFinished, this, [this, runtimeUrl] (FlatpakInstallation *installation, FlatpakResource *resource, const QByteArray &metadata) {
Q_UNUSED(installation);
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;
if (metadata.isEmpty()) {
onFetchMetadataFinished(installation, resource, metadata);
} else {
addResource(resource);
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();
});
job->start();
} else {
addResource(resource);
}
......@@ -723,12 +727,23 @@ void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation)
}
}
void FlatpakBackend::loadRemoteUpdates(FlatpakInstallation *flatpakInstallation)
void FlatpakBackend::loadRemoteUpdates(FlatpakInstallation* installation)
{
FlatpakFetchUpdatesJob *job = new FlatpakFetchUpdatesJob(flatpakInstallation);
connect(job, &FlatpakFetchUpdatesJob::finished, job, &FlatpakFetchUpdatesJob::deleteLater);
connect(job, &FlatpakFetchUpdatesJob::jobFetchUpdatesFinished, this, &FlatpakBackend::onFetchUpdatesFinished);
job->start();
auto fw = new QFutureWatcher<GPtrArray *>(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<GPtrArray *>::finished, this, [this, installation, fw](){
auto refs = fw->result();
onFetchUpdatesFinished(installation, refs);
fw->deleteLater();
});
}
void FlatpakBackend::onFetchUpdatesFinished(FlatpakInstallation *flatpakInstallation, GPtrArray *updates)
......@@ -771,10 +786,11 @@ class FlatpakRefreshAppstreamMetadataJob : public QThread
public:
FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote)
: QThread()
, m_cancellable(g_cancellable_new())
, m_installation(installation)
, m_remote(remote)
{
m_cancellable = g_cancellable_new();
connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater);
}
~FlatpakRefreshAppstreamMetadataJob()
......@@ -818,7 +834,6 @@ private:
void FlatpakBackend::refreshAppstreamMetadata(FlatpakInstallation *installation, FlatpakRemote *remote)
{
FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote);
connect(job, &FlatpakRefreshAppstreamMetadataJob::finished, job, &FlatpakFetchDataJob::deleteLater);
connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote);
job->start();
}
......@@ -884,10 +899,15 @@ bool FlatpakBackend::updateAppMetadata(FlatpakInstallation* flatpakInstallation,
if (QFile::exists(path)) {
return updateAppMetadata(resource, path);
} else {
FlatpakFetchDataJob *job = new FlatpakFetchDataJob(flatpakInstallation, resource, FlatpakFetchDataJob::FetchMetadata);
connect(job, &FlatpakFetchDataJob::finished, job, &FlatpakFetchDataJob::deleteLater);
connect(job, &FlatpakFetchDataJob::jobFetchMetadataFinished, this, &FlatpakBackend::onFetchMetadataFinished);
job->start();
auto fw = new QFutureWatcher<QByteArray>(this);
fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource));
connect(fw, &QFutureWatcher<QByteArray>::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;
}
......@@ -996,14 +1016,18 @@ bool FlatpakBackend::updateAppSizeFromRemote(FlatpakInstallation *flatpakInstall
return false;
}
FlatpakFetchDataJob *job = new FlatpakFetchDataJob(flatpakInstallation, resource, FlatpakFetchDataJob::FetchSize);
connect(job, &FlatpakFetchDataJob::finished, job, &FlatpakFetchDataJob::deleteLater);
connect(job, &FlatpakFetchDataJob::jobFetchSizeFinished, this, &FlatpakBackend::onFetchSizeFinished);
connect(job, &FlatpakFetchDataJob::jobFetchSizeFailed, [resource] () {
resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed);
resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed);
auto futureWatcher = new QFutureWatcher<FlatpakRunnables::SizeInformation>(this);
futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchFlatpakSize, flatpakInstallation, resource));
connect(futureWatcher, &QFutureWatcher<FlatpakRunnables::SizeInformation>::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();
});
job->start();
}
return true;
......
......@@ -27,6 +27,7 @@
#include <resources/AbstractResourcesBackend.h>
#include <QVariantList>
#include <QSharedPointer>
#include <QThreadPool>
#include <AppStreamQt/component.h>
......@@ -108,6 +109,7 @@ private:
GCancellable *m_cancellable;
QVector<FlatpakInstallation *> m_installations;
QThreadPool m_threadPool;
};
#endif // FLATPAKBACKEND_H
......@@ -23,96 +23,74 @@
#include <QDebug>
FlatpakFetchDataJob::FlatpakFetchDataJob(FlatpakInstallation *installation, FlatpakResource *app, FlatpakFetchDataJob::DataKind kind)
: QThread()
, m_app(app)
, m_installation(installation)
, m_kind(kind)
static FlatpakRef * createFakeRef(FlatpakResource *resource)
{
m_cancellable = g_cancellable_new();
}
FlatpakRef *ref = nullptr;
g_autoptr(GError) localError = nullptr;
FlatpakFetchDataJob::~FlatpakFetchDataJob()
{
g_object_unref(m_cancellable);
}
const QString id = QStringLiteral("%1/%2/%3/%4").arg(resource->typeAsString(), resource->flatpakName(), resource->arch(), resource->branch());
ref = flatpak_ref_parse(id.toUtf8().constData(), &localError);
void FlatpakFetchDataJob::cancel()
{
g_cancellable_cancel(m_cancellable);
if (!ref) {
qWarning() << "Failed to create fake ref: " << localError->message;
}
return ref;
}
void FlatpakFetchDataJob::run()
namespace FlatpakRunnables
{
QByteArray fetchMetadata(FlatpakInstallation *installation, FlatpakResource *app)
{
g_autoptr(GCancellable) cancellable = g_cancellable_new();
g_autoptr(GError) localError = nullptr;
if (m_kind == FetchMetadata) {
QByteArray metadataContent;
g_autoptr(GBytes) data = nullptr;
g_autoptr(FlatpakRef) fakeRef = nullptr;
if (m_app->origin().isEmpty()) {
qWarning() << "Failed to get metadata file because of missing origin";
Q_EMIT jobFetchMetadataFailed();
return;
}
fakeRef = createFakeRef(m_app);
if (!fakeRef) {
Q_EMIT jobFetchMetadataFailed();
return;
}
data = flatpak_installation_fetch_remote_metadata_sync(m_installation, m_app->origin().toUtf8().constData(), fakeRef, m_cancellable, &localError);
if (data) {
gsize len = 0;
metadataContent = QByteArray((char *)g_bytes_get_data(data, &len));
} else {
qWarning() << "Failed to get metadata file: " << localError->message;
Q_EMIT jobFetchMetadataFailed();
return;
}
if (metadataContent.isEmpty()) {
qWarning() << "Failed to get metadata file: empty metadata";
Q_EMIT jobFetchMetadataFailed();
return;
}
Q_EMIT jobFetchMetadataFinished(m_installation, m_app, metadataContent);
} else if (m_kind == FetchSize) {
guint64 downloadSize = 0;
guint64 installedSize = 0;
g_autoptr(FlatpakRef) ref = nullptr;
ref = createFakeRef(m_app);
if (!ref) {
Q_EMIT jobFetchSizeFailed();
return;
}
if (!flatpak_installation_fetch_remote_size_sync(m_installation, m_app->origin().toUtf8().constData(),
ref, &downloadSize, &installedSize, m_cancellable, &localError)) {
qWarning() << "Failed to get remote size of " << m_app->name() << ": " << localError->message;
Q_EMIT jobFetchSizeFailed();
return;
}
Q_EMIT jobFetchSizeFinished(m_app, downloadSize, installedSize);
if (app->origin().isEmpty()) {
qWarning() << "Failed to get metadata file because of missing origin";
return {};
}
g_autoptr(FlatpakRef) fakeRef = createFakeRef(app);
if (!fakeRef) {
return {};
}
QByteArray metadataContent;
g_autoptr(GBytes) data = flatpak_installation_fetch_remote_metadata_sync(installation, app->origin().toUtf8().constData(), fakeRef, cancellable, &localError);
if (data) {
gsize len = 0;
metadataContent = QByteArray((char *)g_bytes_get_data(data, &len));
} else {
qWarning() << "Failed to get metadata file: " << localError->message;
return {};
}
if (metadataContent.isEmpty()) {
qWarning() << "Failed to get metadata file: empty metadata";
return {};
}
return metadataContent;
}
FlatpakRef * FlatpakFetchDataJob::createFakeRef(FlatpakResource *resource)
SizeInformation fetchFlatpakSize(FlatpakInstallation *installation, FlatpakResource *app)
{
FlatpakRef *ref = nullptr;
g_autoptr(GCancellable) cancellable = g_cancellable_new();
g_autoptr(GError) localError = nullptr;
const QString id = QString::fromUtf8("%1/%2/%3/%4").arg(resource->typeAsString()).arg(resource->flatpakName()).arg(resource->arch()).arg(resource->branch());
ref = flatpak_ref_parse(id.toUtf8().constData(), &localError);
SizeInformation ret;
g_autoptr(FlatpakRef) ref = createFakeRef(app);
if (!ref) {
qWarning() << "Failed to create fake ref: " << localError->message;
return ret;
}
return ref;
if (!flatpak_installation_fetch_remote_size_sync(installation, app->origin().toUtf8().constData(), ref, &ret.downloadSize, &ret.installedSize, cancellable, &localError)) {
qWarning() << "Failed to get remote size of " << app->name() << ": " << localError->message;
return ret;
}
ret.valid = true;
return ret;
}
}
......@@ -21,44 +21,26 @@
#ifndef FLATPAKFETCHDATAJOB_H
#define FLATPAKFETCHDATAJOB_H
#include <QByteArray>
extern "C" {
#include <flatpak.h>
#include <gio/gio.h>
#include <glib.h>
}
#include <QThread>
class FlatpakResource;
class FlatpakFetchDataJob : public QThread
namespace FlatpakRunnables
{
Q_OBJECT
public:
enum DataKind {
FetchMetadata = 0,
FetchSize = 1,
struct SizeInformation {
bool valid = false;
guint64 downloadSize;
guint64 installedSize;
};
FlatpakFetchDataJob(FlatpakInstallation *installation, FlatpakResource *app, DataKind kind);
~FlatpakFetchDataJob();
void cancel();
void run() override;
SizeInformation fetchFlatpakSize(FlatpakInstallation *installation, FlatpakResource *app);
Q_SIGNALS:
void jobFetchMetadataFailed();
void jobFetchMetadataFinished(FlatpakInstallation *installation, FlatpakResource *resource, const QByteArray &metadata);
void jobFetchSizeFailed();
void jobFetchSizeFinished(FlatpakResource *resource, int downloadSize, int installedSize);
private:
FlatpakRef * createFakeRef(FlatpakResource *resource);
GCancellable *m_cancellable;
FlatpakResource *m_app;
FlatpakInstallation *m_installation;
DataKind m_kind;
};
QByteArray fetchMetadata(FlatpakInstallation *installation, FlatpakResource *app);
}
#endif // FLATPAKFETCHDATAJOB_H
......
/***************************************************************************
* Copyright © 2017 Jan Grulich <jgrulich@redhat.com> *
* *
* 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 <http://www.gnu.org/licenses/>. *
***************************************************************************/
#include "FlatpakFetchUpdatesJob.h"
#include <QDebug>
FlatpakFetchUpdatesJob::FlatpakFetchUpdatesJob(FlatpakInstallation *installation)
: QThread()
, m_installation(installation)
{
m_cancellable = g_cancellable_new();
}
FlatpakFetchUpdatesJob::~FlatpakFetchUpdatesJob()
{
g_object_unref(m_cancellable);
}
void FlatpakFetchUpdatesJob::cancel()
{
g_cancellable_cancel(m_cancellable);
}
void FlatpakFetchUpdatesJob::run()
{
g_autoptr(GError) localError = nullptr;
GPtrArray *refs = nullptr;
refs = flatpak_installation_list_installed_refs_for_update(m_installation, m_cancellable, &localError);
if (!refs) {
qWarning() << "Failed to get list of installed refs for listing updates: " << localError->message;
return;
}
Q_EMIT jobFetchUpdatesFinished(m_installation, refs);
}
/***************************************************************************
* Copyright © 2017 Jan Grulich <jgrulich@redhat.com> *
* *
* 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 <http://www.gnu.org/licenses/>. *
***************************************************************************/
#ifndef FLATPAKFETCHUPDATESJOB_H
#define FLATPAKFETCHUPDATESJOB_H
extern "C" {
#include <flatpak.h>
#include <gio/gio.h>
#include <glib.h>
}
#include <QThread>
class FlatpakFetchUpdatesJob : public QThread
{
Q_OBJECT
public:
FlatpakFetchUpdatesJob(FlatpakInstallation *installation);
~FlatpakFetchUpdatesJob();
void cancel();
void run() override;
Q_SIGNALS:
void jobFetchUpdatesFinished(FlatpakInstallation *installation, GPtrArray *updates);
private:
GCancellable *m_cancellable;
FlatpakInstallation *m_installation;
};
#endif // FLATPAKFETCHUPDATESJOB_H
......@@ -20,12 +20,13 @@
***************************************************************************/
#include "FlatpakNotifier.h"
#include "FlatpakFetchUpdatesJob.h"
#include <glib.h>
#include <QDebug>
#include <QTimer>
#include <QtConcurrentRun>
#include <QFutureWatcher>
static void installationChanged(GFileMonitor *monitor, GFile *child, GFile *other_file, GFileMonitorEvent event_type, gpointer self)
{
......@@ -131,12 +132,23 @@ void FlatpakNotifier::onFetchUpdatesFinished(FlatpakInstallation *flatpakInstall
}
}
void FlatpakNotifier::loadRemoteUpdates(FlatpakInstallation *flatpakInstallation)
void FlatpakNotifier::loadRemoteUpdates(FlatpakInstallation *installation)
{
FlatpakFetchUpdatesJob *job = new FlatpakFetchUpdatesJob(flatpakInstallation);
connect(job, &FlatpakFetchUpdatesJob::finished, job, &FlatpakFetchUpdatesJob::deleteLater);
connect(job, &FlatpakFetchUpdatesJob::jobFetchUpdatesFinished, this, &FlatpakNotifier::onFetchUpdatesFinished);
job->start();
auto fw = new QFutureWatcher<GPtrArray *>(this);
fw->setFuture(QtConcurrent::run( [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<GPtrArray *>::finished, this, [this, installation, fw](){
auto refs = fw->result();
onFetchUpdatesFinished(installation, refs);
fw->deleteLater();
});
}
bool FlatpakNotifier::setupFlatpakInstallations(GError **error)
......
......@@ -52,6 +52,7 @@ SnapBackend::SnapBackend(QObject* parent)
m_valid = request->error() == QSnapdRequest::NoError;
if (!m_valid) {
qWarning() << "snap problem at initialize:" << request->errorString();
return;
}
}
connect(m_reviews, &SnapReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment