Commit 88d16643 authored by Carl Schwan's avatar Carl Schwan 🚴 Committed by Aleix Pol Gonzalez
Browse files

Revamp homepage

Based on Carl Schwan's work on !81

BUG: 431316
Fixes #19
parent e8b0fdc0
Pipeline #257152 passed with stage
in 1 minute and 15 seconds
/*
* SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "AbstractAppsModel.h"
#include "discover_debug.h"
#include <KConfigGroup>
#include <KIO/StoredTransferJob>
#include <KSharedConfig>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QStandardPaths>
#include <QtGlobal>
#include <libdiscover_debug.h>
#include <resources/ResourcesModel.h>
#include <resources/StoredResultsStream.h>
#include <utils.h>
class BestInResultsStream : public QObject
{
Q_OBJECT
public:
BestInResultsStream(const QSet<ResultsStream *> &streams)
: QObject()
{
connect(this, &BestInResultsStream::finished, this, &QObject::deleteLater);
Q_ASSERT(!streams.contains(nullptr));
if (streams.isEmpty()) {
QTimer::singleShot(0, this, &BestInResultsStream::clear);
}
for (auto stream : streams) {
m_streams.insert(stream);
connect(stream, &ResultsStream::resourcesFound, this, [this](const QVector<AbstractResource *> &resources) {
m_resources.append(resources.constFirst());
});
connect(stream, &QObject::destroyed, this, &BestInResultsStream::streamDestruction);
}
}
void streamDestruction(QObject *obj)
{
m_streams.remove(obj);
clear();
}
void clear()
{
if (m_streams.isEmpty()) {
Q_EMIT finished(m_resources);
}
}
Q_SIGNALS:
void finished(QVector<AbstractResource *> resources);
private:
QVector<AbstractResource *> m_resources;
QSet<QObject *> m_streams;
};
AbstractAppsModel::AbstractAppsModel()
{
connect(ResourcesModel::global(), &ResourcesModel::currentApplicationBackendChanged, this, &AbstractAppsModel::refreshCurrentApplicationBackend);
refreshCurrentApplicationBackend();
}
void AbstractAppsModel::refreshCurrentApplicationBackend()
{
auto backend = ResourcesModel::global()->currentApplicationBackend();
if (m_backend == backend)
return;
if (m_backend) {
disconnect(m_backend, &AbstractResourcesBackend::fetchingChanged, this, &AbstractAppsModel::refresh);
disconnect(m_backend, &AbstractResourcesBackend::resourceRemoved, this, &AbstractAppsModel::removeResource);
}
m_backend = backend;
if (backend) {
connect(backend, &AbstractResourcesBackend::fetchingChanged, this, &AbstractAppsModel::refresh);
connect(backend, &AbstractResourcesBackend::resourceRemoved, this, &AbstractAppsModel::removeResource);
}
Q_EMIT currentApplicationBackendChanged(m_backend);
}
void AbstractAppsModel::setUris(const QVector<QUrl> &uris)
{
if (!m_backend)
return;
QSet<ResultsStream *> streams;
for (const auto &uri : uris) {
AbstractResourcesBackend::Filters filter;
filter.resourceUrl = uri;
streams << m_backend->search(filter);
}
if (!streams.isEmpty()) {
auto stream = new BestInResultsStream(streams);
acquireFetching(true);
connect(stream, &BestInResultsStream::finished, this, &AbstractAppsModel::setResources);
}
}
static void filterDupes(QVector<AbstractResource *> &resources)
{
QSet<QString> found;
for (auto it = resources.begin(); it != resources.end();) {
auto id = (*it)->appstreamId();
if (found.contains(id)) {
it = resources.erase(it);
} else {
found.insert(id);
++it;
}
}
}
void AbstractAppsModel::acquireFetching(bool f)
{
if (f)
m_isFetching++;
else
m_isFetching--;
if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) {
Q_EMIT isFetchingChanged();
}
Q_ASSERT(m_isFetching >= 0);
}
void AbstractAppsModel::setResources(const QVector<AbstractResource *> &_resources)
{
auto resources = _resources;
filterDupes(resources);
if (m_resources != resources) {
// TODO: sort like in the json files
beginResetModel();
m_resources = resources;
endResetModel();
}
acquireFetching(false);
}
void AbstractAppsModel::removeResource(AbstractResource *resource)
{
int index = m_resources.indexOf(resource);
if (index < 0)
return;
beginRemoveRows({}, index, index);
m_resources.removeAt(index);
endRemoveRows();
}
QVariant AbstractAppsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || role != Qt::UserRole)
return {};
auto res = m_resources.value(index.row());
if (!res)
return {};
return QVariant::fromValue<QObject *>(res);
}
int AbstractAppsModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_resources.count();
}
QHash<int, QByteArray> AbstractAppsModel::roleNames() const
{
return {{Qt::UserRole, "application"}};
}
#include "AbstractAppsModel.moc"
/*
* SPDX-FileCopyrightText: 2016-2022 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#ifndef ABSTRACTAPPSMODEL_H
#define ABSTRACTAPPSMODEL_H
#include "resources/AbstractResourcesBackend.h"
#include <QAbstractListModel>
class AbstractAppsModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool isFetching READ isFetching NOTIFY isFetchingChanged)
Q_PROPERTY(AbstractResourcesBackend *currentApplicationBackend READ currentApplicationBackend NOTIFY currentApplicationBackendChanged)
public:
AbstractAppsModel();
void setResources(const QVector<AbstractResource *> &resources);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
QHash<int, QByteArray> roleNames() const override;
AbstractResourcesBackend *currentApplicationBackend() const
{
return m_backend;
}
bool isFetching() const
{
return m_isFetching != 0;
}
virtual void refresh() = 0;
Q_SIGNALS:
void isFetchingChanged();
void currentApplicationBackendChanged(AbstractResourcesBackend *currentApplicationBackend);
protected:
void refreshCurrentApplicationBackend();
void setUris(const QVector<QUrl> &uris);
void removeResource(AbstractResource *resource);
void acquireFetching(bool f);
private:
QVector<AbstractResource *> m_resources;
int m_isFetching = 0;
AbstractResourcesBackend *m_backend = nullptr;
};
#endif // ABSTRACTAPPSMODEL_H
......@@ -17,6 +17,8 @@ add_executable(plasma-discover ${plasma_discover_SRCS}
DiscoverObject.cpp
DiscoverDeclarativePlugin.cpp
AbstractAppsModel.cpp
OdrsAppsModel.cpp
FeaturedModel.cpp
PaginateModel.cpp
UnityLauncher.cpp
......
......@@ -9,6 +9,7 @@
#include "DiscoverBackendsFactory.h"
#include "DiscoverDeclarativePlugin.h"
#include "FeaturedModel.h"
#include "OdrsAppsModel.h"
#include "PaginateModel.h"
#include "UnityLauncher.h"
#include <Transaction/TransactionModel.h>
......@@ -105,6 +106,7 @@ DiscoverObject::DiscoverObject(CompactMode mode, const QVariantMap &initialPrope
qmlRegisterType<UnityLauncher>("org.kde.discover.app", 1, 0, "UnityLauncher");
qmlRegisterType<PaginateModel>("org.kde.discover.app", 1, 0, "PaginateModel");
qmlRegisterType<FeaturedModel>("org.kde.discover.app", 1, 0, "FeaturedModel");
qmlRegisterType<OdrsAppsModel>("org.kde.discover.app", 1, 0, "OdrsAppsModel");
qmlRegisterType<PowerManagementInterface>("org.kde.discover.app", 1, 0, "PowerManagementInterface");
qmlRegisterType<OurSortFilterProxyModel>("org.kde.discover.app", 1, 0, "QSortFilterProxyModel");
#ifdef WITH_FEEDBACK
......
......@@ -62,36 +62,18 @@ FeaturedModel::FeaturedModel()
});
refreshCurrentApplicationBackend();
}
void FeaturedModel::refreshCurrentApplicationBackend()
{
auto backend = ResourcesModel::global()->currentApplicationBackend();
if (m_backend == backend)
return;
if (m_backend) {
disconnect(m_backend, &AbstractResourcesBackend::fetchingChanged, this, &FeaturedModel::refresh);
disconnect(m_backend, &AbstractResourcesBackend::resourceRemoved, this, &FeaturedModel::removeResource);
}
m_backend = backend;
if (backend) {
connect(backend, &AbstractResourcesBackend::fetchingChanged, this, &FeaturedModel::refresh);
connect(backend, &AbstractResourcesBackend::resourceRemoved, this, &FeaturedModel::removeResource);
}
if (backend && QFile::exists(*featuredCache))
refresh();
Q_EMIT currentApplicationBackendChanged(m_backend);
connect(this, &AbstractAppsModel::currentApplicationBackendChanged, this, [this] {
auto backend = currentApplicationBackend();
if (backend && QFile::exists(*featuredCache))
refresh();
});
}
void FeaturedModel::refresh()
{
// usually only useful if launching just fwupd or kns backends
if (!m_backend)
if (!currentApplicationBackend())
return;
acquireFetching(true);
......@@ -115,98 +97,3 @@ void FeaturedModel::refresh()
});
setUris(uris);
}
void FeaturedModel::setUris(const QVector<QUrl> &uris)
{
if (!m_backend)
return;
QSet<ResultsStream *> streams;
for (const auto &uri : uris) {
AbstractResourcesBackend::Filters filter;
filter.resourceUrl = uri;
streams << m_backend->search(filter);
}
if (!streams.isEmpty()) {
auto stream = new StoredResultsStream(streams);
acquireFetching(true);
connect(stream, &StoredResultsStream::finishedResources, this, &FeaturedModel::setResources);
}
}
static void filterDupes(QVector<AbstractResource *> &resources)
{
QSet<QString> found;
for (auto it = resources.begin(); it != resources.end();) {
auto id = (*it)->appstreamId();
if (found.contains(id)) {
it = resources.erase(it);
} else {
found.insert(id);
++it;
}
}
}
void FeaturedModel::acquireFetching(bool f)
{
if (f)
m_isFetching++;
else
m_isFetching--;
if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) {
Q_EMIT isFetchingChanged();
}
Q_ASSERT(m_isFetching >= 0);
}
void FeaturedModel::setResources(const QVector<AbstractResource *> &_resources)
{
auto resources = _resources;
filterDupes(resources);
if (m_resources != resources) {
// TODO: sort like in the json files
beginResetModel();
m_resources = resources;
endResetModel();
}
acquireFetching(false);
}
void FeaturedModel::removeResource(AbstractResource *resource)
{
int index = m_resources.indexOf(resource);
if (index < 0)
return;
beginRemoveRows({}, index, index);
m_resources.removeAt(index);
endRemoveRows();
}
QVariant FeaturedModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || role != Qt::UserRole)
return {};
auto res = m_resources.value(index.row());
if (!res)
return {};
return QVariant::fromValue<QObject *>(res);
}
int FeaturedModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_resources.count();
}
QHash<int, QByteArray> FeaturedModel::roleNames() const
{
return {{Qt::UserRole, "application"}};
}
#include "moc_FeaturedModel.cpp"
......@@ -7,56 +7,24 @@
#ifndef FEATUREDMODEL_H
#define FEATUREDMODEL_H
#include <QAbstractListModel>
#include "AbstractAppsModel.h"
#include <QPointer>
namespace KIO
{
class StoredTransferJob;
}
class AbstractResource;
class AbstractResourcesBackend;
class FeaturedModel : public QAbstractListModel
class FeaturedModel : public AbstractAppsModel
{
Q_OBJECT
Q_PROPERTY(bool isFetching READ isFetching NOTIFY isFetchingChanged)
Q_PROPERTY(AbstractResourcesBackend *currentApplicationBackend READ currentApplicationBackend NOTIFY currentApplicationBackendChanged)
public:
FeaturedModel();
~FeaturedModel() override
{
}
void setResources(const QVector<AbstractResource *> &resources);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
QHash<int, QByteArray> roleNames() const override;
AbstractResourcesBackend *currentApplicationBackend() const
{
return m_backend;
}
bool isFetching() const
{
return m_isFetching != 0;
}
Q_SIGNALS:
void isFetchingChanged();
void currentApplicationBackendChanged(AbstractResourcesBackend *currentApplicationBackend);
private:
void refreshCurrentApplicationBackend();
void setUris(const QVector<QUrl> &uris);
void refresh();
void removeResource(AbstractResource *resource);
void acquireFetching(bool f);
QVector<AbstractResource *> m_resources;
int m_isFetching = 0;
AbstractResourcesBackend *m_backend = nullptr;
void refresh() override;
};
#endif // FEATUREDMODEL_H
/*
* SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "OdrsAppsModel.h"
#include "appstream/AppStreamIntegration.h"
#include <ReviewsBackend/Rating.h>
#include <utils.h>
OdrsAppsModel::OdrsAppsModel()
{
auto x = AppStreamIntegration::global()->reviews();
connect(x.get(), &OdrsReviewsBackend::ratingsReady, this, &OdrsAppsModel::refresh);
if (!x->top().isEmpty()) {
refresh();
}
}
void OdrsAppsModel::refresh()
{
const auto top = AppStreamIntegration::global()->reviews()->top();
setUris(kTransform<QVector<QUrl>>(top, [](auto r) {
return QUrl("appstream://" + r->packageName());
}));
}
/*
* SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#ifndef OARSAPPSMODEL_H
#define OARSAPPSMODEL_H
#include "AbstractAppsModel.h"
class OdrsAppsModel : public AbstractAppsModel
{
Q_OBJECT
public:
OdrsAppsModel();
void refresh() override;
};
#endif // OARSAPPSMODEL_H
/*
* SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
* SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.4
import QtQuick 2.15
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.1
import QtQml.Models 2.15
import org.kde.discover 2.0
import org.kde.discover.app 1.0
import "navigation.js" as Navigation
......@@ -15,7 +17,7 @@ import org.kde.kirigami 2.19 as Kirigami
DiscoverPage
{
id: page
title: i18n("Featured")
title: i18n("Discover")
objectName: "featured"
actions.main: window.wideScreen ? searchAction : null
......@@ -39,16 +41,16 @@ DiscoverPage
}
Kirigami.LoadingPlaceholder {
visible: appsRep.count === 0 && appsRep.model.isFetching
visible: featuredModel.isFetching
anchors.centerIn: parent
}
Loader {
active: appsRep.count === 0 && !appsRep.model.isFetching
active: featuredModel.count === 0 && !featuredModel.isFetching
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
sourceComponent: Kirigami.PlaceholderMessage {
readonly property var helpfulError: appsRep.model.currentApplicationBackend.explainDysfunction()
readonly property var helpfulError: featuredModel.currentApplicationBackend.explainDysfunction()
icon.name: helpfulError.iconName
text: i18n("Unable to load applications")
explanation: helpfulError.errorMessage
......@@ -76,40 +78,105 @@ DiscoverPage
Layout.fillWidth: true
visible: Kirigami.Settings.isMobile && inlineMessage.visible
}
}
Kirigami.InlineMessage {
id: inlineMessage
icon.name: updateAction.icon.name
showCloseButton: true
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing * 2
text: i18n("Updates are available")
visible: Kirigami.Settings.isMobile && ResourcesModel.updatesCount > 0
actions: Kirigami.Action {
icon.name: "go-next"
text: i18nc("Short for 'show updates'", "Show")
onTriggered: updateAction.trigger()
Kirigami.CardsLayout {
id: apps
maximumColumns: 4
rowSpacing: Kirigami.Units.largeSpacing
columnSpacing: Kirigami.Units.largeSpacing
maximumColumnWidth: Kirigami.Units.gridUnit * 6
Layout.preferredWidth: Math.max(maximumColumnWidth, Math.min((width / columns) - columnSpacing))
Kirigami.Heading {
Layout.topMargin: Kirigami.Units.largeSpacing * 2
Layout.columnSpan: apps.columns
text: i18nc("@title:group", "Editor's Choice")
visible: !featuredModel.isFetching
}
Repeater {
model: FeaturedModel {
id: featuredModel
}
delegate: GridApplicationDelegate { visible: !featuredModel.isFetching }
}
}
Kirigami.Heading {
Layout.topMargin: Kirigami.Units.largeSpacing * 2
Layout.columnSpan: apps.columns