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:
@@ -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})
|
||||
+55
@@ -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 {};
|
||||
}
|
||||
+25
@@ -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
|
||||
}
|
||||
+169
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+178
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -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…")
|
||||
}
|
||||
}
|
||||
+201
@@ -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
|
||||
Reference in New Issue
Block a user