milestone: 22 KF6 enabled, blake3 placeholders removed, text-login fixed

- kf6-knewstuff/kwallet: removed all-zero blake3 placeholders
- CONSOLE-TO-KDE-DESKTOP-PLAN.md: 20→22 KF6 enabled count
- BOOT-PROCESS-IMPROVEMENT-PLAN.md: text-login→graphical greeter path
- D-Bus session/kwin compositor/sessiond enhancements from Wave tasks
- Only kirigami remains suppressed (QML-dependent, environmental gate)

Zero warnings. 24 commits total.
This commit is contained in:
2026-04-29 14:48:47 +01:00
parent cb2e75e640
commit edb68153e3
621 changed files with 1034826 additions and 223 deletions
@@ -0,0 +1,81 @@
# SPDX-FileCopyrightText: KDE Contributors
# SPDX-License-Identifier: BSD-2-Clause
add_subdirectory(private)
add_library(knewstuff_qml_STATIC STATIC)
target_sources(knewstuff_qml_STATIC PRIVATE
quickengine.cpp
quicksettings.cpp
quickitemsmodel.cpp
quickquestionlistener.cpp
searchpresetmodel.cpp
categoriesmodel.cpp
commentsmodel.cpp
)
ecm_qt_declare_logging_category(knewstuff_qml_STATIC
HEADER knewstuffquick_debug.h
IDENTIFIER KNEWSTUFFQUICK
CATEGORY_NAME kf.newstuff.quick
OLD_CATEGORY_NAMES org.kde.knewstuff.quick
DESCRIPTION "knewstuff (qtquick)"
EXPORT KNEWSTUFF
)
set_property(TARGET knewstuff_qml_STATIC PROPERTY POSITION_INDEPENDENT_CODE ON)
target_link_libraries(knewstuff_qml_STATIC PUBLIC
Qt6::Core
Qt6::Gui # QImage
Qt6::Qml
KF6::ConfigCore
KF6::I18n
KF6::NewStuffCore
)
ecm_add_qml_module(newstuffqmlplugin URI "org.kde.newstuff" VERSION 1.0)
target_sources(newstuffqmlplugin PRIVATE
qmlplugin.cpp
author.cpp
downloadlinkinfo.cpp
)
ecm_target_qml_sources(newstuffqmlplugin VERSION 1.1 SOURCES
qml/Button.qml
qml/Dialog.qml
qml/DialogContent.qml
qml/DownloadItemsSheet.qml
qml/EntryDetails.qml
qml/Page.qml
qml/QuestionAsker.qml
)
ecm_target_qml_sources(newstuffqmlplugin VERSION 1.81 SOURCES
qml/Action.qml
)
ecm_target_qml_sources(newstuffqmlplugin VERSION 1.85 SOURCES
qml/UploadPage.qml
)
ecm_target_qml_sources(newstuffqmlplugin PRIVATE PATH private SOURCES
qml/private/ConditionalLoader.qml
qml/private/EntryCommentDelegate.qml
qml/private/EntryCommentsPage.qml
qml/private/EntryScreenshots.qml
qml/private/ErrorDisplayer.qml
qml/private/GridTileDelegate.qml
qml/private/Rating.qml
qml/private/Shadow.qml
)
ecm_target_qml_sources(newstuffqmlplugin PRIVATE PATH private/entrygriddelegates SOURCES
qml/private/entrygriddelegates/BigPreviewDelegate.qml
qml/private/entrygriddelegates/FeedbackOverlay.qml
qml/private/entrygriddelegates/TileDelegate.qml
)
target_link_libraries (newstuffqmlplugin PRIVATE knewstuff_qml_STATIC)
ecm_finalize_qml_module(newstuffqmlplugin DESTINATION ${KDE_INSTALL_QMLDIR})
@@ -0,0 +1,184 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "author.h"
#include "quickengine.h"
#include "core/author.h"
#include "core/provider.h"
#include <memory>
namespace KNewStuffQuick
{
// This caching will want to eventually go into the Provider level (and be more generalised)
typedef QHash<QString, std::shared_ptr<KNSCore::Author>> AllAuthorsHash;
Q_GLOBAL_STATIC(AllAuthorsHash, allAuthors)
class AuthorPrivate
{
public:
AuthorPrivate(Author *qq)
: q(qq)
{
}
Author *const q;
bool componentCompleted{false};
Engine *engine{nullptr};
QString providerId;
QString username;
QSharedPointer<KNSCore::Provider> provider;
void resetConnections()
{
if (!componentCompleted) {
return;
}
if (provider) {
provider->disconnect(q);
}
if (engine) {
provider = engine->provider(providerId);
if (!provider) {
provider = engine->defaultProvider();
}
}
if (provider) {
QObject::connect(provider.get(), &KNSCore::Provider::personLoaded, q, [this](const std::shared_ptr<KNSCore::Author> author) {
allAuthors()->insert(QStringLiteral("%1 %2").arg(provider->id(), author->id()), author);
Q_EMIT q->dataChanged();
});
author(); // Check and make sure...
}
}
// TODO Having a shared ptr on a QSharedData class doesn't make sense
std::shared_ptr<KNSCore::Author> author()
{
std::shared_ptr<KNSCore::Author> ret;
if (provider && !username.isEmpty()) {
ret = allAuthors()->value(QStringLiteral("%1 %2").arg(provider->id(), username));
if (!ret.get()) {
provider->loadPerson(username);
}
}
return ret;
}
};
}
using namespace KNewStuffQuick;
Author::Author(QObject *parent)
: QObject(parent)
, d(new AuthorPrivate(this))
{
connect(this, &Author::engineChanged, &Author::dataChanged);
connect(this, &Author::providerIdChanged, &Author::dataChanged);
connect(this, &Author::usernameChanged, &Author::dataChanged);
}
Author::~Author() = default;
void Author::classBegin()
{
}
void Author::componentComplete()
{
d->componentCompleted = true;
d->resetConnections();
}
Engine *Author::engine() const
{
return d->engine;
}
void Author::setEngine(Engine *newEngine)
{
if (d->engine != newEngine) {
d->engine = newEngine;
d->resetConnections();
Q_EMIT engineChanged();
}
}
QString Author::providerId() const
{
return d->providerId;
}
void Author::setProviderId(const QString &providerId)
{
if (d->providerId != providerId) {
d->providerId = providerId;
d->resetConnections();
Q_EMIT providerIdChanged();
}
}
QString Author::username() const
{
return d->username;
}
void Author::setUsername(const QString &username)
{
if (d->username != username) {
d->username = username;
d->resetConnections();
Q_EMIT usernameChanged();
}
}
QString Author::name() const
{
std::shared_ptr<KNSCore::Author> author = d->author();
if (author.get() && !author->name().isEmpty()) {
return author->name();
}
return d->username;
}
QString Author::description() const
{
std::shared_ptr<KNSCore::Author> author = d->author();
if (author.get()) {
return author->description();
}
return QString{};
}
QString Author::homepage() const
{
std::shared_ptr<KNSCore::Author> author = d->author();
if (author.get()) {
return author->homepage();
}
return QString{};
}
QString Author::profilepage() const
{
std::shared_ptr<KNSCore::Author> author = d->author();
if (author.get()) {
return author->profilepage();
}
return QString{};
}
QUrl Author::avatarUrl() const
{
std::shared_ptr<KNSCore::Author> author = d->author();
if (author.get()) {
return author->avatarUrl();
}
return QUrl{};
}
#include "moc_author.cpp"
@@ -0,0 +1,80 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KNSQUICK_AUTHOR_H
#define KNSQUICK_AUTHOR_H
#include <QObject>
#include <QQmlParserStatus>
#include <entry.h>
#include "quickengine.h"
// TODO This is not a class for exposing data QtQuick, but for implementing it's fetching in the first place.
// Also it depends on the QML parser status, this is kindof ugly...
namespace KNewStuffQuick
{
class AuthorPrivate;
/**
* @short Encapsulates a KNSCore::Author for use in Qt Quick
*
* This class takes care of initialisation of a KNSCore::Author when assigned an engine, provider ID and username.
* If the data is not yet cached, it will be requested from the provider, and updated for display
* @since 5.63
*/
class Author : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
/**
* The NewStuffQuickEngine to interact with servers through
*/
Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged)
/**
* The ID of the provider which the user is registered on
*/
Q_PROPERTY(QString providerId READ providerId WRITE setProviderId NOTIFY providerIdChanged)
/**
* The user ID for the user this object represents
*/
Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged)
Q_PROPERTY(QString name READ name NOTIFY dataChanged)
Q_PROPERTY(QString description READ description NOTIFY dataChanged)
Q_PROPERTY(QString homepage READ homepage NOTIFY dataChanged)
Q_PROPERTY(QString profilepage READ profilepage NOTIFY dataChanged)
Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY dataChanged)
public:
explicit Author(QObject *parent = nullptr);
~Author() override;
void classBegin() override;
void componentComplete() override;
Engine *engine() const;
void setEngine(Engine *newEngine);
Q_SIGNAL void engineChanged();
QString providerId() const;
void setProviderId(const QString &providerId);
Q_SIGNAL void providerIdChanged();
QString username() const;
void setUsername(const QString &username);
Q_SIGNAL void usernameChanged();
QString name() const;
QString description() const;
QString homepage() const;
QString profilepage() const;
QUrl avatarUrl() const;
Q_SIGNAL void dataChanged();
private:
const std::unique_ptr<AuthorPrivate> d;
};
}
#endif // KNSQUICK_AUTHOR_H
@@ -0,0 +1,85 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "categoriesmodel.h"
#include "provider.h"
#include <KLocalizedString>
CategoriesModel::CategoriesModel(KNSCore::EngineBase *parent)
: QAbstractListModel(parent)
, m_engine(parent)
{
connect(m_engine, &KNSCore::EngineBase::signalCategoriesMetadataLoded, this, [this]() {
beginResetModel();
endResetModel();
});
}
CategoriesModel::~CategoriesModel() = default;
QHash<int, QByteArray> CategoriesModel::roleNames() const
{
static const QHash<int, QByteArray> roles{{NameRole, "name"}, {IdRole, "id"}, {DisplayNameRole, "displayName"}};
return roles;
}
int CategoriesModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_engine->categoriesMetadata().count() + 1;
}
QVariant CategoriesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
const QList<KNSCore::Provider::CategoryMetadata> categoriesMetadata = m_engine->categoriesMetadata();
if (index.row() == 0) {
switch (role) {
case NameRole:
return QString();
case IdRole:
return 0;
case DisplayNameRole:
return i18nc("The first entry in the category selection list (also the default value)", "All Categories");
default:
return QStringLiteral("Unknown role");
}
} else if (index.row() <= categoriesMetadata.count()) {
const KNSCore::Provider::CategoryMetadata category = categoriesMetadata[index.row() - 1];
switch (role) {
case NameRole:
return category.name;
case IdRole:
return category.id;
case DisplayNameRole:
return category.displayName;
default:
return QStringLiteral("Unknown role");
}
}
return QVariant();
}
QString CategoriesModel::idToDisplayName(const QString &id) const // TODO KF6 unused?
{
QString dispName = i18nc("The string passed back in the case the requested category is not known", "Unknown Category");
const auto metaData = m_engine->categoriesMetadata();
for (const KNSCore::Provider::CategoryMetadata &cat : metaData) {
if (cat.id == id) {
dispName = cat.displayName;
break;
}
}
return dispName;
}
#include "moc_categoriesmodel.cpp"
@@ -0,0 +1,48 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef CATEGORIESMODEL_H
#define CATEGORIESMODEL_H
#include <QAbstractListModel>
#include "enginebase.h"
/**
* @short A model which shows the categories found in an Engine
* @since 5.63
*/
class CategoriesModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit CategoriesModel(KNSCore::EngineBase *parent);
~CategoriesModel() override;
enum Roles {
NameRole = Qt::UserRole + 1,
IdRole,
DisplayNameRole,
};
Q_ENUM(Roles)
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* Get the display name for the category with the id passed to the function
*
* @param id The ID of the category you want to get the display name for
* @return The display name (or the translated string "Unknown Category" for the requested category
*/
Q_INVOKABLE QString idToDisplayName(const QString &id) const;
private:
KNSCore::EngineBase *const m_engine;
};
#endif // CATEGORIESMODEL_H
@@ -0,0 +1,132 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "commentsmodel.h"
#include "core/commentsmodel.h"
namespace KNewStuffQuick
{
class CommentsModelPrivate
{
public:
CommentsModelPrivate(CommentsModel *qq)
: q(qq)
{
}
CommentsModel *q;
ItemsModel *itemsModel{nullptr};
KNSCore::Entry entry;
bool componentCompleted{false};
CommentsModel::IncludedComments includedComments{CommentsModel::IncludeAllComments};
QSharedPointer<KNSCore::Provider> provider;
void resetConnections()
{
if (componentCompleted && itemsModel) {
q->setSourceModel(qobject_cast<QAbstractListModel *>(
itemsModel->data(itemsModel->index(itemsModel->indexOfEntry(entry)), ItemsModel::CommentsModelRole).value<QObject *>()));
}
}
bool hasReview(const QModelIndex &index, bool checkParents = false)
{
bool result{false};
if (q->sourceModel()) {
if (q->sourceModel()->data(index, KNSCore::CommentsModel::ScoreRole).toInt() > 0) {
result = true;
}
if (result == false && checkParents) {
QModelIndex parentIndex = q->sourceModel()->index(q->sourceModel()->data(index, KNSCore::CommentsModel::ParentIndexRole).toInt(), 0);
if (parentIndex.isValid()) {
result = hasReview(parentIndex, true);
}
}
}
return result;
}
};
}
using namespace KNewStuffQuick;
CommentsModel::CommentsModel(QObject *parent)
: QSortFilterProxyModel(parent)
, d(new CommentsModelPrivate(this))
{
}
CommentsModel::~CommentsModel() = default;
void KNewStuffQuick::CommentsModel::classBegin()
{
}
void KNewStuffQuick::CommentsModel::componentComplete()
{
d->componentCompleted = true;
d->resetConnections();
}
ItemsModel *CommentsModel::itemsModel() const
{
return d->itemsModel;
}
void CommentsModel::setItemsModel(ItemsModel *newItemsModel)
{
if (d->itemsModel != newItemsModel) {
d->itemsModel = newItemsModel;
d->resetConnections();
Q_EMIT itemsModelChanged();
}
}
KNSCore::Entry CommentsModel::entry() const
{
return d->entry;
}
void CommentsModel::setEntry(const KNSCore::Entry &entry)
{
d->entry = entry;
d->resetConnections();
Q_EMIT entryChanged();
}
CommentsModel::IncludedComments KNewStuffQuick::CommentsModel::includedComments() const
{
return d->includedComments;
}
void KNewStuffQuick::CommentsModel::setIncludedComments(CommentsModel::IncludedComments includedComments)
{
if (d->includedComments != includedComments) {
d->includedComments = includedComments;
invalidateFilter();
Q_EMIT includedCommentsChanged();
}
}
bool KNewStuffQuick::CommentsModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
bool result{false};
switch (d->includedComments) {
case IncludeOnlyReviews:
result = d->hasReview(sourceModel()->index(sourceRow, 0, sourceParent));
break;
case IncludeReviewsAndReplies:
result = d->hasReview(sourceModel()->index(sourceRow, 0, sourceParent), true);
break;
case IncludeAllComments:
default:
result = true;
break;
}
return result;
}
#include "moc_commentsmodel.cpp"
@@ -0,0 +1,96 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KNSQUICK_COMMENTSMODEL_H
#define KNSQUICK_COMMENTSMODEL_H
#include "quickitemsmodel.h"
#include <entry.h>
#include <QQmlParserStatus>
#include <QSortFilterProxyModel>
#include <memory>
namespace KNewStuffQuick
{
class CommentsModelPrivate;
/**
* @short Encapsulates a KNSCore::CommentsModel for use in Qt Quick
*
* This class takes care of initialisation of a KNSCore::CommentsModel when assigned an engine,
* providerId and entryId. If the data is not yet cached, it will be requested from the provider,
* and updated for display
* @since 5.63
*/
class CommentsModel : public QSortFilterProxyModel, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
/**
* The KNewStuffQuick::ItemsModel to interact with servers through
*/
Q_PROPERTY(ItemsModel *itemsModel READ itemsModel WRITE setItemsModel NOTIFY itemsModelChanged)
/**
* The index in the model of the entry to fetch comments for
*/
Q_PROPERTY(KNSCore::Entry entry READ entry WRITE setEntry NOTIFY entryChanged)
/**
* Which types of comments should be included
* @default AllComments
* @since 5.65
*/
Q_PROPERTY(KNewStuffQuick::CommentsModel::IncludedComments includedComments READ includedComments WRITE setIncludedComments NOTIFY includedCommentsChanged)
public:
/**
* The options which can be set for which comments to include
* @since 5.65
*/
enum IncludedComments {
IncludeAllComments = 0, //< All comments should be included
IncludeOnlyReviews = 1, //< Only comments which have a rating (and thus is considered a review) should be included
IncludeReviewsAndReplies = 2, //< Reviews (as OnlyReviews), except child comments are also included
};
Q_ENUM(IncludedComments)
explicit CommentsModel(QObject *parent = nullptr);
~CommentsModel() override;
void classBegin() override;
void componentComplete() override;
ItemsModel *itemsModel() const;
void setItemsModel(ItemsModel *newItemsModel);
Q_SIGNAL void itemsModelChanged();
KNSCore::Entry entry() const;
void setEntry(const KNSCore::Entry &entry);
Q_SIGNAL void entryChanged();
/**
* Which comments should be included
* @since 5.65
*/
CommentsModel::IncludedComments includedComments() const;
/**
* Set which comments should be included
* @since 5.65
*/
void setIncludedComments(CommentsModel::IncludedComments includedComments);
/**
* Fired when the value of includedComments changes
* @since 5.65
*/
Q_SIGNAL void includedCommentsChanged();
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
const std::unique_ptr<CommentsModelPrivate> d;
};
}
#endif // KNSQUICK_COMMENTSMODEL_H
@@ -0,0 +1,103 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "downloadlinkinfo.h"
#include <KFormat>
#include <QMimeDatabase>
class DownloadLinkInfoPrivate : public QSharedData
{
public:
QString name;
QString priceAmount;
QString distributionType;
QString descriptionLink;
int id = 0;
bool isDownloadtypeLink = true;
quint64 size = 0;
QString mimeType;
QString icon;
};
DownloadLinkInfo::DownloadLinkInfo(const KNSCore::Entry::DownloadLinkInformation &data)
: d(new DownloadLinkInfoPrivate)
{
d->name = data.name;
d->priceAmount = data.priceAmount;
d->distributionType = data.distributionType;
d->descriptionLink = data.descriptionLink;
d->id = data.id;
d->isDownloadtypeLink = data.isDownloadtypeLink;
d->size = data.size;
QMimeDatabase db;
for (QString string : data.tags) {
if (string.startsWith(QStringLiteral("data##mimetype="))) {
d->mimeType = string.split(QStringLiteral("=")).last();
}
}
d->icon = db.mimeTypeForName(d->mimeType).iconName();
if (d->icon.isEmpty()) {
d->icon = db.mimeTypeForName(d->mimeType).genericIconName();
}
if (d->icon.isEmpty()) {
d->icon = QStringLiteral("download");
}
}
DownloadLinkInfo::DownloadLinkInfo(const DownloadLinkInfo &) = default;
DownloadLinkInfo &DownloadLinkInfo::operator=(const DownloadLinkInfo &) = default;
DownloadLinkInfo::~DownloadLinkInfo() = default;
QString DownloadLinkInfo::name() const
{
return d->name;
}
QString DownloadLinkInfo::priceAmount() const
{
return d->priceAmount;
}
QString DownloadLinkInfo::distributionType() const
{
return d->distributionType;
}
QString DownloadLinkInfo::descriptionLink() const
{
return d->descriptionLink;
}
int DownloadLinkInfo::id() const
{
return d->id;
}
bool DownloadLinkInfo::isDownloadtypeLink() const
{
return d->isDownloadtypeLink;
}
quint64 DownloadLinkInfo::size() const
{
return d->size;
}
QString DownloadLinkInfo::formattedSize() const
{
static const KFormat formatter;
if (d->size == 0) {
return QString();
}
return formatter.formatByteSize(d->size * 1000);
}
QString DownloadLinkInfo::icon() const
{
return d->icon;
}
#include "moc_downloadlinkinfo.cpp"
@@ -0,0 +1,55 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef DOWNLOADLINKINFO_H
#define DOWNLOADLINKINFO_H
#include "KNSCore/Entry"
#include <QSharedData>
class DownloadLinkInfoPrivate;
/**
* @short One downloadable item as contained within one content item
*
* A simple data container which wraps a KNSCore::Entry::DownloadLinkInformation
* instance and provides property accessors for each of the pieces of information stored
* in it.
*/
class DownloadLinkInfo
{
Q_GADGET
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString priceAmount READ priceAmount CONSTANT)
Q_PROPERTY(QString distributionType READ distributionType CONSTANT)
Q_PROPERTY(QString descriptionLink READ descriptionLink CONSTANT)
Q_PROPERTY(int id READ id CONSTANT)
Q_PROPERTY(bool isDownloadtypeLink READ isDownloadtypeLink CONSTANT)
Q_PROPERTY(quint64 size READ size CONSTANT)
Q_PROPERTY(QString formattedSize READ formattedSize CONSTANT)
Q_PROPERTY(QString icon READ icon CONSTANT)
public:
explicit DownloadLinkInfo(const KNSCore::Entry::DownloadLinkInformation &data);
DownloadLinkInfo(const DownloadLinkInfo &);
DownloadLinkInfo &operator=(const DownloadLinkInfo &);
~DownloadLinkInfo();
QString name() const;
QString priceAmount() const;
QString distributionType() const;
QString descriptionLink() const;
int id() const;
bool isDownloadtypeLink() const;
quint64 size() const;
QString formattedSize() const;
QString icon() const;
private:
QSharedDataPointer<DownloadLinkInfoPrivate> d;
};
#endif // DOWNLOADLINKINFO_H
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: BSD-2-Clause
# SPDX-FileCopyrightText: Harald Sitter <sitter@kde.org>
ecm_add_qml_module(newstuffqmlpluginprivate URI "org.kde.newstuff.private" GENERATE_PLUGIN_SOURCE VERSION 1.0)
target_sources(newstuffqmlpluginprivate PRIVATE transientmagicianassistant.cpp)
ecm_qt_declare_logging_category(newstuffqmlpluginprivate
HEADER knewstuffquickprivate_debug.h
IDENTIFIER KNEWSTUFFQUICKPRIVATE
CATEGORY_NAME kf.newstuff.quick.private
OLD_CATEGORY_NAMES org.kde.knewstuff.quick.private
DESCRIPTION "knewstuff (qtquick private)"
EXPORT KNEWSTUFF
)
target_link_libraries(newstuffqmlpluginprivate PRIVATE Qt::Quick)
ecm_finalize_qml_module(newstuffqmlpluginprivate DESTINATION ${KDE_INSTALL_QMLDIR})
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
#include "transientmagicianassistant.h"
#include <QQuickItem>
#include <QQuickWindow>
#include "knewstuffquickprivate_debug.h"
void TransientMagicianAssistant::classBegin()
{
}
void TransientMagicianAssistant::componentComplete()
{
auto optionalWindow = findWindowParent();
if (!optionalWindow) {
qCWarning(KNEWSTUFFQUICKPRIVATE) << "Unexpectedly have not found a window as parent of TransientMagicianAssistant";
return;
}
auto window = optionalWindow.value();
if (window->transientParent()) {
return;
}
qCWarning(KNEWSTUFFQUICKPRIVATE)
<< "You have not set a transientParent on KNewStuff.Dialog or .Action. This may cause severe problems with window and lifetime management. "
"We'll try to fix the situation automatically but you should really provide an explicit transientParent";
qCDebug(KNEWSTUFFQUICKPRIVATE) << "Applying transient parent magic assistance to " << window << "🪄";
for (auto windowAncestor = qobject_cast<QObject *>(window)->parent(); windowAncestor; windowAncestor = windowAncestor->parent()) {
if (auto item = qobject_cast<QQuickItem *>(windowAncestor); item && item->window()) {
qCDebug(KNEWSTUFFQUICKPRIVATE) << window << "is now transient for" << item->window();
connect(item, &QQuickItem::windowChanged, window, [window](QQuickWindow *newParent) {
window->setTransientParent(newParent);
});
window->setTransientParent(item->window());
return;
}
}
qCWarning(KNEWSTUFFQUICKPRIVATE) << "Failed to do magic. Found no suitable window to become transient for.";
}
std::optional<QQuickWindow *> TransientMagicianAssistant::findWindowParent()
{
// Finds the KNewStuff.Dialog though practically it is always parent()->parent() it is a bit tidier to search
// for it instead.
for (auto ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
if (auto window = qobject_cast<QQuickWindow *>(ancestor)) {
return window;
}
}
return {};
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
#pragma once
#include <optional>
#include <QObject>
#include <QQmlEngine>
#include <QQmlParserStatus>
class QQuickWindow;
class TransientMagicianAssistant : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
QML_ELEMENT
public:
using QObject::QObject;
void classBegin() override;
void componentComplete() override;
std::optional<QQuickWindow *> findWindowParent();
};
@@ -0,0 +1,203 @@
/*
SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief An action which when triggered will open a NewStuff.Dialog or a NewStuff.Page, depending on settings
*
* This component is equivalent to the old Button component, but functions in more modern applications
*
* The following is a simple example of how to use this Action to show wallpapers from the KDE Store, on a
* system where Plasma has been installed (and consequently the wallpaper knsrc file is available). This also
* shows how to make the action push a page to a pageStack rather than opening a dialog:
*
\code{.qml}
import org.kde.newstuff as NewStuff
NewStuff.Action {
configFile: "wallpaper.knsrc"
text: i18n("&Get New Wallpapers…")
pageStack: applicationWindow().pageStack
onEntryEvent: function(entry, event) {
if (event === NewStuff.Entry.StatusChangedEvent) {
// A entry was installed, updated or removed
} else if (event === NewStuff.Entry.AdoptedEvent) {
// The "AdoptionCommand" from the knsrc file was run for the given entry.
// This should not require refreshing the data for the model
}
}
}
\endcode
*
* @see NewStuff.Button
* @since 5.81
*/
import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
import org.kde.newstuff.private as NewStuffPrivate
Kirigami.Action {
id: component
/*
* The configuration file is not aliased, because then we end up initialising the
* Engine immediately the Action is instantiated, which we want to avoid (as that
* is effectively a phone-home scenario, and causes internet traffic in situations
* where it would not seem likely that there should be any).
* If we want, in the future, to add some status display to the Action (such as "there
* are updates to be had" or somesuch, then we can do this, but until that choice is
* made, let's not)
*/
/**
* The configuration file to use for the Page created by this action
*/
property string configFile
/**
* The view mode of the page spawned by this action, which overrides the
* default one (ViewMode.Tiles). This should be set using the
* NewStuff.Page.ViewMode enum. Note that ViewMode.Icons has been removed,
* and asking for it will return ViewMode.Tiles.
* @see NewStuff.Page.ViewMode
*/
property int viewMode: NewStuff.Page.ViewMode.Tiles
/**
* If this is set, the action will push a NewStuff.Page onto this page stack
* (and request it is made visible if triggered again). If you do not set this
* property, the action will spawn a NewStuff.Dialog instead.
* @note If you are building a KCM, set this to your ```kcm``` object.
*/
property Item pageStack
/**
* The engine which handles the content in this Action
* This will be null until the action has been triggered the first time
*/
readonly property NewStuff.Engine engine: component._private.engine
/**
* This forwards the entry changed event from the QtQuick engine
* @see Engine::entryEvent
*/
signal entryEvent(var entry, int event)
/**
* If this is true (default is false), the action will be shown when the Kiosk settings are such
* that Get Hot New Stuff is disallowed (and any other time enabled is set to false).
* Usually you would want to leave this alone, but occasionally you may have a reason to
* leave a action in place that the user is unable to enable.
*/
property bool visibleWhenDisabled: false
/**
* The parent window for the dialog created by invoking the action
*
* @since 6.1
*/
// TODO KF7: make this required. without it we have a hard time doing complex window management in systemsettings
property Window transientParent
/**
* Show the page/dialog (same as activating the action), if allowed by the Kiosk settings
*/
function showHotNewStuff() {
component._private.showHotNewStuff();
}
onTriggered: showHotNewStuff()
icon.name: "get-hot-new-stuff"
visible: enabled || visibleWhenDisabled
enabled: NewStuff.Settings.allowedByKiosk
onEnabledChanged: {
// If the user resets this when kiosk has disallowed ghns, force enabled back to false
if (enabled && !NewStuff.Settings.allowedByKiosk) {
enabled = false;
}
}
readonly property QtObject _private: QtObject {
property NewStuff.Engine engine: pageItem ? pageItem.engine : null
// Probably wants to be deleted and cleared if the "mode" changes at runtime...
property /* NewStuff.Dialog | NewStuff.Page */QtObject pageItem
readonly property Connections engineConnections: Connections {
target: component.engine
function onEntryEvent(entry, event) {
component.entryEvent(entry, event);
}
}
function showHotNewStuff() {
if (NewStuff.Settings.allowedByKiosk) {
if (component.pageStack !== null) {
if (component._private.pageItem // If we already have a page created...
&& (component.pageStack.columnView !== undefined // first make sure that this pagestack is a Kirigami-style one (otherwise just assume we're ok)
&& component.pageStack.columnView.contains(component._private.pageItem))) // and then check if the page is still in the stack before attempting to...
{
// ...set the already existing page as the current page
component.pageStack.currentItem = component._private.pageItem;
} else {
component._private.pageItem = newStuffPage.createObject(component);
component.pageStack.push(component._private.pageItem);
}
} else {
newStuffDialog.open();
}
} else {
// make some noise, because silently doing nothing is a bit annoying
}
}
property Component newStuffPage: Component {
NewStuff.Page {
configFile: component.configFile
viewMode: component.viewMode
}
}
property Item newStuffDialog: Loader {
id: dialogLoader
// Use this function to open the dialog. It seems roundabout, but this ensures
// that the dialog is not constructed until we want it to be shown the first time,
// since it will initialise itself on the first load (which causes it to phone
// home) and we don't want that until the user explicitly asks for it.
function open() {
if (item) {
item.open();
} else {
active = true;
}
}
onLoaded: {
component._private.pageItem = item;
item.open();
}
active: false
asynchronous: true
sourceComponent: NewStuff.Dialog {
transientParent: component.transientParent
configFile: component.configFile
viewMode: component.viewMode
onClosing: {
// Unload the dialog when it is closed otherwise it gets stuck in memory because of the weird
// constructs we have in play here between nested objects and loaders and what not.
// Since the dialog is a top level window it would then prevent the QApplication from quitting.
dialogLoader.active = false
component._private.pageItem = null
}
NewStuffPrivate.TransientMagicianAssistant {}
}
}
}
}
@@ -0,0 +1,131 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A button which when clicked will open a dialog with a NewStuff.Page at the base
*
* This component is equivalent to the old Button
* @see KNewStuff::Button
* @since 5.63
*/
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.newstuff as NewStuff
QQC2.Button {
id: component
/*
* The configuration file is not aliased, because then we end up initialising the
* Engine immediately the Button is shown, which we want to avoid (as that
* is effectively a phone-home scenario, and causes internet traffic in situations
* where it would not seem likely that there should be any).
* If we want, in the future, to add some status display to Button (such as "there
* are updates to be had" or somesuch, then we can do this, but until that choice is
* made, let's not)
*/
/**
* The configuration file to use for this button
*/
property string configFile
/**
* Set the text that should appear on the button. Will be set as
* i18nd("knewstuff6", "Download New %1…").
*
* @note For the sake of consistency, you should NOT override the text property, just set this one
*/
property string downloadNewWhat: i18ndc("knewstuff6", "Used to construct the button's label (which will become Download New 'this value'…)", "Stuff")
text: i18nd("knewstuff6", "Download New %1…", downloadNewWhat)
/**
* The view mode of the dialog spawned by this button, which overrides the
* default one (ViewMode.Tiles). This should be set using the
* NewStuff.Page.ViewMode enum. Note that ViewMode.Icons has been removed,
* and asking for it will return ViewMode.Tiles.
* @see NewStuff.Page.ViewMode
*/
property int viewMode: NewStuff.Page.ViewMode.Tiles
/**
* emitted when the Hot New Stuff dialog is about to be shown, usually
* as a result of the user having click on the button
*/
signal aboutToShowDialog()
/**
* The engine which handles the content in this Button
*/
property NewStuff.Engine engine
/**
* This forwards the entryEvent from the QtQuick engine
* @see Engine::entryEvent
* @since 5.82
*/
signal entryEvent(var entry, int event)
property Connections engineConnections: Connections {
target: component.engine
function onEntryEvent(entry, event) {
component.entryEvent(entry, event);
}
}
/**
* If this is true (default is false), the button will be shown when the Kiosk settings are such
* that Get Hot New Stuff is disallowed (and any other time enabled is set to false).
* Usually you would want to leave this alone, but occasionally you may have a reason to
* leave a button in place that the user is unable to enable.
*/
property bool visibleWhenDisabled: false
/**
* @internal The NewStuff dialog that is opened by the button.
* Use showDialog() to create and open the dialog.
*/
property NewStuff.Dialog __ghnsDialog
/**
* Show the dialog (same as clicking the button), if allowed by the Kiosk settings
*/
function showDialog() {
if (!NewStuff.Settings.allowedByKiosk) {
// make some noise, because silently doing nothing is a bit annoying
console.warn("Not allowed by Kiosk");
return;
}
component.aboutToShowDialog();
// Use this function to open the dialog. It seems roundabout, but this ensures
// that the dialog is not constructed until we want it to be shown the first time,
// since it will initialise itself/compile itself when using Loader on the first
// load and we don't want that until the user explicitly asks for it.
if (component.__ghnsDialog === null) {
const dialogComponent = Qt.createComponent("Dialog.qml");
component.__ghnsDialog = dialogComponent.createObject(component, {
"configFile": Qt.binding(() => component.configFile),
"viewMode": Qt.binding(() => component.viewMode),
});
dialogComponent.destroy();
}
component.__ghnsDialog.open();
component.engine = component.__ghnsDialog.engine;
}
onClicked: showDialog()
icon.name: "get-hot-new-stuff"
visible: enabled || visibleWhenDisabled
enabled: NewStuff.Settings.allowedByKiosk
onEnabledChanged: {
// If the user resets this when kiosk has disallowed ghns, force enabled back to false
if (enabled && !NewStuff.Settings.allowedByKiosk) {
enabled = false;
}
}
}
@@ -0,0 +1,118 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A dialog which has a NewStuff.Page at the base
*
* This component is equivalent to the old DownloadDialog, but you should consider
* using NewStuff.Page instead for a more modern style of integration into your
* application's flow.
* @see KNewStuff::DownloadDialog
* @since 5.63
*/
import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
Window {
id: component
// Keep in sync with the implicit sizes in DialogContent.qml and the default
// size in dialog.cpp
width: Math.min(Kirigami.Units.gridUnit * 44, Screen.width)
height: Math.min(Kirigami.Units.gridUnit * 30, Screen.height)
/**
* The configuration file to use for this button
*/
property alias configFile: newStuffPage.configFile
/**
* Set the text that should appear as the dialog's title. Will be set as
* i18nd("knewstuff6", "Download New %1").
*
* @default The name defined by your knsrc config file
* @note For the sake of consistency, you should NOT override the title property, just set this one
*/
property string downloadNewWhat: engine.name
title: component.downloadNewWhat.length > 0
? i18ndc("knewstuff6", "The dialog title when we know which type of stuff is being requested", "Download New %1", component.downloadNewWhat)
: i18ndc("knewstuff6", "A placeholder title used in the dialog when there is no better title available", "Download New Stuff")
/**
* The engine which handles the content in this dialog
*/
property alias engine: newStuffPage.engine
/**
* The default view mode of the dialog spawned by this button. This should be
* set using the NewStuff.Page.ViewMode enum
* @see NewStuff.Page.ViewMode
*/
property alias viewMode: newStuffPage.viewMode
/**
* emitted when the Hot New Stuff dialog is about to be shown, usually
* as a result of the user having click on the button
*/
signal aboutToShowDialog()
/**
* This forwards the entryEvent from the QtQuick engine
* @see Engine::entryEvent
* @since 5.82
*/
signal entryEvent(var entry, int event)
property Connections engineConnections: Connections {
target: component.engine
function onEntryEvent(entry, event) {
component.entryEvent(entry, event);
}
}
/**
* Show the details page for a specific entry.
* If you call this function before the engine initialisation has been completed,
* the action itself will be postponed until that has happened.
* @param providerId The provider ID for the entry you wish to show details for
* @param entryId The unique ID for the entry you wish to show details for
* @since 5.79
*/
function showEntryDetails(providerId, entryId) {
newStuffPage.__showEntryDetails(providerId, entryId);
}
function open() {
component.visible = true;
}
onVisibleChanged: {
if (visible) {
newStuffPage.engine.revalidateCacheEntries();
}
}
color: Kirigami.Theme.backgroundColor
NewStuff.DialogContent {
id: newStuffPage
anchors.fill: parent
downloadNewWhat: component.downloadNewWhat
Keys.onEscapePressed: event => component.close()
}
Component {
id: uploadPage
NewStuff.UploadPage {
objectName: "uploadPage"
engine: newStuffPage.engine
}
}
}
@@ -0,0 +1,64 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief The contents of the NewStuff.Dialog component
*
* This component is equivalent to the old DownloadWidget, but you should consider
* using NewStuff.Page instead for a more modern style of integration into your
* application's flow.
* @see KNewStuff::DownloadWidget
* @since 5.63
*/
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
Kirigami.ApplicationItem {
id: component
property alias downloadNewWhat: newStuffPage.title
/**
* The configuration file to use for this button
*/
property alias configFile: newStuffPage.configFile
/**
* The engine which handles the content in this dialog
*/
property alias engine: newStuffPage.engine
/**
* The default view mode of the dialog spawned by this button. This should be
* set using the NewStuff.Page.ViewMode enum
* @see NewStuff.Page.ViewMode
*/
property alias viewMode: newStuffPage.viewMode
function __showEntryDetails(providerId, entryId) {
newStuffPage.showEntryDetails(providerId, entryId);
}
// Keep in sync with the default sizes in Dialog.qml and dialog.cpp
implicitWidth: Kirigami.Units.gridUnit * 44
implicitHeight: Kirigami.Units.gridUnit * 30
pageStack.defaultColumnWidth: pageStack.width
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.Auto
pageStack.globalToolBar.canContainHandles: true
pageStack.initialPage: NewStuff.Page {
id: newStuffPage
showUploadAction: false
}
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
}
}
@@ -0,0 +1,99 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigami.delegates as KirigamiDelegates
import org.kde.newstuff as NewStuff
/**
* @brief An overlay sheet for showing a list of download options for one entry
*
* This is used by the NewStuff.Page component
* @since 5.63
*/
Kirigami.Dialog {
id: component
property var entry
// Ensure the dialog does not get too small
height: Math.max(Math.round(root.height - (root.height / 4)), Kirigami.Units.gridUnit * 20)
width: Math.max(Math.round(root.width - (root.width / 4)), Kirigami.Units.gridUnit * 25)
property alias downloadLinks: itemsView.model
signal itemPicked(var entry, int downloadItemId, string downloadName)
showCloseButton: false
title: i18nd("knewstuff6", "Pick Your Installation Option")
ListView {
id: itemsView
clip: true
headerPositioning: ListView.InlineHeader
header: QQC2.Label {
width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin
padding: Kirigami.Units.largeSpacing
text: i18nd("knewstuff6", "Please select the option you wish to install from the list of downloadable items below. If it is unclear which you should chose out of the available options, please contact the author of this item and ask that they clarify this through the naming of the items.")
wrapMode: Text.Wrap
}
delegate: QQC2.ItemDelegate {
id: delegate
width: itemsView.width
icon.name: modelData.icon
text: modelData.name
Kirigami.Theme.useAlternateBackgroundColor: true
// Don't need a highlight, hover, or pressed effects
highlighted: false
hoverEnabled: false
down: false
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
// TODO: switch to just IconTitle once it exists, since we don't need
// the subtitle here and are only using a Kirigami delegate for the visual
// consistency it offers
Kirigami.IconTitleSubtitle {
Layout.fillWidth: true
icon.name: delegate.icon.name
title: delegate.text
selected: delegate.highlighted
wrapMode: Text.WrapAnywhere
}
QQC2.Label {
text: modelData.formattedSize
color: delegate.highlighted
? Kirigami.Theme.highlightedTextColor
: Kirigami.Theme.textColor
}
QQC2.ToolButton {
id: installButton
text: i18nd("knewstuff6", "Install")
icon.name: "install-symbolic"
onClicked: {
component.close();
component.itemPicked(component.entry, modelData.id, modelData.name);
}
}
}
}
}
}
@@ -0,0 +1,262 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A Kirigami.Page component used for displaying the details for a single entry
*
* This component is equivalent to the details view in the old DownloadDialog
* @see KNewStuff::DownloadDialog
* @since 5.63
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCMUtils
import org.kde.newstuff as NewStuff
import "private" as Private
KCMUtils.SimpleKCM {
id: component
property NewStuff.ItemsModel newStuffModel
property var entry
property string name
property var author
property alias shortSummary: shortSummaryItem.text
property alias summary: summaryItem.text
property alias previews: screenshotsItem.screenshotsModel
property string homepage
property string donationLink
property int status
property int commentsCount
property int rating
property int downloadCount
property var downloadLinks
property string providerId
property int entryType
Component.onCompleted: {
updateContents();
newStuffModel.engine.updateEntryContents(component.entry);
}
Connections {
target: newStuffModel
function onEntryChanged(changedEntry) {
if (entry === changedEntry) {
updateContents();
}
}
}
function updateContents() {
component.providerId = entry.providerId;
component.status = entry.status;
component.author = entry.author;
component.name = entry.name;
component.shortSummary = entry.shortSummary;
component.summary = entry.summary;
component.homepage = entry.homepage;
component.donationLink = entry.donationLink;
component.status = entry.status;
component.commentsCount = entry.numberOfComments;
component.rating = entry.rating;
component.downloadCount = entry.downloadCount;
const modelIndex = newStuffModel.index(newStuffModel.indexOfEntry(entry), 0);
component.previews = newStuffModel.data(modelIndex, NewStuff.ItemsModel.PreviewsRole);
component.downloadLinks = newStuffModel.data(modelIndex, NewStuff.ItemsModel.DownloadLinksRole);
}
NewStuff.DownloadItemsSheet {
id: downloadItemsSheet
parent: component.QQC2.Overlay.overlay
onItemPicked: (entry, downloadItemId, downloadName) => {
const entryName = component.newStuffModel.data(component.newStuffModel.index(downloadItemId, 0), NewStuff.ItemsModel.NameRole);
applicationWindow().showPassiveNotification(i18ndc("knewstuff6", "A passive notification shown when installation of an item is initiated", "Installing %1 from %2", downloadName, entryName), 1500);
component.newStuffModel.engine.installLinkId(component.entry, downloadItemId);
}
}
Private.ErrorDisplayer {
engine: component.newStuffModel.engine
active: component.isCurrentPage
}
NewStuff.Author {
id: entryAuthor
engine: component.newStuffModel.engine
providerId: component.providerId
username: author.name
}
title: i18ndc("knewstuff6", "Combined title for the entry details page made of the name of the entry, and the author's name", "%1 by %2", component.name, entryAuthor.name)
actions: [
Kirigami.Action {
text: component.downloadLinks.length === 1
? i18ndc("knewstuff6", "Request installation of this item, available when there is exactly one downloadable item", "Install")
: i18ndc("knewstuff6", "Show installation options, where there is more than one downloadable item", "Install…")
icon.name: "install"
onTriggered: source => {
if (component.downloadLinks.length === 1) {
newStuffModel.engine.installLinkId(component.entry, NewStuff.ItemsModel.FirstLinkId);
} else {
downloadItemsSheet.downloadLinks = component.downloadLinks;
downloadItemsSheet.entry = component.index;
downloadItemsSheet.open();
}
}
enabled: component.status === NewStuff.Entry.Downloadable || component.status === NewStuff.Entry.Deleted
visible: enabled
},
Kirigami.Action {
text: i18ndc("knewstuff6", "Request updating of this item", "Update")
icon.name: "update-none"
onTriggered: source => newStuffModel.update(component.entry, NewStuff.ItemsModel.AutoDetectLinkId)
enabled: component.status === NewStuff.Entry.Updateable
visible: enabled
},
Kirigami.Action {
text: i18ndc("knewstuff6", "Request uninstallation of this item", "Uninstall")
icon.name: "edit-delete"
onTriggered: source => newStuffModel.engine.uninstall(component.entry)
enabled: component.status === NewStuff.Entry.Installed || component.status === NewStuff.Entry.Updateable
visible: enabled
}
]
ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.AbstractCard {
id: statusCard
readonly property string message: {
switch (component.status) {
case NewStuff.Entry.Downloadable:
case NewStuff.Entry.Installed:
case NewStuff.Entry.Updateable:
case NewStuff.Entry.Deleted:
return "";
case NewStuff.Entry.Installing:
return i18ndc("knewstuff6", "Status message to be shown when the entry is in the process of being installed OR uninstalled", "Currently working on the item %1 by %2. Please wait…", component.name, entryAuthor.name);
case NewStuff.Entry.Updating:
return i18ndc("knewstuff6", "Status message to be shown when the entry is in the process of being updated", "Currently updating the item %1 by %2. Please wait…", component.name, entryAuthor.name);
default:
return i18ndc("knewstuff6", "Status message which should only be shown when the entry has been given some unknown or invalid status.", "This item is currently in an invalid or unknown state. <a href=\"https://bugs.kde.org/enter_bug.cgi?product=frameworks-knewstuff\">Please report this to the KDE Community in a bug report</a>.");
}
}
visible: opacity > 0
opacity: message.length > 0 ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
}
}
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
contentItem: RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
text: statusCard.message
wrapMode: Text.Wrap
onLinkActivated: link => Qt.openUrlExternally(link)
}
QQC2.BusyIndicator {
running: statusCard.opacity > 0
}
}
}
Item {
Layout.fillWidth: true
height: Kirigami.Units.gridUnit * 3
}
Private.EntryScreenshots {
id: screenshotsItem
Layout.fillWidth: true
}
Kirigami.Heading {
id: shortSummaryItem
wrapMode: Text.Wrap
Layout.fillWidth: true
}
Kirigami.FormLayout {
Layout.fillWidth: true
Kirigami.LinkButton {
Kirigami.FormData.label: i18nd("knewstuff6", "Comments and Reviews:")
enabled: component.commentsCount > 0
text: i18ndc("knewstuff6", "A link which, when clicked, opens a new sub page with comments (comments with or without ratings) for this entry", "%1 Reviews and Comments", component.commentsCount)
onClicked: mouse => pageStack.push(commentsPage)
}
Private.Rating {
id: ratingsItem
Kirigami.FormData.label: i18nd("knewstuff6", "Rating:")
rating: component.rating
}
Kirigami.UrlButton {
Kirigami.FormData.label: i18nd("knewstuff6", "Homepage:")
text: i18ndc("knewstuff6", "A link which, when clicked, opens the website associated with the entry (this could be either one specific to the project, the author's homepage, or any other website they have chosen for the purpose)", "Open the homepage for %1", component.name)
url: component.homepage
visible: url !== ""
}
Kirigami.UrlButton {
Kirigami.FormData.label: i18nd("knewstuff6", "How To Donate:")
text: i18ndc("knewstuff6", "A link which, when clicked, opens a website with information on donation in support of the entry", "Find out how to donate to this project")
url: component.donationLink
visible: url !== ""
}
}
Kirigami.SelectableLabel {
id: summaryItem
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
textFormat: Text.RichText
}
Component {
id: commentsPage
Private.EntryCommentsPage {
itemsModel: component.newStuffModel
entry: component.entry
entryName: component.name
entryAuthorId: component.author.name
entryProviderId: component.providerId
}
}
}
}
@@ -0,0 +1,492 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A Kirigami.Page component used for managing KNS entries
*
* This component is functionally equivalent to the old DownloadDialog
* @see KNewStuff::DownloadDialog
* @since 5.63
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kcmutils as KCMUtils
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
import "private" as Private
import "private/entrygriddelegates" as EntryGridDelegates
KCMUtils.GridViewKCM {
id: root
/**
* @brief The configuration file which describes the application (knsrc)
*
* The format and location of this file is found in the documentation for
* KNS3::DownloadDialog
*/
property alias configFile: newStuffEngine.configFile
readonly property alias engine: newStuffEngine
/**
* Whether or not to show the Upload... context action
* Usually this will be bound to the engine's property which usually defines
* this, but you can override it programmatically by setting it here.
* @since 5.85
* @see KNSCore::Engine::uploadEnabled
*/
property alias showUploadAction: uploadAction.visible
/**
* Show the details page for a specific entry.
* If you call this function before the engine initialisation has been completed,
* the action itself will be postponed until that has happened.
* @param providerId The provider ID for the entry you wish to show details for
* @param entryId The unique ID for the entry you wish to show details for
* @since 5.79
*/
function showEntryDetails(providerId, entryId) {
_showEntryDetailsThrottle.enabled = true;
_showEntryDetailsThrottle.entry = newStuffEngine. __createEntry(providerId, entryId);
if (newStuffEngine.busyState === NewStuff.Engine.Initializing) {
_showEntryDetailsThrottle.queryWhenInitialized = true;
} else {
_showEntryDetailsThrottle.requestDetails();
}
}
// Helper for loading and showing entry details
Connections {
id: _showEntryDetailsThrottle
target: newStuffModel.engine
enabled: false
property var entry
property bool queryWhenInitialized: false
function requestDetails() {
newStuffEngine.updateEntryContents(entry);
queryWhenInitialized = false;
}
function onBusyStateChanged() {
if (queryWhenInitialized && newStuffEngine.busyState !== NewStuff.Engine.Initializing) {
requestDetails();
queryWhenInitialized = false;
}
}
function onSignalEntryEvent(changedEntry, event) {
if (event === NewStuff.Engine.DetailsLoadedEvent && changedEntry === entry) { // only uniqueId and providerId are checked for equality
enabled = false;
pageStack.push(detailsPage, {
newStuffModel,
providerId: changedEntry.providerId,
entry: changedEntry,
});
}
}
}
Connections {
id: _restoreSearchState
target: pageStack
enabled: false
function onCurrentIndexChanged() {
if (pageStack.currentIndex === 0) {
newStuffEngine.restoreSearch();
_restoreSearchState.enabled = false;
}
}
}
property string uninstallLabel: i18ndc("knewstuff6", "Request uninstallation of this item", "Uninstall")
property string useLabel: engine.useLabel
property int viewMode: Page.ViewMode.Tiles
// TODO KF7: remove Icons
enum ViewMode {
Tiles,
Icons,
Preview
}
// Otherwise the first item will be focused, see BUG: 424894
Component.onCompleted: {
view.currentIndex = -1;
}
title: newStuffEngine.name
headerPaddingEnabled: false
header: Kirigami.InlineMessage {
readonly property bool riskyContent: newStuffEngine.contentWarningType === NewStuff.Engine.Executables
visible: !loadingOverlay.visible
type: riskyContent ? Kirigami.MessageType.Warning : Kirigami.MessageType.Information
position: Kirigami.InlineMessage.Position.Header
text: riskyContent
? xi18ndc("knewstuff6", "@info displayed as InlineMessage", "Use caution when accessing user-created content shown here, as it may contain executable code that hasn't been tested by KDE or your distributor for safety, stability, or quality.")
: i18ndc("knewstuff6", "@info displayed as InlineMessage", "User-created content shown here hasn't been tested by KDE or your distributor for functionality or quality.")
}
NewStuff.Engine {
id: newStuffEngine
}
NewStuff.QuestionAsker {
parent: root.QQC2.Overlay.overlay
}
Private.ErrorDisplayer {
engine: newStuffEngine
active: root.isCurrentPage
}
QQC2.ActionGroup { id: viewFilterActionGroup }
QQC2.ActionGroup { id: viewSortingActionGroup }
actions: [
Kirigami.Action {
visible: newStuffEngine.needsLazyLoadSpinner
displayComponent: QQC2.BusyIndicator {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
}
},
Kirigami.Action {
text: {
if (newStuffEngine.filter === 0) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "All");
} else if (newStuffEngine.filter === 1) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "Installed");
} else if (newStuffEngine.filter === 2) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "Updateable");
} else {
// then it's ExactEntryId and we want to probably just ignore that
}
}
checkable: false
icon.name: "view-filter"
Kirigami.Action {
icon.name: "package-available"
text: i18ndc("knewstuff6", "@option:radio similar to combobox item, List option which will set the filter to show everything", "All")
checkable: true
checked: newStuffEngine.filter === 0
onTriggered: source => {
newStuffEngine.filter = 0;
}
QQC2.ActionGroup.group: viewFilterActionGroup
}
Kirigami.Action {
icon.name: "package-installed-updated"
text: i18ndc("knewstuff6", "@option:radio similar to combobox item, List option which will set the filter so only installed items are shown", "Installed")
checkable: true
checked: newStuffEngine.filter === 1
onTriggered: source => {
newStuffEngine.filter = 1;
}
QQC2.ActionGroup.group: viewFilterActionGroup
}
Kirigami.Action {
icon.name: "package-installed-outdated"
text: i18ndc("knewstuff6", "@option:radio similar to combobox item, List option which will set the filter so only installed items with updates available are shown", "Updateable")
checkable: true
checked: newStuffEngine.filter === 2
onTriggered: source => {
newStuffEngine.filter = 2;
}
QQC2.ActionGroup.group: viewFilterActionGroup
}
},
Kirigami.Action {
text: {
if (newStuffEngine.sortOrder === 0) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "Sort: Release date");
} else if (newStuffEngine.sortOrder === 1) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "Sort: Name");
} else if (newStuffEngine.sortOrder === 2) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "Sort: Rating");
} else if (newStuffEngine.sortOrder === 3) {
return i18ndc("knewstuff6", "@action:button opening menu similar to combobox, filter list", "Sort: Downloads");
} else {
}
}
checkable: false
icon.name: "view-sort"
Kirigami.Action {
icon.name: "sort-name"
text: i18ndc("knewstuff6", "@option:radio in menu, List option which will set the sort order to be alphabetical based on the name", "Name")
checkable: true
checked: newStuffEngine.sortOrder === 1
onTriggered: source => {
newStuffEngine.sortOrder = 1;
}
QQC2.ActionGroup.group: viewSortingActionGroup
}
Kirigami.Action {
icon.name: "rating"
text: i18ndc("knewstuff6", "@option:radio in menu, List option which will set the sort order to based on user ratings", "Rating")
checkable: true
checked: newStuffEngine.sortOrder === 2
onTriggered: source => {
newStuffEngine.sortOrder = 2;
}
QQC2.ActionGroup.group: viewSortingActionGroup
}
Kirigami.Action {
icon.name: "download"
text: i18ndc("knewstuff6", "@option:radio similar to combobox item, List option which will set the sort order to based on number of downloads", "Downloads")
checkable: true
checked: newStuffEngine.sortOrder === 3
onTriggered: source => {
newStuffEngine.sortOrder = 3;
}
QQC2.ActionGroup.group: viewSortingActionGroup
}
Kirigami.Action {
icon.name: "change-date-symbolic"
text: i18ndc("knewstuff6", "@option:radio similar to combobox item, List option which will set the sort order to based on when items were most recently updated", "Release date")
checkable: true
checked: newStuffEngine.sortOrder === 0
onTriggered: source => {
newStuffEngine.sortOrder = 0;
}
QQC2.ActionGroup.group: viewSortingActionGroup
}
},
Kirigami.Action {
id: uploadAction
text: i18nd("knewstuff6", "Upload…")
tooltip: i18nd("knewstuff6", "Learn how to add your own hot new stuff to this list")
icon.name: "upload-media"
visible: newStuffEngine.uploadEnabled
onTriggered: source => {
pageStack.push(uploadPage);
}
},
Kirigami.Action {
text: i18nd("knewstuff6", "Go to…")
icon.name: "go-next"
id: searchModelActions
visible: children.length > 0
},
Kirigami.Action {
text: i18nd("knewstuff6", "Search…")
icon.name: "system-search"
displayHint: Kirigami.DisplayHint.KeepVisible
displayComponent: Kirigami.SearchField {
id: searchField
enabled: engine.isValid
focusSequence: "Ctrl+F"
placeholderText: i18nd("knewstuff6", "Search…")
text: newStuffEngine.searchTerm
onAccepted: {
newStuffEngine.searchTerm = searchField.text;
}
Component.onCompleted: {
if (!Kirigami.InputMethod.willShowOnActive) {
forceActiveFocus();
}
}
}
}
]
Instantiator {
id: searchPresetInstatiator
model: newStuffEngine.searchPresetModel
Kirigami.Action {
required property int index
text: model.displayName
icon.name: model.iconName
onTriggered: source => {
const curIndex = newStuffEngine.searchPresetModel.index(index, 0);
newStuffEngine.searchPresetModel.loadSearch(curIndex);
}
}
onObjectAdded: (index, object) => {
searchModelActions.children.push(object);
}
}
Connections {
target: newStuffEngine.searchPresetModel
function onModelReset() {
searchModelActions.children = [];
}
}
footer: RowLayout {
spacing: Kirigami.Units.smallSpacing
visible: visibleChildren.length > 0
height: visible ? implicitHeight : 0
QQC2.Label {
visible: categoriesCombo.count > 2
text: i18nd("knewstuff6", "Category:")
}
QQC2.ComboBox {
id: categoriesCombo
Layout.fillWidth: true
visible: count > 2
model: newStuffEngine.categories
textRole: "displayName"
onCurrentIndexChanged: {
newStuffEngine.categoriesFilter = model.data(model.index(currentIndex, 0), NewStuff.CategoriesModel.NameRole);
}
}
QQC2.Button {
Layout.alignment: Qt.AlignRight
text: i18nd("knewstuff6", "Contribute Your Own…")
icon.name: "upload-media"
visible: newStuffEngine.uploadEnabled && !uploadAction.visible
onClicked: {
pageStack.push(uploadPage);
}
}
}
view.model: NewStuff.ItemsModel {
id: newStuffModel
engine: newStuffEngine
}
NewStuff.DownloadItemsSheet {
id: downloadItemsSheet
parent: root.QQC2.Overlay.overlay
onItemPicked: (entry, downloadItemId) => {
newStuffModel.engine.installLinkId(entry, downloadItemId);
}
}
view.implicitCellWidth: switch (root.viewMode) {
case Page.ViewMode.Preview:
return Kirigami.Units.gridUnit * 25;
case Page.ViewMode.Tiles:
case Page.ViewMode.Icons:
default:
return Kirigami.Units.gridUnit * 30;
}
view.implicitCellHeight: switch (root.viewMode) {
case Page.ViewMode.Preview:
return Kirigami.Units.gridUnit * 25;
case Page.ViewMode.Tiles:
case Page.ViewMode.Icons:
default:
return Math.round(view.implicitCellWidth / 3);
}
view.delegate: switch (root.viewMode) {
case Page.ViewMode.Preview:
return bigPreviewDelegate;
case Page.ViewMode.Tiles:
case Page.ViewMode.Icons:
default:
return tileDelegate;
}
Component {
id: bigPreviewDelegate
EntryGridDelegates.BigPreviewDelegate { }
}
Component {
id: tileDelegate
EntryGridDelegates.TileDelegate {
useLabel: root.useLabel
uninstallLabel: root.uninstallLabel
}
}
Component {
id: detailsPage
NewStuff.EntryDetails { }
}
Component {
id: uploadPage
NewStuff.UploadPage {
engine: newStuffEngine
}
}
Item {
id: loadingOverlay
anchors.fill: parent
opacity: newStuffEngine.isLoading && !newStuffEngine.needsLazyLoadSpinner ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
}
}
visible: opacity > 0
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
}
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
text: newStuffEngine.busyMessage
}
}
}
@@ -0,0 +1,180 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A component used to forward questions from KNewStuff's engine to the UI
*
* This component is equivalent to the WidgetQuestionListener
* @see KNewStuff::WidgetQuestionListener
* @see KNewStuffCore::Question
* @since 5.63
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
import org.kde.newstuff.core as NewStuffCore
QQC2.Dialog {
id: dialog
property int questionType
anchors.centerIn: parent
modal: true
focus: true
margins: Kirigami.Units.largeSpacing
padding: Kirigami.Units.largeSpacing
standardButtons: {
switch (questionType) {
case NewStuffCore.Question.SelectFromListQuestion:
case NewStuffCore.Question.InputTextQuestion:
case NewStuffCore.Question.PasswordQuestion:
case NewStuffCore.Question.ContinueCancelQuestion:
// QQC2 Dialog standardButtons does not have a Continue button...
return QQC2.Dialog.Ok | QQC2.Dialog.Cancel;
case NewStuffCore.Question.YesNoQuestion:
return QQC2.Dialog.Yes | QQC2.Dialog.No;
default:
return QQC2.Dialog.NoButton;
}
}
Connections {
target: NewStuff.QuickQuestionListener
function onAskListQuestion(title, question, list) {
dialog.questionType = NewStuffCore.Question.SelectFromListQuestion;
dialog.title = title;
questionLabel.text = question;
for (var i = 0; i < list.length; i++) {
listView.model.append({ text: list[i] });
}
listView.currentIndex = 0;
listView.visible = true;
dialog.open();
}
function onAskContinueCancelQuestion(title, question) {
dialog.questionType = NewStuffCore.Question.ContinueCancelQuestion;
dialog.title = title;
questionLabel.text = question;
dialog.open();
}
function onAskTextInputQuestion(title, question) {
dialog.questionType = NewStuffCore.Question.InputTextQuestion;
dialog.title = title;
questionLabel.text = question;
textInput.visible = true;
dialog.open();
}
function onAskPasswordQuestion(title, question) {
dialog.questionType = NewStuffCore.Question.PasswordQuestion;
dialog.title = title;
questionLabel.text = question;
textInput.echoMode = QQC2.TextInput.PasswordEchoOnEdit;
textInput.visible = true;
dialog.open();
}
function onAskYesNoQuestion(title, question) {
dialog.questionType = NewStuffCore.Question.YesNoQuestion;
dialog.title = title;
questionLabel.text = question;
dialog.open();
}
}
function passResponse(responseIsContinue) {
let input = "";
switch (dialog.questionType) {
case NewStuffCore.Question.SelectFromListQuestion:
input = listView.currentItem.text;
listView.model.clear();
listView.visible = false;
break;
case NewStuffCore.Question.InputTextQuestion:
input = textInput.text;
textInput.text = "";
textInput.visible = false;
break;
case NewStuffCore.Question.PasswordQuestion:
input = textInput.text;
textInput.text = "";
textInput.visible = false;
textInput.echoMode = QQC2.TextInput.Normal;
break;
case NewStuffCore.Question.ContinueCancelQuestion:
case NewStuffCore.Question.YesNoQuestion:
default:
// Nothing special to do for these types of question, we just pass along the positive or negative response
break;
}
NewStuff.QuickQuestionListener.passResponse(responseIsContinue, input);
}
ColumnLayout {
id: layout
readonly property real maxWidth: {
const bounds = dialog.parent;
if (!bounds) {
return 0;
}
return bounds.width - (dialog.leftPadding + dialog.leftMargin + dialog.rightMargin + dialog.rightPadding);
}
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
id: questionLabel
Layout.maximumWidth: layout.maxWidth
wrapMode: Text.Wrap
}
ListView {
id: listView
spacing: Kirigami.Units.smallSpacing
Layout.maximumWidth: layout.maxWidth
Layout.fillWidth: true
visible: false
model: ListModel { }
delegate: QQC2.ItemDelegate {
width: listView.width
text: model.text
}
}
QQC2.TextField {
id: textInput
Layout.maximumWidth: layout.maxWidth
Layout.fillWidth: true
visible: false
}
}
onAccepted: {
passResponse(true);
}
onRejected: {
passResponse(false);
}
}
@@ -0,0 +1,165 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A Kirigami.Page component used for showing how to upload KNS entries to a service
*
* This page shows a short guide for uploading new content to the service provided by a KNewStuff
* provider. This attempts to use the information available through the provider itself, and
* shows a link to the service's web page, and email in case it is not the KDE Store.
*
* While there are not currently any services which support direct OCS API based uploading of
* new content, we still need a way to guide people to how to do this, hence this component's
* simplistic nature.
*
* This component is functionally equivalent to the old UploadDialog
* @see KNewStuff::UploadDialog
* @since 5.85
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
import "private" as Private
Kirigami.ScrollablePage {
id: component
/**
* The NewStuffQuick Engine instance used to display content for this item.
* You can either pass in one that has already been set up (such as from a
* NewStuff.Page or NewStuff.Dialog), or you can construct a new one yourself,
* simply by doing something like this (which will use the wallpapers configuration):
\code
NewStuff.UploadPage {
engine: NewStuff.Engine {
configFile: "wallpapers.knsrc"
}
}
\endcode
*/
required property NewStuff.Engine engine
title: i18nc("@knewstuff6", "Upload New Stuff: %1", engine.name)
NewStuff.QuestionAsker {
parent: component.QQC2.Overlay.overlay
}
Private.ErrorDisplayer {
engine: component.engine
active: component.isCurrentPage
}
ColumnLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Item {
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
implicitHeight: uploaderBusy.running
? uploaderBusy.height + uploaderBusyInfo.height + Kirigami.Units.largeSpacing * 4
: 0
visible: uploaderBusy.running
opacity: uploaderBusy.running ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
QQC2.BusyIndicator {
id: uploaderBusy
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.verticalCenter
bottomMargin: Kirigami.Units.largeSpacing
}
running: component.engine.isLoading && component.engine.isValid
}
QQC2.Label {
id: uploaderBusyInfo
anchors {
top: parent.verticalCenter
left: parent.left
right: parent.right
margins: Kirigami.Units.largeSpacing
}
horizontalAlignment: Text.AlignHCenter
text: i18ndc("knewstuff6", "A text shown beside a busy indicator suggesting that data is being fetched", "Updating information…")
}
}
Repeater {
model: NewStuff.ProvidersModel {
engine: component.engine
}
Kirigami.Card {
enabled: !uploaderBusy.running
banner {
title: {
if (model.name === "api.kde-look.org") {
return i18ndc("knewstuff6", "The name of the KDE Store", "KDE Store");
} else if (model.name !== "") {
return model.name;
} else if (component.engine.name !== "") {
return component.engine.name;
} else {
return i18ndc("knewstuff6", "An unnamed provider", "Your Provider");
}
}
titleIcon: model.icon.toString() === "" ? "get-hot-new-stuff" : model.icon
}
actions: [
Kirigami.Action {
visible: model.website !== ""
text: i18ndc("knewstuff6", "Text for an action which causes the specified website to be opened using the user's system default browser", "Open Website: %1", model.website)
onTriggered: source => {
Qt.openUrlExternally(model.website);
}
},
Kirigami.Action {
visible: model.contactEmail !== "" && model.name !== "api.kde-look.org"
text: i18ndc("knewstuff6", "Text for an action which will attempt to send an email using the user's system default email client", "Send Email To: %1", model.contactEmail)
onTriggered: source => {
Qt.openUrlExternally("mailto:" + model.contactEmail);
}
}
]
contentItem: QQC2.Label {
wrapMode: Text.Wrap
text: model.name === "api.kde-look.org"
? i18ndc("knewstuff6", "A description of how to upload content to a generic provider", "To upload new entries, or to add content to an existing entry on the KDE Store, please open the website and log in. Once you have done this, you will be able to find the My Products entry in the menu which pops up when you click your user icon. Click on this entry to go to the product management system, where you can work on your products.")
: i18ndc("knewstuff6", "A description of how to upload content to the KDE Store specifically", "To upload new entries, or to add content to an existing entry, please open the provider's website and follow the instructions there. You will likely need to create a user and log in to a product management system, where you will need to follow the instructions for how to add. Alternatively, you might be required to contact the managers of the site directly to get new content added.")
}
}
}
}
}
@@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick
import QtQuick.Layouts
Loader {
property Component componentTrue
property Component componentFalse
property bool condition
Layout.minimumHeight: item ? item.Layout.minimumHeight : 0
Layout.minimumWidth: item ? item.Layout.minimumWidth : 0
sourceComponent: condition ? componentTrue : componentFalse
}
@@ -0,0 +1,169 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A card based delegate for showing a comment from a KNewStuffQuick::QuickCommentsModel
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
RowLayout {
id: component
/**
* The KNSQuick Engine object which handles all our content
*/
property NewStuff.Engine engine
/**
* The username of the author of whatever the comment is attached to
*/
property string entryAuthorId
/**
* The provider ID as supplied by the entry the comment is attached to
*/
property string entryProviderId
/**
* The username of the comment's author
*/
property string author
/**
* The OCS score, an integer from 1 to 100. It will be interpreted
* as a 5 star rating, with half star support (0-10)
*/
property int score
/**
* The title or subject line for the comment
*/
property string title
/**
* The actual text of the comment
*/
property alias reviewText: reviewLabel.text
/**
* The depth of the comment (in essence, how many parents the comment has)
*/
property int depth
spacing: 0
property NewStuff.Author commentAuthor: NewStuff.Author {
engine: component.engine
providerId: component.entryProviderId
username: component.author
}
Repeater {
model: component.depth
delegate: Rectangle {
Layout.fillHeight: true
Layout.minimumWidth: Kirigami.Units.largeSpacing
Layout.maximumWidth: Kirigami.Units.largeSpacing
color: Qt.tint(Kirigami.Theme.textColor, Qt.alpha(Kirigami.Theme.backgroundColor, 0.8))
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
width: 1
color: Kirigami.Theme.backgroundColor
}
}
}
ColumnLayout {
Item {
visible: component.depth === 0
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.largeSpacing
Layout.maximumHeight: Kirigami.Units.largeSpacing
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
visible: (component.title !== "" || component.score !== 0)
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Kirigami.Heading {
id: titleLabel
text: ((component.title === "") ? i18ndc("knewstuff6", "Placeholder title for when a comment has no subject, but does have a rating", "<i>(no title)</i>") : component.title)
level: 4
Layout.fillWidth: true
}
Rating {
id: ratingStars
rating: component.score
reverseLayout: true
}
Item {
Layout.minimumWidth: Kirigami.Units.largeSpacing
Layout.maximumWidth: Kirigami.Units.largeSpacing
}
}
QQC2.Label {
id: reviewLabel
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
wrapMode: Text.Wrap
}
RowLayout {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
Kirigami.UrlButton {
id: authorLabel
visible: url !== ""
url: (component.commentAuthor.homepage === "") ? component.commentAuthor.profilepage : component.commentAuthor.homepage
text: (component.author === component.entryAuthorId)
? i18ndc("knewstuff6", "The author label in case the comment was written by the author of the content entry the comment is attached to", "%1 <i>(author)</i>", component.commentAuthor.name)
: component.commentAuthor.name
}
QQC2.Label {
visible: !authorLabel.visible
text: authorLabel.text
}
Image {
id: authorIcon
Layout.maximumWidth: height
Layout.minimumWidth: height
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
fillMode: Image.PreserveAspectFit
source: component.commentAuthor.avatarUrl
Kirigami.Icon {
anchors.fill: parent
source: "user"
visible: opacity > 0
opacity: authorIcon.status === Image.Ready ? 0 : 1
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } }
}
}
Item {
Layout.minimumWidth: Kirigami.Units.largeSpacing
Layout.maximumWidth: Kirigami.Units.largeSpacing
}
}
Item {
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.largeSpacing
Layout.maximumHeight: Kirigami.Units.largeSpacing
}
}
}
@@ -0,0 +1,90 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
/**
* @brief A Kirigami.Page component used for displaying a NewStuff entry's comments
*/
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
Kirigami.ScrollablePage {
id: component
property string entryName
property string entryAuthorId
property string entryProviderId
property alias entry: commentsModel.entry
property alias itemsModel: commentsModel.itemsModel
title: i18ndc("knewstuff6", "Title for the page containing a view of the comments for the entry", "Comments and Reviews for %1", component.entryName)
actions: [
Kirigami.Action {
text: i18ndc("knewstuff6", "Title for the item which is checked when all comments should be shown", "Show All Comments")
checked: commentsModel.includedComments === NewStuff.CommentsModel.IncludeAllComments
checkable: true
onTriggered: source => {
commentsModel.includedComments = NewStuff.CommentsModel.IncludeAllComments;
}
},
Kirigami.Action {
text: i18ndc("knewstuff6", "Title for the item which is checked when only comments which are reviews should be shown", "Show Reviews Only")
checked: commentsModel.includedComments === NewStuff.CommentsModel.IncludeOnlyReviews
checkable: true
onTriggered: source => {
commentsModel.includedComments = NewStuff.CommentsModel.IncludeOnlyReviews;
}
},
Kirigami.Action {
text: i18ndc("knewstuff6", "Title for the item which is checked when comments which are reviews, and their children should be shown", "Show Reviews and Replies")
checked: commentsModel.includedComments === NewStuff.CommentsModel.IncludeReviewsAndReplies
checkable: true
onTriggered: source => {
commentsModel.includedComments = NewStuff.CommentsModel.IncludeReviewsAndReplies;
}
}
]
ErrorDisplayer {
engine: component.itemsModel.engine
active: component.isCurrentPage
}
ListView {
id: commentsView
model: NewStuff.CommentsModel {
id: commentsModel
}
Layout.fillWidth: true
header: Item {
anchors {
left: parent.left
right: parent.right
}
height: Kirigami.Units.largeSpacing
}
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
delegate: EntryCommentDelegate {
width: {
const view = ListView.view;
return view ? view.width - view.leftMargin - view.rightMargin : 0;
}
engine: component.itemsModel.engine
entryAuthorId: component.entryAuthorId
entryProviderId: component.entryProviderId
author: model.username
score: model.score
title: model.subject
reviewText: model.text
depth: model.depth
}
}
}
@@ -0,0 +1,184 @@
/*
SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
Flickable {
id: root
property alias screenshotsModel: screenshotsRep.model
readonly property alias count: screenshotsRep.count
property int currentIndex: -1
property Item currentItem: screenshotsRep.itemAt(currentIndex)
Layout.preferredHeight: Kirigami.Units.gridUnit * 13
contentHeight: height
contentWidth: screenshotsLayout.width
QQC2.Popup {
id: overlay
parent: applicationWindow().QQC2.Overlay.overlay
modal: true
clip: false
x: (parent.width - width) / 2
y: (parent.height - height) / 2
readonly property real proportion: overlayImage.sourceSize.width > 1
? overlayImage.sourceSize.height / overlayImage.sourceSize.width
: 1
height: overlayImage.status === Image.Loading
? Kirigami.Units.gridUnit * 5
: Math.min(parent.height * 0.9, (parent.width * 0.9) * proportion, overlayImage.sourceSize.height)
width: height / proportion
QQC2.BusyIndicator {
id: indicator
visible: running
running: overlayImage.status === Image.Loading
anchors.fill: parent
}
// Only animate the images in the detail view; in the overview it would be irritating and not useful anyway due to the small previews
AnimatedImage {
id: overlayImage
anchors.fill: parent
source: root.currentItem ? root.currentItem.imageSource : ""
fillMode: Image.PreserveAspectFit
smooth: true
}
QQC2.Button {
anchors {
right: parent.left
verticalCenter: parent.verticalCenter
}
action: leftAction
visible: leftAction.visible
}
QQC2.Button {
anchors {
left: parent.right
verticalCenter: parent.verticalCenter
}
action: rightAction
visible: rightAction.visible
}
Kirigami.Action {
id: leftAction
icon.name: "arrow-left"
enabled: overlay.visible && visible
visible: root.currentIndex >= 1 && !indicator.running
onTriggered: source => {
root.currentIndex = (root.currentIndex - 1) % root.count;
}
}
Kirigami.Action {
id: rightAction
icon.name: "arrow-right"
enabled: overlay.visible && visible
visible: root.currentIndex < (root.count - 1) && !indicator.running
onTriggered: source => {
root.currentIndex = (root.currentIndex + 1) % root.count;
}
}
}
Row {
id: screenshotsLayout
height: root.contentHeight
spacing: Kirigami.Units.largeSpacing
leftPadding: spacing
rightPadding: spacing
focus: overlay.visible
Keys.onLeftPressed: event => {
if (leftAction.visible) {
leftAction.trigger();
}
}
Keys.onRightPressed: event => {
if (rightAction.visible) {
rightAction.trigger();
}
}
Repeater {
id: screenshotsRep
delegate: MouseArea {
readonly property url imageSource: modelData
readonly property real proportion: thumbnail.sourceSize.width > 1
? thumbnail.sourceSize.height / thumbnail.sourceSize.width
: 1
anchors.verticalCenter: parent.verticalCenter
width: Math.max(50, height / proportion)
height: screenshotsLayout.height - 2 * Kirigami.Units.largeSpacing
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
root.currentIndex = index
overlay.open()
}
Kirigami.ShadowedRectangle {
visible: thumbnail.status === Image.Ready
anchors.fill: thumbnail
Kirigami.Theme.colorSet: Kirigami.Theme.View
shadow.size: Kirigami.Units.largeSpacing
shadow.color: Qt.rgba(0, 0, 0, 0.3)
}
QQC2.BusyIndicator {
running: thumbnail.status === Image.Loading
anchors.centerIn: parent
}
AnimatedImage {
id: thumbnail
source: modelData
height: parent.height
fillMode: Image.PreserveAspectFit
smooth: true
}
}
}
}
clip: true
readonly property var leftShadow: Shadow {
parent: root
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
edge: Qt.LeftEdge
width: Math.max(0, Math.min(root.width / 5, root.contentX))
}
readonly property var rightShadow: Shadow {
parent: root
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
edge: Qt.RightEdge
width: Math.max(0, Math.min(root.contentWidth - root.contentX - root.width) / 5)
}
}
@@ -0,0 +1,89 @@
/*
SPDX-FileCopyrightText: 2020 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
Kirigami.PromptDialog {
id: component
title: i18ndc("knewstuff6", "Title for a dialog box which shows error messages", "An Error Occurred");
standardButtons: Kirigami.Dialog.NoButton
property bool active: true
property NewStuff.Engine engine
readonly property Connections connection: Connections {
target: component.engine
function onErrorCode(errorCode, message, metadata) {
component.showError(errorCode, message, metadata);
}
}
property var errorsToShow: []
function showError(errorCode, errorMessage, errorMetadata) {
if (active === true) {
errorsToShow.push({
code: errorCode,
message: errorMessage,
metadata: errorMetadata
});
showNextError();
}
}
onVisibleChanged: displayThrottle.start()
property QtObject displayThrottle: Timer {
interval: Kirigami.Units.shortDuration
onTriggered: showNextError()
}
function showNextError() {
if (visible === false && errorsToShow.length > 0) {
currentError = errorsToShow.shift();
open();
}
}
property var currentError: null
RowLayout {
implicitWidth: Kirigami.Units.gridUnit * 10
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
Layout.alignment: Qt.AlignVCenter
visible: source !== ""
source: {
if (currentError === null) {
return "";
} else if (currentError.code === NewStuff.ErrorCode.TryAgainLaterError) {
return "accept_time_event";
} else {
return "dialog-warning";
}
}
}
Kirigami.SelectableLabel {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
wrapMode: Text.Wrap
textFormat: TextEdit.AutoText
onLinkActivated: link => Qt.openUrlExternally(link)
text: {
if (currentError === null) {
return "";
} else if (currentError.code === NewStuff.ErrorCode.TryAgainLaterError) {
return currentError.message + "\n\n" + i18n("Please try again later.")
} else {
return currentError.message;
}
}
}
}
}
@@ -0,0 +1,177 @@
/*
SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Templates as T
import org.kde.kirigami as Kirigami
/**
* Base delegate for KControlmodules based on Grid views of thumbnails
* Use the onClicked signal handler for managing the main action when
* the user clicks on the tile, modified from the original GridDelegate
* from the KCM module
* @inherits QtQuick.Templates.ItemDelegate
*/
T.ItemDelegate {
id: delegate
/**
* toolTip: string
* string for a tooltip for the whole delegate
*/
property string toolTip
/**
* tile: Item
* the item actually implementing the tile: the visualization is up to the implementation
*/
property alias tile: contentArea.data
/**
* thumbnailAvailable: bool
* Set it to true when a tile is actually available: when false,
* a default icon will be shown instead of the actual tile.
*/
property bool thumbnailAvailable: false
/**
* actions: list<Action>
* A list of extra actions for the thumbnails. They will be shown as
* icons on the bottom-right corner of the tile on mouse over
*/
property list<QtObject> actions
/**
* actionsAnchors: anchors
* The anchors of the actions listing
*/
property alias actionsAnchors: actionsScope.anchors
/**
* thumbnailArea: Item
* The item that will contain the thumbnail within the delegate
*/
property Item thumbnailArea : contentArea
width: GridView.view.cellWidth
height: GridView.view.cellHeight
hoverEnabled: true
Kirigami.ShadowedRectangle {
id: tile
anchors.centerIn: parent
width: Kirigami.Settings.isMobile
? delegate.width - Kirigami.Units.gridUnit
: Math.min(delegate.GridView.view.implicitCellWidth, delegate.width - Kirigami.Units.gridUnit)
height: Math.min(delegate.GridView.view.implicitCellHeight, delegate.height - Kirigami.Units.gridUnit)
radius: Kirigami.Units.smallSpacing
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
shadow.xOffset: 0
shadow.yOffset: 2
shadow.size: 10
shadow.color: Qt.rgba(0, 0, 0, 0.3)
color: {
// Otherwise the first item is focused, BUG: 417843
// We should rethink this when fixing the keyboard navigation
/*if (delegate.GridView.isCurrentItem) {
return Kirigami.Theme.highlightColor;
} else */ if (parent.hovered) {
// Match appearance of hovered list items
return Qt.alpha(Kirigami.Theme.highlightColor, 0.5);
} else {
return Kirigami.Theme.backgroundColor;
}
}
Behavior on color {
ColorAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.OutQuad
}
}
Rectangle {
id: contentArea
radius: Kirigami.Units.smallSpacing/2
anchors {
fill: parent
margins: Kirigami.Units.smallSpacing
}
color: Kirigami.Theme.backgroundColor
}
Kirigami.Icon {
parent: thumbnailArea
visible: !delegate.thumbnailAvailable
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.large
height: width
source: delegate.text === i18nd("knewstuff6", "None") ? "edit-none" : "view-preview"
}
Rectangle {
anchors.fill: contentArea
visible: actionsColumn.children.length > 0
opacity: Kirigami.Settings.isMobile || delegate.hovered || (actionsScope.focus) ? 1 : 0
radius: Kirigami.Units.smallSpacing
color: Kirigami.Settings.isMobile ? "transparent" : Qt.rgba(1, 1, 1, 0.2)
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.OutQuad
}
}
FocusScope {
id: actionsScope
anchors {
right: parent.right
rightMargin: Kirigami.Units.smallSpacing
top: parent.top
topMargin: Kirigami.Units.smallSpacing
}
width: actionsColumn.width
height: actionsColumn.height
ColumnLayout {
id: actionsColumn
Repeater {
model: delegate.actions
delegate: QQC2.Button {
icon.name: modelData.iconName
text: modelData.text
activeFocusOnTab: focus || delegate.focus
onClicked: modelData.trigger()
enabled: modelData.enabled
visible: modelData.visible
//NOTE: there aren't any global settings where to take "official" tooltip timeouts
QQC2.ToolTip.delay: 1000
QQC2.ToolTip.timeout: 5000
QQC2.ToolTip.visible: (Kirigami.Settings.isMobile ? pressed : hovered) && modelData.tooltip.length > 0
QQC2.ToolTip.text: modelData.tooltip
}
}
}
}
}
}
QQC2.ToolTip.delay: 1000
QQC2.ToolTip.timeout: 5000
QQC2.ToolTip.visible: hovered && delegate.toolTip.length > 0
QQC2.ToolTip.text: toolTip
}
@@ -0,0 +1,83 @@
/*
SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
RowLayout {
id: view
property bool editable: false
property int max: 100
property int rating: 0
property real starSize: Kirigami.Units.gridUnit
property bool reverseLayout: false
clip: true
spacing: 0
readonly property int ratingIndex: Math.floor((theRepeater.count * view.rating) / view.max)
readonly property bool ratingHalf: (theRepeater.count * view.rating) % view.max >= view.max / 2
QQC2.Label {
Layout.minimumWidth: view.starSize
Layout.minimumHeight: view.starSize
visible: view.reverseLayout
text: ratingAsText.text
}
Item {
visible: view.reverseLayout
Layout.minimumHeight: view.starSize;
Layout.minimumWidth: Kirigami.Units.smallSpacing;
Layout.maximumWidth: Kirigami.Units.smallSpacing;
}
Repeater {
id: theRepeater
model: 5
delegate: Kirigami.Icon {
Layout.minimumWidth: view.starSize
Layout.minimumHeight: view.starSize
Layout.preferredWidth: view.starSize
Layout.preferredHeight: view.starSize
source: index < view.ratingIndex
? "rating"
: (view.ratingHalf && index === view.ratingIndex
? (view.LayoutMirroring.enabled ? "rating-half-rtl" : "rating-half")
: "rating-unrated")
opacity: (view.editable && mouse.item.containsMouse) ? 0.7 : 1
ConditionalLoader {
id: mouse
anchors.fill: parent
condition: view.editable
componentTrue: MouseArea {
hoverEnabled: true
onClicked: mouse => {
rating = (max/theRepeater.model*(index+1));
}
}
componentFalse: null
}
}
}
Item {
visible: !view.reverseLayout
Layout.minimumHeight: view.starSize;
Layout.minimumWidth: Kirigami.Units.smallSpacing;
Layout.maximumWidth: Kirigami.Units.smallSpacing;
}
QQC2.Label {
id: ratingAsText
Layout.minimumWidth: view.starSize
Layout.minimumHeight: view.starSize
visible: !view.reverseLayout
text: i18ndc("knewstuff6", "A text representation of the rating, shown as a fraction of the max value", "(%1/%2)", view.rating / 10, view.max / 10)
}
}
@@ -0,0 +1,43 @@
/*
SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import org.kde.kirigami as Kirigami
Item {
id: shadow
property int edge: Qt.LeftEdge
width: Kirigami.Units.gridUnit / 2
height: Kirigami.Units.gridUnit / 2
Rectangle {
x: shadow.width / 2 - width / 2
y: shadow.height / 2 - height / 2
width: (shadow.edge === Qt.LeftEdge || shadow.edge === Qt.RightEdge) ? shadow.height : shadow.width
height: (shadow.edge === Qt.LeftEdge || shadow.edge === Qt.RightEdge) ? shadow.width : shadow.height
rotation: {
switch (shadow.edge) {
case Qt.TopEdge: return 0;
case Qt.LeftEdge: return 270;
case Qt.RightEdge: return 90;
case Qt.BottomEdge: return 180;
}
}
gradient: Gradient {
GradientStop {
position: 0.3
color: Qt.rgba(0, 0, 0, 0.1)
}
GradientStop {
position: 1.0
color: "transparent"
}
}
}
}
@@ -0,0 +1,178 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
import ".." as Private
Private.GridTileDelegate {
id: component
property var entry: model.entry
function showDetails() {
pageStack.push(detailsPage, {
newStuffModel: GridView.view.model,
entry: model.entry,
});
}
actionsAnchors.topMargin: bigPreview.height + Kirigami.Units.smallSpacing * 2
actions: [
Kirigami.Action {
text: root.useLabel
icon.name: "dialog-ok-apply"
onTriggered: source => {
newStuffModel.engine.adoptEntry(model.entry);
}
enabled: (entry.status === NewStuff.Entry.Installed || entry.status === NewStuff.Entry.Updateable) && newStuffEngine.hasAdoptionCommand
visible: enabled
},
Kirigami.Action {
text: model.downloadLinks.length === 1
? i18ndc("knewstuff6", "Request installation of this item, available when there is exactly one downloadable item", "Install")
: i18ndc("knewstuff6", "Show installation options, where there is more than one downloadable item", "Install…")
icon.name: "install"
onTriggered: source => {
if (model.downloadLinks.length === 1) {
newStuffModel.engine.installLinkId(model.entry, NewStuff.ItemsModel.FirstLinkId);
} else {
downloadItemsSheet.downloadLinks = model.downloadLinks;
downloadItemsSheet.entry = model.entry;
downloadItemsSheet.open();
}
}
enabled: entry.status === NewStuff.Entry.Downloadable || entry.status === NewStuff.Entry.Deleted
visible: enabled
},
Kirigami.Action {
text: i18ndc("knewstuff6", "Request updating of this item", "Update")
icon.name: "update-none"
onTriggered: source => {
newStuffModel.engine.installLatest(model.entry);
}
enabled: entry.status === NewStuff.Entry.Updateable
visible: enabled
},
Kirigami.Action {
text: root.uninstallLabel
icon.name: "edit-delete"
onTriggered: source => {
newStuffModel.engine.uninstall(model.entry);
}
enabled: entry.status === NewStuff.Entry.Installed || entry.status === NewStuff.Entry.Updateable
visible: enabled
}
]
thumbnailArea: bigPreview
thumbnailAvailable: model.previewsSmall.length > 0
tile: Item {
anchors {
fill: parent
margins: Kirigami.Units.smallSpacing
}
ColumnLayout {
anchors.fill: parent
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: width / 5
Layout.maximumHeight: width / 1.8
Kirigami.ShadowedRectangle {
visible: bigPreview.status === Image.Ready
anchors.centerIn: bigPreview
width: Math.min(bigPreview.paintedWidth, bigPreview.width)
height: Math.min(bigPreview.paintedHeight, bigPreview.height)
Kirigami.Theme.colorSet: Kirigami.Theme.View
shadow.size: 10
shadow.color: Qt.rgba(0, 0, 0, 0.3)
}
Image {
id: bigPreview
asynchronous: true
fillMode: Image.PreserveAspectCrop
source: thumbnailAvailable ? model.previews[0] : ""
anchors.fill: parent
}
Kirigami.Icon {
id: updateAvailableBadge
opacity: (entry.status === NewStuff.Entry.Updateable) ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
anchors {
top: parent.top
left: parent.left
}
height: Kirigami.Units.iconSizes.medium
width: height
source: "package-installed-outdated"
}
Kirigami.Icon {
id: installedBadge
opacity: (entry.status === NewStuff.Entry.Installed) ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } }
anchors {
top: parent.top
left: parent.left
}
height: Kirigami.Units.iconSizes.medium
width: height
source: "package-installed-updated"
}
}
Private.Rating {
Layout.fillWidth: true
rating: model.rating
}
Kirigami.Heading {
Layout.fillWidth: true
level: 5
elide: Text.ElideRight
text: i18ndc("knewstuff6", "The number of times the item has been downloaded", "%1 downloads", model.downloadCount)
}
Kirigami.Heading {
Layout.fillWidth: true
elide: Text.ElideRight
level: 3
text: model.name
}
Kirigami.Heading {
Layout.fillWidth: true
elide: Text.ElideRight
level: 4
textFormat: Text.StyledText
text: i18ndc("knewstuff6", "Subheading for the tile view, located immediately underneath the name of the item", "By <i>%1</i>", model.author.name)
}
QQC2.Label {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: Kirigami.Units.gridUnit
Layout.maximumHeight: Kirigami.Units.gridUnit * 3
wrapMode: Text.Wrap
text: model.shortSummary.length > 0 ? model.shortSummary : model.summary
elide: Text.ElideRight
}
clip: true // We are dealing with content over which we have very little control. Sometimes that means being a bit abrupt.
}
FeedbackOverlay {
anchors.fill: parent
newStuffModel: component.GridView.view.model
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
component.showDetails();
}
}
}
}
@@ -0,0 +1,65 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
Item {
property QtObject newStuffModel
visible: opacity > 0
opacity: (model.entry.status === NewStuff.Entry.Installing || model.entry.status === NewStuff.Entry.Updating) ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration } }
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
opacity: 0.9
}
QQC2.BusyIndicator {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.verticalCenter
bottomMargin: Kirigami.Units.smallSpacing
}
running: parent.visible
}
QQC2.Label {
id: statusLabel
Connections {
target: newStuffModel
function onEntryChanged(entry) {
const status = entry.status;
if (status === NewStuff.Entry.Downloadable
|| status === NewStuff.Entry.Installed
|| status === NewStuff.Entry.Updateable
|| status === NewStuff.Entry.Deleted) {
statusLabel.text = "";
} else if (status === NewStuff.Entry.Installing) {
statusLabel.text = i18ndc("knewstuff6", "Label for the busy indicator showing an item is being installed OR uninstalled", "Working…");
} else if (status === NewStuff.Entry.Updating) {
statusLabel.text = i18ndc("knewstuff6", "Label for the busy indicator showing an item is in the process of being updated", "Updating…");
} else {
statusLabel.text = i18ndc("knewstuff6", "Label for the busy indicator which should only be shown when the entry has been given some unknown or invalid status.", "Invalid or unknown state. <a href=\"https://bugs.kde.org/enter_bug.cgi?product=frameworks-knewstuff\">Please report this to the KDE Community in a bug report</a>.");
}
}
}
onLinkActivated: link => Qt.openUrlExternally(link)
anchors {
top: parent.verticalCenter
left: parent.left
right: parent.right
margins: Kirigami.Units.smallSpacing
}
horizontalAlignment: Text.AlignHCenter
// TODO: This is where we'd want to put the download progress and cancel button as well
text: i18ndc("knewstuff6", "Label for the busy indicator showing an item is installing", "Installing…")
}
}
@@ -0,0 +1,201 @@
/*
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.newstuff as NewStuff
import ".." as Private
Private.GridTileDelegate {
id: component
property var entry: model.entry
property string useLabel
property string uninstallLabel
function showDetails() {
if (entry.entryType === NewStuff.Entry.GroupEntry) {
newStuffEngine.storeSearch();
newStuffEngine.searchTerm = model.payload;
} else {
pageStack.push(detailsPage, {
newStuffModel: GridView.view.model,
entry,
});
}
}
actions: [
Kirigami.Action {
text: component.useLabel
icon.name: "dialog-ok-apply"
onTriggered: source => {
newStuffModel.engine.adoptEntry(model.entry);
}
enabled: (entry.status === NewStuff.Entry.Installed || entry.status === NewStuff.Entry.Updateable) && newStuffEngine.hasAdoptionCommand
visible: enabled
},
Kirigami.Action {
text: model.downloadLinks.length === 1
? i18ndc("knewstuff6", "Request installation of this item, available when there is exactly one downloadable item", "Install")
: i18ndc("knewstuff6", "Show installation options, where there is more than one downloadable item", "Install…")
icon.name: "install"
onTriggered: source => {
if (model.downloadLinks.length === 1) {
newStuffEngine.installLinkId(entry, NewStuff.ItemsModel.FirstLinkId);
} else {
downloadItemsSheet.downloadLinks = model.downloadLinks;
downloadItemsSheet.entry = entry;
downloadItemsSheet.open();
}
}
enabled: entry.status === NewStuff.Entry.Downloadable || entry.status === NewStuff.Entry.Deleted
visible: enabled
},
Kirigami.Action {
text: i18ndc("knewstuff6", "Request updating of this item", "Update")
icon.name: "update-none"
onTriggered: source => {
newStuffEngine.installLatest(entry);
}
enabled: entry.status === NewStuff.Entry.Updateable
visible: enabled
},
Kirigami.Action {
text: component.uninstallLabel
icon.name: "edit-delete"
onTriggered: source => {
newStuffEngine.uninstall(model.entry);
}
enabled: entry.status === NewStuff.Entry.Installed || entry.status === NewStuff.Entry.Updateable
visible: enabled && hovered
}
]
thumbnailArea: tilePreview
thumbnailAvailable: model.previewsSmall.length > 0
tile: Item {
anchors {
fill: parent
margins: Kirigami.Units.smallSpacing
}
GridLayout {
anchors.fill: parent
columns: 2
ColumnLayout {
Layout.minimumWidth: view.implicitCellWidth / 5
Layout.maximumWidth: view.implicitCellWidth / 5
Item {
Layout.fillWidth: true
Layout.minimumHeight: width
Layout.maximumHeight: width
Kirigami.ShadowedRectangle {
visible: tilePreview.status === Image.Ready
anchors.centerIn: tilePreview
width: Math.min(tilePreview.paintedWidth, tilePreview.width)
height: Math.min(tilePreview.paintedHeight, tilePreview.height)
Kirigami.Theme.colorSet: Kirigami.Theme.View
shadow.size: Kirigami.Units.largeSpacing
shadow.color: Qt.rgba(0, 0, 0, 0.3)
}
Image {
id: tilePreview
asynchronous: true
fillMode: Image.PreserveAspectFit
source: thumbnailAvailable ? model.previewsSmall[0] : ""
anchors {
fill: parent
margins: Kirigami.Units.smallSpacing
}
verticalAlignment: Image.AlignTop
}
Kirigami.Icon {
id: updateAvailableBadge
opacity: (entry.status === NewStuff.Entry.Updateable) ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
anchors {
top: parent.top
left: parent.left
margins: -Kirigami.Units.smallSpacing
}
height: Kirigami.Units.iconSizes.smallMedium
width: height
source: "package-installed-outdated"
}
Kirigami.Icon {
id: installedBadge
opacity: (entry.status === NewStuff.Entry.Installed) ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
anchors {
top: parent.top
left: parent.left
margins: -Kirigami.Units.smallSpacing
}
height: Kirigami.Units.iconSizes.smallMedium
width: height
source: "package-installed-updated"
}
}
Item {
Layout.fillHeight: true
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Kirigami.Heading {
Layout.fillWidth: true
elide: Text.ElideRight
level: 3
text: entry.name
}
Kirigami.Heading {
Layout.fillWidth: true
elide: Text.ElideRight
level: 4
textFormat: Text.StyledText
text: i18ndc("knewstuff6", "Subheading for the tile view, located immediately underneath the name of the item", "By <i>%1</i>", entry.author.name)
}
QQC2.Label {
Layout.fillWidth: true
Layout.fillHeight: true
wrapMode: Text.Wrap
text: entry.shortSummary.length > 0 ? entry.shortSummary : entry.summary
elide: Text.ElideRight
clip: true // We are dealing with content over which we have very little control. Sometimes that means being a bit abrupt.
}
}
Private.Rating {
Layout.fillWidth: true
rating: entry.rating
visible: entry.entryType === NewStuff.Entry.CatalogEntry
}
Kirigami.Heading {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
level: 5
elide: Text.ElideRight
text: i18ndc("knewstuff6", "The number of times the item has been downloaded", "%1 downloads", entry.downloadCount)
visible: entry.entryType === NewStuff.Entry.CatalogEntry
}
}
FeedbackOverlay {
anchors.fill: parent
newStuffModel: component.GridView.view.model
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
component.showDetails();
}
}
}
}
@@ -0,0 +1,92 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "qmlplugin.h"
#include "author.h"
#include "categoriesmodel.h"
#include "commentsmodel.h"
#include "quickengine.h"
#include "quickitemsmodel.h"
#include "quickquestionlistener.h"
#include "quicksettings.h"
#include "searchpresetmodel.h"
#include "provider.h"
#include "providersmodel.h"
#include "question.h"
#include <QQmlEngine>
#include <qqml.h>
void QmlPlugins::initializeEngine(QQmlEngine * /*engine*/, const char *)
{
}
void QmlPlugins::registerTypes(const char *uri)
{
const char *coreUri{"org.kde.newstuff.core"};
// Initial version
qmlRegisterType<Engine>(uri, 1, 0, "Engine");
qmlRegisterType<ItemsModel>(uri, 1, 0, "ItemsModel");
// Version 1.62
qmlRegisterType<KNewStuffQuick::Author>(uri, 1, 62, "Author");
qmlRegisterType<KNewStuffQuick::CommentsModel>(uri, 1, 62, "CommentsModel");
qmlRegisterUncreatableType<CategoriesModel>(
uri,
1,
0,
"CategoriesModel",
QStringLiteral("This should only be created by the Engine, and provides the categories available in that engine"));
qmlRegisterUncreatableMetaObject(KNSCore::Provider::staticMetaObject,
coreUri,
1,
62,
"Provider",
QStringLiteral("Error: this only exists to forward enums"));
qmlRegisterUncreatableMetaObject(KNSCore::Question::staticMetaObject,
coreUri,
1,
62,
"Question",
QStringLiteral("Error: this only exists to forward enums"));
qmlRegisterSingletonType<KNewStuffQuick::QuickQuestionListener>(uri,
1,
62,
"QuickQuestionListener",
[](QQmlEngine *engine, QJSEngine * /*scriptEngine*/) -> QObject * {
engine->setObjectOwnership(KNewStuffQuick::QuickQuestionListener::instance(),
QQmlEngine::CppOwnership);
return KNewStuffQuick::QuickQuestionListener::instance();
});
qmlRegisterUncreatableMetaObject(KNSCore::Entry::staticMetaObject, uri, 1, 91, "Entry", QStringLiteral("Entries should only be created by the engine"));
qmlRegisterUncreatableMetaObject(KNSCore::ErrorCode::staticMetaObject,
uri,
1,
91,
"ErrorCode",
QStringLiteral("Only for access to the KNSCore::ErrorCode enum"));
// Version 1.81
qmlRegisterSingletonType<KNewStuffQuick::Settings>(uri, 1, 81, "Settings", [](QQmlEngine *engine, QJSEngine * /*scriptEngine*/) -> QObject * {
engine->setObjectOwnership(KNewStuffQuick::Settings::instance(), QQmlEngine::CppOwnership);
return KNewStuffQuick::Settings::instance();
});
// Version 1.83
qmlRegisterUncreatableType<SearchPresetModel>(
uri,
1,
83,
"SearchPresetModel",
QStringLiteral("This should only be created by the Engine, and provides the SearchPresets available in that engine"));
// Version 1.85
qmlRegisterType<KNSCore::ProvidersModel>(uri, 1, 85, "ProvidersModel");
}
#include "moc_qmlplugin.cpp"
@@ -0,0 +1,22 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef _QML_PLUGIN_H
#define _QML_PLUGIN_H
#include <QQmlExtensionPlugin>
// TODO KF6 just make this a c++ file only plugin
class QmlPlugins : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void initializeEngine(QQmlEngine *engine, const char *uri) override;
void registerTypes(const char *uri) override;
};
#endif
@@ -0,0 +1,614 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "quickengine.h"
#include "cache.h"
#include "errorcode.h"
#include "imageloader_p.h"
#include "installation_p.h"
#include "knewstuffquick_debug.h"
#include "quicksettings.h"
#include <KLocalizedString>
#include <QQmlInfo>
#include <QTimer>
#include "categoriesmodel.h"
#include "quickquestionlistener.h"
#include "searchpresetmodel.h"
#include "../core/enginebase_p.h"
#include "../core/providerbase_p.h"
#include "../core/providercore.h"
#include "../core/providercore_p.h"
// Could be made :public EngineBasePrivate so we don't have two distinct d pointers
class EnginePrivate
{
public:
bool isValid = false;
CategoriesModel *categoriesModel = nullptr;
SearchPresetModel *searchPresetModel = nullptr;
QString configFile;
QTimer searchTimer;
Engine::BusyState busyState;
QString busyMessage;
// the current request from providers
KNSCore::SearchRequest currentRequest;
KNSCore::SearchRequest storedRequest;
// the page that is currently displayed, so it is not requested repeatedly
int currentPage = -1;
// when requesting entries from a provider, how many to ask for
int pageSize = 20;
int numDataJobs = 0;
int numPictureJobs = 0;
int numInstallJobs = 0;
};
Engine::Engine(QObject *parent)
: KNSCore::EngineBase(parent)
, d(new EnginePrivate)
, dd(KNSCore::EngineBase::d.get())
{
connect(this, &KNSCore::EngineBase::providerAdded, this, [this](auto core) {
connect(core->d->base, &KNSCore::ProviderBase::entriesLoaded, this, [this](const auto &request, const auto &entries) {
d->currentPage = qMax<int>(request.page(), d->currentPage);
qCDebug(KNEWSTUFFQUICK) << "loaded page " << request.page() << "current page" << d->currentPage << "count:" << entries.count();
if (request.filter() != KNSCore::Filter::Updates) {
dd->cache->insertRequest(request, entries);
}
Q_EMIT signalEntriesLoaded(entries);
--d->numDataJobs;
updateStatus();
});
connect(core->d->base, &KNSCore::ProviderBase::entryDetailsLoaded, this, [this](const auto &entry) {
--d->numDataJobs;
updateStatus();
Q_EMIT signalEntryEvent(entry, KNSCore::Entry::DetailsLoadedEvent);
});
});
const auto setBusy = [this](Engine::BusyState state, const QString &msg) {
setBusyState(state);
d->busyMessage = msg;
};
setBusy(BusyOperation::Initializing, i18n("Loading data")); // For the user this should be the same as initializing
KNewStuffQuick::QuickQuestionListener::instance();
d->categoriesModel = new CategoriesModel(this);
connect(d->categoriesModel, &QAbstractListModel::modelReset, this, &Engine::categoriesChanged);
d->searchPresetModel = new SearchPresetModel(this);
connect(d->searchPresetModel, &QAbstractListModel::modelReset, this, &Engine::searchPresetModelChanged);
d->searchTimer.setSingleShot(true);
d->searchTimer.setInterval(1000);
connect(&d->searchTimer, &QTimer::timeout, this, &Engine::reloadEntries);
connect(installation(), &KNSCore::Installation::signalInstallationFinished, this, [this]() {
--d->numInstallJobs;
updateStatus();
});
connect(installation(), &KNSCore::Installation::signalInstallationFailed, this, [this](const QString &message) {
--d->numInstallJobs;
Q_EMIT signalErrorCode(KNSCore::ErrorCode::InstallationError, message, QVariant());
});
connect(this, &EngineBase::signalProvidersLoaded, this, &Engine::updateStatus);
connect(this, &EngineBase::signalProvidersLoaded, this, [this]() {
d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
d->currentRequest.filter(),
d->currentRequest.searchTerm(),
EngineBase::categories(),
d->currentRequest.page(),
d->currentRequest.pageSize());
});
connect(this,
&KNSCore::EngineBase::signalErrorCode,
this,
[setBusy, this](const KNSCore::ErrorCode::ErrorCode &error, const QString &message, const QVariant &metadata) {
Q_EMIT errorCode(error, message, metadata);
if (error == KNSCore::ErrorCode::ProviderError || error == KNSCore::ErrorCode::ConfigFileError) {
// This means loading the config or providers file failed entirely and we cannot complete the
// initialisation. It also means the engine is done loading, but that nothing will
// work, and we need to inform the user of this.
setBusy({}, QString());
}
// Emit the signal later, currently QML is not connected to the slot
if (error == KNSCore::ErrorCode::ConfigFileError) {
QTimer::singleShot(0, [this, error, message, metadata]() {
Q_EMIT errorCode(error, message, metadata);
});
}
});
connect(this, &Engine::signalEntryEvent, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
// Just forward the event but not do anything more
Q_EMIT entryEvent(entry, event);
});
//
// And finally, let's just make sure we don't miss out the various things here getting changed
// In other words, when we're asked to reset the view, actually do that
connect(this, &Engine::signalResetView, this, &Engine::categoriesFilterChanged);
connect(this, &Engine::signalResetView, this, &Engine::filterChanged);
connect(this, &Engine::signalResetView, this, &Engine::sortOrderChanged);
connect(this, &Engine::signalResetView, this, &Engine::searchTermChanged);
}
bool Engine::init(const QString &configfile)
{
const bool valid = EngineBase::init(configfile);
if (valid) {
connect(this, &Engine::signalEntryEvent, dd->cache.get(), [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
if (event == KNSCore::Entry::StatusChangedEvent) {
dd->cache->registerChangedEntry(entry);
}
});
const auto slotEntryChanged = [this](const KNSCore::Entry &entry) {
Q_EMIT signalEntryEvent(entry, KNSCore::Entry::StatusChangedEvent);
};
// Don't connect KNSCore::Installation::signalEntryChanged as is already forwarded to
// Transaction, which in turn is forwarded to our slotEntryChanged, so avoids a double emission
connect(dd->cache.get(), &KNSCore::Cache2::entryChanged, this, slotEntryChanged);
}
return valid;
}
void Engine::updateStatus()
{
QString busyMessage;
BusyState state;
if (d->numPictureJobs > 0) {
// If it is loading previews or data is irrelevant for the user
busyMessage = i18n("Loading data");
state |= BusyOperation::LoadingPreview;
}
if (d->numInstallJobs > 0) {
busyMessage = i18n("Installing");
state |= BusyOperation::InstallingEntry;
}
if (d->numDataJobs > 0) {
busyMessage = i18n("Loading data");
state |= BusyOperation::LoadingData;
}
d->busyMessage = busyMessage;
setBusyState(state);
}
bool Engine::needsLazyLoadSpinner()
{
return d->numDataJobs > 0 || d->numPictureJobs;
}
Engine::~Engine() = default;
void Engine::setBusyState(BusyState state)
{
d->busyState = state;
Q_EMIT busyStateChanged();
}
Engine::BusyState Engine::busyState() const
{
return d->busyState;
}
QString Engine::busyMessage() const
{
return d->busyMessage;
}
QString Engine::configFile() const
{
return d->configFile;
}
void Engine::setConfigFile(const QString &newFile)
{
if (d->configFile != newFile) {
d->configFile = newFile;
Q_EMIT configFileChanged();
if (KNewStuffQuick::Settings::instance()->allowedByKiosk()) {
d->isValid = init(newFile);
Q_EMIT categoriesFilterChanged();
Q_EMIT filterChanged();
Q_EMIT sortOrderChanged();
Q_EMIT searchTermChanged();
} else {
// This is not an error message in the proper sense, and the message is not intended to look like an error (as there is really
// nothing the user can do to fix it, and we just tell them so they're not wondering what's wrong)
Q_EMIT errorCode(
KNSCore::ErrorCode::ConfigFileError,
i18nc("An informational message which is shown to inform the user they are not authorized to use GetHotNewStuff functionality",
"You are not authorized to Get Hot New Stuff. If you think this is in error, please contact the person in charge of your permissions."),
QVariant());
}
}
}
CategoriesModel *Engine::categories() const
{
return d->categoriesModel;
}
QStringList Engine::categoriesFilter() const
{
return d->currentRequest.categories();
}
void Engine::setCategoriesFilter(const QStringList &newCategoriesFilter)
{
if (d->currentRequest.categories() != newCategoriesFilter) {
d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
d->currentRequest.filter(),
d->currentRequest.searchTerm(),
newCategoriesFilter,
d->currentRequest.page(),
d->currentRequest.pageSize());
reloadEntries();
Q_EMIT categoriesFilterChanged();
}
}
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
KNSCore::Provider::Filter Engine::filter() const
{
return [filter = filter2()] {
switch (filter) {
case KNSCore::Filter::None:
return KNSCore::Provider::None;
case KNSCore::Filter::Installed:
return KNSCore::Provider::Installed;
case KNSCore::Filter::Updates:
return KNSCore::Provider::Updates;
case KNSCore::Filter::ExactEntryId:
return KNSCore::Provider::ExactEntryId;
}
return KNSCore::Provider::None;
}();
}
#endif
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
void Engine::setFilter(KNSCore::Provider::Filter newFilter_)
{
setFilter2([newFilter_] {
switch (newFilter_) {
case KNSCore::Provider::None:
return KNSCore::Filter::None;
case KNSCore::Provider::Installed:
return KNSCore::Filter::Installed;
case KNSCore::Provider::Updates:
return KNSCore::Filter::Updates;
case KNSCore::Provider::ExactEntryId:
return KNSCore::Filter::ExactEntryId;
}
return KNSCore::Filter::None;
}());
}
#endif
KNSCore::Filter Engine::filter2() const
{
return d->currentRequest.filter();
}
void Engine::setFilter2(KNSCore::Filter newFilter)
{
if (d->currentRequest.filter() != newFilter) {
d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
newFilter,
d->currentRequest.searchTerm(),
d->currentRequest.categories(),
d->currentRequest.page(),
d->currentRequest.pageSize());
reloadEntries();
Q_EMIT filterChanged();
}
}
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
KNSCore::Provider::SortMode Engine::sortOrder() const
{
return [mode = sortOrder2()] {
switch (mode) {
case KNSCore::SortMode::Newest:
return KNSCore::Provider::Newest;
case KNSCore::SortMode::Alphabetical:
return KNSCore::Provider::Alphabetical;
case KNSCore::SortMode::Rating:
return KNSCore::Provider::Rating;
case KNSCore::SortMode::Downloads:
return KNSCore::Provider::Downloads;
}
return KNSCore::Provider::Rating;
}();
}
#endif
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
void Engine::setSortOrder(KNSCore::Provider::SortMode mode_)
{
setSortOrder2([mode_] {
switch (mode_) {
case KNSCore::Provider::Newest:
return KNSCore::SortMode::Newest;
case KNSCore::Provider::Alphabetical:
return KNSCore::SortMode::Alphabetical;
case KNSCore::Provider::Rating:
return KNSCore::SortMode::Rating;
case KNSCore::Provider::Downloads:
return KNSCore::SortMode::Downloads;
}
return KNSCore::SortMode::Rating;
}());
}
#endif
KNSCore::SortMode Engine::sortOrder2() const
{
return d->currentRequest.sortMode();
}
void Engine::setSortOrder2(KNSCore::SortMode mode)
{
if (d->currentRequest.sortMode() != mode) {
d->currentRequest = KNSCore::SearchRequest(mode,
d->currentRequest.filter(),
d->currentRequest.searchTerm(),
d->currentRequest.categories(),
d->currentRequest.page(),
d->currentRequest.pageSize());
reloadEntries();
Q_EMIT sortOrderChanged();
}
}
QString Engine::searchTerm() const
{
return d->currentRequest.searchTerm();
}
void Engine::setSearchTerm(const QString &searchTerm)
{
if (d->isValid && d->currentRequest.searchTerm() != searchTerm) {
d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
d->currentRequest.filter(),
searchTerm,
d->currentRequest.categories(),
d->currentRequest.page(),
d->currentRequest.pageSize());
Q_EMIT searchTermChanged();
}
KNSCore::Entry::List cacheEntries = dd->cache->requestFromCache(d->currentRequest);
if (!cacheEntries.isEmpty()) {
reloadEntries();
} else {
d->searchTimer.start();
}
}
SearchPresetModel *Engine::searchPresetModel() const
{
return d->searchPresetModel;
}
bool Engine::isValid()
{
return d->isValid;
}
void Engine::updateEntryContents(const KNSCore::Entry &entry)
{
const auto core = dd->providerCores.value(entry.providerId());
if (!core) {
qCWarning(KNEWSTUFFQUICK) << "Provider was not found" << entry.providerId();
return;
}
const auto base = core->d->base;
if (!base->isInitialized()) {
qCWarning(KNEWSTUFFQUICK) << "Provider was not initialized" << base << entry.providerId();
return;
}
base->loadEntryDetails(entry);
}
void Engine::reloadEntries()
{
Q_EMIT signalResetView();
d->currentPage = -1;
d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
d->currentRequest.filter(),
d->currentRequest.searchTerm(),
d->currentRequest.categories(),
0,
d->currentRequest.pageSize());
d->numDataJobs = 0;
const auto providersList = dd->providerCores;
for (const auto &core : providersList) {
const auto &base = core->d->base;
if (base->isInitialized()) {
if (d->currentRequest.filter() == KNSCore::Filter::Installed || d->currentRequest.filter() == KNSCore::Filter::Updates) {
// when asking for installed entries, never use the cache
base->loadEntries(d->currentRequest);
} else {
// take entries from cache until there are no more
KNSCore::Entry::List cacheEntries;
KNSCore::Entry::List lastCache = dd->cache->requestFromCache(d->currentRequest);
while (!lastCache.isEmpty()) {
qCDebug(KNEWSTUFFQUICK) << "From cache";
cacheEntries << lastCache;
d->currentPage = d->currentRequest.page();
d->currentRequest = d->currentRequest.nextPage();
lastCache = dd->cache->requestFromCache(d->currentRequest);
}
// Since the cache has no more pages, reset the request's page
if (d->currentPage >= 0) {
d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
d->currentRequest.filter(),
d->currentRequest.searchTerm(),
d->currentRequest.categories(),
d->currentPage,
d->currentRequest.pageSize());
}
if (!cacheEntries.isEmpty()) {
Q_EMIT signalEntriesLoaded(cacheEntries);
} else {
qCDebug(KNEWSTUFFQUICK) << "From provider";
base->loadEntries(d->currentRequest);
++d->numDataJobs;
updateStatus();
}
}
}
}
}
void Engine::loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type)
{
qCDebug(KNEWSTUFFQUICK) << "START preview: " << entry.name() << type;
auto l = new KNSCore::ImageLoader(entry, type, this);
connect(l, &KNSCore::ImageLoader::signalPreviewLoaded, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type) {
qCDebug(KNEWSTUFFQUICK) << "FINISH preview: " << entry.name() << type;
Q_EMIT signalEntryPreviewLoaded(entry, type);
--d->numPictureJobs;
updateStatus();
});
connect(l, &KNSCore::ImageLoader::signalError, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type, const QString &errorText) {
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ImageError, errorText, QVariantList() << entry.name() << type);
qCDebug(KNEWSTUFFQUICK) << "ERROR preview: " << errorText << entry.name() << type;
--d->numPictureJobs;
updateStatus();
});
l->start();
++d->numPictureJobs;
updateStatus();
}
void Engine::adoptEntry(const KNSCore::Entry &entry)
{
registerTransaction(KNSCore::Transaction::adopt(this, entry));
}
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
void Engine::install(const KNSCore::Entry &entry, int linkId)
{
qmlWarning(this) << "org.kde.newstuff.core.Engine.install is deprecated. Use installLinkId or installLatest";
auto transaction = KNSCore::Transaction::install(this, entry, linkId);
registerTransaction(transaction);
if (!transaction->isFinished()) {
++d->numInstallJobs;
}
}
#endif
void Engine::installLinkId(const KNSCore::Entry &entry, quint8 linkId)
{
auto transaction = KNSCore::Transaction::installLinkId(this, entry, linkId);
registerTransaction(transaction);
if (!transaction->isFinished()) {
++d->numInstallJobs;
}
}
void Engine::installLatest(const KNSCore::Entry &entry)
{
auto transaction = KNSCore::Transaction::installLatest(this, entry);
registerTransaction(transaction);
if (!transaction->isFinished()) {
++d->numInstallJobs;
}
}
void Engine::uninstall(const KNSCore::Entry &entry)
{
registerTransaction(KNSCore::Transaction::uninstall(this, entry));
}
void Engine::registerTransaction(KNSCore::Transaction *transaction)
{
connect(transaction, &KNSCore::Transaction::signalErrorCode, this, &EngineBase::signalErrorCode);
connect(transaction, &KNSCore::Transaction::signalMessage, this, &EngineBase::signalMessage);
connect(transaction, &KNSCore::Transaction::signalEntryEvent, this, &Engine::signalEntryEvent);
}
void Engine::requestMoreData()
{
qCDebug(KNEWSTUFFQUICK) << "Get more data! current page: " << d->currentPage << " requested: " << d->currentRequest.page();
if (d->currentPage < d->currentRequest.page()) {
return;
}
d->currentRequest = d->currentRequest.nextPage();
doRequest();
}
void Engine::doRequest()
{
const auto cores = dd->providerCores;
for (const auto &core : cores) {
const auto &base = core->d->base;
if (base->isInitialized()) {
base->loadEntries(d->currentRequest);
++d->numDataJobs;
updateStatus();
}
}
}
void Engine::revalidateCacheEntries()
{
// This gets called from QML, because in QtQuick we reuse the engine, BUG: 417985
// We can't handle this in the cache, because it can't access the configuration of the engine
if (dd->cache) {
const auto cores = dd->providerCores;
for (const auto &core : cores) {
const auto &base = core->d->base;
if (base && base->isInitialized()) {
const KNSCore::Entry::List cacheBefore = dd->cache->registryForProvider(base->id());
dd->cache->removeDeletedEntries();
const KNSCore::Entry::List cacheAfter = dd->cache->registryForProvider(base->id());
// If the user has deleted them in the background we have to update the state to deleted
for (const auto &oldCachedEntry : cacheBefore) {
if (!cacheAfter.contains(oldCachedEntry)) {
KNSCore::Entry removedEntry = oldCachedEntry;
removedEntry.setEntryDeleted();
Q_EMIT signalEntryEvent(removedEntry, KNSCore::Entry::StatusChangedEvent);
}
}
}
}
}
}
void Engine::restoreSearch()
{
d->searchTimer.stop();
d->currentRequest = d->storedRequest;
if (dd->cache) {
KNSCore::Entry::List cacheEntries = dd->cache->requestFromCache(d->currentRequest);
if (!cacheEntries.isEmpty()) {
reloadEntries();
} else {
d->searchTimer.start();
}
} else {
qCWarning(KNEWSTUFFQUICK) << "Attempted to call restoreSearch() without a correctly initialized engine. You will likely get unexpected behaviour.";
}
}
void Engine::storeSearch()
{
d->storedRequest = d->currentRequest;
}
@@ -0,0 +1,272 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef ENGINE_H
#define ENGINE_H
#include <QObject>
#include <QQmlListProperty>
#include "categoriesmodel.h"
#include "enginebase.h"
#include "entry.h"
#include "errorcode.h"
#include "provider.h"
#include "searchpresetmodel.h"
#include "transaction.h"
class EnginePrivate;
/**
* KNSCore::EngineBase for interfacing with QML
*
* @see ItemsModel
*/
class Engine : public KNSCore::EngineBase
{
Q_OBJECT
Q_PROPERTY(QString configFile READ configFile WRITE setConfigFile NOTIFY configFileChanged)
Q_PROPERTY(bool isLoading READ isLoading NOTIFY busyStateChanged)
Q_PROPERTY(bool needsLazyLoadSpinner READ needsLazyLoadSpinner NOTIFY busyStateChanged)
Q_PROPERTY(bool hasAdoptionCommand READ hasAdoptionCommand NOTIFY configFileChanged)
Q_PROPERTY(QString name READ name NOTIFY configFileChanged)
Q_PROPERTY(bool isValid READ isValid NOTIFY configFileChanged)
Q_PROPERTY(CategoriesModel *categories READ categories NOTIFY categoriesChanged)
Q_PROPERTY(QStringList categoriesFilter READ categoriesFilter WRITE setCategoriesFilter RESET resetCategoriesFilter NOTIFY categoriesFilterChanged)
Q_PROPERTY(KNSCore::Provider::Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
Q_PROPERTY(KNSCore::Filter filter2 READ filter2 WRITE setFilter2 NOTIFY filterChanged)
Q_PROPERTY(KNSCore::Provider::SortMode sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged)
Q_PROPERTY(KNSCore::SortMode sortOrder2 READ sortOrder2 WRITE setSortOrder2 NOTIFY sortOrderChanged)
Q_PROPERTY(QString searchTerm READ searchTerm WRITE setSearchTerm RESET resetSearchTerm NOTIFY searchTermChanged)
Q_PROPERTY(SearchPresetModel *searchPresetModel READ searchPresetModel NOTIFY searchPresetModelChanged)
/**
* Current state of the engine, the state con contain multiple operations
* an empty BusyState represents the idle status
* @since 5.74
*/
Q_PROPERTY(BusyState busyState READ busyState WRITE setBusyState NOTIFY busyStateChanged)
Q_PROPERTY(QString busyMessage READ busyMessage NOTIFY busyStateChanged)
public:
explicit Engine(QObject *parent = nullptr);
~Engine() override;
Q_DISABLE_COPY_MOVE(Engine)
enum class BusyOperation {
Initializing = 1,
LoadingData,
LoadingPreview,
InstallingEntry,
};
Q_DECLARE_FLAGS(BusyState, BusyOperation)
Q_ENUM(BusyOperation)
enum EntryEvent { // TODO KF6 remove in favor of using NewStuff.Entry values
UnknownEvent = KNSCore::Entry::UnknownEvent,
StatusChangedEvent = KNSCore::Entry::StatusChangedEvent,
AdoptedEvent = KNSCore::Entry::AdoptedEvent,
DetailsLoadedEvent = KNSCore::Entry::DetailsLoadedEvent,
};
Q_ENUM(EntryEvent)
QString configFile() const;
void setConfigFile(const QString &newFile);
Q_SIGNAL void configFileChanged();
Engine::BusyState busyState() const;
QString busyMessage() const;
void setBusyState(Engine::BusyState state);
/**
* Signal gets emitted when the busy state changes
* @since 5.74
*/
Q_SIGNAL void busyStateChanged();
/**
* Whether or not the engine is performing its initial loading operations
* @since 5.65
*/
bool isLoading() const
{
// When installing entries, we don't want to block the UI
return busyState().toInt() != 0 && ((busyState() & BusyOperation::InstallingEntry) != BusyOperation::InstallingEntry);
}
CategoriesModel *categories() const;
Q_SIGNAL void categoriesChanged();
QStringList categoriesFilter() const;
void setCategoriesFilter(const QStringList &newCategoriesFilter);
Q_INVOKABLE void resetCategoriesFilter()
{
setCategoriesFilter(categoriesFilter());
}
Q_SIGNAL void categoriesFilterChanged();
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
/// @deprecated since 6.9 Use filter2
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use filter2")
KNSCore::Provider::Filter filter() const;
#endif
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
/// @deprecated since 6.9 Use setFilter2
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use setFilter2")
void setFilter(KNSCore::Provider::Filter filter);
#endif
[[nodiscard]] KNSCore::Filter filter2() const;
void setFilter2(KNSCore::Filter filter);
Q_SIGNAL void filterChanged();
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
/// @deprecated since 6.9 Use sortOrder2
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use sortOrder2")
KNSCore::Provider::SortMode sortOrder() const;
#endif
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
/// @deprecated since 6.9 Use setSortOrder2
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use setSortOrder2")
void setSortOrder(KNSCore::Provider::SortMode newSortOrder);
#endif
[[nodiscard]] KNSCore::SortMode sortOrder2() const;
void setSortOrder2(KNSCore::SortMode newSortOrder);
Q_SIGNAL void sortOrderChanged();
QString searchTerm() const;
void setSearchTerm(const QString &newSearchTerm);
Q_INVOKABLE void resetSearchTerm()
{
setSearchTerm(QString());
}
Q_SIGNAL void searchTermChanged();
SearchPresetModel *searchPresetModel() const;
Q_SIGNAL void searchPresetModelChanged();
Q_INVOKABLE void updateEntryContents(const KNSCore::Entry &entry);
Q_INVOKABLE KNSCore::Entry __createEntry(const QString &providerId, const QString &entryId)
{
KNSCore::Entry e;
e.setProviderId(providerId);
e.setUniqueId(entryId);
return e;
}
bool isValid();
void reloadEntries();
void loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type);
/**
* Adopt an entry using the adoption command. This will also take care of displaying error messages
* @param entry Entry that should be adopted
* @see signalErrorCode
* @see signalEntryEvent
* @since 5.77
*/
Q_INVOKABLE void adoptEntry(const KNSCore::Entry &entry);
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
/**
* Installs an entry's payload file. This includes verification, if
* necessary, as well as decompression and other steps according to the
* application's *.knsrc file.
*
* @param entry Entry to be installed
*
* @see signalInstallationFinished
* @see signalInstallationFailed
* @deprecated since 6.9, use installLatest or installLinkId instead
*/
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "use installLatest or installLinkId instead")
Q_INVOKABLE void install(const KNSCore::Entry &entry, int linkId = 1);
#endif
/**
* Performs an install on the given @p entry
*
* @param linkId specifies which of the assets we want to see installed.
* @since 6.9
*/
Q_INVOKABLE void installLinkId(const KNSCore::Entry &entry, quint8 linkId);
/**
* Performs an install of the latest version on the given @p entry
*
* The latest version is determined using heuristics. If you want tight control over which offering gets installed
* you need to use installLinkId and manually figure out the id.
*
* @since 6.9
*/
Q_INVOKABLE void installLatest(const KNSCore::Entry &entry);
/**
* Uninstalls an entry. It reverses the steps which were performed
* during the installation.
*
* @param entry The entry to uninstall
*/
Q_INVOKABLE void uninstall(const KNSCore::Entry &entry);
void requestMoreData();
Q_INVOKABLE void revalidateCacheEntries();
Q_INVOKABLE void restoreSearch();
Q_INVOKABLE void storeSearch();
Q_SIGNALS:
void signalResetView();
/**
* This is fired for events related directly to a single Entry instance
* The intermediate states Updating and Installing are not forwarded. In case you
* need those you have to listen to the signals of the KNSCore::Engine instance of the engine property.
*
* As an example, if you need to know when the status of an entry changes, you might write:
\code
function onEntryEvent(entry, event) {
if (event == NewStuff.Engine.StatusChangedEvent) {
myModel.ghnsEntryChanged(entry);
}
}
\endcode
*
* nb: The above example is also how one would port a handler for the old changedEntries signal
*
* @see Entry::EntryEvent for details on which specific event is being notified
*/
void entryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event);
/**
* Fires in the case of any critical or serious errors, such as network or API problems.
* This forwards the signal from KNSCore::Engine::signalErrorCode, but with QML friendly
* enumerations.
* @param errorCode Represents the specific type of error which has occurred
* @param message A human-readable message which can be shown to the end user
* @param metadata Any additional data which might be helpful to further work out the details of the error (see KNSCore::Entry::ErrorCode for the
* metadata details)
* @see KNSCore::Engine::signalErrorCode
* @since 5.84
*/
void errorCode(KNSCore::ErrorCode::ErrorCode errorCode, const QString &message, const QVariant &metadata);
void entryPreviewLoaded(const KNSCore::Entry &, KNSCore::Entry::PreviewType);
void signalEntriesLoaded(const KNSCore::Entry::List &entries); ///@internal
void signalEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event); ///@internal
private:
bool init(const QString &configfile) override;
void updateStatus() override;
bool needsLazyLoadSpinner();
Q_SIGNAL void signalEntryPreviewLoaded(const KNSCore::Entry &, KNSCore::Entry::PreviewType);
void registerTransaction(KNSCore::Transaction *transactions);
void doRequest();
const std::unique_ptr<EnginePrivate> d;
KNSCore::EngineBasePrivate *dd;
};
#endif // ENGINE_H
@@ -0,0 +1,329 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "quickitemsmodel.h"
#include "core/commentsmodel.h"
#include "downloadlinkinfo.h"
#include "itemsmodel.h"
#include "quickengine.h"
#include <KShell>
#include <QProcess>
class ItemsModelPrivate
{
public:
ItemsModelPrivate(ItemsModel *qq)
: q(qq)
, model(nullptr)
, engine(nullptr)
{
}
ItemsModel *q;
KNSCore::ItemsModel *model;
Engine *engine;
QHash<QString, KNSCore::CommentsModel *> commentsModels;
bool initModel()
{
if (model) {
return true;
}
if (!engine) {
return false;
}
model = new KNSCore::ItemsModel(engine, q);
q->connect(engine, &Engine::signalProvidersLoaded, engine, &Engine::reloadEntries);
// Entries have been fetched and should be shown:
q->connect(engine, &Engine::signalEntriesLoaded, model, [this](const KNSCore::Entry::List &entries) {
model->slotEntriesLoaded(entries);
});
q->connect(engine, &Engine::entryEvent, model, [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
if (event == KNSCore::Entry::DetailsLoadedEvent && engine->filter() != KNSCore::Provider::Installed
&& engine->filter() != KNSCore::Provider::Updates) {
model->slotEntriesLoaded(KNSCore::Entry::List{entry});
}
});
// Check if we need intermediate states
q->connect(engine, &Engine::entryEvent, q, [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
onEntryEvent(entry, event);
});
q->connect(engine, &Engine::signalResetView, model, &KNSCore::ItemsModel::clearEntries);
q->connect(model, &KNSCore::ItemsModel::loadPreview, engine, &Engine::loadPreview);
q->connect(engine, &Engine::entryPreviewLoaded, model, &KNSCore::ItemsModel::slotEntryPreviewLoaded);
q->connect(model, &KNSCore::ItemsModel::rowsInserted, q, &ItemsModel::rowsInserted);
q->connect(model, &KNSCore::ItemsModel::rowsRemoved, q, &ItemsModel::rowsRemoved);
q->connect(model, &KNSCore::ItemsModel::dataChanged, q, &ItemsModel::dataChanged);
q->connect(model, &KNSCore::ItemsModel::modelAboutToBeReset, q, &ItemsModel::modelAboutToBeReset);
q->connect(model, &KNSCore::ItemsModel::modelReset, q, &ItemsModel::modelReset);
return true;
}
void onEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event)
{
if (event == KNSCore::Entry::StatusChangedEvent) {
model->slotEntryChanged(entry);
Q_EMIT q->entryChanged(entry);
// If we update/uninstall an entry we have to update the UI, see BUG: 425135
if (engine->filter() == KNSCore::Provider::Updates && entry.status() != KNSCore::Entry::Updateable && entry.status() != KNSCore::Entry::Updating) {
model->removeEntry(entry);
} else if (engine->filter() == KNSCore::Provider::Installed && entry.status() == KNSCore::Entry::Deleted) {
model->removeEntry(entry);
}
}
if (event == KNSCore::Entry::DetailsLoadedEvent) {
model->slotEntryChanged(entry);
Q_EMIT q->entryChanged(entry);
}
}
};
ItemsModel::ItemsModel(QObject *parent)
: QAbstractListModel(parent)
, d(new ItemsModelPrivate(this))
{
}
ItemsModel::~ItemsModel() = default;
QHash<int, QByteArray> ItemsModel::roleNames() const
{
static const QHash<int, QByteArray> roles = QHash<int, QByteArray>{
{Qt::DisplayRole, "display"},
{NameRole, "name"},
{UniqueIdRole, "uniqueId"},
{CategoryRole, "category"},
{HomepageRole, "homepage"},
{AuthorRole, "author"},
{LicenseRole, "license"},
{ShortSummaryRole, "shortSummary"},
{SummaryRole, "summary"},
{ChangelogRole, "changelog"},
{VersionRole, "version"},
{ReleaseDateRole, "releaseDate"},
{UpdateVersionRole, "updateVersion"},
{UpdateReleaseDateRole, "updateReleaseDate"},
{PayloadRole, "payload"},
{Qt::DecorationRole, "decoration"},
{PreviewsSmallRole, "previewsSmall"},
{PreviewsRole, "previews"},
{InstalledFilesRole, "installedFiles"},
{UnInstalledFilesRole, "uninstalledFiles"},
{RatingRole, "rating"},
{NumberOfCommentsRole, "numberOfComments"},
{DownloadCountRole, "downloadCount"},
{NumberFansRole, "numberFans"},
{NumberKnowledgebaseEntriesRole, "numberKnowledgebaseEntries"},
{KnowledgebaseLinkRole, "knowledgebaseLink"},
{DownloadLinksRole, "downloadLinks"},
{DonationLinkRole, "donationLink"},
{ProviderIdRole, "providerId"},
{SourceRole, "source"},
{EntryRole, "entry"},
};
return roles;
}
int ItemsModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
if (d->initModel()) {
return d->model->rowCount(QModelIndex());
}
return 0;
}
QVariant ItemsModel::data(const QModelIndex &index, int role) const
{
if (index.isValid() && d->initModel()) {
KNSCore::Entry entry = d->model->data(d->model->index(index.row()), Qt::UserRole).value<KNSCore::Entry>();
switch (role) {
case NameRole:
case Qt::DisplayRole:
return entry.name();
case EntryRole:
return QVariant::fromValue(entry);
case UniqueIdRole:
return entry.uniqueId();
case CategoryRole:
return entry.category();
case HomepageRole:
return entry.homepage();
break;
case AuthorRole: {
KNSCore::Author author = entry.author();
QVariantMap returnAuthor;
returnAuthor[QStringLiteral("id")] = author.id();
returnAuthor[QStringLiteral("name")] = author.name();
returnAuthor[QStringLiteral("email")] = author.email();
returnAuthor[QStringLiteral("homepage")] = author.homepage();
returnAuthor[QStringLiteral("jabber")] = author.jabber();
returnAuthor[QStringLiteral("avatarUrl")] = author.avatarUrl();
returnAuthor[QStringLiteral("description")] = author.description();
return returnAuthor;
} break;
case LicenseRole:
return entry.license();
case ShortSummaryRole:
return entry.shortSummary();
case SummaryRole:
return entry.summary();
case ChangelogRole:
return entry.changelog();
case VersionRole:
return entry.version();
case ReleaseDateRole:
return entry.releaseDate();
case UpdateVersionRole:
return entry.updateVersion();
case UpdateReleaseDateRole:
return entry.updateReleaseDate();
case PayloadRole:
return entry.payload();
case Qt::DecorationRole:
return entry.previewUrl(KNSCore::Entry::PreviewSmall1);
case PreviewsSmallRole: {
QStringList previews;
previews << entry.previewUrl(KNSCore::Entry::PreviewSmall1);
previews << entry.previewUrl(KNSCore::Entry::PreviewSmall2);
previews << entry.previewUrl(KNSCore::Entry::PreviewSmall3);
while (!previews.isEmpty() && previews.last().isEmpty()) {
previews.takeLast();
}
return previews;
}
case PreviewsRole: {
QStringList previews;
previews << entry.previewUrl(KNSCore::Entry::PreviewBig1);
previews << entry.previewUrl(KNSCore::Entry::PreviewBig2);
previews << entry.previewUrl(KNSCore::Entry::PreviewBig3);
while (!previews.isEmpty() && previews.last().isEmpty()) {
previews.takeLast();
}
return previews;
}
case InstalledFilesRole:
return entry.installedFiles();
case UnInstalledFilesRole:
return entry.uninstalledFiles();
case RatingRole:
return entry.rating();
case NumberOfCommentsRole:
return entry.numberOfComments();
case DownloadCountRole:
return entry.downloadCount();
case NumberFansRole:
return entry.numberFans();
case NumberKnowledgebaseEntriesRole:
return entry.numberKnowledgebaseEntries();
case KnowledgebaseLinkRole:
return entry.knowledgebaseLink();
case DownloadLinksRole: {
// This would be good to cache... but it also needs marking as dirty, somehow...
const QList<KNSCore::Entry::DownloadLinkInformation> dllinks = entry.downloadLinkInformationList();
QVariantList list;
for (const KNSCore::Entry::DownloadLinkInformation &link : dllinks) {
list.append(QVariant::fromValue(DownloadLinkInfo(link)));
}
if (list.isEmpty() && !entry.payload().isEmpty()) {
KNSCore::Entry::DownloadLinkInformation data;
data.descriptionLink = entry.payload();
list.append(QVariant::fromValue(DownloadLinkInfo(data)));
}
return QVariant::fromValue(list);
}
case DonationLinkRole:
return entry.donationLink();
case ProviderIdRole:
return entry.providerId();
case SourceRole: {
KNSCore::Entry::Source src = entry.source();
switch (src) {
case KNSCore::Entry::Cache:
return QStringLiteral("Cache");
case KNSCore::Entry::Online:
return QStringLiteral("Online");
case KNSCore::Entry::Registry:
return QStringLiteral("Registry");
default:
return QStringLiteral("Unknown source - shouldn't be possible");
}
}
case CommentsModelRole: {
KNSCore::CommentsModel *commentsModel{nullptr};
if (!d->commentsModels.contains(entry.uniqueId())) {
commentsModel = new KNSCore::CommentsModel(d->engine);
commentsModel->setEntry(entry);
d->commentsModels[entry.uniqueId()] = commentsModel;
} else {
commentsModel = d->commentsModels[entry.uniqueId()];
}
return QVariant::fromValue(commentsModel);
}
default:
return QStringLiteral("Unknown role");
}
}
return QVariant();
}
bool ItemsModel::canFetchMore(const QModelIndex &parent) const
{
return !parent.isValid() && d->engine && d->engine->categoriesMetadata().count() > 0;
}
void ItemsModel::fetchMore(const QModelIndex &parent)
{
if (parent.isValid() || !d->engine) {
return;
}
d->engine->requestMoreData();
}
Engine *ItemsModel::engine() const
{
return d->engine;
}
void ItemsModel::setEngine(Engine *newEngine)
{
if (d->engine != newEngine) {
beginResetModel();
d->engine = newEngine;
if (d->model) {
d->model->deleteLater();
d->model = nullptr;
}
Q_EMIT engineChanged();
endResetModel();
}
}
int ItemsModel::indexOfEntryId(const QString &providerId, const QString &entryId)
{
int idx{-1};
if (d->engine && d->model) {
for (int i = 0; i < rowCount(); ++i) {
KNSCore::Entry testEntry = d->model->data(d->model->index(i), Qt::UserRole).value<KNSCore::Entry>();
if (providerId == testEntry.providerId() && entryId == testEntry.uniqueId()) {
idx = i;
break;
}
}
}
return idx;
}
#include "moc_quickitemsmodel.cpp"
@@ -0,0 +1,144 @@
/*
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef ITEMSMODEL_H
#define ITEMSMODEL_H
#include <QAbstractListModel>
#include "entry.h"
#include "quickengine.h"
#include <memory>
class ItemsModelPrivate;
/**
* @short A model which shows the contents found in an Engine
*
* Use an instance of this model to show the content items represented by the configuration
* file passed to an engine. The following sample assumes you are using the Engine component,
* however it is also possible to pass a KNSCore::EngineBase instance created from C++ to this
* property, if you have specific requirements not covered by the convenience component.
*
* Most data in the model is simple, but the DownloadLinks role will return a list of
* DownloadLinkInfo entries, which you will need to manage in some way.
*
* You might also look at NewStuffList, NewStuffItem, and the other items, to see some more
* detail on what can be done with the data.
*
* @see NewStuffList
* @see NewStuffItem
* @see NewStuffPage
* @see NewStuffEntryDetails
* @see NewStuffEntryComments
*
* \code
import org.kde.newstuff as NewStuff
Item {
NewStuff.ItemsModel {
id: newStuffModel
engine: newStuffEngine
}
NewStuff.Engine {
id: newStuffEngine
configFile: "/some/filesystem/location/wallpaper.knsrc"
onBusyMessageChanged: () => console.log("KNS Message: " + newStuffEngine.busyMessage)
onErrorCode: (code, message, metadata) => console.log("KNS Error: " + message)
}
}
\endcode
*/
class ItemsModel : public QAbstractListModel
{
Q_OBJECT
/**
* The NewStuffQuickEngine to show items from
*/
Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged REQUIRED)
public:
explicit ItemsModel(QObject *parent = nullptr);
~ItemsModel() override;
enum Roles {
NameRole = Qt::UserRole + 1,
UniqueIdRole,
CategoryRole,
HomepageRole,
AuthorRole,
LicenseRole,
ShortSummaryRole,
SummaryRole,
ChangelogRole,
VersionRole,
ReleaseDateRole,
UpdateVersionRole,
UpdateReleaseDateRole,
PayloadRole,
PreviewsSmallRole, ///@< this will return a list here, rather than be tied so tightly to the remote api
PreviewsRole, ///@< this will return a list here, rather than be tied so tightly to the remote api
InstalledFilesRole,
UnInstalledFilesRole,
RatingRole,
NumberOfCommentsRole,
DownloadCountRole,
NumberFansRole,
NumberKnowledgebaseEntriesRole,
KnowledgebaseLinkRole,
DownloadLinksRole,
DonationLinkRole,
ProviderIdRole,
SourceRole,
CommentsModelRole,
EntryRole,
};
Q_ENUM(Roles)
// The lists in OCS are one-indexed, and that isn't how one usually does things in C++.
// Consequently, this enum removes what would seem like magic numbers from the code, and
// makes their meaning more explicit.
enum LinkId { // TODO KF6 reuse this enum in the transaction, we currently use magic numbers there
AutoDetectLinkId = -1,
FirstLinkId = 1,
};
Q_ENUM(LinkId)
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
Engine *engine() const;
void setEngine(Engine *newEngine);
Q_SIGNAL void engineChanged();
/**
* Get the index of an entry based on that entry's unique ID
* @param providerId The provider inside of which you wish to search for an entry
* @param entryId The unique ID within the given provider of the entry you want to know the index of
* @return The index of the entry. In case the entry is not found, -1 is returned
* @see KNSCore::Entry::uniqueId()
* @since 5.79
*/
Q_INVOKABLE int indexOfEntryId(const QString &providerId, const QString &entryId);
Q_INVOKABLE int indexOfEntry(const KNSCore::Entry &e)
{
return indexOfEntryId(e.providerId(), e.uniqueId());
}
/**
* @brief Fired when an entry's data changes
*
* @param entry The entry which has changed
*/
Q_SIGNAL void entryChanged(const KNSCore::Entry &entry);
private:
const std::unique_ptr<ItemsModelPrivate> d;
};
#endif // ITEMSMODEL_H
@@ -0,0 +1,90 @@
/*
This file is part of KNewStuffQuick.
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "quickquestionlistener.h"
#include "core/question.h"
#include <QCoreApplication>
#include <QPointer>
using namespace KNewStuffQuick;
Q_GLOBAL_STATIC(QuickQuestionListener, s_quickQuestionListener)
QuickQuestionListener *QuickQuestionListener::instance()
{
return s_quickQuestionListener;
}
QuickQuestionListener::~QuickQuestionListener()
{
if (m_question) {
m_question->setResponse(KNSCore::Question::CancelResponse);
}
}
void QuickQuestionListener::askQuestion(KNSCore::Question *question)
{
switch (question->questionType()) {
case KNSCore::Question::SelectFromListQuestion:
Q_EMIT askListQuestion(question->title(), question->question(), question->list());
break;
case KNSCore::Question::ContinueCancelQuestion:
Q_EMIT askContinueCancelQuestion(question->title(), question->question());
break;
case KNSCore::Question::InputTextQuestion:
Q_EMIT askTextInputQuestion(question->title(), question->question());
break;
case KNSCore::Question::PasswordQuestion:
Q_EMIT askPasswordQuestion(question->title(), question->question());
break;
case KNSCore::Question::YesNoQuestion:
default:
Q_EMIT askYesNoQuestion(question->title(), question->question());
break;
}
m_question = question;
}
void KNewStuffQuick::QuickQuestionListener::passResponse(bool responseIsContinue, QString input)
{
if (m_question) {
if (responseIsContinue) {
m_question->setResponse(input);
switch (m_question->questionType()) {
case KNSCore::Question::ContinueCancelQuestion:
m_question->setResponse(KNSCore::Question::ContinueResponse);
break;
case KNSCore::Question::YesNoQuestion:
m_question->setResponse(KNSCore::Question::YesResponse);
break;
case KNSCore::Question::SelectFromListQuestion:
case KNSCore::Question::InputTextQuestion:
case KNSCore::Question::PasswordQuestion:
default:
m_question->setResponse(KNSCore::Question::OKResponse);
break;
}
} else {
switch (m_question->questionType()) {
case KNSCore::Question::YesNoQuestion:
m_question->setResponse(KNSCore::Question::NoResponse);
break;
case KNSCore::Question::SelectFromListQuestion:
case KNSCore::Question::InputTextQuestion:
case KNSCore::Question::PasswordQuestion:
case KNSCore::Question::ContinueCancelQuestion:
default:
m_question->setResponse(KNSCore::Question::CancelResponse);
break;
}
}
m_question.clear();
}
}
#include "moc_quickquestionlistener.cpp"
@@ -0,0 +1,40 @@
/*
This file is part of KNewStuffQuick.
SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KNSQ_QUICKQUESTIONLISTENER_H
#define KNSQ_QUICKQUESTIONLISTENER_H
#include "core/questionlistener.h"
#include <QPointer>
namespace KNewStuffQuick
{
class QuickQuestionListener : public KNSCore::QuestionListener
{
Q_OBJECT
Q_DISABLE_COPY(QuickQuestionListener)
public:
static QuickQuestionListener *instance();
~QuickQuestionListener() override;
Q_SLOT void askQuestion(KNSCore::Question *question) override;
Q_SIGNAL void askListQuestion(QString title, QString question, QStringList list);
Q_SIGNAL void askContinueCancelQuestion(QString title, QString question);
Q_SIGNAL void askTextInputQuestion(QString title, QString question);
Q_SIGNAL void askPasswordQuestion(QString title, QString question);
Q_SIGNAL void askYesNoQuestion(QString title, QString question);
Q_SLOT void passResponse(bool responseIsContinue, QString input);
QuickQuestionListener() = default; // Only used by Q_GLOBAL_STATIC
private:
QPointer<KNSCore::Question> m_question;
};
}
#endif // KNSQ_QUICKQUESTIONLISTENER_H
@@ -0,0 +1,26 @@
/*
This file is part of KNewStuffQuick.
SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "quicksettings.h"
#include <KAuthorized>
#include <QCoreApplication>
Q_GLOBAL_STATIC(KNewStuffQuick::Settings, s_settings)
KNewStuffQuick::Settings *KNewStuffQuick::Settings::instance()
{
return s_settings;
}
bool KNewStuffQuick::Settings::allowedByKiosk() const
{
return KAuthorized::authorize(KAuthorized::GHNS);
}
#include "moc_quicksettings.cpp"
@@ -0,0 +1,35 @@
/*
This file is part of KNewStuffQuick.
SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KNSQ_QUICKSETTINGS_H
#define KNSQ_QUICKSETTINGS_H
#include <QObject>
namespace KNewStuffQuick
{
/**
* An object for handling KNewStuff related settings which make sense to handle without
* instantiating an engine (specifically, for now, whether or not this is allowed by
* the user's Kiosk settings)
* @since 5.81
*/
class Settings : public QObject
{
Q_OBJECT
Q_PROPERTY(bool allowedByKiosk READ allowedByKiosk CONSTANT)
public:
static Settings *instance();
bool allowedByKiosk() const;
/// @internal
Settings()
{
}
};
}
#endif // KNSQ_QUICKSETTINGS_H
@@ -0,0 +1,123 @@
/*
SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "searchpresetmodel.h"
#include "knewstuffquick_debug.h"
#include <KLocalizedString>
#include "../core/enginebase_p.h"
SearchPresetModel::SearchPresetModel(KNSCore::EngineBase *engine)
: QAbstractListModel(engine)
, m_engine(engine)
{
connect(m_engine, qOverload<const QList<KNSCore::SearchPreset> &>(&KNSCore::EngineBase::signalSearchPresetsLoaded), this, [this]() {
beginResetModel();
endResetModel();
});
}
SearchPresetModel::~SearchPresetModel() = default;
QHash<int, QByteArray> SearchPresetModel::roleNames() const
{
static const QHash<int, QByteArray> roles{{DisplayNameRole, "displayName"}, {IconRole, "iconName"}};
return roles;
}
QVariant SearchPresetModel::data(const QModelIndex &index, int role) const
{
if (index.isValid() && checkIndex(index)) {
const QList<KNSCore::SearchPreset> presets = m_engine->d->searchPresets;
const KNSCore::SearchPreset &preset = presets[index.row()];
if (role == DisplayNameRole) {
if (QString name = preset.displayName(); !name.isEmpty()) {
return name;
}
switch (preset.type()) {
case KNSCore::SearchPreset::Type::GoBack:
return i18nc("Knewstuff5", "Back");
case KNSCore::SearchPreset::Type::Popular:
return i18nc("Knewstuff5", "Popular");
case KNSCore::SearchPreset::Type::Featured:
return i18nc("Knewstuff5", "Featured");
case KNSCore::SearchPreset::Type::Start:
return i18nc("Knewstuff5", "Restart");
case KNSCore::SearchPreset::Type::New:
return i18nc("Knewstuff5", "New");
case KNSCore::SearchPreset::Type::Root:
return i18nc("Knewstuff5", "Home");
case KNSCore::SearchPreset::Type::Shelf:
return i18nc("Knewstuff5", "Shelf");
case KNSCore::SearchPreset::Type::FolderUp:
return i18nc("Knewstuff5", "Up");
case KNSCore::SearchPreset::Type::Recommended:
return i18nc("Knewstuff5", "Recommended");
case KNSCore::SearchPreset::Type::Subscription:
return i18nc("Knewstuff5", "Subscriptions");
case KNSCore::SearchPreset::Type::AllEntries:
return i18nc("Knewstuff5", "All Entries");
case KNSCore::SearchPreset::Type::NoPresetType:
break;
}
return i18nc("Knewstuff5", "Search Preset: %1", preset.request().searchTerm());
}
if (role == IconRole) {
if (QString name = preset.iconName(); !name.isEmpty()) {
return name;
}
switch (preset.type()) {
case KNSCore::SearchPreset::Type::GoBack:
return QStringLiteral("arrow-left");
case KNSCore::SearchPreset::Type::Popular:
case KNSCore::SearchPreset::Type::Featured:
case KNSCore::SearchPreset::Type::Recommended:
return QStringLiteral("rating");
case KNSCore::SearchPreset::Type::New:
return QStringLiteral("change-date-symbolic");
case KNSCore::SearchPreset::Type::Start:
return QStringLiteral("start-over");
case KNSCore::SearchPreset::Type::Root:
return QStringLiteral("go-home");
case KNSCore::SearchPreset::Type::Shelf:
case KNSCore::SearchPreset::Type::Subscription:
return QStringLiteral("bookmark");
case KNSCore::SearchPreset::Type::FolderUp:
return QStringLiteral("arrow-up");
case KNSCore::SearchPreset::Type::AllEntries:
case KNSCore::SearchPreset::Type::NoPresetType:
break;
}
return QStringLiteral("search");
}
}
return QVariant();
}
int SearchPresetModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_engine->d->searchPresets.count();
}
void SearchPresetModel::loadSearch(const QModelIndex &index)
{
if (index.row() >= rowCount() || !index.isValid()) {
qCWarning(KNEWSTUFFQUICK) << "index SearchPresetModel::loadSearch invalid" << index;
return;
}
const auto preset = m_engine->d->searchPresets.at(index.row());
m_engine->search(preset.request());
}
#include "moc_searchpresetmodel.cpp"
@@ -0,0 +1,44 @@
/*
SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef SEARCHPRESETMODEL_H
#define SEARCHPRESETMODEL_H
#include "enginebase.h"
#include <QAbstractListModel>
/**
* @brief The SearchPresetModel class
*
* this class handles search presets.
* @since 5.83
*/
class SearchPresetModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit SearchPresetModel(KNSCore::EngineBase *parent);
~SearchPresetModel() override;
enum Roles {
DisplayNameRole = Qt::UserRole + 1,
IconRole,
};
Q_ENUM(Roles)
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE void loadSearch(const QModelIndex &index);
private:
KNSCore::EngineBase *const m_engine;
};
#endif // SEARCHPRESETMODEL_H