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,14 @@
|
||||
# SPDX-FileCopyrightText: KDE Contributors
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
add_subdirectory(core)
|
||||
|
||||
add_subdirectory(qtquick)
|
||||
add_subdirectory(tools)
|
||||
|
||||
add_subdirectory(widgets)
|
||||
ecm_qt_install_logging_categories(
|
||||
EXPORT KNEWSTUFF
|
||||
FILE knewstuff.categories
|
||||
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# SPDX-FileCopyrightText: none
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
# Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources.
|
||||
# The results are stored in a pseudo .cpp file to be picked up by xgettext.
|
||||
lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg`
|
||||
if [ -n "$lst" ] ; then
|
||||
$EXTRACTRC $lst >> rc.cpp
|
||||
fi
|
||||
|
||||
# Extract strings from all source files.
|
||||
# If your framework depends on KI18n, use $XGETTEXT. If it uses Qt translation
|
||||
# system, use $EXTRACT_TR_STRINGS.
|
||||
$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/knewstuff6.pot
|
||||
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "atticaprovider_p.h"
|
||||
|
||||
#include "commentsmodel.h"
|
||||
#include "entry_p.h"
|
||||
#include "question.h"
|
||||
#include "tagsfilterchecker.h"
|
||||
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
#include <QCollator>
|
||||
#include <QDomDocument>
|
||||
#include <QTimer>
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include <attica/accountbalance.h>
|
||||
#include <attica/config.h>
|
||||
#include <attica/content.h>
|
||||
#include <attica/downloaditem.h>
|
||||
#include <attica/listjob.h>
|
||||
#include <attica/person.h>
|
||||
#include <attica/provider.h>
|
||||
#include <attica/providermanager.h>
|
||||
|
||||
#include "atticarequester_p.h"
|
||||
#include "categorymetadata.h"
|
||||
#include "categorymetadata_p.h"
|
||||
|
||||
using namespace Attica;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
AtticaProvider::AtticaProvider(const QStringList &categories, const QString &additionalAgentInformation)
|
||||
: mInitialized(false)
|
||||
{
|
||||
// init categories map with invalid categories
|
||||
for (const QString &category : categories) {
|
||||
mCategoryMap.insert(category, Attica::Category());
|
||||
}
|
||||
|
||||
connect(&m_providerManager, &ProviderManager::providerAdded, this, [this, additionalAgentInformation](const Attica::Provider &provider) {
|
||||
providerLoaded(provider);
|
||||
m_provider.setAdditionalAgentInformation(additionalAgentInformation);
|
||||
});
|
||||
connect(&m_providerManager, &ProviderManager::authenticationCredentialsMissing, this, &AtticaProvider::onAuthenticationCredentialsMissing);
|
||||
}
|
||||
|
||||
AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString &additionalAgentInformation)
|
||||
: mInitialized(false)
|
||||
{
|
||||
// init categories map with invalid categories
|
||||
for (const QString &category : categories) {
|
||||
mCategoryMap.insert(category, Attica::Category());
|
||||
}
|
||||
providerLoaded(provider);
|
||||
m_provider.setAdditionalAgentInformation(additionalAgentInformation);
|
||||
}
|
||||
|
||||
QString AtticaProvider::id() const
|
||||
{
|
||||
return m_providerId;
|
||||
}
|
||||
|
||||
void AtticaProvider::onAuthenticationCredentialsMissing(const Attica::Provider &)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "Authentication missing!";
|
||||
// FIXME Show authentication dialog
|
||||
}
|
||||
|
||||
bool AtticaProvider::setProviderXML(const QDomElement &xmldata)
|
||||
{
|
||||
if (xmldata.tagName() != QLatin1String("provider")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME this is quite ugly, repackaging the xml into a string
|
||||
QDomDocument doc(QStringLiteral("temp"));
|
||||
qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString();
|
||||
|
||||
doc.appendChild(xmldata.cloneNode(true));
|
||||
m_providerManager.addProviderFromXml(doc.toString());
|
||||
|
||||
if (!m_providerManager.providers().isEmpty()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString();
|
||||
} else {
|
||||
qCCritical(KNEWSTUFFCORE) << "Could not load provider.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AtticaProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries)
|
||||
{
|
||||
mCachedEntries = cachedEntries;
|
||||
}
|
||||
|
||||
void AtticaProvider::providerLoaded(const Attica::Provider &provider)
|
||||
{
|
||||
m_name = provider.name();
|
||||
m_icon = provider.icon();
|
||||
qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name();
|
||||
|
||||
m_provider = provider;
|
||||
m_provider.setAdditionalAgentInformation(name());
|
||||
m_providerId = provider.baseUrl().host();
|
||||
|
||||
Attica::ListJob<Attica::Category> *job = m_provider.requestCategories();
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::listOfCategoriesLoaded);
|
||||
job->start();
|
||||
}
|
||||
|
||||
void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob)
|
||||
{
|
||||
if (!jobSuccess(listJob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys();
|
||||
|
||||
auto *job = static_cast<Attica::ListJob<Attica::Category> *>(listJob);
|
||||
const Category::List categoryList = job->itemList();
|
||||
|
||||
QList<CategoryMetadata> categoryMetadataList;
|
||||
for (const Category &category : categoryList) {
|
||||
if (mCategoryMap.contains(category.name())) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName();
|
||||
// If there is only the placeholder category, replace it
|
||||
if (mCategoryMap.contains(category.name()) && !mCategoryMap.value(category.name()).isValid()) {
|
||||
mCategoryMap.replace(category.name(), category);
|
||||
} else {
|
||||
mCategoryMap.insert(category.name(), category);
|
||||
}
|
||||
|
||||
categoryMetadataList << CategoryMetadata(new CategoryMetadataPrivate{
|
||||
.id = category.id(),
|
||||
.name = category.name(),
|
||||
.displayName = category.displayName(),
|
||||
});
|
||||
}
|
||||
}
|
||||
std::sort(categoryMetadataList.begin(), categoryMetadataList.end(), [](const auto &i, const auto &j) -> bool {
|
||||
const QString a(i.displayName().isEmpty() ? i.name() : i.displayName());
|
||||
const QString b(j.displayName().isEmpty() ? j.name() : j.displayName());
|
||||
|
||||
return (QCollator().compare(a, b) < 0);
|
||||
});
|
||||
|
||||
bool correct = false;
|
||||
for (auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it != itEnd; ++it) {
|
||||
if (!it.value().isValid()) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Could not find category" << it.key();
|
||||
} else {
|
||||
correct = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (correct) {
|
||||
mInitialized = true;
|
||||
Q_EMIT providerInitialized(this);
|
||||
Q_EMIT categoriesMetadataLoaded(categoryMetadataList);
|
||||
} else {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("All categories are missing"), QVariant());
|
||||
}
|
||||
}
|
||||
|
||||
bool AtticaProvider::isInitialized() const
|
||||
{
|
||||
return mInitialized;
|
||||
}
|
||||
|
||||
void AtticaProvider::loadEntries(const KNSCore::SearchRequest &request)
|
||||
{
|
||||
auto requester = new AtticaRequester(request, this, this);
|
||||
connect(requester, &AtticaRequester::entryDetailsLoaded, this, &AtticaProvider::entryDetailsLoaded);
|
||||
connect(requester, &AtticaRequester::entriesLoaded, this, [this, requester](const KNSCore::Entry::List &list) {
|
||||
Q_EMIT entriesLoaded(requester->request(), list);
|
||||
});
|
||||
connect(requester, &AtticaRequester::loadingDone, this, [this, requester] {
|
||||
Q_EMIT loadingDone(requester->request());
|
||||
});
|
||||
connect(requester, &AtticaRequester::loadingFailed, this, [this, requester] {
|
||||
Q_EMIT loadingFailed(requester->request());
|
||||
});
|
||||
requester->start();
|
||||
}
|
||||
|
||||
void AtticaProvider::loadEntryDetails(const KNSCore::Entry &entry)
|
||||
{
|
||||
ItemJob<Content> *job = m_provider.requestContent(entry.uniqueId());
|
||||
connect(job, &BaseJob::finished, this, [this, entry] {
|
||||
Q_EMIT entryDetailsLoaded(entry);
|
||||
});
|
||||
job->start();
|
||||
}
|
||||
|
||||
void AtticaProvider::loadPayloadLink(const KNSCore::Entry &entry, int linkId)
|
||||
{
|
||||
Attica::Content content = mCachedContent.value(entry.uniqueId());
|
||||
const DownloadDescription desc = content.downloadUrlDescription(linkId);
|
||||
|
||||
if (desc.hasPrice()) {
|
||||
// Ask for balance, then show information...
|
||||
ItemJob<AccountBalance> *job = m_provider.requestAccountBalance();
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::accountBalanceLoaded);
|
||||
mDownloadLinkJobs[job] = qMakePair(entry, linkId);
|
||||
job->start();
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "get account balance";
|
||||
} else {
|
||||
ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(linkId));
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
|
||||
mDownloadLinkJobs[job] = qMakePair(entry, linkId);
|
||||
job->start();
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId();
|
||||
}
|
||||
}
|
||||
|
||||
void AtticaProvider::loadComments(const Entry &entry, int commentsPerPage, int page)
|
||||
{
|
||||
ListJob<Attica::Comment> *job = m_provider.requestComments(Attica::Comment::ContentComment, entry.uniqueId(), QStringLiteral("0"), page, commentsPerPage);
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::loadedComments);
|
||||
job->start();
|
||||
}
|
||||
|
||||
QList<std::shared_ptr<KNSCore::Comment>> getCommentsList(const Attica::Comment::List &comments, std::shared_ptr<KNSCore::Comment> parent)
|
||||
{
|
||||
QList<std::shared_ptr<KNSCore::Comment>> knsComments;
|
||||
for (const Attica::Comment &comment : comments) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children";
|
||||
auto knsComment = std::make_shared<KNSCore::Comment>();
|
||||
knsComment->id = comment.id();
|
||||
knsComment->subject = comment.subject();
|
||||
knsComment->text = comment.text();
|
||||
knsComment->childCount = comment.childCount();
|
||||
knsComment->username = comment.user();
|
||||
knsComment->date = comment.date();
|
||||
knsComment->score = comment.score();
|
||||
knsComment->parent = parent;
|
||||
knsComments << knsComment;
|
||||
if (comment.childCount() > 0) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.count();
|
||||
knsComments << getCommentsList(comment.children(), knsComment);
|
||||
qCDebug(KNEWSTUFFCORE) << "After getting the children, we now have the following number of comments:" << knsComments.count();
|
||||
}
|
||||
}
|
||||
return knsComments;
|
||||
}
|
||||
|
||||
void AtticaProvider::loadedComments(Attica::BaseJob *baseJob)
|
||||
{
|
||||
if (!jobSuccess(baseJob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *job = static_cast<ListJob<Attica::Comment> *>(baseJob);
|
||||
Attica::Comment::List comments = job->itemList();
|
||||
|
||||
QList<std::shared_ptr<KNSCore::Comment>> receivedComments = getCommentsList(comments, nullptr);
|
||||
Q_EMIT commentsLoaded(receivedComments);
|
||||
}
|
||||
|
||||
void AtticaProvider::loadPerson(const QString &username)
|
||||
{
|
||||
if (m_provider.hasPersonService()) {
|
||||
ItemJob<Attica::Person> *job = m_provider.requestPerson(username);
|
||||
job->setProperty("username", username);
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::loadedPerson);
|
||||
job->start();
|
||||
}
|
||||
}
|
||||
|
||||
void AtticaProvider::loadedPerson(Attica::BaseJob *baseJob)
|
||||
{
|
||||
if (!jobSuccess(baseJob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *job = static_cast<ItemJob<Attica::Person> *>(baseJob);
|
||||
Attica::Person person = job->result();
|
||||
|
||||
auto author = std::make_shared<KNSCore::Author>();
|
||||
// This is a touch hack-like, but it ensures we actually have the data in case it is not returned by the server
|
||||
author->setId(job->property("username").toString());
|
||||
author->setName(QStringLiteral("%1 %2").arg(person.firstName(), person.lastName()).trimmed());
|
||||
author->setHomepage(person.homepage());
|
||||
author->setProfilepage(person.extendedAttribute(QStringLiteral("profilepage")));
|
||||
author->setAvatarUrl(person.avatarUrl());
|
||||
author->setDescription(person.extendedAttribute(QStringLiteral("description")));
|
||||
Q_EMIT personLoaded(author);
|
||||
}
|
||||
|
||||
void AtticaProvider::loadedConfig(Attica::BaseJob *baseJob)
|
||||
{
|
||||
if (!jobSuccess(baseJob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *job = dynamic_cast<ItemJob<Attica::Config> *>(baseJob);
|
||||
Attica::Config config = job->result();
|
||||
m_version = config.version();
|
||||
m_supportsSsl = config.ssl();
|
||||
m_contactEmail = config.contact();
|
||||
const auto protocol = [&config] {
|
||||
QString protocol{QStringLiteral("http")};
|
||||
if (config.ssl()) {
|
||||
protocol = QStringLiteral("https");
|
||||
}
|
||||
return protocol;
|
||||
}();
|
||||
m_website = [&config, &protocol] {
|
||||
// There is usually no protocol in the website and host, but in case
|
||||
// there is, trust what's there
|
||||
if (config.website().contains(QLatin1String("://"))) {
|
||||
return QUrl(config.website());
|
||||
}
|
||||
return QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.website()));
|
||||
}();
|
||||
m_host = [&config, &protocol] {
|
||||
if (config.host().contains(QLatin1String("://"))) {
|
||||
return QUrl(config.host());
|
||||
}
|
||||
return QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.host()));
|
||||
}();
|
||||
|
||||
Q_EMIT basicsLoaded();
|
||||
}
|
||||
|
||||
void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob)
|
||||
{
|
||||
if (!jobSuccess(baseJob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *job = static_cast<ItemJob<AccountBalance> *>(baseJob);
|
||||
AccountBalance item = job->result();
|
||||
|
||||
QPair<Entry, int> pair = mDownloadLinkJobs.take(job);
|
||||
Entry entry(pair.first);
|
||||
Content content = mCachedContent.value(entry.uniqueId());
|
||||
if (content.downloadUrlDescription(pair.second).priceAmount() < item.balance()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(pair.second).priceAmount()
|
||||
<< " balance: " << item.balance();
|
||||
Question question;
|
||||
question.setEntry(entry);
|
||||
question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price",
|
||||
"This item costs %1 %2.\nDo you want to buy it?",
|
||||
item.currency(),
|
||||
content.downloadUrlDescription(pair.second).priceAmount()));
|
||||
if (question.ask() == Question::YesResponse) {
|
||||
ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(pair.second));
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
|
||||
mDownloadLinkJobs[job] = qMakePair(entry, pair.second);
|
||||
job->start();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(0).priceAmount()
|
||||
<< " balance: " << item.balance();
|
||||
Q_EMIT signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", //
|
||||
item.balance(),
|
||||
content.downloadUrlDescription(0).priceAmount()));
|
||||
}
|
||||
}
|
||||
|
||||
void AtticaProvider::downloadItemLoaded(BaseJob *baseJob)
|
||||
{
|
||||
if (!jobSuccess(baseJob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *job = static_cast<ItemJob<DownloadItem> *>(baseJob);
|
||||
DownloadItem item = job->result();
|
||||
|
||||
Entry entry = mDownloadLinkJobs.take(job).first;
|
||||
entry.setPayload(QString(item.url().toString()));
|
||||
Q_EMIT payloadLinkLoaded(entry);
|
||||
}
|
||||
|
||||
void AtticaProvider::vote(const Entry &entry, uint rating)
|
||||
{
|
||||
PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating);
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::votingFinished);
|
||||
job->start();
|
||||
}
|
||||
|
||||
void AtticaProvider::votingFinished(Attica::BaseJob *job)
|
||||
{
|
||||
if (!jobSuccess(job)) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded."));
|
||||
}
|
||||
|
||||
void AtticaProvider::becomeFan(const Entry &entry)
|
||||
{
|
||||
PostJob *job = m_provider.becomeFan(entry.uniqueId());
|
||||
connect(job, &BaseJob::finished, this, &AtticaProvider::becomeFanFinished);
|
||||
job->start();
|
||||
}
|
||||
|
||||
void AtticaProvider::becomeFanFinished(Attica::BaseJob *job)
|
||||
{
|
||||
if (!jobSuccess(job)) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT signalInformation(i18n("You are now a fan."));
|
||||
}
|
||||
|
||||
bool AtticaProvider::jobSuccess(Attica::BaseJob *job)
|
||||
{
|
||||
if (job->metadata().error() == Attica::Metadata::NoError) {
|
||||
return true;
|
||||
}
|
||||
qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message();
|
||||
|
||||
if (job->metadata().error() == Attica::Metadata::NetworkError) {
|
||||
if (job->metadata().statusCode() == 503) {
|
||||
QDateTime retryAfter;
|
||||
static const QByteArray retryAfterKey{"Retry-After"};
|
||||
for (const QNetworkReply::RawHeaderPair &headerPair : job->metadata().headers()) {
|
||||
if (headerPair.first == retryAfterKey) {
|
||||
// Retry-After is not a known header, so we need to do a bit of running around to make that work
|
||||
// Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
|
||||
// So, simple workaround, just pass it through a dummy request and get a formatted date out (the
|
||||
// cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
|
||||
QNetworkRequest dummyRequest;
|
||||
dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
|
||||
retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
static const KFormat formatter;
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::TryAgainLaterError,
|
||||
i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
|
||||
formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
|
||||
{retryAfter});
|
||||
} else {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::NetworkError,
|
||||
i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()),
|
||||
job->metadata().statusCode());
|
||||
}
|
||||
}
|
||||
if (job->metadata().error() == Attica::Metadata::OcsError) {
|
||||
if (job->metadata().statusCode() == 200) {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
|
||||
i18n("Too many requests to server. Please try again in a few minutes."),
|
||||
job->metadata().statusCode());
|
||||
} else if (job->metadata().statusCode() == 405) {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
|
||||
i18n("The Open Collaboration Services instance %1 does not support the attempted function.", name()),
|
||||
job->metadata().statusCode());
|
||||
} else {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
|
||||
i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()),
|
||||
job->metadata().statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (auto searchRequestVar = job->property("searchRequest"); searchRequestVar.isValid()) {
|
||||
auto req = searchRequestVar.value<SearchRequest>();
|
||||
Q_EMIT loadingFailed(req);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AtticaProvider::updateOnFirstBasicsGet()
|
||||
{
|
||||
if (!m_basicsGot) {
|
||||
m_basicsGot = true;
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
Attica::ItemJob<Attica::Config> *configJob = m_provider.requestConfig();
|
||||
connect(configJob, &BaseJob::finished, this, &AtticaProvider::loadedConfig);
|
||||
configJob->start();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
QString AtticaProvider::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
QUrl AtticaProvider::icon() const
|
||||
{
|
||||
return m_icon;
|
||||
}
|
||||
|
||||
QString AtticaProvider::version()
|
||||
{
|
||||
updateOnFirstBasicsGet();
|
||||
return m_version;
|
||||
}
|
||||
|
||||
QUrl AtticaProvider::website()
|
||||
{
|
||||
updateOnFirstBasicsGet();
|
||||
return m_website;
|
||||
}
|
||||
|
||||
QUrl AtticaProvider::host()
|
||||
{
|
||||
updateOnFirstBasicsGet();
|
||||
return m_host;
|
||||
}
|
||||
|
||||
QString AtticaProvider::contactEmail()
|
||||
{
|
||||
updateOnFirstBasicsGet();
|
||||
return m_contactEmail;
|
||||
}
|
||||
|
||||
bool AtticaProvider::supportsSsl()
|
||||
{
|
||||
updateOnFirstBasicsGet();
|
||||
return m_supportsSsl;
|
||||
}
|
||||
|
||||
} // namespace KNSCore
|
||||
|
||||
#include "moc_atticaprovider_p.cpp"
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_ATTICAPROVIDER_P_H
|
||||
#define KNEWSTUFF3_ATTICAPROVIDER_P_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
|
||||
#include <attica/content.h>
|
||||
#include <attica/provider.h>
|
||||
#include <attica/providermanager.h>
|
||||
|
||||
#include "providerbase_p.h"
|
||||
|
||||
namespace Attica
|
||||
{
|
||||
class BaseJob;
|
||||
}
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
/**
|
||||
* @short KNewStuff Attica Provider class.
|
||||
*
|
||||
* This class provides accessors for the provider object.
|
||||
* It should not be used directly by the application.
|
||||
* This class is the base class and will be instantiated for
|
||||
* websites that implement the Open Collaboration Services.
|
||||
*
|
||||
* @author Frederik Gladhorn <gladhorn@kde.org>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class /* for autotest: */ KNEWSTUFFCORE_EXPORT AtticaProvider : public ProviderBase
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AtticaProvider(const QStringList &categories, const QString &additionalAgentInformation);
|
||||
AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString &additionalAgentInformation);
|
||||
|
||||
QString id() const override;
|
||||
|
||||
/**
|
||||
* set the provider data xml, to initialize the provider
|
||||
*/
|
||||
bool setProviderXML(const QDomElement &xmldata) override;
|
||||
|
||||
bool isInitialized() const override;
|
||||
void setCachedEntries(const KNSCore::Entry::List &cachedEntries) override;
|
||||
|
||||
void loadEntries(const KNSCore::SearchRequest &request) override;
|
||||
void loadEntryDetails(const KNSCore::Entry &entry) override;
|
||||
void loadPayloadLink(const Entry &entry, int linkId) override;
|
||||
|
||||
void loadComments(const KNSCore::Entry &entry, int commentsPerPage, int page) override;
|
||||
void loadPerson(const QString &username) override;
|
||||
|
||||
bool userCanVote() override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
void vote(const Entry &entry, uint rating) override;
|
||||
|
||||
bool userCanBecomeFan() override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
void becomeFan(const Entry &entry) override;
|
||||
|
||||
Attica::Provider *provider()
|
||||
{
|
||||
return &m_provider;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString name() const override;
|
||||
[[nodiscard]] QUrl icon() const override;
|
||||
[[nodiscard]] QString version() override;
|
||||
[[nodiscard]] QUrl website() override;
|
||||
[[nodiscard]] QUrl host() override;
|
||||
[[nodiscard]] QString contactEmail() override;
|
||||
[[nodiscard]] bool supportsSsl() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void providerLoaded(const Attica::Provider &provider);
|
||||
void listOfCategoriesLoaded(Attica::BaseJob *);
|
||||
void downloadItemLoaded(Attica::BaseJob *job);
|
||||
void accountBalanceLoaded(Attica::BaseJob *job);
|
||||
void onAuthenticationCredentialsMissing(const Attica::Provider &);
|
||||
void votingFinished(Attica::BaseJob *);
|
||||
void becomeFanFinished(Attica::BaseJob *job);
|
||||
void loadedComments(Attica::BaseJob *job);
|
||||
void loadedPerson(Attica::BaseJob *job);
|
||||
void loadedConfig(Attica::BaseJob *job);
|
||||
|
||||
private:
|
||||
bool jobSuccess(Attica::BaseJob *job);
|
||||
void updateOnFirstBasicsGet();
|
||||
|
||||
// the attica categories we are interested in (e.g. Wallpaper, Application, Vocabulary File...)
|
||||
QMultiHash<QString, Attica::Category> mCategoryMap;
|
||||
|
||||
Attica::ProviderManager m_providerManager;
|
||||
Attica::Provider m_provider;
|
||||
|
||||
KNSCore::Entry::List mCachedEntries;
|
||||
QHash<QString, Attica::Content> mCachedContent;
|
||||
|
||||
// Associate job and entry, this is needed when fetching
|
||||
// download links or the account balance in order to continue
|
||||
// when the result is there.
|
||||
QHash<Attica::BaseJob *, QPair<Entry, int>> mDownloadLinkJobs;
|
||||
|
||||
bool mInitialized;
|
||||
QString m_providerId;
|
||||
bool m_basicsGot = false;
|
||||
QString m_name;
|
||||
QUrl m_icon;
|
||||
QString m_version;
|
||||
QUrl m_website;
|
||||
QUrl m_host;
|
||||
QString m_contactEmail;
|
||||
bool m_supportsSsl = true;
|
||||
|
||||
Q_DISABLE_COPY(AtticaProvider)
|
||||
friend class AtticaRequester;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,294 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#include "atticarequester_p.h"
|
||||
|
||||
#include "commentsmodel.h"
|
||||
#include "entry_p.h"
|
||||
#include "question.h"
|
||||
#include "tagsfilterchecker.h"
|
||||
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
#include <QCollator>
|
||||
#include <QDomDocument>
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include <attica/accountbalance.h>
|
||||
#include <attica/config.h>
|
||||
#include <attica/content.h>
|
||||
#include <attica/downloaditem.h>
|
||||
#include <attica/listjob.h>
|
||||
#include <attica/person.h>
|
||||
#include <attica/provider.h>
|
||||
#include <attica/providermanager.h>
|
||||
|
||||
#include "atticaprovider_p.h"
|
||||
#include "searchrequest_p.h"
|
||||
|
||||
using namespace Attica;
|
||||
|
||||
namespace
|
||||
{
|
||||
Attica::Provider::SortMode atticaSortMode(KNSCore::SortMode sortMode)
|
||||
{
|
||||
switch (sortMode) {
|
||||
case KNSCore::SortMode::Newest:
|
||||
return Attica::Provider::Newest;
|
||||
case KNSCore::SortMode::Alphabetical:
|
||||
return Attica::Provider::Alphabetical;
|
||||
case KNSCore::SortMode::Downloads:
|
||||
return Attica::Provider::Downloads;
|
||||
case KNSCore::SortMode::Rating:
|
||||
return Attica::Provider::Rating;
|
||||
}
|
||||
qWarning() << "Unmapped sortMode" << sortMode;
|
||||
return Attica::Provider::Rating;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
AtticaRequester::AtticaRequester(const KNSCore::SearchRequest &request, AtticaProvider *provider, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_request(request)
|
||||
, m_provider(provider)
|
||||
{
|
||||
}
|
||||
|
||||
void AtticaRequester::detailsLoaded(BaseJob *job)
|
||||
{
|
||||
if (m_provider->jobSuccess(job)) {
|
||||
auto *contentJob = dynamic_cast<ItemJob<Content> *>(job);
|
||||
Content content = contentJob->result();
|
||||
auto entry = entryFromAtticaContent(content);
|
||||
entry.setEntryRequestedId(job->property("providedEntryId").toString()); // The ResultsStream should still known that this entry was for its query
|
||||
Q_EMIT entryDetailsLoaded(entry);
|
||||
qCDebug(KNEWSTUFFCORE) << "check update finished: " << entry.name();
|
||||
}
|
||||
|
||||
if (m_updateJobs.remove(job) && m_updateJobs.isEmpty()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "check update finished.";
|
||||
QList<Entry> updatable;
|
||||
for (const Entry &entry : std::as_const(m_provider->mCachedEntries)) {
|
||||
if (entry.status() == KNSCore::Entry::Updateable) {
|
||||
updatable.append(entry);
|
||||
}
|
||||
}
|
||||
qDebug() << "UPDATABLE" << updatable;
|
||||
Q_EMIT entriesLoaded(updatable);
|
||||
Q_EMIT loadingDone();
|
||||
}
|
||||
}
|
||||
|
||||
void AtticaRequester::checkForUpdates()
|
||||
{
|
||||
if (m_provider->mCachedEntries.isEmpty()) {
|
||||
Q_EMIT loadingDone();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const Entry &entry : std::as_const(m_provider->mCachedEntries)) {
|
||||
ItemJob<Content> *job = m_provider->m_provider.requestContent(entry.uniqueId());
|
||||
connect(job, &BaseJob::finished, this, &AtticaRequester::detailsLoaded);
|
||||
m_updateJobs.insert(job);
|
||||
job->start();
|
||||
qCDebug(KNEWSTUFFCORE) << "Checking for update: " << entry.name();
|
||||
}
|
||||
}
|
||||
|
||||
Entry::List AtticaRequester::installedEntries() const
|
||||
{
|
||||
Entry::List entries;
|
||||
for (const Entry &entry : std::as_const(m_provider->mCachedEntries)) {
|
||||
if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
|
||||
entries.append(entry);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
void AtticaRequester::start()
|
||||
{
|
||||
QMetaObject::invokeMethod(this, &AtticaRequester::startInternal, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void AtticaRequester::categoryContentsLoaded(BaseJob *job)
|
||||
{
|
||||
if (!m_provider->jobSuccess(job)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *listJob = dynamic_cast<ListJob<Content> *>(job);
|
||||
const Content::List contents = listJob->itemList();
|
||||
|
||||
Entry::List entries;
|
||||
TagsFilterChecker checker(m_provider->tagFilter());
|
||||
TagsFilterChecker downloadschecker(m_provider->downloadTagFilter());
|
||||
for (const Content &content : contents) {
|
||||
if (!content.isValid()) {
|
||||
qCDebug(KNEWSTUFFCORE)
|
||||
<< "Filtered out an invalid entry. This suggests something is not right on the originating server. Please contact the administrators of"
|
||||
<< m_provider->name() << "and inform them there is an issue with content in the category or categories" << m_request.d->categories;
|
||||
continue;
|
||||
}
|
||||
if (checker.filterAccepts(content.tags())) {
|
||||
bool filterAcceptsDownloads = true;
|
||||
if (content.downloads() > 0) {
|
||||
filterAcceptsDownloads = false;
|
||||
const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
|
||||
for (const Attica::DownloadDescription &dli : descs) {
|
||||
if (downloadschecker.filterAccepts(dli.tags())) {
|
||||
filterAcceptsDownloads = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filterAcceptsDownloads) {
|
||||
m_provider->mCachedContent.insert(content.id(), content);
|
||||
entries.append(entryFromAtticaContent(content));
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on download filter" << m_provider->downloadTagFilter();
|
||||
}
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << m_provider->tagFilter();
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "loaded: " << m_request.d->hashForRequest() << " count: " << entries.size();
|
||||
Q_EMIT entriesLoaded(entries);
|
||||
Q_EMIT loadingDone();
|
||||
}
|
||||
|
||||
void AtticaRequester::startInternal()
|
||||
{
|
||||
switch (m_request.d->filter) {
|
||||
case KNSCore::Filter::None:
|
||||
break;
|
||||
case KNSCore::Filter::ExactEntryId: {
|
||||
ItemJob<Content> *job = m_provider->m_provider.requestContent(m_request.d->searchTerm);
|
||||
job->setProperty("providedEntryId", m_request.d->searchTerm);
|
||||
connect(job, &BaseJob::finished, this, &AtticaRequester::detailsLoaded);
|
||||
job->start();
|
||||
return;
|
||||
}
|
||||
case KNSCore::Filter::Installed:
|
||||
if (m_request.d->page == 0) {
|
||||
Q_EMIT entriesLoaded(installedEntries());
|
||||
Q_EMIT loadingDone();
|
||||
} else { // We always load everything on the first page. The caller may fetchMore and try to read further pages though.
|
||||
Q_EMIT loadingDone();
|
||||
}
|
||||
return;
|
||||
case KNSCore::Filter::Updates:
|
||||
checkForUpdates();
|
||||
return;
|
||||
}
|
||||
|
||||
Attica::Provider::SortMode sorting = atticaSortMode(m_request.d->sortMode);
|
||||
Attica::Category::List categoriesToSearch;
|
||||
|
||||
if (m_request.d->categories.isEmpty()) {
|
||||
// search in all categories
|
||||
categoriesToSearch = m_provider->mCategoryMap.values();
|
||||
} else {
|
||||
categoriesToSearch.reserve(m_request.d->categories.size());
|
||||
for (const QString &categoryName : std::as_const(m_request.d->categories)) {
|
||||
categoriesToSearch.append(m_provider->mCategoryMap.values(categoryName));
|
||||
}
|
||||
}
|
||||
|
||||
ListJob<Content> *job =
|
||||
m_provider->m_provider.searchContents(categoriesToSearch, m_request.d->searchTerm, sorting, m_request.d->page, m_request.d->pageSize);
|
||||
job->setProperty("searchRequest", QVariant::fromValue(m_request));
|
||||
connect(job, &BaseJob::finished, this, &AtticaRequester::categoryContentsLoaded);
|
||||
job->start();
|
||||
}
|
||||
|
||||
Entry AtticaRequester::entryFromAtticaContent(const Attica::Content &content)
|
||||
{
|
||||
Entry entry;
|
||||
|
||||
entry.setProviderId(m_provider->id());
|
||||
entry.setUniqueId(content.id());
|
||||
entry.setStatus(KNSCore::Entry::Downloadable);
|
||||
entry.setVersion(content.version());
|
||||
entry.setReleaseDate(content.updated().date());
|
||||
entry.setCategory(content.attribute(QStringLiteral("typeid")));
|
||||
|
||||
qDebug() << "looking for cache entry";
|
||||
auto index = m_provider->mCachedEntries.indexOf(entry);
|
||||
qDebug() << "looking for cache entry" << index;
|
||||
if (index >= 0) {
|
||||
Entry &cacheEntry = m_provider->mCachedEntries[index];
|
||||
qDebug() << "cache entry" << cacheEntry << cacheEntry.version() << entry.version();
|
||||
// check if updateable
|
||||
if (((cacheEntry.status() == KNSCore::Entry::Installed) || (cacheEntry.status() == KNSCore::Entry::Updateable))
|
||||
&& ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) {
|
||||
cacheEntry.setStatus(KNSCore::Entry::Updateable);
|
||||
cacheEntry.setUpdateVersion(entry.version());
|
||||
cacheEntry.setUpdateReleaseDate(entry.releaseDate());
|
||||
}
|
||||
entry = cacheEntry;
|
||||
} else {
|
||||
m_provider->mCachedEntries.append(entry);
|
||||
}
|
||||
|
||||
entry.setName(content.name());
|
||||
entry.setHomepage(content.detailpage());
|
||||
entry.setRating(content.rating());
|
||||
entry.setNumberOfComments(content.numberOfComments());
|
||||
entry.setDownloadCount(content.downloads());
|
||||
entry.setNumberFans(content.attribute(QStringLiteral("fans")).toInt());
|
||||
entry.setDonationLink(content.attribute(QStringLiteral("donationpage")));
|
||||
entry.setKnowledgebaseLink(content.attribute(QStringLiteral("knowledgebasepage")));
|
||||
entry.setNumberKnowledgebaseEntries(content.attribute(QStringLiteral("knowledgebaseentries")).toInt());
|
||||
entry.setHomepage(content.detailpage());
|
||||
|
||||
entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("1")), Entry::PreviewSmall1);
|
||||
entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("2")), Entry::PreviewSmall2);
|
||||
entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("3")), Entry::PreviewSmall3);
|
||||
|
||||
entry.setPreviewUrl(content.previewPicture(QStringLiteral("1")), Entry::PreviewBig1);
|
||||
entry.setPreviewUrl(content.previewPicture(QStringLiteral("2")), Entry::PreviewBig2);
|
||||
entry.setPreviewUrl(content.previewPicture(QStringLiteral("3")), Entry::PreviewBig3);
|
||||
|
||||
entry.setLicense(content.license());
|
||||
Author author;
|
||||
author.setId(content.author());
|
||||
author.setName(content.author());
|
||||
author.setHomepage(content.attribute(QStringLiteral("profilepage")));
|
||||
entry.setAuthor(author);
|
||||
|
||||
entry.setSource(Entry::Online);
|
||||
entry.setSummary(content.description());
|
||||
entry.setShortSummary(content.summary());
|
||||
entry.setChangelog(content.changelog());
|
||||
entry.setTags(content.tags());
|
||||
|
||||
const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
|
||||
entry.d->mDownloadLinkInformationList.clear();
|
||||
entry.d->mDownloadLinkInformationList.reserve(descs.size());
|
||||
for (const Attica::DownloadDescription &desc : descs) {
|
||||
entry.d->mDownloadLinkInformationList.append({.name = desc.name(),
|
||||
.priceAmount = desc.priceAmount(),
|
||||
.distributionType = desc.distributionType(),
|
||||
.descriptionLink = desc.link(),
|
||||
.id = desc.id(),
|
||||
.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload,
|
||||
.size = desc.size(),
|
||||
.tags = desc.tags(),
|
||||
.version = desc.version()});
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
[[nodiscard]] KNSCore::SearchRequest AtticaRequester::request() const
|
||||
{
|
||||
return m_request;
|
||||
}
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <attica/content.h>
|
||||
#include <attica/provider.h>
|
||||
#include <attica/providermanager.h>
|
||||
|
||||
#include "entry.h"
|
||||
#include "searchrequest.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class AtticaProvider;
|
||||
|
||||
class AtticaRequester : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AtticaRequester(const KNSCore::SearchRequest &request, AtticaProvider *provider, QObject *parent = nullptr);
|
||||
void start();
|
||||
[[nodiscard]] KNSCore::SearchRequest request() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void entriesLoaded(const KNSCore::Entry::List &list);
|
||||
void loadingDone();
|
||||
void loadingFailed();
|
||||
void entryDetailsLoaded(const KNSCore::Entry &entry);
|
||||
|
||||
private Q_SLOTS:
|
||||
void detailsLoaded(Attica::BaseJob *job);
|
||||
void categoryContentsLoaded(Attica::BaseJob *job);
|
||||
|
||||
private:
|
||||
void startInternal();
|
||||
void checkForUpdates();
|
||||
[[nodiscard]] Entry::List installedEntries() const;
|
||||
[[nodiscard]] Entry entryFromAtticaContent(const Attica::Content &content);
|
||||
|
||||
KNSCore::SearchRequest m_request;
|
||||
AtticaProvider *m_provider;
|
||||
QSet<Attica::BaseJob *> m_updateJobs;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,188 @@
|
||||
# SPDX-FileCopyrightText: KDE Contributors
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
set(KNEWSTUFFCORE_INSTALL_INCLUDEDIR "${KDE_INSTALL_INCLUDEDIR_KF}/KNewStuffCore")
|
||||
|
||||
set(KNewStuffCore_SRCS
|
||||
author.cpp
|
||||
commentsmodel.cpp
|
||||
cache.cpp
|
||||
enginebase.cpp
|
||||
entry.cpp
|
||||
imageloader.cpp
|
||||
installation.cpp
|
||||
itemsmodel.cpp
|
||||
provider.cpp
|
||||
providersmodel.cpp
|
||||
tagsfilterchecker.cpp
|
||||
xmlloader.cpp
|
||||
errorcode.cpp
|
||||
resultsstream.cpp
|
||||
transaction.cpp
|
||||
|
||||
# A system by which queries can be passed to the user, and responses
|
||||
# gathered, depending on implementation. See question.h for details.
|
||||
question.cpp
|
||||
questionmanager.cpp
|
||||
questionlistener.cpp
|
||||
|
||||
providercore.cpp
|
||||
providerbase.cpp
|
||||
categorymetadata.cpp
|
||||
searchrequest.cpp
|
||||
searchpreset.cpp
|
||||
cache2.cpp
|
||||
providerbubblewrap.cpp
|
||||
|
||||
../attica/atticaprovider.cpp
|
||||
../attica/atticarequester.cpp
|
||||
../staticxml/staticxmlprovider.cpp
|
||||
)
|
||||
if(KF6Syndication_FOUND)
|
||||
set(KNewStuffCore_syndication_SRCS
|
||||
../opds/opdsprovider.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(knscore_jobs_static STATIC)
|
||||
target_sources(knscore_jobs_static PRIVATE
|
||||
# A set of minimal KJob based classes, designed to replace the
|
||||
# more powerful KIO based system in places where KIO is not available
|
||||
# for one reason or another.
|
||||
jobs/downloadjob.cpp
|
||||
jobs/filecopyjob.cpp
|
||||
jobs/filecopyworker.cpp
|
||||
jobs/httpjob.cpp
|
||||
jobs/httpworker.cpp
|
||||
)
|
||||
target_link_libraries(knscore_jobs_static PUBLIC Qt6::Network KF6::I18n KF6::CoreAddons KF6::Package)
|
||||
target_include_directories(knscore_jobs_static PRIVATE ${CMAKE_BINARY_DIR})
|
||||
# Needed to link this static lib to shared libs
|
||||
set_property(TARGET knscore_jobs_static PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
ecm_qt_declare_logging_category(knscore_jobs_static
|
||||
HEADER knewstuffcore_debug.h
|
||||
IDENTIFIER KNEWSTUFFCORE
|
||||
CATEGORY_NAME kf.newstuff.core
|
||||
OLD_CATEGORY_NAMES org.kde.knewstuff.core
|
||||
DESCRIPTION "knewstuff (Core Lib)"
|
||||
EXPORT KNEWSTUFF
|
||||
)
|
||||
|
||||
add_library(KF6NewStuffCore ${KNewStuffCore_SRCS} ${KNewStuffCore_syndication_SRCS})
|
||||
add_library(KF6::NewStuffCore ALIAS KF6NewStuffCore )
|
||||
|
||||
set_target_properties(KF6NewStuffCore PROPERTIES
|
||||
VERSION ${KNEWSTUFF_VERSION}
|
||||
SOVERSION ${KNEWSTUFF_SOVERSION}
|
||||
EXPORT_NAME NewStuffCore
|
||||
)
|
||||
|
||||
ecm_generate_export_header(KF6NewStuffCore
|
||||
EXPORT_FILE_NAME knewstuffcore_export.h
|
||||
BASE_NAME KNewStuffCore
|
||||
GROUP_BASE_NAME KF
|
||||
VERSION ${KF_VERSION}
|
||||
USE_VERSION_HEADER
|
||||
VERSION_BASE_NAME KNewStuff
|
||||
DEPRECATED_BASE_VERSION 0
|
||||
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
|
||||
DEPRECATION_VERSIONS 6.9
|
||||
)
|
||||
|
||||
set(KNewStuffCore_BUILD_INCLUDE_DIRS
|
||||
${KNewStuff_BINARY_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
target_include_directories(KF6NewStuffCore
|
||||
PUBLIC "$<BUILD_INTERFACE:${KNewStuffCore_BUILD_INCLUDE_DIRS}>"
|
||||
INTERFACE
|
||||
"$<INSTALL_INTERFACE:${KNEWSTUFFCORE_INSTALL_INCLUDEDIR}>"
|
||||
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KNewStuff>" # module version header
|
||||
)
|
||||
|
||||
target_link_libraries(KF6NewStuffCore
|
||||
PUBLIC
|
||||
KF6::Attica # For interacting with ocs providers, public for uploaddialog slots
|
||||
KF6::CoreAddons
|
||||
Qt6::Gui # For QImage
|
||||
PRIVATE
|
||||
KF6::Archive # For decompressing archives
|
||||
KF6::I18n # For translations
|
||||
KF6::ConfigCore
|
||||
KF6::Package
|
||||
Qt6::Xml
|
||||
knscore_jobs_static
|
||||
)
|
||||
|
||||
if(KF6Syndication_FOUND)
|
||||
target_compile_definitions(KF6NewStuffCore PRIVATE -DSYNDICATION_FOUND="${KF6Syndication_FOUND}")
|
||||
target_link_libraries(KF6NewStuffCore
|
||||
PRIVATE
|
||||
KF6::Syndication #OPDS
|
||||
)
|
||||
endif()
|
||||
|
||||
ecm_generate_headers(KNewStuffCore_CamelCase_HEADERS
|
||||
HEADER_NAMES
|
||||
Author
|
||||
Cache
|
||||
EngineBase
|
||||
Entry
|
||||
ErrorCode
|
||||
ItemsModel
|
||||
Provider
|
||||
ProvidersModel
|
||||
Question
|
||||
QuestionListener
|
||||
QuestionManager
|
||||
ResultsStream
|
||||
TagsFilterChecker
|
||||
Transaction
|
||||
SearchRequest
|
||||
ProviderCore
|
||||
CategoryMetadata
|
||||
SearchPreset
|
||||
|
||||
REQUIRED_HEADERS KNewStuffCore_HEADERS
|
||||
OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/KNSCore
|
||||
)
|
||||
|
||||
install(TARGETS KF6NewStuffCore EXPORT KF6NewStuffCoreTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
install(
|
||||
FILES
|
||||
${KNewStuffCore_CamelCase_HEADERS}
|
||||
${KNewStuffCore_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/knewstuffcore_export.h
|
||||
DESTINATION
|
||||
${KNEWSTUFFCORE_INSTALL_INCLUDEDIR}/KNSCore
|
||||
COMPONENT Devel
|
||||
)
|
||||
|
||||
if(BUILD_QCH)
|
||||
ecm_add_qch(
|
||||
KF6NewStuffCore_QCH
|
||||
NAME KNewStuffCore
|
||||
BASE_NAME KF6NewStuffCore
|
||||
VERSION ${KF_VERSION}
|
||||
ORG_DOMAIN org.kde
|
||||
SOURCES ${KNewStuffCore_HEADERS}
|
||||
LINK_QCHS
|
||||
KF6Attica_QCH
|
||||
KF6CoreAddons_QCH
|
||||
INCLUDE_DIRS
|
||||
${KNewStuffCore_BUILD_INCLUDE_DIRS}
|
||||
BLANK_MACROS
|
||||
KNEWSTUFFCORE_EXPORT
|
||||
KNEWSTUFFCORE_DEPRECATED
|
||||
KNEWSTUFFCORE_DEPRECATED_EXPORT
|
||||
"KNEWSTUFFCORE_DEPRECATED_VERSION(x, y, t)"
|
||||
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
|
||||
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
|
||||
COMPONENT Devel
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "author.h"
|
||||
|
||||
#include <QHash>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class AuthorPrivate : public QSharedData
|
||||
{
|
||||
public:
|
||||
QString id;
|
||||
QString profilepage;
|
||||
QUrl avatarUrl;
|
||||
QString description;
|
||||
|
||||
QString name;
|
||||
QString email;
|
||||
QString jabber;
|
||||
QString homepage;
|
||||
};
|
||||
}
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
Author::Author()
|
||||
: d(new AuthorPrivate())
|
||||
{
|
||||
}
|
||||
|
||||
KNSCore::Author::Author(const KNSCore::Author &other)
|
||||
: d(other.d)
|
||||
{
|
||||
}
|
||||
|
||||
Author &Author::operator=(const Author &rhs)
|
||||
{
|
||||
if (&rhs != this) {
|
||||
d = rhs.d;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Author::~Author() = default;
|
||||
|
||||
void KNSCore::Author::setId(const QString &id)
|
||||
{
|
||||
d->id = id;
|
||||
}
|
||||
|
||||
QString KNSCore::Author::id() const
|
||||
{
|
||||
return d->id;
|
||||
}
|
||||
|
||||
void Author::setName(const QString &name)
|
||||
{
|
||||
d->name = name;
|
||||
}
|
||||
|
||||
QString Author::name() const
|
||||
{
|
||||
return d->name;
|
||||
}
|
||||
|
||||
void Author::setEmail(const QString &email)
|
||||
{
|
||||
d->email = email;
|
||||
}
|
||||
|
||||
QString Author::email() const
|
||||
{
|
||||
return d->email;
|
||||
}
|
||||
|
||||
void Author::setJabber(const QString &jabber)
|
||||
{
|
||||
d->jabber = jabber;
|
||||
}
|
||||
|
||||
QString Author::jabber() const
|
||||
{
|
||||
return d->jabber;
|
||||
}
|
||||
|
||||
void Author::setHomepage(const QString &homepage)
|
||||
{
|
||||
d->homepage = homepage;
|
||||
}
|
||||
|
||||
QString Author::homepage() const
|
||||
{
|
||||
return d->homepage;
|
||||
}
|
||||
|
||||
void Author::setProfilepage(const QString &profilepage)
|
||||
{
|
||||
d->profilepage = profilepage;
|
||||
}
|
||||
|
||||
QString Author::profilepage() const
|
||||
{
|
||||
return d->profilepage;
|
||||
}
|
||||
|
||||
void Author::setAvatarUrl(const QUrl &avatarUrl)
|
||||
{
|
||||
d->avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
QUrl Author::avatarUrl() const
|
||||
{
|
||||
return d->avatarUrl;
|
||||
}
|
||||
|
||||
void Author::setDescription(const QString &description)
|
||||
{
|
||||
d->description = description;
|
||||
}
|
||||
|
||||
QString Author::description() const
|
||||
{
|
||||
return d->description;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_AUTHOR_P_H
|
||||
#define KNEWSTUFF3_AUTHOR_P_H
|
||||
|
||||
#include <QSharedData>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class AuthorPrivate;
|
||||
|
||||
/**
|
||||
* @short KNewStuff author information.
|
||||
*
|
||||
* This class provides accessor methods to the author data
|
||||
* as used by KNewStuff.
|
||||
* It should probably not be used directly by the application.
|
||||
*
|
||||
* @author Josef Spillner (spillner@kde.org)
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT Author
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name READ name)
|
||||
Q_PROPERTY(QString email READ email)
|
||||
|
||||
public:
|
||||
explicit Author();
|
||||
Author(const Author &other);
|
||||
Author &operator=(const Author &other);
|
||||
~Author();
|
||||
|
||||
/**
|
||||
* Sets the user ID of the author.
|
||||
*/
|
||||
void setId(const QString &id);
|
||||
|
||||
/**
|
||||
* Retrieve the author's user ID
|
||||
* @return the author's user ID
|
||||
*/
|
||||
QString id() const;
|
||||
|
||||
/**
|
||||
* Sets the full name of the author.
|
||||
*/
|
||||
void setName(const QString &name);
|
||||
|
||||
/**
|
||||
* Retrieve the author's name.
|
||||
*
|
||||
* @return author name
|
||||
*/
|
||||
QString name() const;
|
||||
|
||||
/**
|
||||
* Sets the email address of the author.
|
||||
*/
|
||||
void setEmail(const QString &email);
|
||||
|
||||
/**
|
||||
* Retrieve the author's email address.
|
||||
*
|
||||
* @return author email address
|
||||
*/
|
||||
QString email() const;
|
||||
|
||||
/**
|
||||
* Sets the jabber address of the author.
|
||||
*/
|
||||
void setJabber(const QString &jabber);
|
||||
|
||||
/**
|
||||
* Retrieve the author's jabber address.
|
||||
*
|
||||
* @return author jabber address
|
||||
*/
|
||||
QString jabber() const;
|
||||
|
||||
/**
|
||||
* Sets the homepage of the author.
|
||||
*/
|
||||
void setHomepage(const QString &homepage);
|
||||
|
||||
/**
|
||||
* Retrieve the author's homepage.
|
||||
*
|
||||
* @return author homepage
|
||||
*/
|
||||
QString homepage() const;
|
||||
|
||||
/**
|
||||
* Sets the profile page of the author, usually located on the server hosting the content.
|
||||
*/
|
||||
void setProfilepage(const QString &profilepage);
|
||||
|
||||
/**
|
||||
* Retrieve the author's profile page.
|
||||
*
|
||||
* @return author profile page
|
||||
*/
|
||||
QString profilepage() const;
|
||||
|
||||
/**
|
||||
* Sets the url for the user's avatar image
|
||||
*/
|
||||
void setAvatarUrl(const QUrl &avatarUrl);
|
||||
|
||||
/**
|
||||
* Retrieve the url of the user's avatar image
|
||||
*
|
||||
* @return a url for the user's avatar (may be empty)
|
||||
*/
|
||||
QUrl avatarUrl() const;
|
||||
|
||||
/**
|
||||
* Retrieve the user's description text
|
||||
*
|
||||
* @return A long(ish)-form text describing this user, usually self-entered
|
||||
*/
|
||||
QString description() const;
|
||||
/**
|
||||
* Set the user's description
|
||||
*/
|
||||
void setDescription(const QString &description);
|
||||
|
||||
private:
|
||||
QSharedDataPointer<AuthorPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "cache.h"
|
||||
|
||||
#include "cache2_p.h"
|
||||
#include "compat_p.h"
|
||||
|
||||
class KNSCore::CachePrivate
|
||||
{
|
||||
public:
|
||||
QSharedPointer<Cache2> cache2;
|
||||
};
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
Cache::Cache(const QString &appName)
|
||||
: QObject(nullptr)
|
||||
, d(new CachePrivate({Cache2::getCache(appName)}))
|
||||
{
|
||||
}
|
||||
|
||||
QSharedPointer<Cache> Cache::getCache(const QString &appName)
|
||||
{
|
||||
return QSharedPointer<Cache>(new Cache(appName)); // internally this hits the cache2 registry
|
||||
}
|
||||
|
||||
Cache::~Cache() = default;
|
||||
|
||||
void Cache::readRegistry()
|
||||
{
|
||||
d->cache2->readRegistry();
|
||||
}
|
||||
|
||||
Entry::List Cache::registryForProvider(const QString &providerId)
|
||||
{
|
||||
return d->cache2->registryForProvider(providerId);
|
||||
}
|
||||
|
||||
Entry::List Cache::registry() const
|
||||
{
|
||||
return d->cache2->registry();
|
||||
}
|
||||
|
||||
void Cache::writeRegistry()
|
||||
{
|
||||
d->cache2->writeRegistry();
|
||||
}
|
||||
|
||||
void Cache::registerChangedEntry(const KNSCore::Entry &entry)
|
||||
{
|
||||
d->cache2->registerChangedEntry(entry);
|
||||
}
|
||||
|
||||
void Cache::insertRequest(const KNSCore::Provider::SearchRequest &request, const KNSCore::Entry::List &entries)
|
||||
{
|
||||
d->cache2->insertRequest(KNSCompat::searchRequestFromLegacy(request), entries);
|
||||
}
|
||||
|
||||
Entry::List Cache::requestFromCache(const KNSCore::Provider::SearchRequest &request)
|
||||
{
|
||||
return d->cache2->requestFromCache(KNSCompat::searchRequestFromLegacy(request));
|
||||
}
|
||||
|
||||
void KNSCore::Cache::removeDeletedEntries()
|
||||
{
|
||||
d->cache2->removeDeletedEntries();
|
||||
}
|
||||
|
||||
KNSCore::Entry KNSCore::Cache::entryFromInstalledFile(const QString &installedFile) const
|
||||
{
|
||||
return d->cache2->entryFromInstalledFile(installedFile);
|
||||
}
|
||||
|
||||
#include "moc_cache.cpp"
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef CACHE_H
|
||||
#define CACHE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
|
||||
#include "entry.h"
|
||||
#include "provider.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
#include <memory.h>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class CachePrivate;
|
||||
class KNEWSTUFFCORE_EXPORT KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Do not use the cache directly.") Cache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* Returns an instance of a shared cache for appName
|
||||
* That way it is made sure, that there do not exist different
|
||||
* instances of cache, with different contents
|
||||
* @param appName The file name of the registry - this is usually
|
||||
* the application name, it will be stored in "apps/knewstuff3/appname.knsregistry"
|
||||
*/
|
||||
static QSharedPointer<Cache> getCache(const QString &appName);
|
||||
|
||||
~Cache() override;
|
||||
|
||||
/// Read the installed entries (on startup)
|
||||
void readRegistry();
|
||||
/// All entries that have been installed by a certain provider
|
||||
Entry::List registryForProvider(const QString &providerId);
|
||||
|
||||
/// All entries known by the cache (this corresponds with entries which are installed, regardless of status)
|
||||
Entry::List registry() const;
|
||||
|
||||
/// Save the list of installed entries
|
||||
void writeRegistry();
|
||||
|
||||
void insertRequest(const KNSCore::Provider::SearchRequest &, const KNSCore::Entry::List &entries);
|
||||
Entry::List requestFromCache(const KNSCore::Provider::SearchRequest &);
|
||||
|
||||
/**
|
||||
* This will run through all entries in the cache, and remove all entries
|
||||
* where all the installed files they refer to no longer exist.
|
||||
*
|
||||
* This cannot be done wholesale for all caches, as some consumers will allow
|
||||
* this to happen (or indeed expect it to), and so we have to do this on a
|
||||
* per-type basis
|
||||
*
|
||||
* This will also cause the cache store to be updated
|
||||
*
|
||||
* @since 5.71
|
||||
*/
|
||||
void removeDeletedEntries();
|
||||
|
||||
/**
|
||||
* Get the entry which installed the passed file. If no entry lists the
|
||||
* passed file as having been installed by it, an invalid entry will be
|
||||
* returned.
|
||||
* @param installedFile The full path name for an installed file
|
||||
* @return An entry if one was found, or an invalid entry if no entry says it installed that file
|
||||
* since 5.74
|
||||
*/
|
||||
KNSCore::Entry entryFromInstalledFile(const QString &installedFile) const;
|
||||
|
||||
/**
|
||||
* Emitted when the cache has changed underneath us, and need users of the cache to know
|
||||
* that this has happened.
|
||||
* @param entry The entry which has changed
|
||||
* @since 5.75
|
||||
*/
|
||||
Q_SIGNAL void entryChanged(const KNSCore::Entry &entry);
|
||||
|
||||
public Q_SLOTS:
|
||||
void registerChangedEntry(const KNSCore::Entry &entry);
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(Cache)
|
||||
Cache(const QString &appName);
|
||||
|
||||
private:
|
||||
std::unique_ptr<CachePrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "cache2_p.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDomElement>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include <QXmlStreamReader>
|
||||
#include <knewstuffcore_debug.h>
|
||||
#include <qstandardpaths.h>
|
||||
|
||||
#include "searchrequest.h"
|
||||
#include "searchrequest_p.h"
|
||||
|
||||
class KNSCore::Cache2Private
|
||||
{
|
||||
public:
|
||||
Cache2Private(Cache2 *qq)
|
||||
: q(qq)
|
||||
{
|
||||
}
|
||||
~Cache2Private()
|
||||
{
|
||||
}
|
||||
|
||||
Cache2 *q;
|
||||
QHash<QString, Entry::List> requestCache;
|
||||
|
||||
QPointer<QTimer> throttleTimer;
|
||||
|
||||
// The file that is used to keep track of downloaded entries
|
||||
QString registryFile;
|
||||
|
||||
QSet<Entry> cache;
|
||||
|
||||
bool dirty = false;
|
||||
bool writingRegistry = false;
|
||||
bool reloadingRegistry = false;
|
||||
|
||||
void throttleWrite()
|
||||
{
|
||||
if (!throttleTimer) {
|
||||
throttleTimer = new QTimer(q);
|
||||
QObject::connect(throttleTimer, &QTimer::timeout, q, [this]() {
|
||||
q->writeRegistry();
|
||||
});
|
||||
throttleTimer->setSingleShot(true);
|
||||
throttleTimer->setInterval(1000);
|
||||
}
|
||||
throttleTimer->start();
|
||||
}
|
||||
};
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
typedef QHash<QString, QWeakPointer<Cache2>> CacheHash;
|
||||
Q_GLOBAL_STATIC(CacheHash, s_caches)
|
||||
Q_GLOBAL_STATIC(QFileSystemWatcher, s_watcher)
|
||||
|
||||
Cache2::Cache2(const QString &appName)
|
||||
: QObject(nullptr)
|
||||
, d(new Cache2Private(this))
|
||||
{
|
||||
const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/knewstuff3/");
|
||||
QDir().mkpath(path);
|
||||
d->registryFile = path + appName + QStringLiteral(".knsregistry");
|
||||
qCDebug(KNEWSTUFFCORE) << "Using registry file: " << d->registryFile;
|
||||
|
||||
s_watcher->addPath(d->registryFile);
|
||||
|
||||
std::function<void()> changeChecker = [this, &changeChecker]() {
|
||||
if (d->writingRegistry) {
|
||||
QTimer::singleShot(0, this, changeChecker);
|
||||
} else {
|
||||
d->reloadingRegistry = true;
|
||||
const QSet<KNSCore::Entry> oldCache = d->cache;
|
||||
d->cache.clear();
|
||||
readRegistry();
|
||||
// First run through the old cache and see if any have disappeared (at
|
||||
// which point we need to set them as available and emit that change)
|
||||
for (const Entry &entry : oldCache) {
|
||||
if (!d->cache.contains(entry) && entry.status() != KNSCore::Entry::Deleted) {
|
||||
Entry removedEntry(entry);
|
||||
removedEntry.setEntryDeleted();
|
||||
Q_EMIT entryChanged(removedEntry);
|
||||
}
|
||||
}
|
||||
// Then run through the new cache and see if there's any that were not
|
||||
// in the old cache (at which point just emit those as having changed,
|
||||
// they're already the correct status)
|
||||
for (const Entry &entry : std::as_const(d->cache)) {
|
||||
auto iterator = oldCache.constFind(entry);
|
||||
if (iterator == oldCache.constEnd()) {
|
||||
Q_EMIT entryChanged(entry);
|
||||
} else if ((*iterator).status() != entry.status()) {
|
||||
// If there are entries which are in both, but which have changed their
|
||||
// status, we should adopt the status from the newly loaded cache in place
|
||||
// of the one in the old cache. In reality, what this means is we just
|
||||
// need to emit the changed signal for anything in the new cache which
|
||||
// doesn't match the old one
|
||||
Q_EMIT entryChanged(entry);
|
||||
}
|
||||
}
|
||||
d->reloadingRegistry = false;
|
||||
}
|
||||
};
|
||||
connect(&*s_watcher, &QFileSystemWatcher::fileChanged, this, [this, changeChecker](const QString &file) {
|
||||
if (file == d->registryFile) {
|
||||
changeChecker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QSharedPointer<Cache2> Cache2::getCache(const QString &appName)
|
||||
{
|
||||
CacheHash::const_iterator it = s_caches()->constFind(appName);
|
||||
if ((it != s_caches()->constEnd()) && !(*it).isNull()) {
|
||||
return QSharedPointer<Cache2>(*it);
|
||||
}
|
||||
|
||||
QSharedPointer<Cache2> p(new Cache2(appName));
|
||||
s_caches()->insert(appName, QWeakPointer<Cache2>(p));
|
||||
QObject::connect(p.data(), &QObject::destroyed, [appName] {
|
||||
if (auto cache = s_caches()) {
|
||||
cache->remove(appName);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
Cache2::~Cache2()
|
||||
{
|
||||
s_watcher->removePath(d->registryFile);
|
||||
}
|
||||
|
||||
void Cache2::readRegistry()
|
||||
{
|
||||
QFile f(d->registryFile);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
if (QFileInfo::exists(d->registryFile)) {
|
||||
qWarning() << "The file " << d->registryFile << " could not be opened.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QXmlStreamReader reader(&f);
|
||||
if (reader.hasError() || !reader.readNextStartElement()) {
|
||||
qCWarning(KNEWSTUFFCORE) << "The file could not be parsed.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader.name() != QLatin1String("hotnewstuffregistry")) {
|
||||
qCWarning(KNEWSTUFFCORE) << "The file doesn't seem to be of interest.";
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto token = reader.readNext(); !reader.atEnd(); token = reader.readNext()) {
|
||||
if (token != QXmlStreamReader::StartElement) {
|
||||
continue;
|
||||
}
|
||||
Entry e;
|
||||
e.setEntryXML(reader);
|
||||
e.setSource(Entry::Cache);
|
||||
d->cache.insert(e);
|
||||
Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement);
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "Cache read... entries: " << d->cache.size();
|
||||
}
|
||||
|
||||
Entry::List Cache2::registryForProvider(const QString &providerId)
|
||||
{
|
||||
Entry::List entries;
|
||||
for (const Entry &e : std::as_const(d->cache)) {
|
||||
if (e.providerId() == providerId) {
|
||||
entries.append(e);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
Entry::List Cache2::registry() const
|
||||
{
|
||||
Entry::List entries;
|
||||
for (const Entry &e : std::as_const(d->cache)) {
|
||||
entries.append(e);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
void Cache2::writeRegistry()
|
||||
{
|
||||
if (!d->dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "Write registry";
|
||||
|
||||
d->writingRegistry = true;
|
||||
QFile f(d->registryFile);
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
qWarning() << "Cannot write meta information to" << d->registryFile;
|
||||
return;
|
||||
}
|
||||
|
||||
QDomDocument doc(QStringLiteral("khotnewstuff3"));
|
||||
doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
|
||||
QDomElement root = doc.createElement(QStringLiteral("hotnewstuffregistry"));
|
||||
doc.appendChild(root);
|
||||
|
||||
for (const Entry &entry : std::as_const(d->cache)) {
|
||||
// Write the entry, unless the policy is CacheNever and the entry is not installed.
|
||||
if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
|
||||
QDomElement exml = entry.entryXML();
|
||||
root.appendChild(exml);
|
||||
}
|
||||
}
|
||||
|
||||
QTextStream metastream(&f);
|
||||
metastream << doc.toByteArray();
|
||||
|
||||
d->dirty = false;
|
||||
d->writingRegistry = false;
|
||||
}
|
||||
|
||||
void Cache2::registerChangedEntry(const KNSCore::Entry &entry)
|
||||
{
|
||||
// If we have intermediate states, like updating or installing we do not want to write them
|
||||
if (entry.status() == KNSCore::Entry::Updating || entry.status() == KNSCore::Entry::Installing) {
|
||||
return;
|
||||
}
|
||||
if (!d->reloadingRegistry) {
|
||||
d->dirty = true;
|
||||
d->cache.remove(entry); // If value already exists in the set, the set is left unchanged
|
||||
d->cache.insert(entry);
|
||||
d->throttleWrite();
|
||||
}
|
||||
}
|
||||
|
||||
void Cache2::insertRequest(const KNSCore::SearchRequest &request, const KNSCore::Entry::List &entries)
|
||||
{
|
||||
// append new entries
|
||||
auto &cacheList = d->requestCache[request.d->hashForRequest()];
|
||||
for (const auto &entry : entries) {
|
||||
if (!cacheList.contains(entry)) {
|
||||
cacheList.append(entry);
|
||||
}
|
||||
}
|
||||
qCDebug(KNEWSTUFFCORE) << request.d->hashForRequest() << " add to cache: " << entries.size() << " keys: " << d->requestCache.keys();
|
||||
}
|
||||
|
||||
Entry::List Cache2::requestFromCache(const KNSCore::SearchRequest &request)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "from cache" << request.d->hashForRequest();
|
||||
return d->requestCache.value(request.d->hashForRequest());
|
||||
}
|
||||
|
||||
void KNSCore::Cache2::removeDeletedEntries()
|
||||
{
|
||||
QMutableSetIterator<KNSCore::Entry> i(d->cache);
|
||||
while (i.hasNext()) {
|
||||
const KNSCore::Entry &entry = i.next();
|
||||
bool installedFileExists{false};
|
||||
const QStringList installedFiles = entry.installedFiles();
|
||||
for (const auto &installedFile : installedFiles) {
|
||||
// Handle the /* notation, BUG: 425704
|
||||
if (installedFile.endsWith(QLatin1String("/*"))) {
|
||||
if (QDir(installedFile.left(installedFile.size() - 2)).exists()) {
|
||||
installedFileExists = true;
|
||||
break;
|
||||
}
|
||||
} else if (QFile::exists(installedFile)) {
|
||||
installedFileExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!installedFileExists) {
|
||||
i.remove();
|
||||
d->dirty = true;
|
||||
}
|
||||
}
|
||||
writeRegistry();
|
||||
}
|
||||
|
||||
KNSCore::Entry KNSCore::Cache2::entryFromInstalledFile(const QString &installedFile) const
|
||||
{
|
||||
for (const Entry &entry : std::as_const(d->cache)) {
|
||||
if (entry.installedFiles().contains(installedFile)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return Entry{};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
|
||||
#include "entry.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
#include <memory.h>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class Cache2Private;
|
||||
class SearchRequest;
|
||||
|
||||
// Exported for our internal QtQuick tech. Do not install this header or use it outside knewstuff!
|
||||
class KNEWSTUFFCORE_EXPORT Cache2 : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* Returns an instance of a shared cache for appName
|
||||
* That way it is made sure, that there do not exist different
|
||||
* instances of cache, with different contents
|
||||
* @param appName The file name of the registry - this is usually
|
||||
* the application name, it will be stored in "apps/knewstuff3/appname.knsregistry"
|
||||
*/
|
||||
static QSharedPointer<Cache2> getCache(const QString &appName);
|
||||
|
||||
~Cache2() override;
|
||||
Q_DISABLE_COPY(Cache2)
|
||||
|
||||
/// Read the installed entries (on startup)
|
||||
void readRegistry();
|
||||
/// All entries that have been installed by a certain provider
|
||||
Entry::List registryForProvider(const QString &providerId);
|
||||
|
||||
/// All entries known by the cache (this corresponds with entries which are installed, regardless of status)
|
||||
Entry::List registry() const;
|
||||
|
||||
/// Save the list of installed entries
|
||||
void writeRegistry();
|
||||
|
||||
void insertRequest(const KNSCore::SearchRequest &, const KNSCore::Entry::List &entries);
|
||||
Entry::List requestFromCache(const KNSCore::SearchRequest &);
|
||||
|
||||
/**
|
||||
* This will run through all entries in the cache, and remove all entries
|
||||
* where all the installed files they refer to no longer exist.
|
||||
*
|
||||
* This cannot be done wholesale for all caches, as some consumers will allow
|
||||
* this to happen (or indeed expect it to), and so we have to do this on a
|
||||
* per-type basis
|
||||
*
|
||||
* This will also cause the cache store to be updated
|
||||
*
|
||||
* @since 5.71
|
||||
*/
|
||||
void removeDeletedEntries();
|
||||
|
||||
/**
|
||||
* Get the entry which installed the passed file. If no entry lists the
|
||||
* passed file as having been installed by it, an invalid entry will be
|
||||
* returned.
|
||||
* @param installedFile The full path name for an installed file
|
||||
* @return An entry if one was found, or an invalid entry if no entry says it installed that file
|
||||
* since 5.74
|
||||
*/
|
||||
KNSCore::Entry entryFromInstalledFile(const QString &installedFile) const;
|
||||
|
||||
/**
|
||||
* Emitted when the cache has changed underneath us, and need users of the cache to know
|
||||
* that this has happened.
|
||||
* @param entry The entry which has changed
|
||||
* @since 5.75
|
||||
*/
|
||||
Q_SIGNAL void entryChanged(const KNSCore::Entry &entry);
|
||||
|
||||
public Q_SLOTS:
|
||||
void registerChangedEntry(const KNSCore::Entry &entry);
|
||||
|
||||
private:
|
||||
Cache2(const QString &appName);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Cache2Private> d;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 "categorymetadata.h"
|
||||
#include "categorymetadata_p.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
KNSCore::CategoryMetadata::CategoryMetadata(CategoryMetadataPrivate *dptr)
|
||||
: d(dptr)
|
||||
{
|
||||
}
|
||||
|
||||
QString KNSCore::CategoryMetadata::id() const
|
||||
{
|
||||
return d->id;
|
||||
}
|
||||
|
||||
QString KNSCore::CategoryMetadata::name() const
|
||||
{
|
||||
return d->name;
|
||||
}
|
||||
|
||||
QString KNSCore::CategoryMetadata::displayName() const
|
||||
{
|
||||
return d->displayName;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 <QString>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class CategoryMetadataPrivate;
|
||||
|
||||
/**
|
||||
* Describes a category
|
||||
* @since 6.9
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT CategoryMetadata
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] QString id() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString displayName() const;
|
||||
|
||||
private:
|
||||
friend class AtticaProvider;
|
||||
friend class ProviderBubbleWrap;
|
||||
CategoryMetadata(CategoryMetadataPrivate *dptr);
|
||||
std::shared_ptr<CategoryMetadataPrivate> d;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 "categorymetadata.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class CategoryMetadataPrivate
|
||||
{
|
||||
public:
|
||||
QString id;
|
||||
QString name;
|
||||
QString displayName;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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 "entry.h"
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class CommentsModelPrivate
|
||||
{
|
||||
public:
|
||||
CommentsModelPrivate(CommentsModel *qq)
|
||||
: q(qq)
|
||||
{
|
||||
}
|
||||
CommentsModel *const q;
|
||||
EngineBase *engine = nullptr;
|
||||
|
||||
Entry entry;
|
||||
|
||||
QList<std::shared_ptr<KNSCore::Comment>> comments;
|
||||
|
||||
enum FetchOptions {
|
||||
NoOption,
|
||||
ClearModel,
|
||||
};
|
||||
bool fetchThrottle = false;
|
||||
void fetch(FetchOptions option = NoOption)
|
||||
{
|
||||
if (fetchThrottle) {
|
||||
return;
|
||||
}
|
||||
fetchThrottle = true;
|
||||
QTimer::singleShot(1, q, [this]() {
|
||||
fetchThrottle = false;
|
||||
});
|
||||
// Sanity checks, because we need a few things to be correct before we can actually fetch comments...
|
||||
if (!engine) {
|
||||
qCWarning(KNEWSTUFFCORE) << "CommentsModel must be parented on a KNSCore::EngineBase instance to be able to fetch comments";
|
||||
}
|
||||
if (!entry.isValid()) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Without an entry to fetch comments for, CommentsModel cannot fetch comments for it";
|
||||
}
|
||||
|
||||
if (engine && entry.isValid()) {
|
||||
QSharedPointer<Provider> provider = engine->provider(entry.providerId());
|
||||
if (option == ClearModel) {
|
||||
q->beginResetModel();
|
||||
comments.clear();
|
||||
provider->disconnect(q);
|
||||
q->connect(provider.data(), &Provider::commentsLoaded, q, [this](const QList<std::shared_ptr<KNSCore::Comment>> &newComments) {
|
||||
QList<std::shared_ptr<KNSCore::Comment>> actualNewComments;
|
||||
for (const std::shared_ptr<KNSCore::Comment> &comment : newComments) {
|
||||
bool commentIsKnown = false;
|
||||
for (const std::shared_ptr<KNSCore::Comment> &existingComment : std::as_const(comments)) {
|
||||
if (existingComment->id == comment->id) {
|
||||
commentIsKnown = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (commentIsKnown) {
|
||||
continue;
|
||||
}
|
||||
actualNewComments << comment;
|
||||
}
|
||||
if (!actualNewComments.isEmpty()) {
|
||||
q->beginInsertRows(QModelIndex(), comments.count(), comments.count() + actualNewComments.count() - 1);
|
||||
qCDebug(KNEWSTUFFCORE) << "Appending" << actualNewComments.count() << "new comments";
|
||||
comments.append(actualNewComments);
|
||||
q->endInsertRows();
|
||||
}
|
||||
});
|
||||
q->endResetModel();
|
||||
}
|
||||
int commentsPerPage = 100;
|
||||
int pageToLoad = comments.count() / commentsPerPage;
|
||||
qCDebug(KNEWSTUFFCORE) << "Loading comments, page" << pageToLoad << "with current comment count" << comments.count() << "out of a total of"
|
||||
<< entry.numberOfComments();
|
||||
provider->loadComments(entry, commentsPerPage, pageToLoad);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
KNSCore::CommentsModel::CommentsModel(EngineBase *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, d(new CommentsModelPrivate(this))
|
||||
{
|
||||
d->engine = parent;
|
||||
}
|
||||
|
||||
KNSCore::CommentsModel::~CommentsModel() = default;
|
||||
|
||||
QHash<int, QByteArray> KNSCore::CommentsModel::roleNames() const
|
||||
{
|
||||
static const QHash<int, QByteArray> roles{
|
||||
{IdRole, "id"},
|
||||
{SubjectRole, "subject"},
|
||||
{TextRole, "text"},
|
||||
{ChildCountRole, "childCound"},
|
||||
{UsernameRole, "username"},
|
||||
{DateRole, "date"},
|
||||
{ScoreRole, "score"},
|
||||
{ParentIndexRole, "parentIndex"},
|
||||
{DepthRole, "depth"},
|
||||
};
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVariant KNSCore::CommentsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index)) {
|
||||
return QVariant();
|
||||
}
|
||||
const std::shared_ptr<KNSCore::Comment> comment = d->comments[index.row()];
|
||||
switch (role) {
|
||||
case IdRole:
|
||||
return comment->id;
|
||||
case SubjectRole:
|
||||
return comment->subject;
|
||||
case TextRole:
|
||||
return comment->text;
|
||||
case ChildCountRole:
|
||||
return comment->childCount;
|
||||
case UsernameRole:
|
||||
return comment->username;
|
||||
case DateRole:
|
||||
return comment->date;
|
||||
case ScoreRole:
|
||||
return comment->score;
|
||||
case ParentIndexRole:
|
||||
return comment->parent ? d->comments.indexOf(comment->parent) : -1;
|
||||
case DepthRole: {
|
||||
int depth{0};
|
||||
if (comment->parent) {
|
||||
std::shared_ptr<KNSCore::Comment> child = comment->parent;
|
||||
while (child) {
|
||||
++depth;
|
||||
child = child->parent;
|
||||
}
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
default:
|
||||
return i18nc("The value returned for an unknown role when requesting data from the model.", "Unknown CommentsModel role");
|
||||
}
|
||||
}
|
||||
|
||||
int KNSCore::CommentsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return d->comments.count();
|
||||
}
|
||||
|
||||
bool KNSCore::CommentsModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return false;
|
||||
}
|
||||
if (d->entry.numberOfComments() > d->comments.count()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void KNSCore::CommentsModel::fetchMore(const QModelIndex &parent)
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return;
|
||||
}
|
||||
d->fetch();
|
||||
}
|
||||
|
||||
const KNSCore::Entry &KNSCore::CommentsModel::entry() const
|
||||
{
|
||||
return d->entry;
|
||||
}
|
||||
|
||||
void KNSCore::CommentsModel::setEntry(const KNSCore::Entry &newEntry)
|
||||
{
|
||||
d->entry = newEntry;
|
||||
d->fetch(CommentsModelPrivate::ClearModel);
|
||||
Q_EMIT entryChanged();
|
||||
}
|
||||
|
||||
#include "moc_commentsmodel.cpp"
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
#ifndef KNSCORE_COMMENTSMODEL_H
|
||||
#define KNSCORE_COMMENTSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "enginebase.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class Entry;
|
||||
|
||||
struct Comment {
|
||||
QString id;
|
||||
QString subject;
|
||||
QString text;
|
||||
int childCount = 0;
|
||||
QString username;
|
||||
QDateTime date;
|
||||
int score = 0;
|
||||
std::shared_ptr<KNSCore::Comment> parent;
|
||||
};
|
||||
class CommentsModelPrivate;
|
||||
|
||||
/**
|
||||
* @brief A model which takes care of the comments for a single Entry
|
||||
*
|
||||
* This model should preferably be constructed by asking the Engine to give a model
|
||||
* instance to you for a specific entry using the commentsForEntry function. If you
|
||||
* insist, you can construct an instance yourself as well, but this is not recommended.
|
||||
*
|
||||
* @see Engine::commentsForEntry(KNSCore::Entry)
|
||||
* @since 5.63
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT CommentsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
/**
|
||||
* The Entry for which this model should handle comments
|
||||
*/
|
||||
Q_PROPERTY(KNSCore::Entry entry READ entry WRITE setEntry NOTIFY entryChanged)
|
||||
public:
|
||||
/**
|
||||
* Construct a new CommentsModel instance.
|
||||
* @note The class is intended to be constructed using the Engine::commentsForEntry function
|
||||
* @see Engine::commentsForEntry(KNSCore::Entry)
|
||||
*/
|
||||
explicit CommentsModel(EngineBase *parent = nullptr);
|
||||
~CommentsModel() override;
|
||||
|
||||
enum Roles {
|
||||
SubjectRole = Qt::DisplayRole,
|
||||
IdRole = Qt::UserRole + 1,
|
||||
TextRole,
|
||||
ChildCountRole,
|
||||
UsernameRole,
|
||||
DateRole,
|
||||
ScoreRole,
|
||||
ParentIndexRole,
|
||||
DepthRole,
|
||||
};
|
||||
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;
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
const KNSCore::Entry &entry() const;
|
||||
void setEntry(const KNSCore::Entry &newEntry);
|
||||
Q_SIGNAL void entryChanged();
|
||||
|
||||
private:
|
||||
friend class CommentsModelPrivate; // For beginResetModel and beginInsertRows method calls
|
||||
const std::unique_ptr<CommentsModelPrivate> d;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // KNSCORE_COMMENTSMODEL_H
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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 "categorymetadata.h"
|
||||
#include "provider.h"
|
||||
#include "searchpreset.h"
|
||||
#include "searchrequest.h"
|
||||
|
||||
namespace KNSCompat
|
||||
{
|
||||
|
||||
inline KNSCore::Provider::SearchRequest searchRequestToLegacy(const KNSCore::SearchRequest &request)
|
||||
{
|
||||
return {
|
||||
[mode = request.sortMode()] {
|
||||
switch (mode) {
|
||||
case KNSCore::SortMode::Alphabetical:
|
||||
return KNSCore::Provider::Alphabetical;
|
||||
case KNSCore::SortMode::Downloads:
|
||||
return KNSCore::Provider::Downloads;
|
||||
case KNSCore::SortMode::Newest:
|
||||
return KNSCore::Provider::Newest;
|
||||
case KNSCore::SortMode::Rating:
|
||||
return KNSCore::Provider::Rating;
|
||||
}
|
||||
return KNSCore::Provider::Rating;
|
||||
}(),
|
||||
[filter = request.filter()] {
|
||||
switch (filter) {
|
||||
case KNSCore::Filter::ExactEntryId:
|
||||
return KNSCore::Provider::ExactEntryId;
|
||||
case KNSCore::Filter::Installed:
|
||||
return KNSCore::Provider::Installed;
|
||||
case KNSCore::Filter::Updates:
|
||||
return KNSCore::Provider::Updates;
|
||||
case KNSCore::Filter::None:
|
||||
return KNSCore::Provider::None;
|
||||
}
|
||||
return KNSCore::Provider::None;
|
||||
}(),
|
||||
request.searchTerm(),
|
||||
request.categories(),
|
||||
request.page(),
|
||||
request.pageSize(),
|
||||
// Note that this loses the id but there's nothing we can do about it. It's why we deprecated it.
|
||||
};
|
||||
}
|
||||
|
||||
inline KNSCore::SearchRequest searchRequestFromLegacy(const KNSCore::Provider::SearchRequest &request)
|
||||
{
|
||||
return {[request] {
|
||||
switch (request.sortMode) {
|
||||
case KNSCore::Provider::SortMode::Alphabetical:
|
||||
return KNSCore::SortMode::Alphabetical;
|
||||
case KNSCore::Provider::SortMode::Downloads:
|
||||
return KNSCore::SortMode::Downloads;
|
||||
case KNSCore::Provider::SortMode::Newest:
|
||||
return KNSCore::SortMode::Newest;
|
||||
case KNSCore::Provider::SortMode::Rating:
|
||||
return KNSCore::SortMode::Rating;
|
||||
}
|
||||
Q_ASSERT(false);
|
||||
return KNSCore::SortMode::Rating;
|
||||
}(),
|
||||
[request] {
|
||||
switch (request.filter) {
|
||||
case KNSCore::Provider::Filter::None:
|
||||
return KNSCore::Filter::None;
|
||||
case KNSCore::Provider::Filter::Installed:
|
||||
return KNSCore::Filter::Installed;
|
||||
case KNSCore::Provider::Filter::Updates:
|
||||
return KNSCore::Filter::Updates;
|
||||
case KNSCore::Provider::Filter::ExactEntryId:
|
||||
return KNSCore::Filter::ExactEntryId;
|
||||
}
|
||||
Q_ASSERT(false);
|
||||
return KNSCore::Filter::None;
|
||||
}(),
|
||||
request.searchTerm,
|
||||
request.categories,
|
||||
request.page,
|
||||
request.pageSize};
|
||||
}
|
||||
|
||||
inline KNSCore::Provider::SearchPreset searchPresetToLegacy(const KNSCore::SearchPreset &preset)
|
||||
{
|
||||
return {
|
||||
.request = searchRequestToLegacy(preset.request()),
|
||||
.displayName = preset.displayName(),
|
||||
.iconName = preset.iconName(),
|
||||
.type =
|
||||
[type = preset.type()] {
|
||||
switch (type) {
|
||||
case KNSCore::SearchPreset::Type::GoBack:
|
||||
return KNSCore::Provider::SearchPresetTypes::GoBack;
|
||||
case KNSCore::SearchPreset::Type::Popular:
|
||||
return KNSCore::Provider::SearchPresetTypes::Popular;
|
||||
case KNSCore::SearchPreset::Type::Featured:
|
||||
return KNSCore::Provider::SearchPresetTypes::Featured;
|
||||
case KNSCore::SearchPreset::Type::Start:
|
||||
return KNSCore::Provider::SearchPresetTypes::Start;
|
||||
case KNSCore::SearchPreset::Type::New:
|
||||
return KNSCore::Provider::SearchPresetTypes::New;
|
||||
case KNSCore::SearchPreset::Type::Root:
|
||||
return KNSCore::Provider::SearchPresetTypes::Root;
|
||||
case KNSCore::SearchPreset::Type::Shelf:
|
||||
return KNSCore::Provider::SearchPresetTypes::Shelf;
|
||||
case KNSCore::SearchPreset::Type::FolderUp:
|
||||
return KNSCore::Provider::SearchPresetTypes::FolderUp;
|
||||
case KNSCore::SearchPreset::Type::Recommended:
|
||||
return KNSCore::Provider::SearchPresetTypes::Recommended;
|
||||
case KNSCore::SearchPreset::Type::Subscription:
|
||||
return KNSCore::Provider::SearchPresetTypes::Subscription;
|
||||
case KNSCore::SearchPreset::Type::AllEntries:
|
||||
return KNSCore::Provider::SearchPresetTypes::AllEntries;
|
||||
case KNSCore::SearchPreset::Type::NoPresetType:
|
||||
return KNSCore::Provider::SearchPresetTypes::NoPresetType;
|
||||
}
|
||||
return KNSCore::Provider::SearchPresetTypes::NoPresetType;
|
||||
}(),
|
||||
.providerId = preset.providerId(),
|
||||
};
|
||||
}
|
||||
|
||||
inline KNSCore::Provider::CategoryMetadata categoryMetadataToLegacy(const KNSCore::CategoryMetadata &metadata)
|
||||
{
|
||||
return {
|
||||
.id = metadata.id(),
|
||||
.name = metadata.name(),
|
||||
.displayName = metadata.displayName(),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace KNSCompat
|
||||
@@ -0,0 +1,562 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2007-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "enginebase.h"
|
||||
#include "enginebase_p.h"
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include <KConfig>
|
||||
#include <KConfigGroup>
|
||||
#include <KFileUtils>
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkRequest>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QThreadStorage>
|
||||
#include <QTimer>
|
||||
|
||||
#include "attica/atticaprovider_p.h"
|
||||
#include "categorymetadata.h"
|
||||
#include "compat_p.h"
|
||||
#include "opds/opdsprovider_p.h"
|
||||
#include "providerbubblewrap_p.h"
|
||||
#include "providercore.h"
|
||||
#include "providercore_p.h"
|
||||
#include "resultsstream.h"
|
||||
#include "searchrequest_p.h"
|
||||
#include "staticxml/staticxmlprovider_p.h"
|
||||
#include "transaction.h"
|
||||
#include "xmlloader_p.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
typedef QHash<QUrl, QPointer<XmlLoader>> EngineProviderLoaderHash;
|
||||
Q_GLOBAL_STATIC(QThreadStorage<EngineProviderLoaderHash>, s_engineProviderLoaders)
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
EngineBasePrivate::EngineBasePrivate(EngineBase *qptr)
|
||||
: q(qptr)
|
||||
{
|
||||
}
|
||||
|
||||
void EngineBasePrivate::addProvider(const QSharedPointer<KNSCore::ProviderCore> &provider)
|
||||
{
|
||||
{ // ProviderCore
|
||||
qCDebug(KNEWSTUFFCORE) << "Engine addProvider called with provider with id " << provider->d->base->id();
|
||||
providerCores.insert(provider->d->base->id(), provider);
|
||||
provider->d->base->setTagFilter(tagFilter);
|
||||
provider->d->base->setDownloadTagFilter(downloadTagFilter);
|
||||
QObject::connect(provider->d->base, &ProviderBase::providerInitialized, q, [this, providerBase = provider->d->base] {
|
||||
qCDebug(KNEWSTUFFCORE) << "providerInitialized" << providerBase->name();
|
||||
providerBase->setCachedEntries(cache->registryForProvider(providerBase->id()));
|
||||
|
||||
for (const auto &core : std::as_const(providerCores)) {
|
||||
if (!core->d->base->isInitialized()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Q_EMIT q->signalProvidersLoaded();
|
||||
});
|
||||
|
||||
QObject::connect(provider->d->base, &ProviderBase::signalError, q, [this, provider](const QString &msg) {
|
||||
Q_EMIT q->signalErrorCode(ErrorCode::ProviderError, msg, providerFileUrl);
|
||||
});
|
||||
QObject::connect(provider->d->base, &ProviderBase::signalErrorCode, q, &EngineBase::signalErrorCode);
|
||||
QObject::connect(provider->d->base, &ProviderBase::signalInformation, q, &EngineBase::signalMessage);
|
||||
QObject::connect(provider->d->base, &ProviderBase::basicsLoaded, q, &EngineBase::providersChanged);
|
||||
Q_EMIT q->providerAdded(provider.get());
|
||||
}
|
||||
|
||||
{ // ProviderBubbleWrap for legacy compatibility
|
||||
QSharedPointer<ProviderBubbleWrap> wrappedProvider(new ProviderBubbleWrap(provider));
|
||||
legacyProviders.insert(wrappedProvider->id(), wrappedProvider);
|
||||
wrappedProvider->setTagFilter(tagFilter);
|
||||
wrappedProvider->setDownloadTagFilter(downloadTagFilter);
|
||||
q->addProvider(wrappedProvider);
|
||||
}
|
||||
|
||||
Q_EMIT q->providersChanged();
|
||||
}
|
||||
|
||||
EngineBase::EngineBase(QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new EngineBasePrivate(this))
|
||||
{
|
||||
connect(d->installation, &Installation::signalInstallationError, this, [this](const QString &message) {
|
||||
Q_EMIT signalErrorCode(ErrorCode::InstallationError, i18n("An error occurred during the installation process:\n%1", message), QVariant());
|
||||
});
|
||||
}
|
||||
|
||||
QStringList EngineBase::availableConfigFiles()
|
||||
{
|
||||
QStringList configSearchLocations;
|
||||
configSearchLocations << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, //
|
||||
QStringLiteral("knsrcfiles"),
|
||||
QStandardPaths::LocateDirectory);
|
||||
configSearchLocations << QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
|
||||
return KFileUtils::findAllUniqueFiles(configSearchLocations, {QStringLiteral("*.knsrc")});
|
||||
}
|
||||
|
||||
EngineBase::~EngineBase()
|
||||
{
|
||||
if (d->cache) {
|
||||
d->cache->writeRegistry();
|
||||
}
|
||||
delete d->atticaProviderManager;
|
||||
delete d->installation;
|
||||
}
|
||||
|
||||
bool EngineBase::init(const QString &configfile)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::EngineBase from" << configfile;
|
||||
|
||||
QString resolvedConfigFilePath;
|
||||
if (QFileInfo(configfile).isAbsolute()) {
|
||||
resolvedConfigFilePath = configfile; // It is an absolute path
|
||||
} else {
|
||||
// Don't do the expensive search unless the config is relative
|
||||
resolvedConfigFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knsrcfiles/%1").arg(configfile));
|
||||
}
|
||||
|
||||
if (!QFileInfo::exists(resolvedConfigFilePath)) {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file does not exist: \"%1\"", configfile), configfile);
|
||||
qCCritical(KNEWSTUFFCORE) << "The knsrc file" << configfile << "does not exist";
|
||||
return false;
|
||||
}
|
||||
|
||||
const KConfig conf(resolvedConfigFilePath);
|
||||
|
||||
if (conf.accessMode() == KConfig::NoAccess) {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile), configfile);
|
||||
qCCritical(KNEWSTUFFCORE) << "The knsrc file" << configfile << "was found but could not be opened.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const KConfigGroup group = conf.hasGroup(QStringLiteral("KNewStuff")) ? conf.group(QStringLiteral("KNewStuff")) : conf.group(QStringLiteral("KNewStuff3"));
|
||||
if (!group.exists()) {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file is invalid: \"%1\"", configfile), configfile);
|
||||
qCCritical(KNEWSTUFFCORE) << configfile << "doesn't contain a KNewStuff or KNewStuff3 section.";
|
||||
return false;
|
||||
}
|
||||
|
||||
d->name = group.readEntry("Name");
|
||||
d->categories = group.readEntry("Categories", QStringList());
|
||||
qCDebug(KNEWSTUFFCORE) << "Categories: " << d->categories;
|
||||
d->adoptionCommand = group.readEntry("AdoptionCommand");
|
||||
d->useLabel = group.readEntry("UseLabel", i18n("Use"));
|
||||
Q_EMIT useLabelChanged();
|
||||
d->uploadEnabled = group.readEntry("UploadEnabled", true);
|
||||
Q_EMIT uploadEnabledChanged();
|
||||
|
||||
d->providerFileUrl = group.readEntry("ProvidersUrl", QUrl(QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml")));
|
||||
if (group.readEntry("UseLocalProvidersFile", false)) {
|
||||
// The local providers file is called "appname.providers", to match "appname.knsrc"
|
||||
d->providerFileUrl = QUrl::fromLocalFile(QLatin1String("%1.providers").arg(configfile.left(configfile.length() - 6)));
|
||||
}
|
||||
|
||||
d->tagFilter = group.readEntry("TagFilter", QStringList(QStringLiteral("ghns_excluded!=1")));
|
||||
d->downloadTagFilter = group.readEntry("DownloadTagFilter", QStringList());
|
||||
|
||||
QByteArray rawContentWarningType = group.readEntry("ContentWarning", QByteArrayLiteral("Static"));
|
||||
bool ok = false;
|
||||
int value = QMetaEnum::fromType<ContentWarningType>().keyToValue(rawContentWarningType.constData(), &ok);
|
||||
if (ok) {
|
||||
d->contentWarningType = static_cast<ContentWarningType>(value);
|
||||
} else {
|
||||
qCWarning(KNEWSTUFFCORE) << "Could not parse ContentWarning, invalid entry" << rawContentWarningType;
|
||||
}
|
||||
|
||||
Q_EMIT contentWarningTypeChanged();
|
||||
|
||||
// Make sure that config is valid
|
||||
QString error;
|
||||
if (!d->installation->readConfig(group, error)) {
|
||||
Q_EMIT signalErrorCode(ErrorCode::ConfigFileError,
|
||||
i18n("Could not initialise the installation handler for %1:\n%2\n"
|
||||
"This is a critical error and should be reported to the application author",
|
||||
configfile,
|
||||
error),
|
||||
configfile);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString configFileBasename = QFileInfo(resolvedConfigFilePath).completeBaseName();
|
||||
|
||||
d->cache = Cache2::getCache(configFileBasename);
|
||||
qCDebug(KNEWSTUFFCORE) << "Cache is" << d->cache << "for" << configFileBasename;
|
||||
d->cache->readRegistry();
|
||||
// This is a facade for cache2, no need to call anything on it.
|
||||
d->legacyCache = Cache::getCache(configFileBasename);
|
||||
|
||||
// Cache cleanup option, to help work around people deleting files from underneath KNewStuff (this
|
||||
// happens a lot with e.g. wallpapers and icons)
|
||||
if (d->installation->uncompressionSetting() == Installation::UseKPackageUncompression) {
|
||||
d->shouldRemoveDeletedEntries = true;
|
||||
}
|
||||
|
||||
d->shouldRemoveDeletedEntries = group.readEntry("RemoveDeadEntries", d->shouldRemoveDeletedEntries);
|
||||
if (d->shouldRemoveDeletedEntries) {
|
||||
d->cache->removeDeletedEntries();
|
||||
}
|
||||
|
||||
loadProviders();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EngineBase::loadProviders()
|
||||
{
|
||||
if (d->providerFileUrl.isEmpty()) {
|
||||
// it would be nicer to move the attica stuff into its own class
|
||||
qCDebug(KNEWSTUFFCORE) << "Using OCS default providers";
|
||||
delete d->atticaProviderManager;
|
||||
d->atticaProviderManager = new Attica::ProviderManager;
|
||||
connect(d->atticaProviderManager, &Attica::ProviderManager::providerAdded, this, &EngineBase::atticaProviderLoaded);
|
||||
connect(d->atticaProviderManager, &Attica::ProviderManager::failedToLoad, this, &EngineBase::slotProvidersFailed);
|
||||
d->atticaProviderManager->loadDefaultProviders();
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "loading providers from " << d->providerFileUrl;
|
||||
Q_EMIT loadingProvider();
|
||||
|
||||
XmlLoader *loader = s_engineProviderLoaders()->localData().value(d->providerFileUrl);
|
||||
if (!loader) {
|
||||
qCDebug(KNEWSTUFFCORE) << "No xml loader for this url yet, so create one and temporarily store that" << d->providerFileUrl;
|
||||
loader = new XmlLoader(this);
|
||||
s_engineProviderLoaders()->localData().insert(d->providerFileUrl, loader);
|
||||
connect(loader, &XmlLoader::signalLoaded, this, [this]() {
|
||||
s_engineProviderLoaders()->localData().remove(d->providerFileUrl);
|
||||
});
|
||||
connect(loader, &XmlLoader::signalFailed, this, [this]() {
|
||||
s_engineProviderLoaders()->localData().remove(d->providerFileUrl);
|
||||
});
|
||||
connect(loader, &XmlLoader::signalHttpError, this, [this](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) {
|
||||
if (status == 503) { // Temporarily Unavailable
|
||||
QDateTime retryAfter;
|
||||
static const QByteArray retryAfterKey{"Retry-After"};
|
||||
for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) {
|
||||
if (headerPair.first == retryAfterKey) {
|
||||
// Retry-After is not a known header, so we need to do a bit of running around to make that work
|
||||
// Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
|
||||
// So, simple workaround, just pass it through a dummy request and get a formatted date out (the
|
||||
// cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
|
||||
QNetworkRequest dummyRequest;
|
||||
dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
|
||||
retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
QTimer::singleShot(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), this, &EngineBase::loadProviders);
|
||||
// if it's a matter of a human moment's worth of seconds, just reload
|
||||
if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) {
|
||||
// more than that, spit out TryAgainLaterError to let the user know what we're doing with their time
|
||||
static const KFormat formatter;
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::TryAgainLaterError,
|
||||
i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
|
||||
formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
|
||||
{retryAfter});
|
||||
}
|
||||
}
|
||||
});
|
||||
loader->load(d->providerFileUrl);
|
||||
}
|
||||
connect(loader, &XmlLoader::signalLoaded, this, &EngineBase::slotProviderFileLoaded);
|
||||
connect(loader, &XmlLoader::signalFailed, this, &EngineBase::slotProvidersFailed);
|
||||
}
|
||||
}
|
||||
|
||||
QString KNSCore::EngineBase::name() const
|
||||
{
|
||||
return d->name;
|
||||
}
|
||||
|
||||
QStringList EngineBase::categories() const
|
||||
{
|
||||
return d->categories;
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
QList<Provider::CategoryMetadata> EngineBase::categoriesMetadata()
|
||||
{
|
||||
QList<Provider::CategoryMetadata> list;
|
||||
for (const auto &data : d->categoriesMetadata) {
|
||||
list.append(Provider::CategoryMetadata{.id = data.id(), .name = data.name(), .displayName = data.displayName()});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
#endif
|
||||
|
||||
QList<CategoryMetadata> EngineBase::categoriesMetadata2()
|
||||
{
|
||||
return d->categoriesMetadata;
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
QList<Provider::SearchPreset> EngineBase::searchPresets()
|
||||
{
|
||||
QList<Provider::SearchPreset> list;
|
||||
for (const auto &preset : d->searchPresets) {
|
||||
// This is slightly mad backwards compat. We back-convert a SearchPreset which requires a convert of
|
||||
// SearchRequest and all the involved enums.
|
||||
// Since this is the only place we need it this has been implemented thusly.
|
||||
// Should someone find it offensive feel free to tear it apart into functions, but understand they are only
|
||||
// used here.
|
||||
list.append(KNSCompat::searchPresetToLegacy(preset));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
#endif
|
||||
|
||||
QList<SearchPreset> EngineBase::searchPresets2()
|
||||
{
|
||||
return d->searchPresets;
|
||||
}
|
||||
|
||||
QString EngineBase::useLabel() const
|
||||
{
|
||||
return d->useLabel;
|
||||
}
|
||||
|
||||
bool EngineBase::uploadEnabled() const
|
||||
{
|
||||
return d->uploadEnabled;
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
void EngineBase::addProvider(QSharedPointer<KNSCore::Provider> /*provider*/)
|
||||
{
|
||||
// Connections are established in the modern variant of this function. No need to do anything.
|
||||
}
|
||||
#endif
|
||||
|
||||
void EngineBase::providerInitialized([[maybe_unused]] Provider *p)
|
||||
{
|
||||
// Unused. Replaced by lambda. Here for ABI stability.
|
||||
}
|
||||
|
||||
void EngineBase::slotProvidersFailed()
|
||||
{
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ProviderError,
|
||||
i18n("Loading of providers from file: %1 failed", d->providerFileUrl.toString()),
|
||||
d->providerFileUrl);
|
||||
}
|
||||
|
||||
void EngineBase::slotProviderFileLoaded(const QDomDocument &doc)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "slotProvidersLoaded";
|
||||
|
||||
bool isAtticaProviderFile = false;
|
||||
|
||||
// get each provider element, and create a provider object from it
|
||||
QDomElement providers = doc.documentElement();
|
||||
|
||||
if (providers.tagName() == QLatin1String("providers")) {
|
||||
isAtticaProviderFile = true;
|
||||
} else if (providers.tagName() != QLatin1String("ghnsproviders") && providers.tagName() != QLatin1String("knewstuffproviders")) {
|
||||
qWarning() << "No document in providers.xml.";
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ProviderError,
|
||||
i18n("Could not load get hot new stuff providers from file: %1", d->providerFileUrl.toString()),
|
||||
d->providerFileUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
QDomElement n = providers.firstChildElement(QStringLiteral("provider"));
|
||||
while (!n.isNull()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Provider attributes: " << n.attribute(QStringLiteral("type"));
|
||||
|
||||
QSharedPointer<KNSCore::ProviderCore> provider;
|
||||
if (isAtticaProviderFile || n.attribute(QStringLiteral("type")).toLower() == QLatin1String("rest")) {
|
||||
provider.reset(new ProviderCore(new AtticaProvider(d->categories, {})));
|
||||
connect(provider->d->base, &ProviderBase::categoriesMetadataLoaded, this, [this](const QList<CategoryMetadata> &categories) {
|
||||
d->categoriesMetadata = categories;
|
||||
Q_EMIT signalCategoriesMetadataLoaded(categories);
|
||||
});
|
||||
#ifdef SYNDICATION_FOUND
|
||||
} else if (n.attribute(QStringLiteral("type")).toLower() == QLatin1String("opds")) {
|
||||
provider.reset(new ProviderCore(new OPDSProvider));
|
||||
connect(provider->d->base, &ProviderBase::searchPresetsLoaded, this, [this](const QList<SearchPreset> &presets) {
|
||||
d->searchPresets = presets;
|
||||
Q_EMIT signalSearchPresetsLoaded(presets);
|
||||
});
|
||||
#endif
|
||||
} else {
|
||||
provider.reset(new ProviderCore(new StaticXmlProvider));
|
||||
}
|
||||
|
||||
if (provider->d->base->setProviderXML(n)) {
|
||||
d->addProvider(provider);
|
||||
} else {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::ProviderError, i18n("Error initializing provider."), d->providerFileUrl);
|
||||
}
|
||||
n = n.nextSiblingElement();
|
||||
}
|
||||
Q_EMIT loadingProvider();
|
||||
}
|
||||
|
||||
void EngineBase::atticaProviderLoaded(const Attica::Provider &atticaProvider)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "atticaProviderLoaded called";
|
||||
if (!atticaProvider.hasContentService()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Found provider: " << atticaProvider.baseUrl() << " but it does not support content";
|
||||
return;
|
||||
}
|
||||
auto provider = QSharedPointer<KNSCore::ProviderCore>(new KNSCore::ProviderCore(new AtticaProvider(atticaProvider, d->categories, {})));
|
||||
d->addProvider(provider);
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
QSharedPointer<Cache> EngineBase::cache() const
|
||||
{
|
||||
return d->legacyCache;
|
||||
}
|
||||
#endif
|
||||
|
||||
void EngineBase::setTagFilter(const QStringList &filter)
|
||||
{
|
||||
d->tagFilter = filter;
|
||||
for (const auto &core : std::as_const(d->providerCores)) {
|
||||
core->d->base->setTagFilter(d->tagFilter);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList EngineBase::tagFilter() const
|
||||
{
|
||||
return d->tagFilter;
|
||||
}
|
||||
|
||||
void KNSCore::EngineBase::addTagFilter(const QString &filter)
|
||||
{
|
||||
d->tagFilter << filter;
|
||||
for (const auto &core : std::as_const(d->providerCores)) {
|
||||
core->d->base->setTagFilter(d->tagFilter);
|
||||
}
|
||||
}
|
||||
|
||||
void EngineBase::setDownloadTagFilter(const QStringList &filter)
|
||||
{
|
||||
d->downloadTagFilter = filter;
|
||||
for (const auto &core : std::as_const(d->providerCores)) {
|
||||
core->d->base->setDownloadTagFilter(d->downloadTagFilter);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList EngineBase::downloadTagFilter() const
|
||||
{
|
||||
return d->downloadTagFilter;
|
||||
}
|
||||
|
||||
void EngineBase::addDownloadTagFilter(const QString &filter)
|
||||
{
|
||||
d->downloadTagFilter << filter;
|
||||
for (const auto &core : std::as_const(d->providerCores)) {
|
||||
core->d->base->setDownloadTagFilter(d->downloadTagFilter);
|
||||
}
|
||||
}
|
||||
|
||||
QList<Attica::Provider *> EngineBase::atticaProviders() const
|
||||
{
|
||||
// This function is absolutely horrific. Unfortunately used in discover.
|
||||
QList<Attica::Provider *> ret;
|
||||
ret.reserve(d->providerCores.size());
|
||||
for (const auto &core : d->providerCores) {
|
||||
if (const auto &provider = qobject_cast<AtticaProvider *>(core->d->base)) {
|
||||
ret.append(provider->provider());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool EngineBase::userCanVote(const Entry &entry)
|
||||
{
|
||||
const auto &core = d->providerCores.value(entry.providerId());
|
||||
return core->d->base->userCanVote();
|
||||
}
|
||||
|
||||
void EngineBase::vote(const Entry &entry, uint rating)
|
||||
{
|
||||
const auto &core = d->providerCores.value(entry.providerId());
|
||||
core->d->base->vote(entry, rating);
|
||||
}
|
||||
|
||||
bool EngineBase::userCanBecomeFan(const Entry &entry)
|
||||
{
|
||||
const auto &core = d->providerCores.value(entry.providerId());
|
||||
return core->d->base->userCanBecomeFan();
|
||||
}
|
||||
|
||||
void EngineBase::becomeFan(const Entry &entry)
|
||||
{
|
||||
const auto &core = d->providerCores.value(entry.providerId());
|
||||
core->d->base->becomeFan(entry);
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
QSharedPointer<Provider> EngineBase::provider(const QString &providerId) const
|
||||
{
|
||||
return d->legacyProviders.value(providerId);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
QSharedPointer<Provider> EngineBase::defaultProvider() const
|
||||
{
|
||||
if (!d->legacyProviders.isEmpty()) {
|
||||
return d->legacyProviders.constBegin().value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
QStringList EngineBase::providerIDs() const
|
||||
{
|
||||
return d->legacyProviders.keys();
|
||||
}
|
||||
|
||||
bool EngineBase::hasAdoptionCommand() const
|
||||
{
|
||||
return !d->adoptionCommand.isEmpty();
|
||||
}
|
||||
|
||||
void EngineBase::updateStatus()
|
||||
{
|
||||
}
|
||||
|
||||
Installation *EngineBase::installation() const
|
||||
{
|
||||
return d->installation;
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
ResultsStream *EngineBase::search(const Provider::SearchRequest &request)
|
||||
{
|
||||
return new ResultsStream(searchRequestFromLegacy(request), this);
|
||||
}
|
||||
#endif
|
||||
|
||||
EngineBase::ContentWarningType EngineBase::contentWarningType() const
|
||||
{
|
||||
return d->contentWarningType;
|
||||
}
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
QList<QSharedPointer<Provider>> EngineBase::providers() const
|
||||
{
|
||||
return d->legacyProviders.values();
|
||||
}
|
||||
#endif
|
||||
|
||||
KNSCore::ResultsStream *KNSCore::EngineBase::search(const KNSCore::SearchRequest &request)
|
||||
{
|
||||
return new ResultsStream(request, this);
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2007-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_ENGINEBASE_H
|
||||
#define KNEWSTUFF3_ENGINEBASE_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QMetaEnum>
|
||||
#include <QObject>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
|
||||
#include "categorymetadata.h"
|
||||
#include "entry.h"
|
||||
#include "errorcode.h"
|
||||
#include "knewstuffcore_export.h"
|
||||
#include "provider.h"
|
||||
#include "searchpreset.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class KJob;
|
||||
class EnginePrivate;
|
||||
class QDomDocument;
|
||||
class SearchPresetModel;
|
||||
|
||||
namespace Attica
|
||||
{
|
||||
class Provider;
|
||||
}
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class Cache;
|
||||
class CommentsModel;
|
||||
class ResultsStream;
|
||||
class EngineBasePrivate;
|
||||
class Installation;
|
||||
class SearchRequest;
|
||||
class ProviderCore;
|
||||
|
||||
/**
|
||||
* KNewStuff engine.
|
||||
* An engine keeps track of data which is available locally and remote
|
||||
* and offers high-level synchronization calls as well as upload and download
|
||||
* primitives using an underlying GHNS protocol.
|
||||
*
|
||||
* This is a base class for different engine implementations
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT EngineBase : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* Text that should be displayed for the adoption button, this defaults to "Use"
|
||||
* @since 5.77
|
||||
*/
|
||||
Q_PROPERTY(QString useLabel READ useLabel NOTIFY useLabelChanged)
|
||||
|
||||
/**
|
||||
* Whether or not the configuration says that the providers are expected to support uploading.
|
||||
* As it stands, this is used to determine whether or not to show the Upload... action where
|
||||
* that is displayed (primarily NewStuff.Page).
|
||||
* @since 5.85
|
||||
*/
|
||||
Q_PROPERTY(bool uploadEnabled READ uploadEnabled NOTIFY uploadEnabledChanged)
|
||||
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
Q_PROPERTY(QStringList providerIDs READ providerIDs NOTIFY providersChanged)
|
||||
|
||||
/**
|
||||
* @copydoc contentWarningType
|
||||
*/
|
||||
Q_PROPERTY(ContentWarningType contentWarningType READ contentWarningType NOTIFY contentWarningTypeChanged)
|
||||
|
||||
public:
|
||||
EngineBase(QObject *parent = nullptr);
|
||||
~EngineBase() override;
|
||||
Q_DISABLE_COPY_MOVE(EngineBase)
|
||||
|
||||
/**
|
||||
* List of all available config files. This list will contain no duplicated filenames.
|
||||
* The returned file paths are absolute.
|
||||
* @since 5.83
|
||||
*/
|
||||
static QStringList availableConfigFiles();
|
||||
|
||||
/**
|
||||
* Initializes the engine. This step is application-specific and relies
|
||||
* on an external configuration file, which determines all the details
|
||||
* about the initialization.
|
||||
*
|
||||
* @param configfile KNewStuff2 configuration file (*.knsrc)
|
||||
* @return \b true if any valid configuration was found, \b false otherwise
|
||||
* @see KNS3::DownloadDialog
|
||||
*/
|
||||
virtual bool init(const QString &configfile);
|
||||
|
||||
/**
|
||||
* The name as defined by the knsrc file
|
||||
* @return The name associated with the engine's configuration file
|
||||
* @since 5.63
|
||||
*/
|
||||
QString name() const;
|
||||
|
||||
/**
|
||||
* Text that should be displayed for the adoption button, this defaults to i18n("Use")
|
||||
* @since 5.77
|
||||
*/
|
||||
QString useLabel() const;
|
||||
|
||||
/**
|
||||
* Signal gets emitted when the useLabel property changes
|
||||
* @since 5.77
|
||||
*/
|
||||
Q_SIGNAL void useLabelChanged();
|
||||
|
||||
/**
|
||||
* Whether or not the configuration says that the providers are expected to support uploading.
|
||||
* @return True if the providers are expected to support uploading
|
||||
* @since 5.85
|
||||
*/
|
||||
bool uploadEnabled() const;
|
||||
|
||||
/**
|
||||
* Fired when the uploadEnabled property changes
|
||||
* @since 5.85
|
||||
*/
|
||||
Q_SIGNAL void uploadEnabledChanged();
|
||||
|
||||
/**
|
||||
* The list of the server-side names of the categories handled by this
|
||||
* engine instance. This corresponds directly to the list of categories
|
||||
* in your knsrc file. This is not supposed to be used as user-facing
|
||||
* strings - @see categoriesMetadata() for that.
|
||||
*
|
||||
* @return The categories which this instance of Engine handles
|
||||
*/
|
||||
QStringList categories() const;
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Get the entries cache for this engine (note that it may be null if the engine is
|
||||
* not yet initialized).
|
||||
* @return The cache for this engine (or null if the engine is not initialized)
|
||||
* @since 5.74
|
||||
* @deprecated since 6.9 Do not use the cache directly
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Do not use the cache directly")
|
||||
QSharedPointer<Cache> cache() const;
|
||||
#endif
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/// @deprecated since 6.9 use categoriesMetadata2
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use categoriesMetadata2")
|
||||
QList<Provider::CategoryMetadata> categoriesMetadata();
|
||||
#endif
|
||||
/**
|
||||
* The list of metadata for the categories handled by this engine instance.
|
||||
* If you wish to show the categories to the user, this is the data to use.
|
||||
* The category name is the string used to set categories for the filter,
|
||||
* and also what is returned by both categories() and categoriesFilter().
|
||||
* The human-readable name is displayName, and the only thing which should
|
||||
* be shown to the user.
|
||||
*
|
||||
* @return The metadata for all categories handled by this engine
|
||||
*/
|
||||
QList<CategoryMetadata> categoriesMetadata2();
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/// @deprecated since 6.9 use searchPresets2
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use searchPresets2")
|
||||
QList<Provider::SearchPreset> searchPresets();
|
||||
#endif
|
||||
/// @since 6.9
|
||||
QList<SearchPreset> searchPresets2();
|
||||
|
||||
/**
|
||||
* @returns the list of attica (OCS) providers this engine is connected to
|
||||
* @since 5.92
|
||||
*/
|
||||
QList<Attica::Provider *> atticaProviders() const;
|
||||
|
||||
/**
|
||||
* Set a filter for results, which filters out all entries which do not match
|
||||
* the filter, as applied to the tags for the entry. This filters only on the
|
||||
* tags specified for the entry itself. To filter the downloadlinks, use
|
||||
* setDownloadTagFilter(QStringList).
|
||||
*
|
||||
* @note The default filter if one is not set from your knsrc file will filter
|
||||
* out entries marked as ghns_excluded=1. To retain this when setting a custom
|
||||
* filter, add "ghns_excluded!=1" as one of the filters.
|
||||
*
|
||||
* @note Some tags provided by OCS do not supply a value (and are simply passed
|
||||
* as a key). These will be interpreted as having the value 1 for filtering
|
||||
* purposes. An example of this might be ghns_excluded, which in reality will
|
||||
* generally be passed through ocs as "ghns_excluded" rather than "ghns_excluded=1"
|
||||
*
|
||||
* @note As tags are metadata, they are provided in the form of adjectives. They
|
||||
* are never supplied as action verbs or instructions (as an example, a good tag
|
||||
* to suggest that for example a wallpaper is painted would be "painted" as opposed
|
||||
* to "paint", and another example might be that an item should be "excluded" as
|
||||
* opposed to "exclude").
|
||||
*
|
||||
* == Examples of use ==
|
||||
* Value for tag "tagname" must be exactly "tagdata":
|
||||
* tagname==tagdata
|
||||
*
|
||||
* Value for tag "tagname" must be different from "tagdata":
|
||||
* tagname!=tagdata
|
||||
*
|
||||
* == KNSRC entry ==
|
||||
* A tag filter line in a .knsrc file, which is a comma separated list of
|
||||
* tag/value pairs, might look like:
|
||||
*
|
||||
* TagFilter=ghns_excluded!=1,data##mimetype==application/cbr+zip,data##mimetype==application/cbr+rar
|
||||
* which would honour the exclusion and filter out anything that does not
|
||||
* include a comic book archive in either zip or rar format in one or more
|
||||
* of the download items.
|
||||
* Notice in particular that there are two data##mimetype entries. Use this
|
||||
* for when a tag may have multiple values.
|
||||
*
|
||||
* TagFilter=application##architecture==x86_64
|
||||
* which would not honour the exclusion, and would filter out all entries
|
||||
* which do not mark themselves as having a 64bit application binary in at
|
||||
* least one download item.
|
||||
*
|
||||
* The value does not current support wildcards. The list should be considered
|
||||
* a binary AND operation (that is, all filter entries must match for the data
|
||||
* entry to be included in the return data)
|
||||
*
|
||||
* @param filter The filter in the form of a list of strings
|
||||
* @see setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void setTagFilter(const QStringList &filter);
|
||||
/**
|
||||
* Gets the current tag filter list
|
||||
* @see setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList tagFilter() const;
|
||||
/**
|
||||
* Add a single filter entry to the entry tag filter. The filter should be in
|
||||
* the same form as the filter lines in the list used by setTagFilter(QStringList)
|
||||
* @param filter The filter in the form of a string
|
||||
* @see setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void addTagFilter(const QString &filter);
|
||||
/**
|
||||
* Sets a filter to be applied to the downloads for an entry. The logic is the
|
||||
* same as used in setTagFilter(QStringList), but vitally, only one downloadlink
|
||||
* is required to match the filter for the list to be valid. If you do not wish
|
||||
* to show the others in your client, you must hide them yourself.
|
||||
*
|
||||
* For an entry to be accepted when a download tag filter is set, it must also
|
||||
* be accepted by the entry filter (so, for example, while a list of downloads
|
||||
* might be accepted, if the entry has ghns_excluded set, and the default entry
|
||||
* filter is set, the entry will still be filtered out).
|
||||
*
|
||||
* In your knsrc file, set DownloadTagFilter to the filter you wish to apply,
|
||||
* using the same logic as described for the entry tagfilter.
|
||||
*
|
||||
* @param filter The filter in the form of a list of strings
|
||||
* @see setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void setDownloadTagFilter(const QStringList &filter);
|
||||
/**
|
||||
* Gets the current downloadlink tag filter list
|
||||
* @see setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList downloadTagFilter() const;
|
||||
/**
|
||||
* Add a single filter entry to the download tag filter. The filter should be in
|
||||
* the same form as the filter lines in the list used by setDownloadsTagFilter(QStringList)
|
||||
* @param filter The filter in the form of a string
|
||||
* @see setTagFilter(QStringList)
|
||||
* @see setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void addDownloadTagFilter(const QString &filter);
|
||||
|
||||
/**
|
||||
* Whether or not a user is able to vote on the passed entry.
|
||||
*
|
||||
* @param entry The entry to check votability on
|
||||
* @return True if the user is able to vote on the entry
|
||||
*/
|
||||
bool userCanVote(const Entry &entry);
|
||||
/**
|
||||
* Cast a vote on the passed entry.
|
||||
*
|
||||
* @param entry The entry to vote on
|
||||
* @param rating A number from 0 to 100, 50 being neutral, 0 being most negative and 100 being most positive.
|
||||
*/
|
||||
void vote(const Entry &entry, uint rating);
|
||||
|
||||
/**
|
||||
* Whether or not the user is allowed to become a fan of
|
||||
* a particular entry.
|
||||
* Not all providers (and consequently entries) support the fan functionality
|
||||
* and you can use this function to determine this ability.
|
||||
* @param entry The entry the user might wish to be a fan of
|
||||
* @return Whether or not it is possible for the user to become a fan of that entry
|
||||
*/
|
||||
bool userCanBecomeFan(const Entry &entry);
|
||||
/**
|
||||
* This will mark the user who is currently authenticated as a fan
|
||||
* of the entry passed to the function.
|
||||
* @param entry The entry the user wants to be a fan of
|
||||
*/
|
||||
void becomeFan(const Entry &entry);
|
||||
// FIXME There is currently no exposed API to remove the fan status
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* The Provider instance with the passed ID
|
||||
*
|
||||
* @param providerId The ID of the Provider to fetch
|
||||
* @return The Provider with the passed ID, or null if non such Provider exists
|
||||
* @since 5.63
|
||||
* @deprecated since 6.9 Do not write provider-specific code
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Do not write provider-specific code")
|
||||
QSharedPointer<Provider> provider(const QString &providerId) const;
|
||||
#endif
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Return the first provider in the providers list (usually the default provider)
|
||||
* @return The first Provider (or null if the engine is not initialized)
|
||||
* @since 5.63
|
||||
* @deprecated since 6.9 Do not write provider-specific code
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Do not write provider-specific code")
|
||||
QSharedPointer<Provider> defaultProvider() const;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The IDs of all providers known by this engine. Use this in combination with
|
||||
* provider(const QString&) to iterate over all providers.
|
||||
* @return The string IDs of all known providers
|
||||
* @since 5.85
|
||||
*/
|
||||
QStringList providerIDs() const;
|
||||
|
||||
/**
|
||||
* Whether or not an adoption command exists for this engine
|
||||
*
|
||||
* @see adoptionCommand(KNSCore::Entry)
|
||||
* @return True if an adoption command exists
|
||||
*/
|
||||
bool hasAdoptionCommand() const;
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Returns a stream object that will fulfill the @p request.
|
||||
*
|
||||
* @since 6.0
|
||||
* @deprecated since 6.9 Use the new search function
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use the new search function")
|
||||
ResultsStream *search(const KNSCore::Provider::SearchRequest &request);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Returns a stream object that will fulfill the @p request.
|
||||
*
|
||||
* @since 6.9
|
||||
*/
|
||||
ResultsStream *search(const KNSCore::SearchRequest &request);
|
||||
|
||||
/**
|
||||
* @brief The ContentWarningType enum
|
||||
* @since 6.1
|
||||
*/
|
||||
enum class ContentWarningType {
|
||||
/**
|
||||
* Content consists of static assets only
|
||||
* Installation should not pose a security risk
|
||||
*/
|
||||
Static,
|
||||
/**
|
||||
* Content may contain scripts or other executable code
|
||||
* Users should be warned to treat it like any other program
|
||||
*/
|
||||
Executables
|
||||
};
|
||||
Q_ENUM(ContentWarningType)
|
||||
|
||||
/**
|
||||
* @brief The level of warning that should be presented to the user
|
||||
* @since 6.1
|
||||
* @see ContentWarningType
|
||||
*/
|
||||
ContentWarningType contentWarningType() const;
|
||||
|
||||
/**
|
||||
* Emitted after the initial config load
|
||||
* @since 6.1
|
||||
*/
|
||||
Q_SIGNAL void contentWarningTypeChanged();
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* Indicates a message to be added to the ui's log, or sent to a messagebox
|
||||
*/
|
||||
void signalMessage(const QString &message);
|
||||
|
||||
void signalProvidersLoaded();
|
||||
|
||||
/**
|
||||
* Fires in the case of any critical or serious errors, such as network or API problems.
|
||||
* @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::Entry::ErrorCode
|
||||
* @since 5.53
|
||||
*/
|
||||
void signalErrorCode(KNSCore::ErrorCode::ErrorCode errorCode, const QString &message, const QVariant &metadata);
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/// @deprecated since 6.9 Use variant with new argument type
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use variant with new argument type")
|
||||
void signalCategoriesMetadataLoded(const QList<Provider::CategoryMetadata> &categories);
|
||||
#endif
|
||||
void signalCategoriesMetadataLoaded(const QList<KNSCore::CategoryMetadata> &categories);
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Fires when the engine has loaded search presets. These represent interesting
|
||||
* searches for the user, such as recommendations.
|
||||
* @since 5.83
|
||||
* @deprecated since 6.9 Use variant with new argument type
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use variant with new argument type")
|
||||
void signalSearchPresetsLoaded(const QList<Provider::SearchPreset> &presets);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Fires when the engine has loaded search presets. These represent interesting
|
||||
* searches for the user, such as recommendations.
|
||||
* @since 6.9
|
||||
*/
|
||||
void signalSearchPresetsLoaded(const QList<KNSCore::SearchPreset> &presets);
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Fired whenever the list of providers changes
|
||||
* @since 5.85
|
||||
* @deprecated since 6.9 Use providerAdded signal
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use providerAdded signal")
|
||||
void providersChanged();
|
||||
#endif
|
||||
|
||||
void loadingProvider();
|
||||
void providerAdded(KNSCore::ProviderCore *provider);
|
||||
|
||||
private:
|
||||
// the .knsrc file was loaded
|
||||
void slotProviderFileLoaded(const QDomDocument &doc);
|
||||
// instead of getting providers from knsrc, use what was configured in ocs systemsettings
|
||||
void atticaProviderLoaded(const Attica::Provider &provider);
|
||||
// called when a provider is ready to work
|
||||
void providerInitialized(KNSCore::Provider *);
|
||||
|
||||
// loading the .knsrc file failed
|
||||
void slotProvidersFailed();
|
||||
|
||||
/**
|
||||
* load providers from the providersurl in the knsrc file
|
||||
* creates providers based on their type and adds them to the list of providers
|
||||
*/
|
||||
void loadProviders();
|
||||
|
||||
protected:
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Add a provider and connect it to the right slots
|
||||
* @deprecated since 6.9 Use providerAdded signal
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use providerAdded signal")
|
||||
virtual void addProvider(QSharedPointer<KNSCore::Provider> provider);
|
||||
#endif
|
||||
virtual void updateStatus();
|
||||
|
||||
friend class ResultsStream;
|
||||
friend class Transaction;
|
||||
friend class TransactionPrivate;
|
||||
friend class EngineBasePrivate;
|
||||
friend class ::SearchPresetModel;
|
||||
Installation *installation() const; // Needed for quick engine
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/// @deprecated since 6.9 Do not write provider-specific code
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Do not write provider-specific code")
|
||||
QList<QSharedPointer<Provider>> providers() const;
|
||||
#endif
|
||||
// FIXME KF7: make this private and declare QuickEngine a friend. this cannot be used from the outside!
|
||||
std::unique_ptr<EngineBasePrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2007-2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_ENGINEBASE_P_H
|
||||
#define KNEWSTUFF3_ENGINEBASE_P_H
|
||||
|
||||
#include "cache.h"
|
||||
#include "cache2_p.h"
|
||||
#include "categorymetadata.h"
|
||||
#include "enginebase.h"
|
||||
#include "installation_p.h"
|
||||
#include "searchpreset.h"
|
||||
#include <Attica/ProviderManager>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class ProviderCore;
|
||||
|
||||
class EngineBasePrivate
|
||||
{
|
||||
public:
|
||||
EngineBase *q;
|
||||
QString name;
|
||||
QStringList categories;
|
||||
QString adoptionCommand;
|
||||
QString useLabel;
|
||||
bool uploadEnabled = false;
|
||||
QUrl providerFileUrl;
|
||||
QStringList tagFilter;
|
||||
QStringList downloadTagFilter;
|
||||
Installation *installation = new Installation();
|
||||
Attica::ProviderManager *atticaProviderManager = nullptr;
|
||||
QList<SearchPreset> searchPresets;
|
||||
QSharedPointer<Cache2> cache;
|
||||
bool shouldRemoveDeletedEntries = false;
|
||||
QList<CategoryMetadata> categoriesMetadata;
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Only here for backwards compatible API") QHash<QString, QSharedPointer<KNSCore::Provider>> legacyProviders;
|
||||
QHash<QString, QSharedPointer<KNSCore::ProviderCore>> providerCores;
|
||||
KNSCore::EngineBase::ContentWarningType contentWarningType = KNSCore::EngineBase::ContentWarningType::Static;
|
||||
|
||||
EngineBasePrivate(EngineBase *qptr);
|
||||
void addProvider(const QSharedPointer<KNSCore::ProviderCore> &provider);
|
||||
|
||||
private:
|
||||
// Don't use this. Use cache instead.
|
||||
friend class EngineBase; // we may use it though for backwards compat, albeit carefully ;)
|
||||
QSharedPointer<Cache> legacyCache;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,743 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "entry.h"
|
||||
#include "entry_p.h"
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QMetaEnum>
|
||||
#include <QStringList>
|
||||
#include <QXmlStreamReader>
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include "xmlloader_p.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
Entry::Entry()
|
||||
: d(new EntryPrivate())
|
||||
{
|
||||
}
|
||||
|
||||
Entry::Entry(const Entry &other)
|
||||
: d(other.d)
|
||||
{
|
||||
}
|
||||
|
||||
Entry &Entry::operator=(const Entry &other)
|
||||
{
|
||||
d = other.d;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Entry::operator<(const KNSCore::Entry &other) const
|
||||
{
|
||||
return d->mUniqueId < other.d->mUniqueId;
|
||||
}
|
||||
|
||||
bool Entry::operator==(const KNSCore::Entry &other) const
|
||||
{
|
||||
return d->mUniqueId == other.d->mUniqueId && d->mProviderId == other.d->mProviderId;
|
||||
}
|
||||
|
||||
Entry::~Entry() = default;
|
||||
|
||||
bool Entry::isValid() const
|
||||
{
|
||||
return !d->mUniqueId.isEmpty(); // This should not use the uniqueId getter due to the fallback!
|
||||
}
|
||||
|
||||
QString Entry::name() const
|
||||
{
|
||||
return d->mName;
|
||||
}
|
||||
|
||||
void Entry::setName(const QString &name)
|
||||
{
|
||||
d->mName = name;
|
||||
}
|
||||
|
||||
QString Entry::uniqueId() const
|
||||
{
|
||||
return d->mUniqueId.isEmpty() ? d->mRequestedUniqueId : d->mUniqueId;
|
||||
}
|
||||
|
||||
void Entry::setUniqueId(const QString &id)
|
||||
{
|
||||
d->mUniqueId = id;
|
||||
}
|
||||
|
||||
QString Entry::providerId() const
|
||||
{
|
||||
return d->mProviderId;
|
||||
}
|
||||
|
||||
void Entry::setProviderId(const QString &id)
|
||||
{
|
||||
d->mProviderId = id;
|
||||
}
|
||||
|
||||
QStringList KNSCore::Entry::tags() const
|
||||
{
|
||||
return d->mTags;
|
||||
}
|
||||
|
||||
void KNSCore::Entry::setTags(const QStringList &tags)
|
||||
{
|
||||
d->mTags = tags;
|
||||
}
|
||||
|
||||
QString Entry::category() const
|
||||
{
|
||||
return d->mCategory;
|
||||
}
|
||||
|
||||
void Entry::setCategory(const QString &category)
|
||||
{
|
||||
d->mCategory = category;
|
||||
}
|
||||
|
||||
QUrl Entry::homepage() const
|
||||
{
|
||||
return d->mHomepage;
|
||||
}
|
||||
|
||||
void Entry::setHomepage(const QUrl &page)
|
||||
{
|
||||
d->mHomepage = page;
|
||||
}
|
||||
|
||||
Author Entry::author() const
|
||||
{
|
||||
return d->mAuthor;
|
||||
}
|
||||
|
||||
void Entry::setAuthor(const KNSCore::Author &author)
|
||||
{
|
||||
d->mAuthor = author;
|
||||
}
|
||||
|
||||
QString Entry::license() const
|
||||
{
|
||||
return d->mLicense;
|
||||
}
|
||||
|
||||
void Entry::setLicense(const QString &license)
|
||||
{
|
||||
d->mLicense = license;
|
||||
}
|
||||
|
||||
QString Entry::summary() const
|
||||
{
|
||||
return d->mSummary;
|
||||
}
|
||||
|
||||
void Entry::setSummary(const QString &summary)
|
||||
{
|
||||
d->mSummary = summary;
|
||||
}
|
||||
|
||||
QString Entry::shortSummary() const
|
||||
{
|
||||
return d->mShortSummary;
|
||||
}
|
||||
|
||||
void Entry::setShortSummary(const QString &summary)
|
||||
{
|
||||
d->mShortSummary = summary;
|
||||
}
|
||||
|
||||
void Entry::setChangelog(const QString &changelog)
|
||||
{
|
||||
d->mChangelog = changelog;
|
||||
}
|
||||
|
||||
QString Entry::changelog() const
|
||||
{
|
||||
return d->mChangelog;
|
||||
}
|
||||
|
||||
QString Entry::version() const
|
||||
{
|
||||
return d->mVersion;
|
||||
}
|
||||
|
||||
void Entry::setVersion(const QString &version)
|
||||
{
|
||||
d->mVersion = version;
|
||||
}
|
||||
|
||||
QDate Entry::releaseDate() const
|
||||
{
|
||||
return d->mReleaseDate;
|
||||
}
|
||||
|
||||
void Entry::setReleaseDate(const QDate &releasedate)
|
||||
{
|
||||
d->mReleaseDate = releasedate;
|
||||
}
|
||||
|
||||
QString Entry::payload() const
|
||||
{
|
||||
return d->mPayload;
|
||||
}
|
||||
|
||||
void Entry::setPayload(const QString &url)
|
||||
{
|
||||
d->mPayload = url;
|
||||
}
|
||||
|
||||
QDate Entry::updateReleaseDate() const
|
||||
{
|
||||
return d->mUpdateReleaseDate;
|
||||
}
|
||||
|
||||
void Entry::setUpdateReleaseDate(const QDate &releasedate)
|
||||
{
|
||||
d->mUpdateReleaseDate = releasedate;
|
||||
}
|
||||
|
||||
QString Entry::updateVersion() const
|
||||
{
|
||||
return d->mUpdateVersion;
|
||||
}
|
||||
|
||||
void Entry::setUpdateVersion(const QString &version)
|
||||
{
|
||||
d->mUpdateVersion = version;
|
||||
}
|
||||
|
||||
QString Entry::previewUrl(PreviewType type) const
|
||||
{
|
||||
return d->mPreviewUrl[type];
|
||||
}
|
||||
|
||||
void Entry::setPreviewUrl(const QString &url, PreviewType type)
|
||||
{
|
||||
d->mPreviewUrl[type] = url;
|
||||
}
|
||||
|
||||
QImage Entry::previewImage(PreviewType type) const
|
||||
{
|
||||
return d->mPreviewImage[type];
|
||||
}
|
||||
|
||||
void Entry::setPreviewImage(const QImage &image, PreviewType type)
|
||||
{
|
||||
d->mPreviewImage[type] = image;
|
||||
}
|
||||
|
||||
int Entry::rating() const
|
||||
{
|
||||
return d->mRating;
|
||||
}
|
||||
|
||||
void Entry::setRating(int rating)
|
||||
{
|
||||
d->mRating = rating;
|
||||
}
|
||||
|
||||
int Entry::numberOfComments() const
|
||||
{
|
||||
return d->mNumberOfComments;
|
||||
}
|
||||
|
||||
void Entry::setNumberOfComments(int comments)
|
||||
{
|
||||
d->mNumberOfComments = comments;
|
||||
}
|
||||
|
||||
int Entry::downloadCount() const
|
||||
{
|
||||
return d->mDownloadCount;
|
||||
}
|
||||
|
||||
void Entry::setDownloadCount(int downloads)
|
||||
{
|
||||
d->mDownloadCount = downloads;
|
||||
}
|
||||
|
||||
int Entry::numberFans() const
|
||||
{
|
||||
return d->mNumberFans;
|
||||
}
|
||||
|
||||
void Entry::setNumberFans(int fans)
|
||||
{
|
||||
d->mNumberFans = fans;
|
||||
}
|
||||
|
||||
QString Entry::donationLink() const
|
||||
{
|
||||
return d->mDonationLink;
|
||||
}
|
||||
|
||||
void Entry::setDonationLink(const QString &link)
|
||||
{
|
||||
d->mDonationLink = link;
|
||||
}
|
||||
|
||||
int Entry::numberKnowledgebaseEntries() const
|
||||
{
|
||||
return d->mNumberKnowledgebaseEntries;
|
||||
}
|
||||
void Entry::setNumberKnowledgebaseEntries(int num)
|
||||
{
|
||||
d->mNumberKnowledgebaseEntries = num;
|
||||
}
|
||||
|
||||
QString Entry::knowledgebaseLink() const
|
||||
{
|
||||
return d->mKnowledgebaseLink;
|
||||
}
|
||||
void Entry::setKnowledgebaseLink(const QString &link)
|
||||
{
|
||||
d->mKnowledgebaseLink = link;
|
||||
}
|
||||
|
||||
Entry::Source Entry::source() const
|
||||
{
|
||||
return d->mSource;
|
||||
}
|
||||
|
||||
void Entry::setEntryType(Entry::EntryType type)
|
||||
{
|
||||
d->mEntryType = type;
|
||||
}
|
||||
|
||||
Entry::EntryType Entry::entryType() const
|
||||
{
|
||||
return d->mEntryType;
|
||||
}
|
||||
|
||||
void Entry::setSource(Source source)
|
||||
{
|
||||
d->mSource = source;
|
||||
}
|
||||
|
||||
KNSCore::Entry::Status Entry::status() const
|
||||
{
|
||||
return d->mStatus;
|
||||
}
|
||||
|
||||
void Entry::setStatus(KNSCore::Entry::Status status)
|
||||
{
|
||||
d->mStatus = status;
|
||||
}
|
||||
|
||||
void KNSCore::Entry::setInstalledFiles(const QStringList &files)
|
||||
{
|
||||
d->mInstalledFiles = files;
|
||||
}
|
||||
|
||||
QStringList KNSCore::Entry::installedFiles() const
|
||||
{
|
||||
return d->mInstalledFiles;
|
||||
}
|
||||
|
||||
QStringList KNSCore::Entry::uninstalledFiles() const
|
||||
{
|
||||
return d->mUnInstalledFiles;
|
||||
}
|
||||
|
||||
int KNSCore::Entry::downloadLinkCount() const
|
||||
{
|
||||
return d->mDownloadLinkInformationList.size();
|
||||
}
|
||||
|
||||
QList<KNSCore::Entry::DownloadLinkInformation> KNSCore::Entry::downloadLinkInformationList() const
|
||||
{
|
||||
const auto infos = d->mDownloadLinkInformationList;
|
||||
QList<KNSCore::Entry::DownloadLinkInformation> ret;
|
||||
ret.reserve(infos.size());
|
||||
for (const auto &info : infos) {
|
||||
ret.append({.name = info.name,
|
||||
.priceAmount = info.priceAmount,
|
||||
.distributionType = info.distributionType,
|
||||
.descriptionLink = info.descriptionLink,
|
||||
.id = info.id,
|
||||
.isDownloadtypeLink = info.isDownloadtypeLink,
|
||||
.size = info.size,
|
||||
.tags = info.tags});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void KNSCore::Entry::appendDownloadLinkInformation(const KNSCore::Entry::DownloadLinkInformation &info)
|
||||
{
|
||||
d->mDownloadLinkInformationList.append({.name = info.name,
|
||||
.priceAmount = info.priceAmount,
|
||||
.distributionType = info.distributionType,
|
||||
.descriptionLink = info.descriptionLink,
|
||||
.id = info.id,
|
||||
.isDownloadtypeLink = info.isDownloadtypeLink,
|
||||
.size = info.size,
|
||||
.tags = info.tags,
|
||||
.version = QString()});
|
||||
}
|
||||
|
||||
void Entry::clearDownloadLinkInformation()
|
||||
{
|
||||
d->mDownloadLinkInformationList.clear();
|
||||
}
|
||||
|
||||
static QXmlStreamReader::TokenType readNextSkipComments(QXmlStreamReader *xml)
|
||||
{
|
||||
do {
|
||||
xml->readNext();
|
||||
} while (xml->tokenType() == QXmlStreamReader::Comment || (xml->tokenType() == QXmlStreamReader::Characters && xml->text().trimmed().isEmpty()));
|
||||
return xml->tokenType();
|
||||
}
|
||||
|
||||
static QString readText(QXmlStreamReader *xml)
|
||||
{
|
||||
Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement);
|
||||
QString ret;
|
||||
const auto token = readNextSkipComments(xml);
|
||||
if (token == QXmlStreamReader::Characters) {
|
||||
ret = xml->text().toString();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static QString readStringTrimmed(QXmlStreamReader *xml)
|
||||
{
|
||||
Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement);
|
||||
QString ret = readText(xml).trimmed();
|
||||
|
||||
if (xml->tokenType() == QXmlStreamReader::Characters) {
|
||||
readNextSkipComments(xml);
|
||||
}
|
||||
Q_ASSERT(xml->tokenType() == QXmlStreamReader::EndElement);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int readInt(QXmlStreamReader *xml)
|
||||
{
|
||||
Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement);
|
||||
int ret = readText(xml).toInt();
|
||||
|
||||
xml->readNext();
|
||||
Q_ASSERT(xml->tokenType() == QXmlStreamReader::EndElement);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool KNSCore::Entry::setEntryXML(QXmlStreamReader &reader)
|
||||
{
|
||||
if (reader.name() != QLatin1String("stuff")) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Parsing Entry from invalid XML. Reader tag name was expected to be \"stuff\", but was found as:" << reader.name();
|
||||
return false;
|
||||
}
|
||||
|
||||
d->mCategory = reader.attributes().value(QStringLiteral("category")).toString();
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
const auto token = readNextSkipComments(&reader);
|
||||
if (token == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
} else if (token != QXmlStreamReader::StartElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reader.name() == QLatin1String("name")) {
|
||||
// TODO maybe do something with the language attribute? QString lang = e.attribute("lang");
|
||||
d->mName = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("author")) {
|
||||
// ### careful, the following variables are string views that become invalid when we
|
||||
// proceed with reading from reader, such as the readStringTrimmed call below does!
|
||||
const auto email = reader.attributes().value(QStringLiteral("email"));
|
||||
const auto jabber = reader.attributes().value(QStringLiteral("im"));
|
||||
const auto homepage = reader.attributes().value(QStringLiteral("homepage"));
|
||||
d->mAuthor.setEmail(email.toString());
|
||||
d->mAuthor.setJabber(jabber.toString());
|
||||
d->mAuthor.setHomepage(homepage.toString());
|
||||
d->mAuthor.setName(readStringTrimmed(&reader));
|
||||
} else if (reader.name() == QLatin1String("providerid")) {
|
||||
d->mProviderId = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("homepage")) {
|
||||
d->mHomepage = QUrl(reader.readElementText(QXmlStreamReader::SkipChildElements));
|
||||
} else if (reader.name() == QLatin1String("licence")) { // krazy:exclude=spelling
|
||||
d->mLicense = readStringTrimmed(&reader);
|
||||
} else if (reader.name() == QLatin1String("summary")) {
|
||||
d->mSummary = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("changelog")) {
|
||||
d->mChangelog = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("version")) {
|
||||
d->mVersion = readStringTrimmed(&reader);
|
||||
} else if (reader.name() == QLatin1String("releasedate")) {
|
||||
d->mReleaseDate = QDate::fromString(readStringTrimmed(&reader), Qt::ISODate);
|
||||
} else if (reader.name() == QLatin1String("preview")) {
|
||||
// TODO support for all 6 image links
|
||||
d->mPreviewUrl[PreviewSmall1] = readStringTrimmed(&reader);
|
||||
} else if (reader.name() == QLatin1String("previewBig")) {
|
||||
d->mPreviewUrl[PreviewBig1] = readStringTrimmed(&reader);
|
||||
} else if (reader.name() == QLatin1String("payload")) {
|
||||
d->mPayload = readStringTrimmed(&reader);
|
||||
} else if (reader.name() == QLatin1String("rating")) {
|
||||
d->mRating = readInt(&reader);
|
||||
} else if (reader.name() == QLatin1String("downloads")) {
|
||||
d->mDownloadCount = readInt(&reader);
|
||||
} else if (reader.name() == QLatin1String("category")) {
|
||||
d->mCategory = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("signature")) {
|
||||
d->mSignature = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("checksum")) {
|
||||
d->mChecksum = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("installedfile")) {
|
||||
d->mInstalledFiles.append(reader.readElementText(QXmlStreamReader::SkipChildElements));
|
||||
} else if (reader.name() == QLatin1String("id")) {
|
||||
d->mUniqueId = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (reader.name() == QLatin1String("tags")) {
|
||||
d->mTags = reader.readElementText(QXmlStreamReader::SkipChildElements).split(QLatin1Char(','));
|
||||
} else if (reader.name() == QLatin1String("status")) {
|
||||
const auto statusText = readText(&reader);
|
||||
if (statusText == QLatin1String("installed")) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Found an installed entry in registry";
|
||||
d->mStatus = KNSCore::Entry::Installed;
|
||||
} else if (statusText == QLatin1String("updateable")) {
|
||||
d->mStatus = KNSCore::Entry::Updateable;
|
||||
}
|
||||
if (reader.tokenType() == QXmlStreamReader::Characters) {
|
||||
readNextSkipComments(&reader);
|
||||
}
|
||||
}
|
||||
Q_ASSERT_X(reader.tokenType() == QXmlStreamReader::EndElement,
|
||||
Q_FUNC_INFO,
|
||||
QStringLiteral("token name was %1 and the type was %2").arg(reader.name().toString(), reader.tokenString()).toLocal8Bit().data());
|
||||
}
|
||||
|
||||
// Validation
|
||||
if (d->mName.isEmpty()) {
|
||||
qWarning() << "Entry: no name given";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (d->mUniqueId.isEmpty()) {
|
||||
if (!d->mPayload.isEmpty()) {
|
||||
d->mUniqueId = d->mPayload;
|
||||
} else {
|
||||
d->mUniqueId = d->mName;
|
||||
}
|
||||
}
|
||||
|
||||
if (d->mPayload.isEmpty()) {
|
||||
qWarning() << "Entry: no payload URL given for: " << d->mName << " - " << d->mUniqueId;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KNSCore::Entry::setEntryXML(const QDomElement &xmldata)
|
||||
{
|
||||
if (xmldata.tagName() != QLatin1String("stuff")) {
|
||||
qWarning() << "Parsing Entry from invalid XML";
|
||||
return false;
|
||||
}
|
||||
|
||||
d->mCategory = xmldata.attribute(QStringLiteral("category"));
|
||||
|
||||
QDomNode n;
|
||||
for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) {
|
||||
QDomElement e = n.toElement();
|
||||
if (e.tagName() == QLatin1String("name")) {
|
||||
// TODO maybe do something with the language attribute? QString lang = e.attribute("lang");
|
||||
d->mName = e.text().trimmed();
|
||||
} else if (e.tagName() == QLatin1String("author")) {
|
||||
QString email = e.attribute(QStringLiteral("email"));
|
||||
QString jabber = e.attribute(QStringLiteral("im"));
|
||||
QString homepage = e.attribute(QStringLiteral("homepage"));
|
||||
d->mAuthor.setName(e.text().trimmed());
|
||||
d->mAuthor.setEmail(email);
|
||||
d->mAuthor.setJabber(jabber);
|
||||
d->mAuthor.setHomepage(homepage);
|
||||
} else if (e.tagName() == QLatin1String("providerid")) {
|
||||
d->mProviderId = e.text();
|
||||
} else if (e.tagName() == QLatin1String("homepage")) {
|
||||
d->mHomepage = QUrl(e.text());
|
||||
} else if (e.tagName() == QLatin1String("licence")) { // krazy:exclude=spelling
|
||||
d->mLicense = e.text().trimmed();
|
||||
} else if (e.tagName() == QLatin1String("summary")) {
|
||||
d->mSummary = e.text();
|
||||
} else if (e.tagName() == QLatin1String("changelog")) {
|
||||
d->mChangelog = e.text();
|
||||
} else if (e.tagName() == QLatin1String("version")) {
|
||||
d->mVersion = e.text().trimmed();
|
||||
} else if (e.tagName() == QLatin1String("releasedate")) {
|
||||
d->mReleaseDate = QDate::fromString(e.text().trimmed(), Qt::ISODate);
|
||||
} else if (e.tagName() == QLatin1String("preview")) {
|
||||
// TODO support for all 6 image links
|
||||
d->mPreviewUrl[PreviewSmall1] = e.text().trimmed();
|
||||
} else if (e.tagName() == QLatin1String("previewBig")) {
|
||||
d->mPreviewUrl[PreviewBig1] = e.text().trimmed();
|
||||
} else if (e.tagName() == QLatin1String("payload")) {
|
||||
d->mPayload = e.text().trimmed();
|
||||
} else if (e.tagName() == QLatin1String("rating")) {
|
||||
d->mRating = e.text().toInt();
|
||||
} else if (e.tagName() == QLatin1String("downloads")) {
|
||||
d->mDownloadCount = e.text().toInt();
|
||||
} else if (e.tagName() == QLatin1String("category")) {
|
||||
d->mCategory = e.text();
|
||||
} else if (e.tagName() == QLatin1String("signature")) {
|
||||
d->mSignature = e.text();
|
||||
} else if (e.tagName() == QLatin1String("checksum")) {
|
||||
d->mChecksum = e.text();
|
||||
} else if (e.tagName() == QLatin1String("installedfile")) {
|
||||
// TODO KF6 Add a "installeddirectory" entry to avoid
|
||||
// all the issues with the "/*" notation which is currently used as a workaround
|
||||
d->mInstalledFiles.append(e.text());
|
||||
} else if (e.tagName() == QLatin1String("id")) {
|
||||
d->mUniqueId = e.text();
|
||||
} else if (e.tagName() == QLatin1String("tags")) {
|
||||
d->mTags = e.text().split(QLatin1Char(','));
|
||||
} else if (e.tagName() == QLatin1String("status")) {
|
||||
QString statusText = e.text();
|
||||
if (statusText == QLatin1String("installed")) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Found an installed entry in registry";
|
||||
d->mStatus = KNSCore::Entry::Installed;
|
||||
} else if (statusText == QLatin1String("updateable")) {
|
||||
d->mStatus = KNSCore::Entry::Updateable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation
|
||||
if (d->mName.isEmpty()) {
|
||||
qWarning() << "Entry: no name given";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (d->mUniqueId.isEmpty()) {
|
||||
if (!d->mPayload.isEmpty()) {
|
||||
d->mUniqueId = d->mPayload;
|
||||
} else {
|
||||
d->mUniqueId = d->mName;
|
||||
}
|
||||
}
|
||||
|
||||
if (d->mPayload.isEmpty()) {
|
||||
qWarning() << "Entry: no payload URL given for: " << d->mName << " - " << d->mUniqueId;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the xml string for the entry
|
||||
*/
|
||||
QDomElement KNSCore::Entry::entryXML() const
|
||||
{
|
||||
Q_ASSERT(!d->mUniqueId.isEmpty());
|
||||
Q_ASSERT(!d->mProviderId.isEmpty());
|
||||
|
||||
QDomDocument doc;
|
||||
|
||||
QDomElement el = doc.createElement(QStringLiteral("stuff"));
|
||||
el.setAttribute(QStringLiteral("category"), d->mCategory);
|
||||
|
||||
QString name = d->mName;
|
||||
|
||||
QDomElement e;
|
||||
e = addElement(doc, el, QStringLiteral("name"), name);
|
||||
// todo: add language attribute
|
||||
(void)addElement(doc, el, QStringLiteral("providerid"), d->mProviderId);
|
||||
|
||||
QDomElement author = addElement(doc, el, QStringLiteral("author"), d->mAuthor.name());
|
||||
if (!d->mAuthor.email().isEmpty()) {
|
||||
author.setAttribute(QStringLiteral("email"), d->mAuthor.email());
|
||||
}
|
||||
if (!d->mAuthor.homepage().isEmpty()) {
|
||||
author.setAttribute(QStringLiteral("homepage"), d->mAuthor.homepage());
|
||||
}
|
||||
if (!d->mAuthor.jabber().isEmpty()) {
|
||||
author.setAttribute(QStringLiteral("im"), d->mAuthor.jabber());
|
||||
}
|
||||
// FIXME: 'jabber' or 'im'? consult with kopete guys...
|
||||
addElement(doc, el, QStringLiteral("homepage"), d->mHomepage.url());
|
||||
(void)addElement(doc, el, QStringLiteral("licence"), d->mLicense); // krazy:exclude=spelling
|
||||
(void)addElement(doc, el, QStringLiteral("version"), d->mVersion);
|
||||
if ((d->mRating > 0) || (d->mDownloadCount > 0)) {
|
||||
(void)addElement(doc, el, QStringLiteral("rating"), QString::number(d->mRating));
|
||||
(void)addElement(doc, el, QStringLiteral("downloads"), QString::number(d->mDownloadCount));
|
||||
}
|
||||
if (!d->mSignature.isEmpty()) {
|
||||
(void)addElement(doc, el, QStringLiteral("signature"), d->mSignature);
|
||||
}
|
||||
if (!d->mChecksum.isEmpty()) {
|
||||
(void)addElement(doc, el, QStringLiteral("checksum"), d->mChecksum);
|
||||
}
|
||||
for (const QString &file : std::as_const(d->mInstalledFiles)) {
|
||||
(void)addElement(doc, el, QStringLiteral("installedfile"), file);
|
||||
}
|
||||
if (!d->mUniqueId.isEmpty()) {
|
||||
addElement(doc, el, QStringLiteral("id"), d->mUniqueId);
|
||||
}
|
||||
|
||||
(void)addElement(doc, el, QStringLiteral("releasedate"), d->mReleaseDate.toString(Qt::ISODate));
|
||||
|
||||
e = addElement(doc, el, QStringLiteral("summary"), d->mSummary);
|
||||
e = addElement(doc, el, QStringLiteral("changelog"), d->mChangelog);
|
||||
e = addElement(doc, el, QStringLiteral("preview"), d->mPreviewUrl[PreviewSmall1]);
|
||||
e = addElement(doc, el, QStringLiteral("previewBig"), d->mPreviewUrl[PreviewBig1]);
|
||||
e = addElement(doc, el, QStringLiteral("payload"), d->mPayload);
|
||||
e = addElement(doc, el, QStringLiteral("tags"), d->mTags.join(QLatin1Char(',')));
|
||||
|
||||
if (d->mStatus == KNSCore::Entry::Installed) {
|
||||
(void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("installed"));
|
||||
}
|
||||
if (d->mStatus == KNSCore::Entry::Updateable) {
|
||||
(void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("updateable"));
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
void KNSCore::Entry::setEntryDeleted()
|
||||
{
|
||||
setStatus(Entry::Deleted);
|
||||
d->mUnInstalledFiles = installedFiles();
|
||||
setInstalledFiles(QStringList());
|
||||
}
|
||||
|
||||
void KNSCore::Entry::setEntryRequestedId(const QString &id)
|
||||
{
|
||||
d->mRequestedUniqueId = id;
|
||||
}
|
||||
|
||||
QString KNSCore::replaceBBCode(const QString &unformattedText)
|
||||
{
|
||||
QString text(unformattedText);
|
||||
text.replace(QLatin1String("[b]"), QLatin1String("<b>"));
|
||||
text.replace(QLatin1String("[/b]"), QLatin1String("</b>"));
|
||||
text.replace(QLatin1String("[i]"), QLatin1String("<i>"));
|
||||
text.replace(QLatin1String("[/i]"), QLatin1String("</i>"));
|
||||
text.replace(QLatin1String("[u]"), QLatin1String("<i>"));
|
||||
text.replace(QLatin1String("[/u]"), QLatin1String("</i>"));
|
||||
text.replace(QLatin1String("\\\""), QLatin1String("\""));
|
||||
text.replace(QLatin1String("\\\'"), QLatin1String("\'"));
|
||||
text.replace(QLatin1String("[li]"), QLatin1String("* ")); // TODO: better replacement for list elements?
|
||||
text.remove(QStringLiteral("[/li]"));
|
||||
text.remove(QStringLiteral("[url]"));
|
||||
text.remove(QStringLiteral("[/url]"));
|
||||
return text;
|
||||
}
|
||||
|
||||
QDebug KNSCore::operator<<(QDebug debug, const KNSCore::Entry &entry)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
|
||||
const static QMetaEnum metaEnum = QMetaEnum::fromType<KNSCore::Entry::Status>();
|
||||
bool deleted = entry.status() == Entry::Status::Deleted;
|
||||
|
||||
debug.nospace() << "KNSCore::Entry(uniqueId: " << entry.uniqueId() << ", name:" << entry.name() << ", status: " << metaEnum.valueToKey(entry.status())
|
||||
<< ", " << (deleted ? "uninstalled" : "installed") << "Files: " // When the entry is installed, it can not have uninstalledFiles
|
||||
<< (deleted ? entry.uninstalledFiles() : entry.installedFiles()) << ')';
|
||||
return debug;
|
||||
}
|
||||
|
||||
#include "moc_entry.cpp"
|
||||
@@ -0,0 +1,578 @@
|
||||
/*
|
||||
knewstuff3/entry.h.
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_ENTRY
|
||||
#define KNEWSTUFF3_ENTRY
|
||||
|
||||
#include <QDate>
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "author.h"
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
class testEntry;
|
||||
class QDomElement;
|
||||
class QXmlStreamReader;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
static const int PreviewWidth = 96;
|
||||
static const int PreviewHeight = 72;
|
||||
class EntryPrivate;
|
||||
|
||||
/**
|
||||
function to remove bb code formatting that opendesktop sends
|
||||
*/
|
||||
KNEWSTUFFCORE_EXPORT QString replaceBBCode(const QString &unformattedText);
|
||||
|
||||
/**
|
||||
* @short KNewStuff data entry container.
|
||||
*
|
||||
* This class provides accessor methods to the data objects
|
||||
* as used by KNewStuff.
|
||||
*
|
||||
* @author Cornelius Schumacher (schumacher@kde.org)
|
||||
* \par Maintainer:
|
||||
* Jeremy Whiting (jpwhiting@kde.org)
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT Entry
|
||||
{
|
||||
Q_GADGET
|
||||
public:
|
||||
typedef QList<Entry> List;
|
||||
Q_PROPERTY(QString providerId READ providerId)
|
||||
Q_PROPERTY(QString uniqueId READ uniqueId)
|
||||
Q_PROPERTY(KNSCore::Entry::Status status READ status)
|
||||
Q_PROPERTY(KNSCore::Entry::EntryType entryType READ entryType)
|
||||
|
||||
Q_PROPERTY(QString name READ name)
|
||||
Q_PROPERTY(KNSCore::Author author READ author)
|
||||
Q_PROPERTY(QString shortSummary READ shortSummary)
|
||||
Q_PROPERTY(QString summary READ summary)
|
||||
// TODO Q_PROPERTY(QString previews READ previews)
|
||||
Q_PROPERTY(QUrl homepage READ homepage)
|
||||
Q_PROPERTY(QString donationLink READ donationLink)
|
||||
Q_PROPERTY(int numberOfComments READ numberOfComments)
|
||||
Q_PROPERTY(int rating READ rating)
|
||||
Q_PROPERTY(int downloadCount READ downloadCount)
|
||||
Q_PROPERTY(QList<KNSCore::Entry::DownloadLinkInformation> downloadLinks READ downloadLinkInformationList)
|
||||
|
||||
/**
|
||||
* Status of the entry. An entry will be downloadable from the provider's
|
||||
* site prior to the download. Once downloaded and installed, it will
|
||||
* be either installed or updateable, implying an out-of-date
|
||||
* installation. Finally, the entry can be deleted and hence show up as
|
||||
* downloadable again.
|
||||
* Entries not taking part in this cycle, for example those in upload,
|
||||
* have an invalid status.
|
||||
*/
|
||||
enum Status {
|
||||
Invalid,
|
||||
Downloadable,
|
||||
Installed,
|
||||
Updateable,
|
||||
Deleted,
|
||||
Installing,
|
||||
Updating,
|
||||
};
|
||||
Q_ENUM(Status)
|
||||
|
||||
/**
|
||||
* Source of the entry, A entry's data is coming from either cache, or an online provider
|
||||
* this helps the engine know which data to use when merging cached entries with online
|
||||
* entry data
|
||||
*/
|
||||
enum Source {
|
||||
Cache,
|
||||
Online,
|
||||
Registry,
|
||||
};
|
||||
|
||||
enum PreviewType {
|
||||
PreviewSmall1,
|
||||
PreviewSmall2,
|
||||
PreviewSmall3,
|
||||
PreviewBig1,
|
||||
PreviewBig2,
|
||||
PreviewBig3,
|
||||
};
|
||||
|
||||
struct DownloadLinkInformation {
|
||||
QString name; // Displayed name.
|
||||
QString priceAmount; // Price formatted as a string.
|
||||
QString distributionType; // OCS Distribution Type, this is for which OS the file is useful.
|
||||
QString descriptionLink; // Link to intermediary description.
|
||||
int id; // Unique integer representing the download number in the list.
|
||||
bool isDownloadtypeLink;
|
||||
quint64 size = 0; // Size in kilobytes.
|
||||
QStringList tags; // variety of tags that can represent mimetype or url location.
|
||||
};
|
||||
|
||||
enum EntryEvent {
|
||||
UnknownEvent = 0, ///< A generic event, not generally used
|
||||
StatusChangedEvent = 1, ///< Used when an event's status is set (use Entry::status() to get the new status)
|
||||
AdoptedEvent = 2, ///< Used when an entry has been successfully adopted (use this to determine whether a call to Engine::adoptEntry() succeeded)
|
||||
DetailsLoadedEvent = 3, ///< Used when more details have been added to an existing entry (such as the full description), and the UI should be updated
|
||||
};
|
||||
Q_ENUM(EntryEvent)
|
||||
|
||||
/**
|
||||
* Represents whether the current entry is an actual catalog entry,
|
||||
* or an entry that represents a set of entries.
|
||||
* @since 5.83
|
||||
*/
|
||||
enum EntryType {
|
||||
CatalogEntry = 0, ///< These are the main entries that KNewStuff can get the details about and download links for.
|
||||
GroupEntry ///< these are entries whose payload is another feed. Currently only used by the OPDS provider.
|
||||
};
|
||||
Q_ENUM(EntryType)
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
Entry();
|
||||
|
||||
Entry(const Entry &other);
|
||||
Entry &operator=(const Entry &other);
|
||||
|
||||
bool operator==(const Entry &other) const;
|
||||
bool operator<(const Entry &other) const;
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
~Entry();
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
/**
|
||||
* Sets the name for this data object.
|
||||
*/
|
||||
void setName(const QString &name);
|
||||
|
||||
/**
|
||||
* Retrieve the name of the data object.
|
||||
*
|
||||
* @return object name (potentially translated)
|
||||
*/
|
||||
QString name() const;
|
||||
|
||||
/**
|
||||
* Set the object's unique ID. This must be unique to the provider.
|
||||
*
|
||||
* @param id The unique ID of this entry as unique to this provider
|
||||
* @see KNSCore::Provider
|
||||
*/
|
||||
void setUniqueId(const QString &id);
|
||||
/**
|
||||
* Get the object's unique ID. This will be unique to the provider.
|
||||
* This is not intended as user-facing information - though it can
|
||||
* be useful for certain purposes, this is supposed to only be used
|
||||
* for keeping track of the entry.
|
||||
*
|
||||
* @return The unique ID of this entry
|
||||
*/
|
||||
QString uniqueId() const;
|
||||
|
||||
/**
|
||||
* Sets the data category, e.g. "KWin Scripts" or "Plasma Theme".
|
||||
*/
|
||||
void setCategory(const QString &category);
|
||||
|
||||
/**
|
||||
* Retrieve the category of the data object. This is the category's
|
||||
* name or ID (as opposed to displayName).
|
||||
*
|
||||
* @see KNSCore::Provider::CategoryMetadata
|
||||
* @see KNSCore::Engine::categories()
|
||||
* @return object category
|
||||
*/
|
||||
QString category() const;
|
||||
|
||||
/**
|
||||
* Set a link to a website containing information about this entry
|
||||
*
|
||||
* @param page The URL representing the entry's website
|
||||
*/
|
||||
void setHomepage(const QUrl &page);
|
||||
/**
|
||||
* A link to a website containing information about this entry
|
||||
*
|
||||
* @return The URL representing the entry's website
|
||||
*/
|
||||
QUrl homepage() const;
|
||||
|
||||
/**
|
||||
* Sets the author of the object.
|
||||
*/
|
||||
void setAuthor(const Author &author);
|
||||
|
||||
/**
|
||||
* Retrieve the author of the object.
|
||||
*
|
||||
* @return object author
|
||||
*/
|
||||
Author author() const;
|
||||
|
||||
/**
|
||||
* Sets the license (abbreviation) applicable to the object.
|
||||
*/
|
||||
void setLicense(const QString &license);
|
||||
|
||||
/**
|
||||
* Retrieve the license name of the object.
|
||||
*
|
||||
* @return object license
|
||||
*/
|
||||
QString license() const;
|
||||
|
||||
/**
|
||||
* Sets a description (which can potentially be very long)
|
||||
*/
|
||||
void setSummary(const QString &summary);
|
||||
|
||||
/**
|
||||
* Retrieve a short description of what the object is all about (should be very short)
|
||||
*
|
||||
* @return object license
|
||||
*/
|
||||
QString shortSummary() const;
|
||||
|
||||
/**
|
||||
* Sets a short description of what the object is all about (should be very short)
|
||||
*/
|
||||
void setShortSummary(const QString &summary);
|
||||
|
||||
/**
|
||||
* Retrieve a (potentially very long) description of the object.
|
||||
*
|
||||
* @return object description
|
||||
*/
|
||||
QString summary() const;
|
||||
|
||||
/**
|
||||
* The user written changelog
|
||||
*/
|
||||
void setChangelog(const QString &changelog);
|
||||
QString changelog() const;
|
||||
|
||||
/**
|
||||
* Sets the version number.
|
||||
*/
|
||||
void setVersion(const QString &version);
|
||||
|
||||
/**
|
||||
* Retrieve the version string of the object.
|
||||
*
|
||||
* @return object version
|
||||
*/
|
||||
QString version() const;
|
||||
|
||||
/**
|
||||
* Sets the release date.
|
||||
*/
|
||||
void setReleaseDate(const QDate &releasedate);
|
||||
|
||||
/**
|
||||
* Retrieve the date of the object's publication.
|
||||
*
|
||||
* @return object release date
|
||||
*/
|
||||
QDate releaseDate() const;
|
||||
|
||||
/**
|
||||
* Sets the version number that is available as update.
|
||||
*/
|
||||
void setUpdateVersion(const QString &version);
|
||||
|
||||
/**
|
||||
* Retrieve the version string of the object that is available as update.
|
||||
*
|
||||
* @return object version
|
||||
*/
|
||||
QString updateVersion() const;
|
||||
|
||||
/**
|
||||
* Sets the release date that is available as update.
|
||||
*/
|
||||
void setUpdateReleaseDate(const QDate &releasedate);
|
||||
|
||||
/**
|
||||
* Retrieve the date of the newer version that is available as update.
|
||||
*
|
||||
* @return object release date
|
||||
*/
|
||||
QDate updateReleaseDate() const;
|
||||
|
||||
/**
|
||||
* Sets the object's file.
|
||||
*/
|
||||
void setPayload(const QString &url);
|
||||
|
||||
/**
|
||||
* Retrieve the file name of the object.
|
||||
*
|
||||
* @return object filename
|
||||
*/
|
||||
QString payload() const;
|
||||
|
||||
/**
|
||||
* Sets the object's preview file, if available. This should be a
|
||||
* picture file.
|
||||
*/
|
||||
void setPreviewUrl(const QString &url, PreviewType type = PreviewSmall1);
|
||||
|
||||
/**
|
||||
* Retrieve the file name of an image containing a preview of the object.
|
||||
*
|
||||
* @return object preview filename
|
||||
*/
|
||||
QString previewUrl(PreviewType type = PreviewSmall1) const;
|
||||
|
||||
/**
|
||||
* This will not be loaded automatically, instead use Engine to load the actual images.
|
||||
*/
|
||||
QImage previewImage(PreviewType type = PreviewSmall1) const;
|
||||
void setPreviewImage(const QImage &image, PreviewType type = PreviewSmall1);
|
||||
|
||||
/**
|
||||
* Set the files that have been installed by the install command.
|
||||
* @param files local file names
|
||||
*/
|
||||
void setInstalledFiles(const QStringList &files);
|
||||
|
||||
/**
|
||||
* Retrieve the locally installed files.
|
||||
* @return file names
|
||||
*/
|
||||
QStringList installedFiles() const;
|
||||
|
||||
/**
|
||||
* Retrieve the locally uninstalled files.
|
||||
* @return file names
|
||||
* @since 4.1
|
||||
*/
|
||||
QStringList uninstalledFiles() const;
|
||||
|
||||
/**
|
||||
* Sets the rating between 0 (worst) and 100 (best).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
void setRating(int rating);
|
||||
|
||||
/**
|
||||
* Retrieve the rating for the object, which has been determined by its
|
||||
* users and thus might change over time.
|
||||
*
|
||||
* @return object rating
|
||||
*/
|
||||
int rating() const;
|
||||
|
||||
/**
|
||||
* Sets the number of comments in the asset
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
void setNumberOfComments(int comments);
|
||||
|
||||
/**
|
||||
* @returns the number of comments against the asset
|
||||
*/
|
||||
int numberOfComments() const;
|
||||
|
||||
/**
|
||||
* Sets the number of downloads.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
void setDownloadCount(int downloads);
|
||||
|
||||
/**
|
||||
* Retrieve the download count for the object, which has been determined
|
||||
* by its hosting sites and thus might change over time.
|
||||
*
|
||||
* @return object download count
|
||||
*/
|
||||
int downloadCount() const;
|
||||
|
||||
/**
|
||||
* How many people have marked themselves as fans of this entry
|
||||
*
|
||||
* @return The number of fans this entry has
|
||||
* @see KNSCore::Engine::becomeFan(const Entry& entry)
|
||||
*/
|
||||
int numberFans() const;
|
||||
/**
|
||||
* Sets how many people are fans.
|
||||
* Note: This is purely informational. To become a fan, call the
|
||||
* KNSCore::Engine::becomeFan function.
|
||||
*
|
||||
* @param fans The number of fans this entry has
|
||||
* @see KNSCore::Engine::becomeFan(const Entry& entry)
|
||||
*/
|
||||
void setNumberFans(int fans);
|
||||
|
||||
/**
|
||||
* The number of entries in the knowledgebase for this entry
|
||||
* @return The number of knowledgebase entries
|
||||
*/
|
||||
int numberKnowledgebaseEntries() const;
|
||||
/**
|
||||
* Set the number of knowledgebase entries for this entry
|
||||
* @param num The number of entries
|
||||
*/
|
||||
void setNumberKnowledgebaseEntries(int num);
|
||||
/**
|
||||
* The link for the knowledgebase for this entry.
|
||||
* @return A string version of the URL for the knowledgebase
|
||||
*/
|
||||
QString knowledgebaseLink() const;
|
||||
/**
|
||||
* Set the link for the knowledgebase.
|
||||
* Note: This is not checked for validity, the caller must do this.
|
||||
* @param link The string version of the URL for the knowledgebase
|
||||
*/
|
||||
void setKnowledgebaseLink(const QString &link);
|
||||
|
||||
/**
|
||||
* The number of available download options for this entry
|
||||
* @return The number of download options
|
||||
*/
|
||||
int downloadLinkCount() const;
|
||||
/**
|
||||
* A list of downloadable data for this entry
|
||||
* @return The list of download options
|
||||
* @see DownloadLinkInformation
|
||||
*/
|
||||
QList<DownloadLinkInformation> downloadLinkInformationList() const;
|
||||
/**
|
||||
* Add a new download option to this entry
|
||||
* @param info The new download option
|
||||
*/
|
||||
void appendDownloadLinkInformation(const DownloadLinkInformation &info);
|
||||
/**
|
||||
* Remove all download options from this entry
|
||||
*/
|
||||
void clearDownloadLinkInformation();
|
||||
|
||||
/**
|
||||
* A string representing the URL for a website where the user can donate
|
||||
* to the author of this entry
|
||||
* @return The string version of the URL for the entry's donation website
|
||||
*/
|
||||
QString donationLink() const;
|
||||
/**
|
||||
* Set a string representation of the URL for the donation website for this entry.
|
||||
* Note: This is not checked for validity, the caller must do this.
|
||||
* @param link String version of the URL for the entry's donation website
|
||||
*/
|
||||
void setDonationLink(const QString &link);
|
||||
|
||||
/**
|
||||
* The set of tags assigned specifically to this content item. This does not include
|
||||
* tags for the download links. To get those, you must concatenate the lists yourself.
|
||||
* @see downloadLinkInformationList()
|
||||
* @see DownloadLinkInformation
|
||||
* @see Engine::setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList tags() const;
|
||||
/**
|
||||
* Set the tags for the content item.
|
||||
* @param tags A string list containing the tags for this entry
|
||||
* @since 5.51
|
||||
*/
|
||||
void setTags(const QStringList &tags);
|
||||
|
||||
/**
|
||||
The id of the provider this entry belongs to
|
||||
*/
|
||||
QString providerId() const;
|
||||
void setProviderId(const QString &id);
|
||||
|
||||
/**
|
||||
The source of this entry can be Cache, Registry or Online - @see source
|
||||
*/
|
||||
void setSource(Source source);
|
||||
Source source() const;
|
||||
|
||||
/**
|
||||
* The entry type is either catalog entry, or group entry.
|
||||
* @since 5.83
|
||||
*/
|
||||
void setEntryType(EntryType type);
|
||||
EntryType entryType() const;
|
||||
|
||||
/**
|
||||
* set the xml for the entry
|
||||
* parses the xml and sets the private members accordingly
|
||||
* used to deserialize data loaded from provider
|
||||
*
|
||||
* @param xmldata string to load xml data from
|
||||
*
|
||||
* @returns whether or not setting the values was successful
|
||||
*
|
||||
* @since 5.36
|
||||
*/
|
||||
bool setEntryXML(QXmlStreamReader &reader);
|
||||
|
||||
/**
|
||||
* Sets the entry's status. If no status is set, the default will be
|
||||
* \ref Invalid.
|
||||
*
|
||||
* Note that while this enum is currently found in KNS3::Entry,
|
||||
* it will be moved to this class once the binary compatibility
|
||||
* lock is lifted for Frameworks 6. For now, you should read any
|
||||
* reference to the KNS3::Entry::Status enumerator as KNSCore::Entry::Status
|
||||
*
|
||||
* @param status New status of the entry
|
||||
*/
|
||||
void setStatus(KNSCore::Entry::Status status);
|
||||
|
||||
/**
|
||||
* Retrieves the entry's status.
|
||||
*
|
||||
* @return Current status of the entry
|
||||
*/
|
||||
KNSCore::Entry::Status status() const;
|
||||
|
||||
/// @internal
|
||||
void setEntryDeleted();
|
||||
|
||||
private:
|
||||
friend class StaticXmlProvider;
|
||||
friend class Cache;
|
||||
friend class Cache2;
|
||||
friend class Installation;
|
||||
friend class AtticaProvider;
|
||||
friend class AtticaRequester;
|
||||
friend class Transaction;
|
||||
friend class TransactionPrivate;
|
||||
friend testEntry;
|
||||
KNEWSTUFFCORE_NO_EXPORT void setEntryRequestedId(const QString &id);
|
||||
QDomElement entryXML() const;
|
||||
bool setEntryXML(const QDomElement &xmldata);
|
||||
QExplicitlySharedDataPointer<EntryPrivate> d;
|
||||
};
|
||||
|
||||
inline size_t qHash(const KNSCore::Entry &entry, size_t seed = 0)
|
||||
{
|
||||
return qHash(entry.uniqueId(), seed);
|
||||
}
|
||||
|
||||
KNEWSTUFFCORE_EXPORT QDebug operator<<(QDebug debug, const KNSCore::Entry &entry);
|
||||
}
|
||||
|
||||
Q_DECLARE_TYPEINFO(KNSCore::Entry, Q_RELOCATABLE_TYPE);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
// SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "entry.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class DownloadLinkInformationV2Private
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
QString priceAmount;
|
||||
QString distributionType;
|
||||
QString descriptionLink;
|
||||
int id = -1;
|
||||
bool isDownloadtypeLink = false;
|
||||
quint64 size = 0;
|
||||
QStringList tags;
|
||||
QString version;
|
||||
};
|
||||
|
||||
class EntryPrivate : public QSharedData
|
||||
{
|
||||
public:
|
||||
EntryPrivate()
|
||||
{
|
||||
qRegisterMetaType<KNSCore::Entry::List>();
|
||||
}
|
||||
|
||||
bool operator==(const EntryPrivate &other) const
|
||||
{
|
||||
return mUniqueId == other.mUniqueId && mProviderId == other.mProviderId;
|
||||
}
|
||||
|
||||
QString mUniqueId;
|
||||
QString mRequestedUniqueId; // We need to map the entry to the request in the ResultsStream, but invalid entries would have an empty ID
|
||||
QString mName;
|
||||
QUrl mHomepage;
|
||||
QString mCategory;
|
||||
QString mLicense;
|
||||
QString mVersion;
|
||||
QDate mReleaseDate = QDate::currentDate();
|
||||
|
||||
// Version and date if a newer version is found (updateable)
|
||||
QString mUpdateVersion;
|
||||
QDate mUpdateReleaseDate;
|
||||
|
||||
Author mAuthor;
|
||||
int mRating = 0;
|
||||
int mNumberOfComments = 0;
|
||||
int mDownloadCount = 0;
|
||||
int mNumberFans = 0;
|
||||
int mNumberKnowledgebaseEntries = 0;
|
||||
QString mKnowledgebaseLink;
|
||||
QString mSummary;
|
||||
QString mShortSummary;
|
||||
QString mChangelog;
|
||||
QString mPayload;
|
||||
QStringList mInstalledFiles;
|
||||
QString mProviderId;
|
||||
QStringList mUnInstalledFiles;
|
||||
QString mDonationLink;
|
||||
QStringList mTags;
|
||||
|
||||
QString mChecksum;
|
||||
QString mSignature;
|
||||
KNSCore::Entry::Status mStatus = Entry::Invalid;
|
||||
Entry::Source mSource = Entry::Online;
|
||||
Entry::EntryType mEntryType = Entry::CatalogEntry;
|
||||
|
||||
QString mPreviewUrl[6];
|
||||
QImage mPreviewImage[6];
|
||||
|
||||
QList<DownloadLinkInformationV2Private> mDownloadLinkInformationList;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "errorcode.h"
|
||||
|
||||
#include "moc_errorcode.cpp"
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
knewstuff3/errorcode.h.
|
||||
SPDX-FileCopyrightText: 2018 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNSCORE_ERRORCODE_H
|
||||
#define KNSCORE_ERRORCODE_H
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
#include <qobjectdefs.h>
|
||||
|
||||
namespace KNSCore::ErrorCode
|
||||
{
|
||||
Q_NAMESPACE_EXPORT(KNEWSTUFFCORE_EXPORT)
|
||||
/**
|
||||
* An enumeration of specific error conditions which might occur and which
|
||||
* users of KNewStuff would want to react to. It is used by both the Engine and
|
||||
* Provider classes.
|
||||
* @since 5.53
|
||||
* TODO: KF6 do not repeat entry properties in the QVariantList
|
||||
*/
|
||||
enum ErrorCode {
|
||||
UnknownError, ///< An unknown error (this should not be used, an error report of this nature should be considered a bug)
|
||||
NetworkError, ///< A network error. In signalErrorCode, this will be accompanied by the QtNetwork error code in the metadata
|
||||
OcsError, ///< An error reported by the OCS API server. In signalErrorCode, this will be accompanied by the OCS error code in the metadata
|
||||
ConfigFileError, ///< The configuration file is missing or somehow incorrect. The configuration file filename will be held in the metadata
|
||||
ProviderError, ///< A provider has failed to load or initialize. The provider file URL or provider URL will be held in the metadata
|
||||
InstallationError, ///< Installation of a content item has failed. If known, the entry's unique ID will be the metadata
|
||||
ImageError, ///< Loading an image has failed. The entry name and preview type which failed will be held in the metadata as a QVariantList
|
||||
AdoptionError, ///< Adopting one entry has failed. The adoption command will be in the metadata as a QVariantList.
|
||||
TryAgainLaterError, ///< Specific error condition for failed network calls which explicitly request an amount of time to wait before retrying (generally
|
||||
///< interpreted as maintenance). The retry will be scheduled automatically, and this code can be used to show the user how long
|
||||
///< they have to wait. The time after which the user can try again can be read as a QDateTime in the metadata. @since 5.84
|
||||
};
|
||||
Q_ENUM_NS(ErrorCode)
|
||||
}
|
||||
#endif // KNSCORE_ERRORCODE_H
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2006, 2007 Josef Spillner <spillner@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "imageloader_p.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
ImageLoader::ImageLoader(const Entry &entry, Entry::PreviewType type, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_entry(entry)
|
||||
, m_previewType(type)
|
||||
{
|
||||
}
|
||||
|
||||
void ImageLoader::start()
|
||||
{
|
||||
QUrl url(m_entry.previewUrl(m_previewType));
|
||||
if (!url.isEmpty()) {
|
||||
m_job = HTTPJob::get(url, NoReload, JobFlag::HideProgressInfo, this);
|
||||
connect(m_job, &KJob::result, this, &ImageLoader::slotDownload);
|
||||
connect(m_job, &HTTPJob::data, this, &ImageLoader::slotData);
|
||||
} else {
|
||||
Q_EMIT signalError(m_entry, m_previewType, QStringLiteral("Empty url"));
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
KJob *ImageLoader::job()
|
||||
{
|
||||
return m_job;
|
||||
}
|
||||
|
||||
void ImageLoader::slotData(KJob * /*job*/, const QByteArray &buf)
|
||||
{
|
||||
m_buffer.append(buf);
|
||||
}
|
||||
|
||||
void ImageLoader::slotDownload(KJob *job)
|
||||
{
|
||||
if (job->error()) {
|
||||
m_buffer.clear();
|
||||
Q_EMIT signalError(m_entry, m_previewType, job->errorText());
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
QImage image;
|
||||
image.loadFromData(std::move(m_buffer));
|
||||
|
||||
if (m_previewType == Entry::PreviewSmall1 || m_previewType == Entry::PreviewSmall2 || m_previewType == Entry::PreviewSmall3) {
|
||||
if (image.width() > PreviewWidth || image.height() > PreviewHeight) {
|
||||
// if the preview is really big, first scale fast to a smaller size, then smooth to desired size
|
||||
if (image.width() > 4 * PreviewWidth || image.height() > 4 * PreviewHeight) {
|
||||
image = image.scaled(2 * PreviewWidth, 2 * PreviewHeight, Qt::KeepAspectRatio, Qt::FastTransformation);
|
||||
}
|
||||
image = image.scaled(PreviewWidth, PreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
} else if (image.width() <= PreviewWidth / 2 && image.height() <= PreviewHeight / 2) {
|
||||
// upscale tiny previews to double size
|
||||
image = image.scaled(2 * image.width(), 2 * image.height());
|
||||
}
|
||||
}
|
||||
|
||||
m_entry.setPreviewImage(image, m_previewType);
|
||||
Q_EMIT signalPreviewLoaded(m_entry, m_previewType);
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
#include "moc_imageloader_p.cpp"
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2006, 2007 Josef Spillner <spillner@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_IMAGELOADER_P_H
|
||||
#define KNEWSTUFF3_IMAGELOADER_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QObject>
|
||||
|
||||
#include "entry.h"
|
||||
#include "jobs/httpjob.h"
|
||||
|
||||
class KJob;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
/**
|
||||
* Convenience class for images with remote sources.
|
||||
*
|
||||
* This class represents a fire-and-forget approach of loading images
|
||||
* in applications. The image will load itself.
|
||||
* Using this class also requires using QAsyncFrame or similar UI
|
||||
* elements which allow for asynchronous image loading.
|
||||
*
|
||||
* This class is used internally by the DownloadDialog class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT ImageLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImageLoader(const Entry &entry, Entry::PreviewType type, QObject *parent);
|
||||
void start();
|
||||
/**
|
||||
* Get the job doing the image loading in the background (to have progress information available)
|
||||
* @return the job
|
||||
*/
|
||||
KJob *job();
|
||||
|
||||
Q_SIGNALS:
|
||||
void signalPreviewLoaded(const KNSCore::Entry &, KNSCore::Entry::PreviewType);
|
||||
void signalError(const KNSCore::Entry &, KNSCore::Entry::PreviewType, const QString &);
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotDownload(KJob *job);
|
||||
void slotData(KJob *job, const QByteArray &buf);
|
||||
|
||||
private:
|
||||
Entry m_entry;
|
||||
const Entry::PreviewType m_previewType;
|
||||
QByteArray m_buffer;
|
||||
HTTPJob *m_job = nullptr;
|
||||
};
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,690 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "installation_p.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryFile>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "karchive.h"
|
||||
#include "knewstuff_version.h"
|
||||
#include "qmimedatabase.h"
|
||||
#include <KRandom>
|
||||
#include <KShell>
|
||||
#include <KTar>
|
||||
#include <KZip>
|
||||
|
||||
#include <KPackage/Package>
|
||||
#include <KPackage/PackageJob>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <knewstuffcore_debug.h>
|
||||
#include <qstandardpaths.h>
|
||||
|
||||
#include "jobs/filecopyjob.h"
|
||||
#include "question.h"
|
||||
#ifdef Q_OS_WIN
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
Installation::Installation(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool Installation::readConfig(const KConfigGroup &group, QString &errorMessage)
|
||||
{
|
||||
// FIXME: add support for several categories later on
|
||||
const QString uncompression = group.readEntry("Uncompress", QStringLiteral("never"));
|
||||
if (uncompression == QLatin1String("always") || uncompression == QLatin1String("true")) {
|
||||
uncompressSetting = AlwaysUncompress;
|
||||
} else if (uncompression == QLatin1String("archive")) {
|
||||
uncompressSetting = UncompressIfArchive;
|
||||
} else if (uncompression == QLatin1String("subdir")) {
|
||||
uncompressSetting = UncompressIntoSubdir;
|
||||
} else if (uncompression == QLatin1String("kpackage")) {
|
||||
uncompressSetting = UseKPackageUncompression;
|
||||
} else if (uncompression == QLatin1String("subdir-archive")) {
|
||||
uncompressSetting = UncompressIntoSubdirIfArchive;
|
||||
} else if (uncompression == QLatin1String("never")) {
|
||||
uncompressSetting = NeverUncompress;
|
||||
} else {
|
||||
errorMessage = QStringLiteral("invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage");
|
||||
qCCritical(KNEWSTUFFCORE) << errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
kpackageStructure = group.readEntry("KPackageStructure");
|
||||
if (uncompressSetting == UseKPackageUncompression && kpackageStructure.isEmpty()) {
|
||||
errorMessage = QStringLiteral("kpackage uncompress setting chosen, but no KPackageStructure specified");
|
||||
qCCritical(KNEWSTUFFCORE) << errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
postInstallationCommand = group.readEntry("InstallationCommand");
|
||||
uninstallCommand = group.readEntry("UninstallCommand");
|
||||
standardResourceDirectory = group.readEntry("StandardResource");
|
||||
targetDirectory = group.readEntry("TargetDir");
|
||||
xdgTargetDirectory = group.readEntry("XdgTargetDir");
|
||||
|
||||
installPath = group.readEntry("InstallPath");
|
||||
absoluteInstallPath = group.readEntry("AbsoluteInstallPath");
|
||||
|
||||
if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty()
|
||||
&& absoluteInstallPath.isEmpty()) {
|
||||
qCCritical(KNEWSTUFFCORE) << "No installation target set";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Installation::install(const Entry &entry)
|
||||
{
|
||||
downloadPayload(entry);
|
||||
}
|
||||
|
||||
void Installation::downloadPayload(const KNSCore::Entry &entry)
|
||||
{
|
||||
if (!entry.isValid()) {
|
||||
Q_EMIT signalInstallationFailed(i18n("Invalid item."), entry);
|
||||
return;
|
||||
}
|
||||
QUrl source = QUrl(entry.payload());
|
||||
|
||||
if (!source.isValid()) {
|
||||
qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload.";
|
||||
Q_EMIT signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name()), entry);
|
||||
return;
|
||||
}
|
||||
|
||||
QString fileName(source.fileName());
|
||||
QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName);
|
||||
tempFile.setAutoRemove(false);
|
||||
if (!tempFile.open()) {
|
||||
return; // ERROR
|
||||
}
|
||||
QUrl destination = QUrl::fromLocalFile(tempFile.fileName());
|
||||
qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination;
|
||||
#ifdef Q_OS_WIN // can't write to the file if it's open, on Windows
|
||||
tempFile.close();
|
||||
#endif
|
||||
|
||||
// FIXME: check for validity
|
||||
FileCopyJob *job = FileCopyJob::file_copy(source, destination, -1, JobFlag::Overwrite | JobFlag::HideProgressInfo);
|
||||
connect(job, &KJob::result, this, &Installation::slotPayloadResult);
|
||||
|
||||
entry_jobs[job] = entry;
|
||||
}
|
||||
|
||||
void Installation::slotPayloadResult(KJob *job)
|
||||
{
|
||||
// for some reason this slot is getting called 3 times on one job error
|
||||
if (entry_jobs.contains(job)) {
|
||||
Entry entry = entry_jobs[job];
|
||||
entry_jobs.remove(job);
|
||||
|
||||
if (job->error()) {
|
||||
const QString errorMessage = i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString());
|
||||
qCWarning(KNEWSTUFFCORE) << errorMessage;
|
||||
Q_EMIT signalInstallationFailed(errorMessage, entry);
|
||||
} else {
|
||||
FileCopyJob *fcjob = static_cast<FileCopyJob *>(job);
|
||||
qCDebug(KNEWSTUFFCORE) << "Copied to" << fcjob->destUrl();
|
||||
QMimeDatabase db;
|
||||
QMimeType mimeType = db.mimeTypeForFile(fcjob->destUrl().toLocalFile());
|
||||
if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) {
|
||||
const auto error = i18n("Cannot install '%1' because it points to a web page. Click <a href='%2'>here</a> to finish the installation.",
|
||||
entry.name(),
|
||||
fcjob->srcUrl().toString());
|
||||
Q_EMIT signalInstallationFailed(error, entry);
|
||||
entry.setStatus(KNSCore::Entry::Invalid);
|
||||
Q_EMIT signalEntryChanged(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
Q_EMIT signalPayloadLoaded(fcjob->destUrl());
|
||||
install(entry, fcjob->destUrl().toLocalFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void KNSCore::Installation::install(KNSCore::Entry entry, const QString &downloadedFile)
|
||||
{
|
||||
qCWarning(KNEWSTUFFCORE) << "Install:" << entry.name() << "from" << downloadedFile;
|
||||
Q_ASSERT(QFileInfo::exists(downloadedFile));
|
||||
|
||||
if (entry.payload().isEmpty()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "No payload associated with:" << entry.name();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO Add async checksum verification
|
||||
|
||||
QString targetPath = targetInstallationPath();
|
||||
QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath);
|
||||
|
||||
if (uncompressionSetting() != UseKPackageUncompression) {
|
||||
if (installedFiles.isEmpty()) {
|
||||
if (entry.status() == KNSCore::Entry::Installing) {
|
||||
entry.setStatus(KNSCore::Entry::Downloadable);
|
||||
} else if (entry.status() == KNSCore::Entry::Updating) {
|
||||
entry.setStatus(KNSCore::Entry::Updateable);
|
||||
}
|
||||
Q_EMIT signalEntryChanged(entry);
|
||||
Q_EMIT signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name()), entry);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.setInstalledFiles(installedFiles);
|
||||
|
||||
auto installationFinished = [this, entry]() {
|
||||
Entry newentry = entry;
|
||||
if (!newentry.updateVersion().isEmpty()) {
|
||||
newentry.setVersion(newentry.updateVersion());
|
||||
}
|
||||
if (newentry.updateReleaseDate().isValid()) {
|
||||
newentry.setReleaseDate(newentry.updateReleaseDate());
|
||||
}
|
||||
newentry.setStatus(KNSCore::Entry::Installed);
|
||||
Q_EMIT signalEntryChanged(newentry);
|
||||
Q_EMIT signalInstallationFinished(newentry);
|
||||
};
|
||||
if (!postInstallationCommand.isEmpty()) {
|
||||
QString scriptArgPath = !installedFiles.isEmpty() ? installedFiles.first() : targetPath;
|
||||
if (scriptArgPath.endsWith(QLatin1Char('*'))) {
|
||||
scriptArgPath = scriptArgPath.left(scriptArgPath.lastIndexOf(QLatin1Char('*')));
|
||||
}
|
||||
QProcess *p = runPostInstallationCommand(scriptArgPath, entry);
|
||||
connect(p, &QProcess::finished, this, [entry, installationFinished, this](int exitCode) {
|
||||
if (exitCode) {
|
||||
Entry newEntry = entry;
|
||||
newEntry.setStatus(KNSCore::Entry::Invalid);
|
||||
Q_EMIT signalEntryChanged(newEntry);
|
||||
} else {
|
||||
installationFinished();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
installationFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Installation::targetInstallationPath() const
|
||||
{
|
||||
// installdir is the target directory
|
||||
QString installdir;
|
||||
|
||||
const bool userScope = true;
|
||||
// installpath also contains the file name if it's a single file, otherwise equal to installdir
|
||||
int pathcounter = 0;
|
||||
// wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty()
|
||||
if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) {
|
||||
QStandardPaths::StandardLocation location = QStandardPaths::TempLocation;
|
||||
// crude translation KStandardDirs names -> QStandardPaths enum
|
||||
if (standardResourceDirectory == QLatin1String("tmp")) {
|
||||
location = QStandardPaths::TempLocation;
|
||||
} else if (standardResourceDirectory == QLatin1String("config")) {
|
||||
location = QStandardPaths::ConfigLocation;
|
||||
}
|
||||
|
||||
if (userScope) {
|
||||
installdir = QStandardPaths::writableLocation(location);
|
||||
} else { // system scope
|
||||
installdir = QStandardPaths::standardLocations(location).constLast();
|
||||
}
|
||||
pathcounter++;
|
||||
}
|
||||
if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) {
|
||||
if (userScope) {
|
||||
installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/');
|
||||
} else { // system scope
|
||||
installdir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, targetDirectory, QStandardPaths::LocateDirectory) + QLatin1Char('/');
|
||||
}
|
||||
pathcounter++;
|
||||
}
|
||||
if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) {
|
||||
installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/');
|
||||
pathcounter++;
|
||||
}
|
||||
if (!installPath.isEmpty()) {
|
||||
#if defined(Q_OS_WIN)
|
||||
WCHAR wPath[MAX_PATH + 1];
|
||||
if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) {
|
||||
installdir = QString::fromUtf16((const char16_t *)wPath) + QLatin1Char('/') + installPath + QLatin1Char('/');
|
||||
} else {
|
||||
installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
|
||||
}
|
||||
#else
|
||||
installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/');
|
||||
#endif
|
||||
pathcounter++;
|
||||
}
|
||||
if (!absoluteInstallPath.isEmpty()) {
|
||||
installdir = absoluteInstallPath + QLatin1Char('/');
|
||||
pathcounter++;
|
||||
}
|
||||
|
||||
if (pathcounter != 1) {
|
||||
qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given.";
|
||||
return QString();
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir;
|
||||
|
||||
// create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!)
|
||||
QDir().mkpath(installdir);
|
||||
|
||||
return installdir;
|
||||
}
|
||||
|
||||
QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::Entry &entry, const QString &payloadfile, const QString installdir)
|
||||
{
|
||||
// Collect all files that were installed
|
||||
QStringList installedFiles;
|
||||
bool isarchive = true;
|
||||
UncompressionOptions uncompressionOpt = uncompressionSetting();
|
||||
|
||||
// respect the uncompress flag in the knsrc
|
||||
if (uncompressionOpt == UseKPackageUncompression) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation";
|
||||
auto resetEntryStatus = [this, entry]() {
|
||||
KNSCore::Entry changedEntry(entry);
|
||||
if (changedEntry.status() == KNSCore::Entry::Installing || changedEntry.status() == KNSCore::Entry::Installed) {
|
||||
changedEntry.setStatus(KNSCore::Entry::Downloadable);
|
||||
} else if (changedEntry.status() == KNSCore::Entry::Updating) {
|
||||
changedEntry.setStatus(KNSCore::Entry::Updateable);
|
||||
}
|
||||
Q_EMIT signalEntryChanged(changedEntry);
|
||||
};
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << payloadfile << "as" << kpackageStructure;
|
||||
auto job = KPackage::PackageJob::update(kpackageStructure, payloadfile);
|
||||
connect(job, &KPackage::PackageJob::finished, this, [this, entry, payloadfile, resetEntryStatus, job]() {
|
||||
if (job->error() == KJob::NoError) {
|
||||
Entry newentry = entry;
|
||||
newentry.setInstalledFiles(QStringList{job->package().path()});
|
||||
// update version and release date to the new ones
|
||||
if (newentry.status() == KNSCore::Entry::Updating) {
|
||||
if (!newentry.updateVersion().isEmpty()) {
|
||||
newentry.setVersion(newentry.updateVersion());
|
||||
}
|
||||
if (newentry.updateReleaseDate().isValid()) {
|
||||
newentry.setReleaseDate(newentry.updateReleaseDate());
|
||||
}
|
||||
}
|
||||
newentry.setStatus(KNSCore::Entry::Installed);
|
||||
// We can remove the downloaded file, because we don't save its location and don't need it to uninstall the entry
|
||||
QFile::remove(payloadfile);
|
||||
Q_EMIT signalEntryChanged(newentry);
|
||||
Q_EMIT signalInstallationFinished(newentry);
|
||||
qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << job->package().path();
|
||||
} else {
|
||||
if (job->error() == KPackage::PackageJob::JobError::NewerVersionAlreadyInstalledError) {
|
||||
Entry newentry = entry;
|
||||
newentry.setStatus(KNSCore::Entry::Installed);
|
||||
newentry.setInstalledFiles(QStringList{job->package().path()});
|
||||
// update version and release date to the new ones
|
||||
if (!newentry.updateVersion().isEmpty()) {
|
||||
newentry.setVersion(newentry.updateVersion());
|
||||
}
|
||||
if (newentry.updateReleaseDate().isValid()) {
|
||||
newentry.setReleaseDate(newentry.updateReleaseDate());
|
||||
}
|
||||
Q_EMIT signalEntryChanged(newentry);
|
||||
Q_EMIT signalInstallationFinished(newentry);
|
||||
qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's "
|
||||
"just make a small fib and say we totally installed that, honest, and we now have files"
|
||||
<< job->package().path();
|
||||
} else {
|
||||
Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText()), entry);
|
||||
resetEntryStatus();
|
||||
qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (uncompressionOpt == AlwaysUncompress || uncompressionOpt == UncompressIntoSubdirIfArchive || uncompressionOpt == UncompressIfArchive
|
||||
|| uncompressionOpt == UncompressIntoSubdir) {
|
||||
// this is weird but a decompression is not a single name, so take the path instead
|
||||
QMimeDatabase db;
|
||||
QMimeType mimeType = db.mimeTypeForFile(payloadfile);
|
||||
qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file";
|
||||
|
||||
// FIXME: check for overwriting, malicious archive entries (../foo) etc.
|
||||
// FIXME: KArchive should provide "safe mode" for this!
|
||||
QScopedPointer<KArchive> archive;
|
||||
|
||||
if (mimeType.inherits(QStringLiteral("application/zip"))) {
|
||||
archive.reset(new KZip(payloadfile));
|
||||
// clang-format off
|
||||
} else if (mimeType.inherits(QStringLiteral("application/tar"))
|
||||
|| mimeType.inherits(QStringLiteral("application/x-tar")) // BUG 450662
|
||||
|| mimeType.inherits(QStringLiteral("application/x-gzip"))
|
||||
|| mimeType.inherits(QStringLiteral("application/x-bzip"))
|
||||
|| mimeType.inherits(QStringLiteral("application/x-lzma"))
|
||||
|| mimeType.inherits(QStringLiteral("application/x-xz"))
|
||||
|| mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar"))
|
||||
|| mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) {
|
||||
// clang-format on
|
||||
archive.reset(new KTar(payloadfile));
|
||||
} else {
|
||||
qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file" << payloadfile;
|
||||
if (uncompressionOpt == AlwaysUncompress) {
|
||||
Q_EMIT signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile), entry);
|
||||
return QStringList();
|
||||
}
|
||||
isarchive = false;
|
||||
}
|
||||
|
||||
if (isarchive) {
|
||||
bool success = archive->open(QIODevice::ReadOnly);
|
||||
if (!success) {
|
||||
qCCritical(KNEWSTUFFCORE) << "Cannot open archive file" << payloadfile;
|
||||
if (uncompressionOpt == AlwaysUncompress) {
|
||||
Q_EMIT signalInstallationError(
|
||||
i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString()),
|
||||
entry);
|
||||
return QStringList();
|
||||
}
|
||||
// otherwise, just copy the file
|
||||
isarchive = false;
|
||||
}
|
||||
|
||||
if (isarchive) {
|
||||
const KArchiveDirectory *dir = archive->directory();
|
||||
// if there is more than an item in the file, and we are requested to do so
|
||||
// put contents in a subdirectory with the same name as the file
|
||||
QString installpath;
|
||||
const bool isSubdir =
|
||||
(uncompressionOpt == UncompressIntoSubdir || uncompressionOpt == UncompressIntoSubdirIfArchive) && dir->entries().count() > 1;
|
||||
if (isSubdir) {
|
||||
installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName();
|
||||
} else {
|
||||
installpath = installdir;
|
||||
}
|
||||
|
||||
if (dir->copyTo(installpath)) {
|
||||
// If we extracted the subdir we want to save it using the /* notation like we would when using the "archive" option
|
||||
// Also if we use an (un)install command we only call it once with the folder as argument and not for each file
|
||||
if (isSubdir) {
|
||||
installedFiles << QDir(installpath).absolutePath() + QLatin1String("/*");
|
||||
} else {
|
||||
installedFiles << archiveEntries(installpath, dir);
|
||||
}
|
||||
} else {
|
||||
qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath;
|
||||
}
|
||||
|
||||
archive->close();
|
||||
QFile::remove(payloadfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "isarchive:" << isarchive;
|
||||
|
||||
// some wallpapers are compressed, some aren't
|
||||
if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper"))
|
||||
|| (uncompressionOpt == NeverUncompress || (uncompressionOpt == UncompressIfArchive && !isarchive)
|
||||
|| (uncompressionOpt == UncompressIntoSubdirIfArchive && !isarchive))) {
|
||||
// no decompress but move to target
|
||||
|
||||
/// @todo when using KIO::get the http header can be accessed and it contains a real file name.
|
||||
// FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names
|
||||
QUrl source = QUrl(entry.payload());
|
||||
qCDebug(KNEWSTUFFCORE) << "installing non-archive from" << source;
|
||||
const QString installpath = QDir(installdir).filePath(source.fileName());
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "Install to file" << installpath;
|
||||
// FIXME: copy goes here (including overwrite checking)
|
||||
// FIXME: what must be done now is to update the cache *again*
|
||||
// in order to set the new payload filename (on root tag only)
|
||||
// - this might or might not need to take uncompression into account
|
||||
// FIXME: for updates, we might need to force an overwrite (that is, deleting before)
|
||||
QFile file(payloadfile);
|
||||
bool success = true;
|
||||
const bool update = ((entry.status() == KNSCore::Entry::Updateable) || (entry.status() == KNSCore::Entry::Updating));
|
||||
|
||||
if (QFile::exists(installpath) && QDir::tempPath() != installdir) {
|
||||
if (!update) {
|
||||
Question question(Question::ContinueCancelQuestion);
|
||||
question.setEntry(entry);
|
||||
question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means "
|
||||
"overwriting it. Do you wish to overwrite the existing file?")
|
||||
+ QStringLiteral("\n'") + installpath + QLatin1Char('\''));
|
||||
question.setTitle(i18n("Overwrite File"));
|
||||
if (question.ask() != Question::ContinueResponse) {
|
||||
return QStringList();
|
||||
}
|
||||
}
|
||||
success = QFile::remove(installpath);
|
||||
}
|
||||
if (success) {
|
||||
// remove in case it's already present and in a temporary directory, so we get to actually use the path again
|
||||
if (installpath.startsWith(QDir::tempPath())) {
|
||||
QFile::remove(installpath);
|
||||
}
|
||||
success = file.rename(installpath);
|
||||
qCWarning(KNEWSTUFFCORE) << "move:" << file.fileName() << "to" << installpath;
|
||||
if (!success) {
|
||||
qCWarning(KNEWSTUFFCORE) << file.errorString();
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
Q_EMIT signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath), entry);
|
||||
qCCritical(KNEWSTUFFCORE) << "Cannot move file" << payloadfile << "to destination" << installpath;
|
||||
return QStringList();
|
||||
}
|
||||
installedFiles << installpath;
|
||||
}
|
||||
}
|
||||
|
||||
return installedFiles;
|
||||
}
|
||||
|
||||
QProcess *Installation::runPostInstallationCommand(const QString &installPath, const KNSCore::Entry &entry)
|
||||
{
|
||||
QString command(postInstallationCommand);
|
||||
QString fileArg(KShell::quoteArg(installPath));
|
||||
command.replace(QLatin1String("%f"), fileArg);
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "Run command:" << command;
|
||||
|
||||
QProcess *ret = new QProcess(this);
|
||||
auto onProcessFinished = [this, command, ret, entry](int exitcode, QProcess::ExitStatus status) {
|
||||
const QString output{QString::fromLocal8Bit(ret->readAllStandardError())};
|
||||
if (status == QProcess::CrashExit) {
|
||||
QString errorMessage = i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2", command, output);
|
||||
Q_EMIT signalInstallationError(errorMessage, entry);
|
||||
qCCritical(KNEWSTUFFCORE) << "Process crashed with command:" << command;
|
||||
} else if (exitcode) {
|
||||
// 130 means Ctrl+C as an exit code this is interpreted by KNewStuff as cancel operation
|
||||
// and no error will be displayed to the user, BUG: 436355
|
||||
if (exitcode == 130) {
|
||||
qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed was aborted by the user";
|
||||
Q_EMIT signalInstallationFinished(entry);
|
||||
} else {
|
||||
Q_EMIT signalInstallationError(
|
||||
i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3",
|
||||
exitcode,
|
||||
command,
|
||||
output),
|
||||
entry);
|
||||
qCCritical(KNEWSTUFFCORE) << "Command" << command << "failed with code" << exitcode;
|
||||
}
|
||||
}
|
||||
sender()->deleteLater();
|
||||
};
|
||||
connect(ret, &QProcess::finished, this, onProcessFinished);
|
||||
|
||||
QStringList args = KShell::splitArgs(command);
|
||||
ret->setProgram(args.takeFirst());
|
||||
ret->setArguments(args);
|
||||
ret->start();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Installation::uninstall(Entry entry)
|
||||
{
|
||||
const auto deleteFilesAndMarkAsUninstalled = [entry, this]() {
|
||||
bool deletionSuccessful = true;
|
||||
const auto lst = entry.installedFiles();
|
||||
for (const QString &file : lst) {
|
||||
// This is used to delete the download location if there are no more entries
|
||||
QFileInfo info(file);
|
||||
if (info.isDir()) {
|
||||
QDir().rmdir(file);
|
||||
} else if (file.endsWith(QLatin1String("/*"))) {
|
||||
QDir dir(file.left(file.size() - 2));
|
||||
bool worked = dir.removeRecursively();
|
||||
if (!worked) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (info.exists() || info.isSymLink()) {
|
||||
bool worked = QFile::remove(file);
|
||||
if (!worked) {
|
||||
qWarning() << "unable to delete file " << file;
|
||||
Q_EMIT signalInstallationFailed(
|
||||
i18n("The removal of %1 failed, as the installed file %2 could not be automatically removed. You can attempt to manually delete "
|
||||
"this file, if you believe this is an error.",
|
||||
entry.name(),
|
||||
file),
|
||||
entry);
|
||||
// Assume that the uninstallation has failed, and reset the entry to an installed state
|
||||
deletionSuccessful = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "unable to delete file " << file << ". file does not exist.";
|
||||
}
|
||||
}
|
||||
}
|
||||
Entry newEntry = entry;
|
||||
if (deletionSuccessful) {
|
||||
newEntry.setEntryDeleted();
|
||||
} else {
|
||||
newEntry.setStatus(KNSCore::Entry::Installed);
|
||||
}
|
||||
|
||||
Q_EMIT signalEntryChanged(newEntry);
|
||||
};
|
||||
|
||||
if (uncompressionSetting() == UseKPackageUncompression) {
|
||||
const auto lst = entry.installedFiles();
|
||||
if (lst.length() == 1) {
|
||||
const QString installedFile{lst.first()};
|
||||
|
||||
KJob *job = KPackage::PackageJob::uninstall(kpackageStructure, installedFile);
|
||||
connect(job, &KJob::result, this, [this, installedFile, entry, job]() {
|
||||
Entry newEntry = entry;
|
||||
if (job->error() == KJob::NoError) {
|
||||
newEntry.setEntryDeleted();
|
||||
Q_EMIT signalEntryChanged(newEntry);
|
||||
} else {
|
||||
Q_EMIT signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText()), entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
deleteFilesAndMarkAsUninstalled();
|
||||
} else {
|
||||
const auto lst = entry.installedFiles();
|
||||
// If there is an uninstall script, make sure it runs without errors
|
||||
if (!uninstallCommand.isEmpty()) {
|
||||
bool validFileExisted = false;
|
||||
for (const QString &file : lst) {
|
||||
QString filePath = file;
|
||||
bool validFile = QFileInfo::exists(filePath);
|
||||
// If we have uncompressed a subdir we write <path>/* in the config, but when calling a script
|
||||
// we want to convert this to a normal path
|
||||
if (!validFile && file.endsWith(QLatin1Char('*'))) {
|
||||
filePath = filePath.left(filePath.lastIndexOf(QLatin1Char('*')));
|
||||
validFile = QFileInfo::exists(filePath);
|
||||
}
|
||||
if (validFile) {
|
||||
validFileExisted = true;
|
||||
QString fileArg(KShell::quoteArg(filePath));
|
||||
QString command(uninstallCommand);
|
||||
command.replace(QLatin1String("%f"), fileArg);
|
||||
|
||||
QStringList args = KShell::splitArgs(command);
|
||||
const QString program = args.takeFirst();
|
||||
QProcess *process = new QProcess(this);
|
||||
process->start(program, args);
|
||||
auto onProcessFinished = [this, command, process, entry, deleteFilesAndMarkAsUninstalled](int, QProcess::ExitStatus status) {
|
||||
if (status == QProcess::CrashExit) {
|
||||
const QString processOutput = QString::fromLocal8Bit(process->readAllStandardError());
|
||||
const QString err = i18n(
|
||||
"The uninstallation process failed to successfully run the command %1\n"
|
||||
"The output of was: \n%2\n"
|
||||
"If you think this is incorrect, you can continue or cancel the uninstallation process",
|
||||
KShell::quoteArg(command),
|
||||
processOutput);
|
||||
Q_EMIT signalInstallationError(err, entry);
|
||||
// Ask the user if he wants to continue, even though the script failed
|
||||
Question question(Question::ContinueCancelQuestion);
|
||||
question.setEntry(entry);
|
||||
question.setQuestion(err);
|
||||
Question::Response response = question.ask();
|
||||
if (response == Question::CancelResponse) {
|
||||
// Use can delete files manually
|
||||
Entry newEntry = entry;
|
||||
newEntry.setStatus(KNSCore::Entry::Installed);
|
||||
Q_EMIT signalEntryChanged(newEntry);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "Command executed successfully:" << command;
|
||||
}
|
||||
deleteFilesAndMarkAsUninstalled();
|
||||
};
|
||||
connect(process, &QProcess::finished, this, onProcessFinished);
|
||||
}
|
||||
}
|
||||
// If the entry got deleted, but the RemoveDeadEntries option was not selected this case can happen
|
||||
if (!validFileExisted) {
|
||||
deleteFilesAndMarkAsUninstalled();
|
||||
}
|
||||
} else {
|
||||
deleteFilesAndMarkAsUninstalled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Installation::UncompressionOptions Installation::uncompressionSetting() const
|
||||
{
|
||||
return uncompressSetting;
|
||||
}
|
||||
|
||||
QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir)
|
||||
{
|
||||
QStringList files;
|
||||
const auto lst = dir->entries();
|
||||
for (const QString &entry : lst) {
|
||||
const auto currentEntry = dir->entry(entry);
|
||||
|
||||
const QString childPath = QDir(path).filePath(entry);
|
||||
if (currentEntry->isFile()) {
|
||||
files << childPath;
|
||||
} else if (currentEntry->isDirectory()) {
|
||||
files << childPath + QStringLiteral("/*");
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
#include "moc_installation_p.cpp"
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_INSTALLATION_P_H
|
||||
#define KNEWSTUFF3_INSTALLATION_P_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <KConfigGroup>
|
||||
|
||||
#include "entry.h"
|
||||
|
||||
class QProcess;
|
||||
class KArchiveDirectory;
|
||||
class KJob;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
/**
|
||||
* @short KNewStuff entry installation.
|
||||
*
|
||||
* The installation class stores all information related to an entry's
|
||||
* installation.
|
||||
*
|
||||
* @author Josef Spillner (spillner@kde.org)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT Installation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
explicit Installation(QObject *parent = nullptr);
|
||||
enum UncompressionOptions {
|
||||
NeverUncompress, ///@< Never attempt to decompress a file, whatever format it is. Matches "never" knsrc setting
|
||||
AlwaysUncompress, ///@< Assume all downloaded files are archives, and attempt to decompress them. Will cause failure if decompression fails. Matches
|
||||
///"always" knsrc setting
|
||||
UncompressIfArchive, ///@< If the file is an archive, decompress it, otherwise just pass it on. Matches "archive" knsrc setting
|
||||
UncompressIntoSubdirIfArchive, ///@< If the file is an archive, decompress it in a subdirectory if it contains multiple files, otherwise just pass it
|
||||
/// on. Matches "subdir-archive" knsrc setting
|
||||
UncompressIntoSubdir, ///@< As Archive, except that if there is more than an item in the file, put contents in a subdirectory with the same name as the
|
||||
/// file. Matches "subdir" knsrc setting
|
||||
UseKPackageUncompression, ///@< Use the internal KPackage support for installing and uninstalling the package. Matches "kpackage" knsrc setting
|
||||
};
|
||||
Q_ENUM(UncompressionOptions)
|
||||
|
||||
bool readConfig(const KConfigGroup &group, QString &errorMessage);
|
||||
|
||||
QString targetInstallationPath() const;
|
||||
|
||||
/**
|
||||
* Returns the uncompression setting, in a computer-readable format
|
||||
*
|
||||
* @return The value of this setting
|
||||
*/
|
||||
UncompressionOptions uncompressionSetting() const;
|
||||
|
||||
public Q_SLOTS:
|
||||
/**
|
||||
* Downloads a payload file. The payload file matching most closely
|
||||
* the current user language preferences will be downloaded.
|
||||
* The file will not be installed set, for this \ref install must
|
||||
* be called.
|
||||
*
|
||||
* @param entry Entry to download payload file for
|
||||
*
|
||||
* @see signalPayloadLoaded
|
||||
* @see signalPayloadFailed
|
||||
*/
|
||||
void downloadPayload(const KNSCore::Entry &entry);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Note that this method is asynchronous and thus the return value will
|
||||
* only report the successful start of the installation.
|
||||
* Note also that while entry is const at this point, it will change later
|
||||
* during the actual installation (the installedFiles list will change, as
|
||||
* will its status)
|
||||
*
|
||||
* @param entry Entry to be installed
|
||||
*
|
||||
* @see signalInstallationFinished
|
||||
* @see signalInstallationFailed
|
||||
*/
|
||||
void install(const KNSCore::Entry &entry);
|
||||
|
||||
/**
|
||||
* Uninstalls an entry. It reverses the steps which were performed
|
||||
* during the installation.
|
||||
*
|
||||
* The entry emitted by signalEntryChanged will be updated with any new information, in particular the following:
|
||||
* <ul>
|
||||
* <li>Status will be set to Deleted, unless the uninstall
|
||||
* script exists with an error and the user chooses to cancel the uninstallation
|
||||
* <li>uninstalledFiles will list files which were removed during uninstallation
|
||||
* <li>installedFiles will become empty
|
||||
* </ul>
|
||||
*
|
||||
* @param entry The entry to deinstall
|
||||
*
|
||||
*/
|
||||
void uninstall(KNSCore::Entry entry);
|
||||
|
||||
void slotPayloadResult(KJob *job);
|
||||
|
||||
Q_SIGNALS:
|
||||
void signalEntryChanged(const KNSCore::Entry &entry);
|
||||
void signalInstallationFinished(const KNSCore::Entry &entry);
|
||||
void signalInstallationFailed(const QString &message, const KNSCore::Entry &entry);
|
||||
/**
|
||||
* An informational signal fired when a serious error occurs during the installation.
|
||||
* @param message The description of the error (a message intended to be human readable)
|
||||
* @since 5.69
|
||||
*/
|
||||
void signalInstallationError(const QString &message, const KNSCore::Entry &entry);
|
||||
|
||||
void signalPayloadLoaded(QUrl payload); // FIXME: return Entry
|
||||
|
||||
private:
|
||||
void install(KNSCore::Entry entry, const QString &downloadedFile);
|
||||
|
||||
QStringList installDownloadedFileAndUncompress(const KNSCore::Entry &entry, const QString &payloadfile, const QString installdir);
|
||||
QProcess *runPostInstallationCommand(const QString &installPath, const KNSCore::Entry &entry);
|
||||
|
||||
static QStringList archiveEntries(const QString &path, const KArchiveDirectory *dir);
|
||||
|
||||
// applications can set this if they want the installed files/directories to be piped into a shell command
|
||||
QString postInstallationCommand;
|
||||
// a custom command to run for the uninstall
|
||||
QString uninstallCommand;
|
||||
// compression policy
|
||||
|
||||
// only one of the five below can be set, that will be the target install path/file name
|
||||
// FIXME: check this when reading the config and make one path out of it if possible?
|
||||
QString standardResourceDirectory;
|
||||
QString targetDirectory;
|
||||
QString xdgTargetDirectory;
|
||||
QString installPath;
|
||||
QString absoluteInstallPath;
|
||||
|
||||
QMap<KJob *, Entry> entry_jobs;
|
||||
|
||||
QString kpackageStructure;
|
||||
UncompressionOptions uncompressSetting = UncompressionOptions::NeverUncompress;
|
||||
|
||||
Q_DISABLE_COPY(Installation)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
knewstuff3/ui/itemsmodel.cpp.
|
||||
SPDX-FileCopyrightText: 2008 Jeremy Whiting <jpwhiting@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "itemsmodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include "enginebase.h"
|
||||
#include "imageloader_p.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class ItemsModelPrivate
|
||||
{
|
||||
public:
|
||||
ItemsModelPrivate(EngineBase *e)
|
||||
: engine(e)
|
||||
{
|
||||
}
|
||||
EngineBase *const engine;
|
||||
// the list of entries
|
||||
QList<Entry> entries;
|
||||
bool hasPreviewImages = false;
|
||||
};
|
||||
ItemsModel::ItemsModel(EngineBase *engine, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, d(new ItemsModelPrivate(engine))
|
||||
{
|
||||
}
|
||||
|
||||
ItemsModel::~ItemsModel() = default;
|
||||
|
||||
int ItemsModel::rowCount(const QModelIndex & /*parent*/) const
|
||||
{
|
||||
return d->entries.count();
|
||||
}
|
||||
|
||||
QVariant ItemsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role != Qt::UserRole) {
|
||||
return QVariant();
|
||||
}
|
||||
Entry entry = d->entries[index.row()];
|
||||
return QVariant::fromValue(entry);
|
||||
}
|
||||
|
||||
int ItemsModel::row(const Entry &entry) const
|
||||
{
|
||||
return d->entries.indexOf(entry);
|
||||
}
|
||||
|
||||
void ItemsModel::slotEntriesLoaded(const KNSCore::Entry::List &entries)
|
||||
{
|
||||
for (const KNSCore::Entry &entry : entries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemsModel::addEntry(const Entry &entry)
|
||||
{
|
||||
// This might be expensive, but it avoids duplicates, which is not awesome for the user
|
||||
if (!d->entries.contains(entry)) {
|
||||
QString preview = entry.previewUrl(Entry::PreviewSmall1);
|
||||
if (!d->hasPreviewImages && !preview.isEmpty()) {
|
||||
d->hasPreviewImages = true;
|
||||
if (rowCount() > 0) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "adding entry " << entry.name() << " to the model";
|
||||
beginInsertRows(QModelIndex(), d->entries.count(), d->entries.count());
|
||||
d->entries.append(entry);
|
||||
endInsertRows();
|
||||
|
||||
if (!preview.isEmpty() && entry.previewImage(Entry::PreviewSmall1).isNull()) {
|
||||
Q_EMIT loadPreview(entry, Entry::PreviewSmall1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ItemsModel::removeEntry(const Entry &entry)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "removing entry " << entry.name() << " from the model";
|
||||
int index = d->entries.indexOf(entry);
|
||||
if (index > -1) {
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
d->entries.removeAt(index);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
|
||||
void ItemsModel::slotEntryChanged(const Entry &entry)
|
||||
{
|
||||
int i = d->entries.indexOf(entry);
|
||||
if (i == -1) {
|
||||
return;
|
||||
}
|
||||
QModelIndex entryIndex = index(i, 0);
|
||||
Q_EMIT dataChanged(entryIndex, entryIndex);
|
||||
}
|
||||
|
||||
void ItemsModel::clearEntries()
|
||||
{
|
||||
beginResetModel();
|
||||
d->entries.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void ItemsModel::slotEntryPreviewLoaded(const Entry &entry, Entry::PreviewType type)
|
||||
{
|
||||
// we only care about the first small preview in the list
|
||||
if (type == Entry::PreviewSmall1) {
|
||||
slotEntryChanged(entry);
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemsModel::hasPreviewImages() const
|
||||
{
|
||||
return d->hasPreviewImages;
|
||||
}
|
||||
|
||||
} // end KNS namespace
|
||||
|
||||
#include "moc_itemsmodel.cpp"
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
knewstuff3/ui/itemsmodel.h.
|
||||
SPDX-FileCopyrightText: 2008 Jeremy Whiting <jpwhiting@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_ITEMSMODEL_P_H
|
||||
#define KNEWSTUFF3_ITEMSMODEL_P_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <memory>
|
||||
|
||||
#include "entry.h"
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
class KJob;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class EngineBase;
|
||||
class ItemsModelPrivate;
|
||||
|
||||
class KNEWSTUFFCORE_EXPORT ItemsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ItemsModel(EngineBase *engine, QObject *parent = nullptr);
|
||||
~ItemsModel() override;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
/**
|
||||
* The row of the entry passed to the function, or -1 if the entry is not contained
|
||||
* within the model.
|
||||
* @since 5.63
|
||||
*/
|
||||
int row(const Entry &entry) const;
|
||||
|
||||
void addEntry(const Entry &entry);
|
||||
void removeEntry(const Entry &entry);
|
||||
|
||||
bool hasPreviewImages() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void jobStarted(KJob *, const QString &label);
|
||||
void loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type);
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotEntryChanged(const KNSCore::Entry &entry);
|
||||
void slotEntriesLoaded(const KNSCore::Entry::List &entries);
|
||||
void clearEntries();
|
||||
void slotEntryPreviewLoaded(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<ItemsModelPrivate> d;
|
||||
};
|
||||
|
||||
} // end KNS namespace
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "downloadjob.h"
|
||||
|
||||
#include "httpworker.h"
|
||||
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::DownloadJobPrivate
|
||||
{
|
||||
public:
|
||||
DownloadJobPrivate() = default;
|
||||
QUrl source;
|
||||
QUrl destination;
|
||||
};
|
||||
|
||||
DownloadJob::DownloadJob(const QUrl &source, const QUrl &destination, int permissions, JobFlags flags, QObject *parent)
|
||||
: FileCopyJob(source, destination, permissions, flags, parent)
|
||||
, d(new DownloadJobPrivate)
|
||||
{
|
||||
d->source = source;
|
||||
d->destination = destination;
|
||||
}
|
||||
|
||||
DownloadJob::DownloadJob(QObject *parent)
|
||||
: FileCopyJob(parent)
|
||||
, d(new DownloadJobPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
DownloadJob::~DownloadJob() = default;
|
||||
|
||||
void DownloadJob::start()
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << Q_FUNC_INFO;
|
||||
HTTPWorker *worker = new HTTPWorker(d->source, d->destination, HTTPWorker::DownloadJob, this);
|
||||
connect(worker, &HTTPWorker::completed, this, &DownloadJob::handleWorkerCompleted);
|
||||
connect(worker, &HTTPWorker::error, this, &DownloadJob::handleWorkerError);
|
||||
worker->startRequest();
|
||||
}
|
||||
|
||||
void DownloadJob::handleWorkerCompleted()
|
||||
{
|
||||
emitResult();
|
||||
}
|
||||
|
||||
void KNSCore::DownloadJob::handleWorkerError(const QString &error)
|
||||
{
|
||||
setError(KJob::UserDefinedError);
|
||||
setErrorText(error);
|
||||
}
|
||||
|
||||
#include "moc_downloadjob.cpp"
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef DOWNLOADJOB_H
|
||||
#define DOWNLOADJOB_H
|
||||
|
||||
#include "filecopyjob.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class DownloadJobPrivate;
|
||||
class DownloadJob : public FileCopyJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DownloadJob(const QUrl &source, const QUrl &destination, int permissions = -1, JobFlags flags = DefaultFlags, QObject *parent = nullptr);
|
||||
explicit DownloadJob(QObject *parent = nullptr);
|
||||
~DownloadJob() override;
|
||||
|
||||
Q_SCRIPTABLE void start() override;
|
||||
|
||||
protected Q_SLOTS:
|
||||
void handleWorkerCompleted();
|
||||
void handleWorkerError(const QString &error);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<DownloadJobPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // DOWNLOADJOB_H
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "filecopyjob.h"
|
||||
|
||||
#include "downloadjob.h"
|
||||
#include "filecopyworker.h"
|
||||
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::FileCopyJobPrivate
|
||||
{
|
||||
public:
|
||||
QUrl source;
|
||||
QUrl destination;
|
||||
int permissions = -1;
|
||||
JobFlags flags = DefaultFlags;
|
||||
|
||||
FileCopyWorker *worker = nullptr;
|
||||
};
|
||||
|
||||
FileCopyJob::FileCopyJob(const QUrl &source, const QUrl &destination, int permissions, JobFlags flags, QObject *parent)
|
||||
: KJob(parent)
|
||||
, d(new FileCopyJobPrivate)
|
||||
{
|
||||
d->source = source;
|
||||
d->destination = destination;
|
||||
d->permissions = permissions;
|
||||
d->flags = flags;
|
||||
}
|
||||
|
||||
FileCopyJob::FileCopyJob(QObject *parent)
|
||||
: KJob(parent)
|
||||
, d(new FileCopyJobPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
FileCopyJob::~FileCopyJob() = default;
|
||||
|
||||
void FileCopyJob::start()
|
||||
{
|
||||
if (d->worker) {
|
||||
// already started...
|
||||
return;
|
||||
}
|
||||
d->worker = new FileCopyWorker(d->source, d->destination, this);
|
||||
connect(d->worker, &FileCopyWorker::progress, this, &FileCopyJob::handleProgressUpdate);
|
||||
connect(d->worker, &FileCopyWorker::completed, this, &FileCopyJob::handleCompleted);
|
||||
connect(d->worker, &FileCopyWorker::error, this, &FileCopyJob::handleError);
|
||||
d->worker->start();
|
||||
}
|
||||
|
||||
QUrl FileCopyJob::destUrl() const
|
||||
{
|
||||
return d->destination;
|
||||
}
|
||||
|
||||
QUrl FileCopyJob::srcUrl() const
|
||||
{
|
||||
return d->source;
|
||||
}
|
||||
|
||||
FileCopyJob *FileCopyJob::file_copy(const QUrl &source, const QUrl &destination, int permissions, JobFlags flags, QObject *parent)
|
||||
{
|
||||
FileCopyJob *job = nullptr;
|
||||
if (source.isLocalFile() && destination.isLocalFile()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "File copy job is local only";
|
||||
job = new FileCopyJob(source, destination, permissions, flags, parent);
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "File copy job is from (or to) a remote URL";
|
||||
job = new DownloadJob(source, destination, permissions, flags, parent);
|
||||
}
|
||||
job->start();
|
||||
return job;
|
||||
}
|
||||
|
||||
void FileCopyJob::handleProgressUpdate(qlonglong current, qlonglong total)
|
||||
{
|
||||
setTotalAmount(KJob::Bytes, total);
|
||||
setProcessedAmount(KJob::Bytes, current);
|
||||
emitPercent(current, total);
|
||||
}
|
||||
|
||||
void FileCopyJob::handleCompleted()
|
||||
{
|
||||
d->worker->deleteLater();
|
||||
d->worker = nullptr;
|
||||
emitResult();
|
||||
}
|
||||
|
||||
void FileCopyJob::handleError(const QString &errorMessage)
|
||||
{
|
||||
d->worker->deleteLater();
|
||||
d->worker = nullptr;
|
||||
setError(UserDefinedError);
|
||||
setErrorText(errorMessage);
|
||||
emitResult();
|
||||
}
|
||||
|
||||
#include "moc_filecopyjob.cpp"
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef FILECOPYJOB_H
|
||||
#define FILECOPYJOB_H
|
||||
|
||||
#include "jobbase.h"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class FileCopyJobPrivate;
|
||||
class FileCopyJob : public KJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FileCopyJob(const QUrl &source, const QUrl &destination, int permissions = -1, JobFlags flags = DefaultFlags, QObject *parent = nullptr);
|
||||
explicit FileCopyJob(QObject *parent = nullptr);
|
||||
~FileCopyJob() override;
|
||||
|
||||
Q_SCRIPTABLE void start() override;
|
||||
|
||||
QUrl destUrl() const;
|
||||
QUrl srcUrl() const;
|
||||
|
||||
// This will create either a FileCopyJob, or an instance of
|
||||
// a subclass, depending on the nature of the URLs passed to
|
||||
// it
|
||||
static FileCopyJob *file_copy(const QUrl &source, const QUrl &destination, int permissions = -1, JobFlags flags = DefaultFlags, QObject *parent = nullptr);
|
||||
|
||||
protected Q_SLOTS:
|
||||
void handleProgressUpdate(qlonglong current, qlonglong total);
|
||||
void handleCompleted();
|
||||
void handleError(const QString &errorMessage);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<FileCopyJobPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // FILECOPYJOB_H
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "filecopyworker.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QFile>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::FileCopyWorkerPrivate
|
||||
{
|
||||
public:
|
||||
FileCopyWorkerPrivate()
|
||||
{
|
||||
}
|
||||
QFile source;
|
||||
QFile destination;
|
||||
};
|
||||
|
||||
FileCopyWorker::FileCopyWorker(const QUrl &source, const QUrl &destination, QObject *parent)
|
||||
: QThread(parent)
|
||||
, d(new FileCopyWorkerPrivate)
|
||||
{
|
||||
d->source.setFileName(source.toLocalFile());
|
||||
d->destination.setFileName(destination.toLocalFile());
|
||||
}
|
||||
|
||||
FileCopyWorker::~FileCopyWorker()
|
||||
{
|
||||
if (isRunning()) {
|
||||
requestInterruption();
|
||||
quit();
|
||||
if (!wait(1s)) {
|
||||
terminate();
|
||||
wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileCopyWorker::run()
|
||||
{
|
||||
if (d->source.open(QIODevice::ReadOnly)) {
|
||||
if (d->destination.open(QIODevice::WriteOnly)) {
|
||||
const qint64 totalSize = d->source.size();
|
||||
|
||||
for (qint64 i = 0; i < totalSize; i += 1024) {
|
||||
d->destination.write(d->source.read(1024));
|
||||
d->source.seek(i);
|
||||
d->destination.seek(i);
|
||||
|
||||
Q_EMIT progress(i, totalSize / 1024);
|
||||
}
|
||||
Q_EMIT completed();
|
||||
} else {
|
||||
Q_EMIT error(i18n("Could not open %1 for writing", d->destination.fileName()));
|
||||
}
|
||||
} else {
|
||||
Q_EMIT error(i18n("Could not open %1 for reading", d->source.fileName()));
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_filecopyworker.cpp"
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef FILECOPYWORKER_H
|
||||
#define FILECOPYWORKER_H
|
||||
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class FileCopyWorkerPrivate;
|
||||
class FileCopyWorker : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FileCopyWorker(const QUrl &source, const QUrl &destination, QObject *parent = nullptr);
|
||||
~FileCopyWorker() override;
|
||||
void run() override;
|
||||
|
||||
Q_SIGNAL void progress(qlonglong current, qlonglong total);
|
||||
Q_SIGNAL void completed();
|
||||
Q_SIGNAL void error(const QString &message);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<FileCopyWorkerPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // FILECOPYWORKER_H
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "httpjob.h"
|
||||
|
||||
#include "httpworker.h"
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::HttpJobPrivate
|
||||
{
|
||||
public:
|
||||
QUrl source;
|
||||
LoadType loadType = Reload;
|
||||
JobFlags flags = DefaultFlags;
|
||||
};
|
||||
|
||||
HTTPJob::HTTPJob(const QUrl &source, LoadType loadType, JobFlags flags, QObject *parent)
|
||||
: KJob(parent)
|
||||
, d(new HttpJobPrivate)
|
||||
{
|
||||
d->source = source;
|
||||
d->loadType = loadType;
|
||||
d->flags = flags;
|
||||
}
|
||||
|
||||
HTTPJob::HTTPJob(QObject *parent)
|
||||
: KJob(parent)
|
||||
, d(new HttpJobPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
HTTPJob::~HTTPJob() = default;
|
||||
|
||||
void HTTPJob::start()
|
||||
{
|
||||
HTTPWorker *worker = new HTTPWorker(d->source, HTTPWorker::GetJob, this);
|
||||
connect(worker, &HTTPWorker::data, this, &HTTPJob::handleWorkerData);
|
||||
connect(worker, &HTTPWorker::completed, this, &HTTPJob::handleWorkerCompleted);
|
||||
connect(worker, &HTTPWorker::error, this, &HTTPJob::handleWorkerError);
|
||||
connect(worker, &HTTPWorker::httpError, this, &HTTPJob::httpError);
|
||||
worker->startRequest();
|
||||
}
|
||||
|
||||
void HTTPJob::handleWorkerData(const QByteArray &data)
|
||||
{
|
||||
Q_EMIT HTTPJob::data(this, data);
|
||||
}
|
||||
|
||||
void HTTPJob::handleWorkerCompleted()
|
||||
{
|
||||
emitResult();
|
||||
}
|
||||
|
||||
void KNSCore::HTTPJob::handleWorkerError(const QString &error)
|
||||
{
|
||||
setError(KJob::UserDefinedError);
|
||||
setErrorText(error);
|
||||
}
|
||||
|
||||
HTTPJob *HTTPJob::get(const QUrl &source, LoadType loadType, JobFlags flags, QObject *parent)
|
||||
{
|
||||
HTTPJob *job = new HTTPJob(source, loadType, flags, parent);
|
||||
QTimer::singleShot(0, job, &HTTPJob::start);
|
||||
return job;
|
||||
}
|
||||
|
||||
#include "moc_httpjob.cpp"
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef HTTPJOB_H
|
||||
#define HTTPJOB_H
|
||||
|
||||
#include "jobbase.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class HttpJobPrivate;
|
||||
class HTTPJob : public KJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HTTPJob(const QUrl &source, LoadType loadType = Reload, JobFlags flags = DefaultFlags, QObject *parent = nullptr);
|
||||
explicit HTTPJob(QObject *parent = nullptr);
|
||||
~HTTPJob() override;
|
||||
|
||||
Q_SLOT void start() override;
|
||||
|
||||
static HTTPJob *get(const QUrl &source, LoadType loadType = Reload, JobFlags flags = DefaultFlags, QObject *parent = nullptr);
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* Data from the worker has arrived.
|
||||
* @param job the job that emitted this signal
|
||||
* @param data data received from the worker.
|
||||
*
|
||||
* End of data (EOD) has been reached if data.size() == 0, however, you
|
||||
* should not be certain of data.size() == 0 ever happening (e.g. in case
|
||||
* of an error), so you should rely on result() instead.
|
||||
*/
|
||||
void data(KJob *job, const QByteArray &data);
|
||||
|
||||
/**
|
||||
* Fired in case there is a http error reported
|
||||
* In some instances this is useful information for our users, and we want to make sure we report this centrally
|
||||
* @param status The HTTP status code (fired in cases where it is perceived by QNetworkReply as an error)
|
||||
* @param rawHeaders The raw HTTP headers for the errored-out network request
|
||||
*/
|
||||
void httpError(int status, QList<QNetworkReply::RawHeaderPair> rawHeaders);
|
||||
|
||||
protected Q_SLOTS:
|
||||
void handleWorkerData(const QByteArray &data);
|
||||
void handleWorkerCompleted();
|
||||
void handleWorkerError(const QString &error);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<HttpJobPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // HTTPJOB_H
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "httpworker.h"
|
||||
|
||||
#include "knewstuff_version.h"
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
#include <QStandardPaths>
|
||||
#include <QStorageInfo>
|
||||
#include <QThread>
|
||||
|
||||
namespace std
|
||||
{
|
||||
template<>
|
||||
struct default_delete<QNetworkReply> {
|
||||
void operator()(QNetworkReply *ptr) const
|
||||
{
|
||||
ptr->abort();
|
||||
ptr->deleteLater();
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
class HTTPWorkerNAM
|
||||
{
|
||||
public:
|
||||
HTTPWorkerNAM()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
const QString cacheLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/knewstuff");
|
||||
cache.setCacheDirectory(cacheLocation);
|
||||
QStorageInfo storageInfo(cacheLocation);
|
||||
cache.setMaximumCacheSize(qMin(50 * 1024 * 1024, (int)(storageInfo.bytesTotal() / 1000)));
|
||||
nam.setCache(&cache);
|
||||
}
|
||||
QNetworkAccessManager nam;
|
||||
QMutex mutex;
|
||||
|
||||
QNetworkReply *get(const QNetworkRequest &request)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
return nam.get(request);
|
||||
}
|
||||
|
||||
QNetworkDiskCache cache;
|
||||
};
|
||||
|
||||
Q_GLOBAL_STATIC(HTTPWorkerNAM, s_httpWorkerNAM)
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::HTTPWorkerPrivate
|
||||
{
|
||||
public:
|
||||
HTTPWorkerPrivate()
|
||||
: jobType(HTTPWorker::GetJob)
|
||||
, reply(nullptr)
|
||||
{
|
||||
}
|
||||
HTTPWorker::JobType jobType;
|
||||
QUrl source;
|
||||
QUrl destination;
|
||||
std::unique_ptr<QNetworkReply> reply;
|
||||
QUrl redirectUrl;
|
||||
|
||||
QFile dataFile;
|
||||
};
|
||||
|
||||
HTTPWorker::HTTPWorker(const QUrl &url, JobType jobType, QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new HTTPWorkerPrivate)
|
||||
{
|
||||
d->jobType = jobType;
|
||||
d->source = url;
|
||||
}
|
||||
|
||||
HTTPWorker::HTTPWorker(const QUrl &source, const QUrl &destination, KNSCore::HTTPWorker::JobType jobType, QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new HTTPWorkerPrivate)
|
||||
{
|
||||
d->jobType = jobType;
|
||||
d->source = source;
|
||||
d->destination = destination;
|
||||
}
|
||||
|
||||
HTTPWorker::~HTTPWorker() = default;
|
||||
|
||||
void HTTPWorker::setUrl(const QUrl &url)
|
||||
{
|
||||
d->source = url;
|
||||
}
|
||||
|
||||
static void addUserAgent(QNetworkRequest &request)
|
||||
{
|
||||
QString agentHeader = QStringLiteral("KNewStuff/%1").arg(QLatin1String(KNEWSTUFF_VERSION_STRING));
|
||||
if (QCoreApplication::instance()) {
|
||||
agentHeader += QStringLiteral("-%1/%2").arg(QCoreApplication::instance()->applicationName(), QCoreApplication::instance()->applicationVersion());
|
||||
}
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, agentHeader);
|
||||
// If the remote supports HTTP/2, then we should definitely be using that
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
|
||||
// Assume that no cache expiration time will be longer than a week, but otherwise prefer the cache
|
||||
// This is mildly hacky, but if we don't do this, we end up with infinite cache expirations in some
|
||||
// cases, which of course isn't really acceptable... See ed62ee20 for a situation where that happened.
|
||||
QNetworkCacheMetaData cacheMeta{s_httpWorkerNAM->cache.metaData(request.url())};
|
||||
if (cacheMeta.isValid()) {
|
||||
const QDateTime nextWeek{QDateTime::currentDateTime().addDays(7)};
|
||||
if (cacheMeta.expirationDate().isValid() && cacheMeta.expirationDate() < nextWeek) {
|
||||
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HTTPWorker::startRequest()
|
||||
{
|
||||
if (d->reply) {
|
||||
// only run one request at a time...
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(d->source);
|
||||
addUserAgent(request);
|
||||
d->reply.reset(s_httpWorkerNAM->get(request));
|
||||
connect(d->reply.get(), &QNetworkReply::readyRead, this, &HTTPWorker::handleReadyRead);
|
||||
connect(d->reply.get(), &QNetworkReply::finished, this, &HTTPWorker::handleFinished);
|
||||
if (d->jobType == DownloadJob) {
|
||||
d->dataFile.setFileName(d->destination.toLocalFile());
|
||||
connect(this, &HTTPWorker::data, this, &HTTPWorker::handleData);
|
||||
}
|
||||
}
|
||||
|
||||
void HTTPWorker::handleReadyRead()
|
||||
{
|
||||
QMutexLocker locker(&s_httpWorkerNAM->mutex);
|
||||
if (d->reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
|
||||
do {
|
||||
Q_EMIT data(d->reply->read(32768));
|
||||
} while (!d->reply->atEnd());
|
||||
}
|
||||
}
|
||||
|
||||
void HTTPWorker::handleFinished()
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << Q_FUNC_INFO << d->reply->url();
|
||||
if (d->reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(KNEWSTUFFCORE) << d->reply->errorString();
|
||||
if (d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() > 100) {
|
||||
// In this case, we're being asked to wait a bit...
|
||||
Q_EMIT httpError(d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), d->reply->rawHeaderPairs());
|
||||
}
|
||||
Q_EMIT error(d->reply->errorString());
|
||||
}
|
||||
|
||||
// Check if the data was obtained from cache or not
|
||||
QString fromCache = d->reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool() ? QStringLiteral("(cached)") : QStringLiteral("(NOT cached)");
|
||||
|
||||
// Handle redirections
|
||||
const QUrl possibleRedirectUrl = d->reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
|
||||
if (!possibleRedirectUrl.isEmpty() && possibleRedirectUrl != d->redirectUrl) {
|
||||
d->redirectUrl = d->reply->url().resolved(possibleRedirectUrl);
|
||||
if (d->redirectUrl.scheme().startsWith(QLatin1String("http"))) {
|
||||
qCDebug(KNEWSTUFFCORE) << d->reply->url().toDisplayString() << "was redirected to" << d->redirectUrl.toDisplayString() << fromCache
|
||||
<< d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
QNetworkRequest request(d->redirectUrl);
|
||||
addUserAgent(request);
|
||||
d->reply.reset(s_httpWorkerNAM->get(request));
|
||||
connect(d->reply.get(), &QNetworkReply::readyRead, this, &HTTPWorker::handleReadyRead);
|
||||
connect(d->reply.get(), &QNetworkReply::finished, this, &HTTPWorker::handleFinished);
|
||||
return;
|
||||
} else {
|
||||
qCWarning(KNEWSTUFFCORE) << "Redirection to" << d->redirectUrl.toDisplayString() << "forbidden.";
|
||||
}
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "Data for" << d->reply->url().toDisplayString() << "was fetched" << fromCache;
|
||||
}
|
||||
|
||||
if (d->dataFile.isOpen()) {
|
||||
d->dataFile.close();
|
||||
}
|
||||
|
||||
d->redirectUrl.clear();
|
||||
Q_EMIT completed();
|
||||
}
|
||||
|
||||
void HTTPWorker::handleData(const QByteArray &data)
|
||||
{
|
||||
// It turns out that opening a file and then leaving it hanging without writing to it immediately will, at times
|
||||
// leave you with a file that suddenly (seemingly magically) no longer exists. Thanks for that.
|
||||
if (!d->dataFile.isOpen()) {
|
||||
if (d->dataFile.open(QIODevice::WriteOnly)) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Opened file" << d->dataFile.fileName() << "for writing.";
|
||||
} else {
|
||||
qCWarning(KNEWSTUFFCORE) << "Failed to open file for writing!";
|
||||
Q_EMIT error(QStringLiteral("Failed to open file %1 for writing!").arg(d->destination.toLocalFile()));
|
||||
}
|
||||
}
|
||||
qCDebug(KNEWSTUFFCORE) << "Writing" << data.length() << "bytes of data to" << d->dataFile.fileName();
|
||||
quint64 written = d->dataFile.write(data);
|
||||
if (d->dataFile.error()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "File has error" << d->dataFile.errorString();
|
||||
}
|
||||
qCDebug(KNEWSTUFFCORE) << "Wrote" << written << "bytes. File is now size" << d->dataFile.size();
|
||||
}
|
||||
|
||||
#include "moc_httpworker.cpp"
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef HTTPWORKER_H
|
||||
#define HTTPWORKER_H
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
|
||||
class QNetworkReply;
|
||||
namespace KNSCore
|
||||
{
|
||||
class HTTPWorkerPrivate;
|
||||
class HTTPWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum JobType {
|
||||
GetJob,
|
||||
DownloadJob, // Much the same as a get... except with a filesystem destination, rather than outputting data
|
||||
};
|
||||
explicit HTTPWorker(const QUrl &url, JobType jobType = GetJob, QObject *parent = nullptr);
|
||||
explicit HTTPWorker(const QUrl &source, const QUrl &destination, JobType jobType = DownloadJob, QObject *parent = nullptr);
|
||||
~HTTPWorker() override;
|
||||
|
||||
void startRequest();
|
||||
|
||||
void setUrl(const QUrl &url);
|
||||
|
||||
Q_SIGNAL void error(QString error);
|
||||
Q_SIGNAL void progress(qlonglong current, qlonglong total);
|
||||
Q_SIGNAL void completed();
|
||||
Q_SIGNAL void data(const QByteArray &data);
|
||||
|
||||
/**
|
||||
* Fired in case there is a http error reported
|
||||
* In some instances this is useful information for our users, and we want to make sure we report this centrally
|
||||
* @param status The HTTP status code (fired in cases where it is perceived by QNetworkReply as an error)
|
||||
* @param rawHeaders The raw HTTP headers for the errored-out network request
|
||||
*/
|
||||
Q_SIGNAL void httpError(int status, QList<QNetworkReply::RawHeaderPair> rawHeaders);
|
||||
|
||||
Q_SLOT void handleReadyRead();
|
||||
Q_SLOT void handleFinished();
|
||||
Q_SLOT void handleData(const QByteArray &data);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<HTTPWorkerPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // HTTPWORKER_H
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef JOBBASE_H
|
||||
#define JOBBASE_H
|
||||
|
||||
#include <KJob>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
enum JobFlag {
|
||||
None = 0,
|
||||
HideProgressInfo = 1,
|
||||
Resume = 2,
|
||||
Overwrite = 4,
|
||||
DefaultFlags = None,
|
||||
};
|
||||
Q_DECLARE_FLAGS(JobFlags, JobFlag)
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(JobFlags)
|
||||
|
||||
enum LoadType {
|
||||
Reload,
|
||||
NoReload,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // JOBBASE_H
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
knewstuff3/provider.cpp
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "provider.h"
|
||||
|
||||
#include "provider_p.h"
|
||||
#include "xmlloader_p.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
QString Provider::SearchRequest::hashForRequest() const
|
||||
{
|
||||
return QString::number((int)sortMode) + QLatin1Char(',') + searchTerm + QLatin1Char(',') + categories.join(QLatin1Char('-')) + QLatin1Char(',')
|
||||
+ QString::number(page) + QLatin1Char(',') + QString::number(pageSize);
|
||||
}
|
||||
|
||||
Provider::Provider()
|
||||
: d(new ProviderPrivate(this))
|
||||
{
|
||||
}
|
||||
|
||||
Provider::~Provider() = default;
|
||||
|
||||
QString Provider::name() const
|
||||
{
|
||||
return d->name;
|
||||
}
|
||||
|
||||
QUrl Provider::icon() const
|
||||
{
|
||||
return d->icon;
|
||||
}
|
||||
|
||||
void Provider::setTagFilter(const QStringList &tagFilter)
|
||||
{
|
||||
d->tagFilter = tagFilter;
|
||||
Q_EMIT tagFilterChanged();
|
||||
}
|
||||
|
||||
QStringList Provider::tagFilter() const
|
||||
{
|
||||
return d->tagFilter;
|
||||
}
|
||||
|
||||
void Provider::setDownloadTagFilter(const QStringList &downloadTagFilter)
|
||||
{
|
||||
d->downloadTagFilter = downloadTagFilter;
|
||||
Q_EMIT downloadTagFilterChanged();
|
||||
}
|
||||
|
||||
QStringList Provider::downloadTagFilter() const
|
||||
{
|
||||
return d->downloadTagFilter;
|
||||
}
|
||||
|
||||
QDebug operator<<(QDebug dbg, const Provider::SearchRequest &search)
|
||||
{
|
||||
QDebugStateSaver saver(dbg);
|
||||
dbg.nospace();
|
||||
dbg << "Provider::SearchRequest(";
|
||||
dbg << "searchTerm: " << search.searchTerm << ',';
|
||||
dbg << "categories: " << search.categories << ',';
|
||||
dbg << "filter: " << search.filter << ',';
|
||||
dbg << "page: " << search.page << ',';
|
||||
dbg << "pageSize: " << search.pageSize;
|
||||
dbg << ')';
|
||||
return dbg;
|
||||
}
|
||||
|
||||
QString Provider::version() const
|
||||
{
|
||||
d->updateOnFirstBasicsGet();
|
||||
return d->version;
|
||||
}
|
||||
|
||||
void Provider::setVersion(const QString &version)
|
||||
{
|
||||
if (d->version != version) {
|
||||
d->version = version;
|
||||
d->throttleBasics();
|
||||
}
|
||||
}
|
||||
|
||||
QUrl Provider::website() const
|
||||
{
|
||||
d->updateOnFirstBasicsGet();
|
||||
return d->website;
|
||||
}
|
||||
|
||||
void Provider::setWebsite(const QUrl &website)
|
||||
{
|
||||
if (d->website != website) {
|
||||
d->website = website;
|
||||
d->throttleBasics();
|
||||
}
|
||||
}
|
||||
|
||||
QUrl Provider::host() const
|
||||
{
|
||||
d->updateOnFirstBasicsGet();
|
||||
return d->host;
|
||||
}
|
||||
|
||||
void Provider::setHost(const QUrl &host)
|
||||
{
|
||||
if (d->host != host) {
|
||||
d->host = host;
|
||||
d->throttleBasics();
|
||||
}
|
||||
}
|
||||
|
||||
QString Provider::contactEmail() const
|
||||
{
|
||||
d->updateOnFirstBasicsGet();
|
||||
return d->contactEmail;
|
||||
}
|
||||
|
||||
void Provider::setContactEmail(const QString &contactEmail)
|
||||
{
|
||||
if (d->contactEmail != contactEmail) {
|
||||
d->contactEmail = contactEmail;
|
||||
d->throttleBasics();
|
||||
}
|
||||
}
|
||||
|
||||
bool Provider::supportsSsl() const
|
||||
{
|
||||
d->updateOnFirstBasicsGet();
|
||||
return d->supportsSsl;
|
||||
}
|
||||
|
||||
void Provider::setSupportsSsl(bool supportsSsl)
|
||||
{
|
||||
if (d->supportsSsl != supportsSsl) {
|
||||
d->supportsSsl = supportsSsl;
|
||||
d->throttleBasics();
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::setName(const QString &name)
|
||||
{
|
||||
d->name = name;
|
||||
}
|
||||
|
||||
void Provider::setIcon(const QUrl &icon)
|
||||
{
|
||||
d->icon = icon;
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_provider.cpp"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
knewstuff3/provider.h
|
||||
This file is part of KNewStuff2.
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_PROVIDER_P_H
|
||||
#define KNEWSTUFF3_PROVIDER_P_H
|
||||
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "entry.h"
|
||||
#include "errorcode.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class ProviderPrivate;
|
||||
struct Comment;
|
||||
|
||||
/**
|
||||
* @short KNewStuff Base Provider class.
|
||||
*
|
||||
* This class provides accessors for the provider object.
|
||||
* It should not be used directly by the application.
|
||||
* This class is the base class and will be instantiated for
|
||||
* static website providers.
|
||||
*
|
||||
* @author Jeremy Whiting <jpwhiting@kde.org>
|
||||
* @deprecated since 6.9 Use ProviderBase to implement providers (only in-tree supported). Use ProviderCore to manage instances of base.
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6,
|
||||
9,
|
||||
"Use ProviderBase to implement providers (only in-tree supported). Use ProviderCore to manage instances of base.") Provider
|
||||
: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString version READ version WRITE setVersion NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QUrl website READ website WRITE setWebsite NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QUrl host READ host WRITE setHost NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QString contactEmail READ contactEmail WRITE setContactEmail NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(bool supportsSsl READ supportsSsl WRITE setSupportsSsl NOTIFY basicsLoaded)
|
||||
public:
|
||||
typedef QList<Provider *> List;
|
||||
|
||||
enum SortMode {
|
||||
Newest,
|
||||
Alphabetical,
|
||||
Rating,
|
||||
Downloads,
|
||||
};
|
||||
Q_ENUM(SortMode)
|
||||
|
||||
enum Filter {
|
||||
None,
|
||||
Installed,
|
||||
Updates,
|
||||
ExactEntryId,
|
||||
};
|
||||
Q_ENUM(Filter)
|
||||
|
||||
/**
|
||||
* used to keep track of a search
|
||||
* @deprecated since 6.9 Use KNSCore::SearchRequest
|
||||
*/
|
||||
struct KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use KNSCore::SearchRequest") SearchRequest {
|
||||
SortMode sortMode;
|
||||
Filter filter;
|
||||
QString searchTerm;
|
||||
QStringList categories;
|
||||
int page;
|
||||
int pageSize;
|
||||
|
||||
SearchRequest(SortMode sortMode_ = Downloads,
|
||||
Filter filter_ = None,
|
||||
const QString &searchTerm_ = QString(),
|
||||
const QStringList &categories_ = QStringList(),
|
||||
int page_ = -1,
|
||||
int pageSize_ = 20)
|
||||
: sortMode(sortMode_)
|
||||
, filter(filter_)
|
||||
, searchTerm(searchTerm_)
|
||||
, categories(categories_)
|
||||
, page(page_)
|
||||
, pageSize(pageSize_)
|
||||
{
|
||||
}
|
||||
|
||||
QString hashForRequest() const;
|
||||
bool operator==(const SearchRequest &other) const
|
||||
{
|
||||
return sortMode == other.sortMode && filter == other.filter && searchTerm == other.searchTerm && categories == other.categories
|
||||
&& page == other.page && pageSize == other.pageSize;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes a category: id/name/displayName
|
||||
* @deprecated since 6.9 Use KNSCore::CategoryMetadata
|
||||
*/
|
||||
struct KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use KNSCore::CategoryMetadata") CategoryMetadata {
|
||||
QString id;
|
||||
QString name;
|
||||
QString displayName;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The SearchPresetTypes enum
|
||||
* the preset type enum is a helper to identify the kind of label and icon
|
||||
* the search preset should have if none are found.
|
||||
* @since 5.83
|
||||
* @deprecated since 6.9 Use KNSCore::SearchPreset::SearchPresetTypes
|
||||
*/
|
||||
enum KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use KNSCore::SearchPreset::SearchPresetTypes") SearchPresetTypes {
|
||||
NoPresetType = 0,
|
||||
GoBack, ///< preset representing the previous search.
|
||||
Root, ///< preset indicating a root directory.
|
||||
Start, ///< preset indicating the first entry.
|
||||
Popular, ///< preset indicating popular items.
|
||||
Featured, ///< preset for featured items.
|
||||
Recommended, ///< preset for recommended. This may be customized by the server per user.
|
||||
Shelf, ///< preset indicating previously acquired items.
|
||||
Subscription, ///< preset indicating items that the user is subscribed to.
|
||||
New, ///< preset indicating new items.
|
||||
FolderUp, ///< preset indicating going up in the search result hierarchy.
|
||||
AllEntries, ///< preset indicating all possible entries, such as a crawlable list. Might be intense to load.
|
||||
};
|
||||
/**
|
||||
* Describes a search request that may come from the provider.
|
||||
* This is used by the OPDS provider to handle the different urls.
|
||||
* @since 5.83
|
||||
* @deprecated since 6.9 Use KNSCore::SearchPreset
|
||||
*/
|
||||
struct KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use KNSCore::SearchPreset") SearchPreset {
|
||||
SearchRequest request;
|
||||
QString displayName;
|
||||
QString iconName;
|
||||
SearchPresetTypes type;
|
||||
QString providerId; // not all providers can handle all search requests.
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
Provider();
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
~Provider() override;
|
||||
|
||||
/**
|
||||
* A unique Id for this provider (the url in most cases)
|
||||
*/
|
||||
virtual QString id() const = 0;
|
||||
|
||||
/**
|
||||
* Set the provider data xml, to initialize the provider.
|
||||
* The Provider needs to have it's ID set in this function and cannot change it from there on.
|
||||
*/
|
||||
virtual bool setProviderXML(const QDomElement &xmldata) = 0;
|
||||
|
||||
virtual bool isInitialized() const = 0;
|
||||
|
||||
virtual void setCachedEntries(const KNSCore::Entry::List &cachedEntries) = 0;
|
||||
|
||||
/**
|
||||
* Retrieves the common name of the provider.
|
||||
*
|
||||
* @return provider name
|
||||
*/
|
||||
virtual QString name() const;
|
||||
|
||||
/**
|
||||
* Retrieves the icon URL for this provider.
|
||||
*
|
||||
* @return icon URL
|
||||
*/
|
||||
virtual QUrl icon() const; // FIXME use QIcon::fromTheme or pixmap?
|
||||
|
||||
/**
|
||||
* load the given search and return given page
|
||||
* @param sortMode string to select the order in which the results are presented
|
||||
* @param searchstring string to search with
|
||||
* @param page page number to load
|
||||
*
|
||||
* Note: the engine connects to loadingFinished() signal to get the result
|
||||
*/
|
||||
virtual void loadEntries(const KNSCore::Provider::SearchRequest &request) = 0;
|
||||
virtual void loadEntryDetails(const KNSCore::Entry &)
|
||||
{
|
||||
}
|
||||
virtual void loadPayloadLink(const Entry &entry, int linkId) = 0;
|
||||
/**
|
||||
* Request a loading of comments from this provider. The engine listens to the
|
||||
* commentsLoaded() signal for the result
|
||||
*
|
||||
* @note Implementation detail: All subclasses should connect to this signal
|
||||
* and point it at a slot which does the actual work, if they support comments.
|
||||
*
|
||||
* @see commentsLoaded(const QList<shared_ptr<KNSCore::Comment>> comments)
|
||||
* @since 5.63
|
||||
*/
|
||||
virtual void loadComments(const KNSCore::Entry &, int /*commentsPerPage*/, int /*page*/)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Request loading of the details for a specific person with the given username.
|
||||
* The engine listens to the personLoaded() for the result
|
||||
*
|
||||
* @note Implementation detail: All subclasses should connect to this signal
|
||||
* and point it at a slot which does the actual work, if they support comments.
|
||||
*
|
||||
* @since 5.63
|
||||
*/
|
||||
virtual void loadPerson(const QString & /*username*/)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Request loading of the basic information for this provider. The engine listens
|
||||
* to the basicsLoaded() signal for the result, which is also the signal the respective
|
||||
* properties listen to.
|
||||
*
|
||||
* This is fired automatically on the first attempt to read one of the properties
|
||||
* which contain this basic information, and you will not need to call it as a user
|
||||
* of the class (just listen to the properties, which will update when the information
|
||||
* has been fetched).
|
||||
*
|
||||
* @note Implementation detail: All subclasses should connect to this signal
|
||||
* and point it at a slot which does the actual work, if they support fetching
|
||||
* this basic information (if the information is set during construction, you will
|
||||
* not need to worry about this).
|
||||
*
|
||||
* @see version()
|
||||
* @see website()
|
||||
* @see host();
|
||||
* @see contactEmail()
|
||||
* @see supportsSsl()
|
||||
* @since 5.85
|
||||
*/
|
||||
virtual void loadBasics()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
QString version() const;
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
void setVersion(const QString &version);
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
QUrl website() const;
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
void setWebsite(const QUrl &website);
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
QUrl host() const;
|
||||
/**
|
||||
* @param host The host used for this provider
|
||||
* @since 5.85
|
||||
*/
|
||||
void setHost(const QUrl &host);
|
||||
/**
|
||||
* The general contact email for this provider
|
||||
* @return The general contact email for this provider
|
||||
* @since 5.85
|
||||
*/
|
||||
QString contactEmail() const;
|
||||
/**
|
||||
* Sets the general contact email address for this provider
|
||||
* @param contactEmail The general contact email for this provider
|
||||
* @since 5.85
|
||||
*/
|
||||
void setContactEmail(const QString &contactEmail);
|
||||
/**
|
||||
* Whether or not the provider supports SSL connections
|
||||
* @return True if the server supports SSL connections, false if not
|
||||
* @since 5.85
|
||||
*/
|
||||
bool supportsSsl() const;
|
||||
/**
|
||||
* Set whether or not the provider supports SSL connections
|
||||
* @param supportsSsl True if the server supports SSL connections, false if not
|
||||
* @since 5.85
|
||||
*/
|
||||
void setSupportsSsl(bool supportsSsl);
|
||||
|
||||
virtual bool userCanVote()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
virtual void vote(const Entry & /*entry*/, uint /*rating*/)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool userCanBecomeFan()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
virtual void becomeFan(const Entry & /*entry*/)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tag filter used for entries by this provider
|
||||
* @param tagFilter The new list of filters
|
||||
* @see Engine::setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void setTagFilter(const QStringList &tagFilter);
|
||||
/**
|
||||
* The tag filter used for downloads by this provider
|
||||
* @return The list of filters
|
||||
* @see Engine::setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList tagFilter() const;
|
||||
/**
|
||||
* Set the tag filter used for download items by this provider
|
||||
* @param downloadTagFilter The new list of filters
|
||||
* @see Engine::setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void setDownloadTagFilter(const QStringList &downloadTagFilter);
|
||||
/**
|
||||
* The tag filter used for downloads by this provider
|
||||
* @return The list of filters
|
||||
* @see Engine::setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList downloadTagFilter() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void providerInitialized(KNSCore::Provider *);
|
||||
|
||||
void loadingFinished(const KNSCore::Provider::SearchRequest &, const KNSCore::Entry::List &);
|
||||
void loadingFailed(const KNSCore::Provider::SearchRequest &);
|
||||
|
||||
void entryDetailsLoaded(const KNSCore::Entry &);
|
||||
void payloadLinkLoaded(const KNSCore::Entry &);
|
||||
/**
|
||||
* Fired when new comments have been loaded
|
||||
* @param comments The list of newly loaded comments, in a depth-first order
|
||||
* @since 5.63
|
||||
*/
|
||||
void commentsLoaded(const QList<std::shared_ptr<KNSCore::Comment>> comments);
|
||||
/**
|
||||
* Fired when the details of a person have been loaded
|
||||
* @param author The person we've just loaded data for
|
||||
* @since 5.63
|
||||
*/
|
||||
void personLoaded(const std::shared_ptr<KNSCore::Author> author);
|
||||
/**
|
||||
* Fired when the provider's basic information has been fetched and updated
|
||||
* @since 5.85
|
||||
*/
|
||||
void basicsLoaded();
|
||||
|
||||
/**
|
||||
* Fires when the provider has loaded search presets. These represent interesting
|
||||
* searches for the user, such as recommendations.
|
||||
* @since 5.83
|
||||
*/
|
||||
void searchPresetsLoaded(const QList<KNSCore::Provider::SearchPreset> &presets);
|
||||
|
||||
void signalInformation(const QString &);
|
||||
void signalError(const QString &);
|
||||
void signalErrorCode(KNSCore::ErrorCode::ErrorCode errorCode, const QString &message, const QVariant &metadata);
|
||||
|
||||
void categoriesMetadataLoded(const QList<KNSCore::Provider::CategoryMetadata> &categories);
|
||||
void tagFilterChanged();
|
||||
void downloadTagFilterChanged();
|
||||
|
||||
protected:
|
||||
void setName(const QString &name);
|
||||
void setIcon(const QUrl &icon);
|
||||
|
||||
private:
|
||||
friend class ProviderBubbleWrap;
|
||||
const std::unique_ptr<ProviderPrivate> d;
|
||||
Q_DISABLE_COPY(Provider)
|
||||
};
|
||||
|
||||
KNEWSTUFFCORE_EXPORT QDebug operator<<(QDebug, const Provider::SearchRequest &);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
// SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
#include "provider.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class ProviderPrivate
|
||||
{
|
||||
public:
|
||||
ProviderPrivate(Provider *qq)
|
||||
: q(qq)
|
||||
{
|
||||
}
|
||||
Provider *const q;
|
||||
QStringList tagFilter;
|
||||
QStringList downloadTagFilter;
|
||||
|
||||
QTimer *basicsThrottle{nullptr};
|
||||
QString version;
|
||||
QUrl website;
|
||||
QUrl host;
|
||||
QString contactEmail;
|
||||
QString name;
|
||||
QUrl icon;
|
||||
bool supportsSsl{false};
|
||||
bool basicsGot{false};
|
||||
|
||||
void updateOnFirstBasicsGet()
|
||||
{
|
||||
if (!basicsGot) {
|
||||
basicsGot = true;
|
||||
QTimer::singleShot(0, q, &Provider::loadBasics);
|
||||
}
|
||||
};
|
||||
void throttleBasics()
|
||||
{
|
||||
if (!basicsThrottle) {
|
||||
basicsThrottle = new QTimer(q);
|
||||
basicsThrottle->setInterval(0);
|
||||
basicsThrottle->setSingleShot(true);
|
||||
QObject::connect(basicsThrottle, &QTimer::timeout, q, &Provider::basicsLoaded);
|
||||
}
|
||||
basicsThrottle->start();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#include "providerbase_p.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
ProviderBase::ProviderBase(QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new ProviderBasePrivate(this))
|
||||
{
|
||||
}
|
||||
|
||||
void ProviderBase::setTagFilter(const QStringList &tagFilter)
|
||||
{
|
||||
d->tagFilter = tagFilter;
|
||||
Q_EMIT tagFilterChanged();
|
||||
}
|
||||
|
||||
QStringList ProviderBase::tagFilter() const
|
||||
{
|
||||
return d->tagFilter;
|
||||
}
|
||||
|
||||
void ProviderBase::setDownloadTagFilter(const QStringList &downloadTagFilter)
|
||||
{
|
||||
d->downloadTagFilter = downloadTagFilter;
|
||||
Q_EMIT downloadTagFilterChanged();
|
||||
}
|
||||
|
||||
QStringList ProviderBase::downloadTagFilter() const
|
||||
{
|
||||
return d->downloadTagFilter;
|
||||
}
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,240 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "entry.h"
|
||||
#include "errorcode.h"
|
||||
|
||||
#include "commentsmodel.h"
|
||||
#include "knewstuffcore_export.h"
|
||||
#include "searchrequest.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class ProviderBase;
|
||||
|
||||
class ProviderBasePrivate
|
||||
{
|
||||
public:
|
||||
ProviderBasePrivate(ProviderBase *qq)
|
||||
: q(qq)
|
||||
{
|
||||
}
|
||||
ProviderBase *q;
|
||||
QStringList tagFilter;
|
||||
QStringList downloadTagFilter;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief ProviderBase Interface
|
||||
* Exported for our qtquick components. Do not install the header or use from the outside!
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT ProviderBase : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString version READ version NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QUrl website READ website NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QUrl host READ host NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QString contactEmail READ contactEmail NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(bool supportsSsl READ supportsSsl NOTIFY basicsLoaded)
|
||||
public:
|
||||
ProviderBase(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* A unique Id for this provider (the url in most cases)
|
||||
*/
|
||||
[[nodiscard]] virtual QString id() const = 0;
|
||||
|
||||
/**
|
||||
* Set the provider data xml, to initialize the provider.
|
||||
* The Provider needs to have it's ID set in this function and cannot change it from there on.
|
||||
*/
|
||||
virtual bool setProviderXML(const QDomElement &xmldata) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool isInitialized() const = 0;
|
||||
|
||||
virtual void setCachedEntries(const KNSCore::Entry::List &cachedEntries) = 0;
|
||||
|
||||
/**
|
||||
* Retrieves the common name of the provider.
|
||||
*
|
||||
* @return provider name
|
||||
*/
|
||||
[[nodiscard]] virtual QString name() const = 0;
|
||||
|
||||
/**
|
||||
* Retrieves the icon URL for this provider.
|
||||
*
|
||||
* @return icon URL
|
||||
*/
|
||||
[[nodiscard]] virtual QUrl icon() const = 0; // FIXME use QIcon::fromTheme or pixmap?
|
||||
|
||||
/**
|
||||
* load the given search and return given page
|
||||
* @param sortMode string to select the order in which the results are presented
|
||||
* @param searchstring string to search with
|
||||
* @param page page number to load
|
||||
*
|
||||
* Note: the engine connects to loadingFinished() signal to get the result
|
||||
*/
|
||||
virtual void loadEntries(const KNSCore::SearchRequest &request) = 0;
|
||||
virtual void loadEntryDetails(const KNSCore::Entry &)
|
||||
{
|
||||
}
|
||||
virtual void loadPayloadLink(const Entry &entry, int linkId) = 0;
|
||||
/**
|
||||
* Request a loading of comments from this provider. The engine listens to the
|
||||
* commentsLoaded() signal for the result
|
||||
*
|
||||
* @note Implementation detail: All subclasses should connect to this signal
|
||||
* and point it at a slot which does the actual work, if they support comments.
|
||||
*
|
||||
* @see commentsLoaded(const QList<shared_ptr<KNSCore::Comment>> comments)
|
||||
* @since 5.63
|
||||
*/
|
||||
virtual void loadComments(const KNSCore::Entry &, int /*commentsPerPage*/, int /*page*/)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Request loading of the details for a specific person with the given username.
|
||||
* The engine listens to the personLoaded() for the result
|
||||
*
|
||||
* @note Implementation detail: All subclasses should connect to this signal
|
||||
* and point it at a slot which does the actual work, if they support comments.
|
||||
*
|
||||
* @since 5.63
|
||||
*/
|
||||
virtual void loadPerson(const QString & /*username*/)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
[[nodiscard]] virtual QString version() = 0;
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
[[nodiscard]] virtual QUrl website() = 0;
|
||||
/**
|
||||
* @since 5.85
|
||||
*/
|
||||
[[nodiscard]] virtual QUrl host() = 0;
|
||||
/**
|
||||
* The general contact email for this provider
|
||||
* @return The general contact email for this provider
|
||||
* @since 5.85
|
||||
*/
|
||||
[[nodiscard]] virtual QString contactEmail() = 0;
|
||||
/**
|
||||
* Whether or not the provider supports SSL connections
|
||||
* @return True if the server supports SSL connections, false if not
|
||||
* @since 5.85
|
||||
*/
|
||||
[[nodiscard]] virtual bool supportsSsl() = 0;
|
||||
|
||||
virtual bool userCanVote()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
virtual void vote(const Entry & /*entry*/, uint /*rating*/)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool userCanBecomeFan()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
virtual void becomeFan(const Entry & /*entry*/)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tag filter used for entries by this provider
|
||||
* @param tagFilter The new list of filters
|
||||
* @see Engine::setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void setTagFilter(const QStringList &tagFilter);
|
||||
/**
|
||||
* The tag filter used for downloads by this provider
|
||||
* @return The list of filters
|
||||
* @see Engine::setTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList tagFilter() const;
|
||||
/**
|
||||
* Set the tag filter used for download items by this provider
|
||||
* @param downloadTagFilter The new list of filters
|
||||
* @see Engine::setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
void setDownloadTagFilter(const QStringList &downloadTagFilter);
|
||||
/**
|
||||
* The tag filter used for downloads by this provider
|
||||
* @return The list of filters
|
||||
* @see Engine::setDownloadTagFilter(QStringList)
|
||||
* @since 5.51
|
||||
*/
|
||||
QStringList downloadTagFilter() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void providerInitialized(KNSCore::ProviderBase *);
|
||||
|
||||
void entriesLoaded(const KNSCore::SearchRequest &, const KNSCore::Entry::List &);
|
||||
void loadingDone(const KNSCore::SearchRequest &);
|
||||
void loadingFailed(const KNSCore::SearchRequest &);
|
||||
|
||||
void entryDetailsLoaded(const KNSCore::Entry &);
|
||||
void payloadLinkLoaded(const KNSCore::Entry &);
|
||||
/**
|
||||
* Fired when new comments have been loaded
|
||||
* @param comments The list of newly loaded comments, in a depth-first order
|
||||
* @since 5.63
|
||||
*/
|
||||
void commentsLoaded(const QList<std::shared_ptr<KNSCore::Comment>> &comments);
|
||||
/**
|
||||
* Fired when the details of a person have been loaded
|
||||
* @param author The person we've just loaded data for
|
||||
* @since 5.63
|
||||
*/
|
||||
void personLoaded(const std::shared_ptr<KNSCore::Author> &author);
|
||||
/**
|
||||
* Fired when the provider's basic information has been fetched and updated
|
||||
* @since 5.85
|
||||
*/
|
||||
void basicsLoaded();
|
||||
|
||||
/**
|
||||
* Fires when the provider has loaded search presets. These represent interesting
|
||||
* searches for the user, such as recommendations.
|
||||
* @since 5.83
|
||||
*/
|
||||
void searchPresetsLoaded(const QList<KNSCore::SearchPreset> &presets);
|
||||
|
||||
void signalInformation(const QString &);
|
||||
void signalError(const QString &);
|
||||
void signalErrorCode(KNSCore::ErrorCode::ErrorCode errorCode, const QString &message, const QVariant &metadata);
|
||||
void categoriesMetadataLoaded(const QList<KNSCore::CategoryMetadata> &categories);
|
||||
void tagFilterChanged();
|
||||
void downloadTagFilterChanged();
|
||||
|
||||
private:
|
||||
friend class ProviderBubbleWrap;
|
||||
std::unique_ptr<ProviderBasePrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#include "providerbubblewrap_p.h"
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "compat_p.h"
|
||||
#include "provider.h"
|
||||
#include "provider_p.h"
|
||||
#include "providerbase_p.h"
|
||||
#include "providercore.h"
|
||||
#include "providercore_p.h"
|
||||
#include "searchrequest_p.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class ProviderBubbleWrap : public Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ProviderBubbleWrap(const QSharedPointer<KNSCore::ProviderCore> &core)
|
||||
: m_core(core)
|
||||
{
|
||||
// These are unidirectional.
|
||||
connect(m_core->d->base, &ProviderBase::basicsLoaded, this, [this] {
|
||||
// The Provider async loads these and eventually emits loaded. Technically they
|
||||
// could get set by the outside, we actively do not support this though.
|
||||
setVersion(m_core->d->base->version());
|
||||
setWebsite(m_core->d->base->website());
|
||||
setHost(m_core->d->base->host());
|
||||
setContactEmail(m_core->d->base->contactEmail());
|
||||
setSupportsSsl(m_core->d->base->supportsSsl());
|
||||
Q_EMIT basicsLoaded();
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::providerInitialized, this, [this](const auto & /*providerBase*/) {
|
||||
Q_EMIT providerInitialized(this);
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::entriesLoaded, this, [this](const auto &request, const auto &entries) {
|
||||
Q_EMIT loadingFinished(KNSCompat::searchRequestToLegacy(request), entries);
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::loadingDone, this, [this](const auto &request) {
|
||||
Q_EMIT loadingFinished(KNSCompat::searchRequestToLegacy(request), {});
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::loadingFailed, this, [this](const auto &request) {
|
||||
Q_EMIT loadingFailed(KNSCompat::searchRequestToLegacy(request));
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::entryDetailsLoaded, this, &Provider::entryDetailsLoaded);
|
||||
connect(m_core->d->base, &ProviderBase::payloadLinkLoaded, this, &Provider::payloadLinkLoaded);
|
||||
connect(m_core->d->base, &ProviderBase::commentsLoaded, this, &Provider::commentsLoaded);
|
||||
connect(m_core->d->base, &ProviderBase::personLoaded, this, &Provider::personLoaded);
|
||||
connect(m_core->d->base, &ProviderBase::searchPresetsLoaded, this, [this](const auto &presets) {
|
||||
QList<KNSCore::Provider::SearchPreset> legacies;
|
||||
legacies.reserve(presets.size());
|
||||
for (const auto &preset : presets) {
|
||||
legacies.append(KNSCompat::searchPresetToLegacy(preset));
|
||||
}
|
||||
Q_EMIT searchPresetsLoaded(legacies);
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::signalInformation, this, &Provider::signalInformation);
|
||||
connect(m_core->d->base, &ProviderBase::signalError, this, &Provider::signalError);
|
||||
connect(m_core->d->base, &ProviderBase::signalErrorCode, this, &Provider::signalErrorCode);
|
||||
connect(m_core->d->base, &ProviderBase::categoriesMetadataLoaded, this, [this](const QList<KNSCore::CategoryMetadata> &categories) {
|
||||
QList<KNSCore::Provider::CategoryMetadata> legacies;
|
||||
legacies.reserve(categories.size());
|
||||
for (const auto &category : categories) {
|
||||
legacies.append(KNSCompat::categoryMetadataToLegacy(category));
|
||||
}
|
||||
Q_EMIT categoriesMetadataLoded(legacies);
|
||||
});
|
||||
|
||||
// These are bidirectional. We do not use public setters for these to avoid change signal loops.
|
||||
// Bit awkward to hop through all the d pointers but this class is not going to outlife KF6 anyway.
|
||||
connect(m_core->d->base, &ProviderBase::tagFilterChanged, this, [this] {
|
||||
d->tagFilter = m_core->d->base->tagFilter();
|
||||
});
|
||||
connect(this, &Provider::tagFilterChanged, this, [this] {
|
||||
m_core->d->base->d->tagFilter = d->tagFilter;
|
||||
});
|
||||
connect(m_core->d->base, &ProviderBase::downloadTagFilterChanged, this, [this] {
|
||||
d->downloadTagFilter = m_core->d->base->downloadTagFilter();
|
||||
});
|
||||
connect(this, &Provider::downloadTagFilterChanged, this, [this] {
|
||||
m_core->d->base->d->downloadTagFilter = d->downloadTagFilter;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QString id() const override
|
||||
{
|
||||
return m_core->d->base->id();
|
||||
}
|
||||
|
||||
bool setProviderXML(const QDomElement &xmldata) override
|
||||
{
|
||||
return m_core->d->base->setProviderXML(xmldata);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isInitialized() const override
|
||||
{
|
||||
return m_core->d->base->isInitialized();
|
||||
}
|
||||
|
||||
void setCachedEntries(const KNSCore::Entry::List &cachedEntries) override
|
||||
{
|
||||
m_core->d->base->setCachedEntries(cachedEntries);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString name() const override
|
||||
{
|
||||
return m_core->d->base->name();
|
||||
}
|
||||
|
||||
[[nodiscard]] QUrl icon() const override
|
||||
{
|
||||
return m_core->d->base->icon();
|
||||
}
|
||||
|
||||
void loadEntries(const KNSCore::Provider::SearchRequest &request) override
|
||||
{
|
||||
m_core->d->base->loadEntries(searchRequestFromLegacy(request));
|
||||
}
|
||||
|
||||
void loadEntryDetails(const KNSCore::Entry &entry) override
|
||||
{
|
||||
m_core->d->base->loadEntryDetails(entry);
|
||||
}
|
||||
|
||||
void loadPayloadLink(const Entry &entry, int linkId) override
|
||||
{
|
||||
m_core->d->base->loadPayloadLink(entry, linkId);
|
||||
}
|
||||
|
||||
void loadComments(const KNSCore::Entry &entry, int commentsPerPage, int page) override
|
||||
{
|
||||
m_core->d->base->loadComments(entry, commentsPerPage, page);
|
||||
}
|
||||
|
||||
void loadPerson(const QString &username) override
|
||||
{
|
||||
m_core->d->base->loadPerson(username);
|
||||
}
|
||||
|
||||
void loadBasics() override
|
||||
{
|
||||
// Noop. Basics (host, website, ssl etc.) now load on-demand.
|
||||
}
|
||||
|
||||
bool userCanVote() override
|
||||
{
|
||||
return m_core->d->base->userCanVote();
|
||||
}
|
||||
|
||||
void vote(const Entry &entry, uint rating) override
|
||||
{
|
||||
m_core->d->base->vote(entry, rating);
|
||||
}
|
||||
|
||||
bool userCanBecomeFan() override
|
||||
{
|
||||
return m_core->d->base->userCanBecomeFan();
|
||||
}
|
||||
|
||||
void becomeFan(const Entry &entry) override
|
||||
{
|
||||
m_core->d->base->becomeFan(entry);
|
||||
}
|
||||
|
||||
private:
|
||||
QSharedPointer<KNSCore::ProviderCore> m_core;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#include "providercore.h"
|
||||
|
||||
#include "providerbase_p.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::ProviderCorePrivate
|
||||
{
|
||||
public:
|
||||
ProviderBase *base;
|
||||
};
|
||||
|
||||
KNSCore::ProviderCore::ProviderCore(ProviderBase *base, QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new ProviderCorePrivate{.base = [this, base] {
|
||||
connect(base, &ProviderBase::basicsLoaded, this, &ProviderCore::basicsLoaded);
|
||||
base->setParent(this);
|
||||
return base;
|
||||
}()})
|
||||
{
|
||||
}
|
||||
|
||||
KNSCore::ProviderCore::~ProviderCore() = default;
|
||||
|
||||
QString KNSCore::ProviderCore::version() const
|
||||
{
|
||||
return d->base->version();
|
||||
}
|
||||
|
||||
QUrl KNSCore::ProviderCore::website() const
|
||||
{
|
||||
return d->base->website();
|
||||
}
|
||||
|
||||
QUrl KNSCore::ProviderCore::host() const
|
||||
{
|
||||
return d->base->host();
|
||||
}
|
||||
|
||||
QString KNSCore::ProviderCore::contactEmail() const
|
||||
{
|
||||
return d->base->contactEmail();
|
||||
}
|
||||
|
||||
bool KNSCore::ProviderCore::supportsSsl() const
|
||||
{
|
||||
return d->base->supportsSsl();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
class Engine;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class ProviderBase;
|
||||
|
||||
/**
|
||||
* @short KNewStuff Base Provider class.
|
||||
*
|
||||
* This class provides accessors for the provider object.
|
||||
* It should not be used directly by the application.
|
||||
* This class is the base class and will be instantiated for
|
||||
* static website providers.
|
||||
*
|
||||
* @author Jeremy Whiting <jpwhiting@kde.org>
|
||||
* @since 6.9
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT ProviderCore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString version READ version NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QUrl website READ website NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QUrl host READ host NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(QString contactEmail READ contactEmail NOTIFY basicsLoaded)
|
||||
Q_PROPERTY(bool supportsSsl READ supportsSsl NOTIFY basicsLoaded)
|
||||
public:
|
||||
~ProviderCore() override;
|
||||
Q_DISABLE_COPY_MOVE(ProviderCore)
|
||||
|
||||
[[nodiscard]] QString version() const;
|
||||
[[nodiscard]] QUrl website() const;
|
||||
[[nodiscard]] QUrl host() const;
|
||||
/**
|
||||
* The general contact email for this provider
|
||||
* @return The general contact email for this provider
|
||||
*/
|
||||
[[nodiscard]] QString contactEmail() const;
|
||||
/**
|
||||
* Whether or not the provider supports SSL connections
|
||||
* @return True if the server supports SSL connections, false if not
|
||||
*/
|
||||
[[nodiscard]] bool supportsSsl() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void basicsLoaded();
|
||||
|
||||
private:
|
||||
friend class EngineBase;
|
||||
friend class EngineBasePrivate;
|
||||
friend class ResultsStream;
|
||||
friend class ProviderBubbleWrap;
|
||||
friend class Transaction;
|
||||
friend class TransactionPrivate;
|
||||
friend class ::Engine; // quick engine
|
||||
ProviderCore(ProviderBase *base, QObject *parent = nullptr);
|
||||
const std::unique_ptr<class ProviderCorePrivate> d;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class ProviderCorePrivate
|
||||
{
|
||||
public:
|
||||
class ProviderBase *base;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2021 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 "providersmodel.h"
|
||||
|
||||
#include "enginebase.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class ProvidersModelPrivate
|
||||
{
|
||||
ProvidersModel *const q;
|
||||
|
||||
public:
|
||||
explicit ProvidersModelPrivate(ProvidersModel *qq)
|
||||
: q(qq)
|
||||
{
|
||||
}
|
||||
|
||||
EngineBase *getEngine() const;
|
||||
void setEngine(EngineBase *engine);
|
||||
|
||||
EngineBase *engine = nullptr;
|
||||
QStringList knownProviders;
|
||||
};
|
||||
|
||||
EngineBase *ProvidersModelPrivate::getEngine() const
|
||||
{
|
||||
return engine;
|
||||
}
|
||||
|
||||
void ProvidersModelPrivate::setEngine(EngineBase *engine)
|
||||
{
|
||||
q->setEngine(engine);
|
||||
}
|
||||
|
||||
ProvidersModel::ProvidersModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, d(new ProvidersModelPrivate(this))
|
||||
{
|
||||
}
|
||||
|
||||
ProvidersModel::~ProvidersModel() = default;
|
||||
|
||||
QHash<int, QByteArray> KNSCore::ProvidersModel::roleNames() const
|
||||
{
|
||||
static const QHash<int, QByteArray> roles{
|
||||
{IdRole, "id"},
|
||||
{NameRole, "name"},
|
||||
{VersionRole, "version"},
|
||||
{WebsiteRole, "website"},
|
||||
{HostRole, "host"},
|
||||
{ContactEmailRole, "contactEmail"},
|
||||
{SupportsSslRole, "supportsSsl"},
|
||||
{IconRole, "icon"},
|
||||
{ObjectRole, "object"},
|
||||
};
|
||||
return roles;
|
||||
}
|
||||
|
||||
int KNSCore::ProvidersModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
return d->knownProviders.count();
|
||||
}
|
||||
|
||||
QVariant KNSCore::ProvidersModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (checkIndex(index) && d->engine) {
|
||||
QSharedPointer<Provider> provider = d->engine->provider(d->knownProviders.value(index.row()));
|
||||
if (provider) {
|
||||
switch (role) {
|
||||
case IdRole:
|
||||
return provider->id();
|
||||
case NameRole:
|
||||
return provider->name();
|
||||
case VersionRole:
|
||||
return provider->version();
|
||||
case WebsiteRole:
|
||||
return provider->website();
|
||||
case HostRole:
|
||||
return provider->host();
|
||||
case ContactEmailRole:
|
||||
return provider->contactEmail();
|
||||
case SupportsSslRole:
|
||||
return provider->supportsSsl();
|
||||
case IconRole:
|
||||
return provider->icon();
|
||||
case ObjectRole:
|
||||
return QVariant::fromValue(provider.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QObject *KNSCore::ProvidersModel::engine() const
|
||||
{
|
||||
return d->engine;
|
||||
}
|
||||
|
||||
void KNSCore::ProvidersModel::setEngine(QObject *engine)
|
||||
{
|
||||
if (d->engine != engine) {
|
||||
if (d->engine) {
|
||||
d->engine->disconnect(this);
|
||||
}
|
||||
d->engine = qobject_cast<EngineBase *>(engine);
|
||||
Q_EMIT engineChanged();
|
||||
if (d->engine) {
|
||||
connect(d->engine, &EngineBase::providersChanged, this, [this]() {
|
||||
beginResetModel();
|
||||
d->knownProviders = d->engine->providerIDs();
|
||||
endResetModel();
|
||||
});
|
||||
beginResetModel();
|
||||
d->knownProviders = d->engine->providerIDs();
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#include "moc_providersmodel.cpp"
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2021 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 KNSCORE_PROVIDERSMODELL_H
|
||||
#define KNSCORE_PROVIDERSMODELL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include "enginebase.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class ProvidersModelPrivate;
|
||||
/**
|
||||
* @brief A model which holds information on all known Providers for a specific Engine
|
||||
*
|
||||
* @since 5.85
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT ProvidersModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
/**
|
||||
* The Engine for which this model displays Providers
|
||||
*/
|
||||
Q_PRIVATE_PROPERTY(d, EngineBase *engine READ getEngine WRITE setEngine NOTIFY engineChanged)
|
||||
public:
|
||||
explicit ProvidersModel(QObject *parent = nullptr);
|
||||
~ProvidersModel() override;
|
||||
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
NameRole,
|
||||
VersionRole,
|
||||
WebsiteRole,
|
||||
HostRole,
|
||||
ContactEmailRole,
|
||||
SupportsSslRole,
|
||||
IconRole,
|
||||
ObjectRole, ///< The actual Provider object. Do not hold this locally and expect it to disappear at a moment's notice
|
||||
};
|
||||
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;
|
||||
|
||||
// TODO KF7: Port property to back to public getter/setter, narrow types to EngineBase
|
||||
QObject *engine() const;
|
||||
void setEngine(QObject *engine);
|
||||
Q_SIGNAL void engineChanged();
|
||||
|
||||
private:
|
||||
std::unique_ptr<ProvidersModelPrivate> d;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // KNSCORE_PROVIDERSMODELL_H
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
This file is part of KNewStuffCore.
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "question.h"
|
||||
|
||||
#include "entry.h"
|
||||
#include "questionmanager.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QEventLoop>
|
||||
#include <optional>
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::QuestionPrivate
|
||||
{
|
||||
public:
|
||||
QuestionPrivate()
|
||||
: questionType(Question::YesNoQuestion)
|
||||
, response()
|
||||
{
|
||||
}
|
||||
QString question;
|
||||
QString title;
|
||||
QStringList list;
|
||||
Entry entry;
|
||||
|
||||
QEventLoop loop;
|
||||
Question::QuestionType questionType;
|
||||
std::optional<Question::Response> response;
|
||||
QString textResponse;
|
||||
};
|
||||
|
||||
Question::Question(QuestionType questionType, QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new QuestionPrivate)
|
||||
{
|
||||
d->questionType = questionType;
|
||||
}
|
||||
|
||||
Question::~Question() = default;
|
||||
|
||||
Question::Response Question::ask()
|
||||
{
|
||||
Q_EMIT QuestionManager::instance() -> askQuestion(this);
|
||||
if (!d->response.has_value()) {
|
||||
d->loop.exec(); // Wait for the setResponse method to quit the event loop
|
||||
}
|
||||
return *d->response;
|
||||
}
|
||||
|
||||
Question::QuestionType Question::questionType() const
|
||||
{
|
||||
return d->questionType;
|
||||
}
|
||||
|
||||
void Question::setQuestionType(Question::QuestionType newType)
|
||||
{
|
||||
d->questionType = newType;
|
||||
}
|
||||
|
||||
void Question::setQuestion(const QString &newQuestion)
|
||||
{
|
||||
d->question = newQuestion;
|
||||
}
|
||||
|
||||
QString Question::question() const
|
||||
{
|
||||
return d->question;
|
||||
}
|
||||
|
||||
void Question::setTitle(const QString &newTitle)
|
||||
{
|
||||
d->title = newTitle;
|
||||
}
|
||||
|
||||
QString Question::title() const
|
||||
{
|
||||
return d->title;
|
||||
}
|
||||
|
||||
void Question::setList(const QStringList &newList)
|
||||
{
|
||||
d->list = newList;
|
||||
}
|
||||
|
||||
QStringList Question::list() const
|
||||
{
|
||||
return d->list;
|
||||
}
|
||||
|
||||
void Question::setResponse(Response response)
|
||||
{
|
||||
d->response = response;
|
||||
d->loop.quit();
|
||||
}
|
||||
|
||||
void Question::setResponse(const QString &response)
|
||||
{
|
||||
d->textResponse = response;
|
||||
}
|
||||
|
||||
QString Question::response() const
|
||||
{
|
||||
return d->textResponse;
|
||||
}
|
||||
|
||||
void Question::setEntry(const Entry &entry)
|
||||
{
|
||||
d->entry = entry;
|
||||
}
|
||||
|
||||
Entry Question::entry() const
|
||||
{
|
||||
return d->entry;
|
||||
}
|
||||
|
||||
#include "moc_question.cpp"
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
This file is part of KNewStuffCore.
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNS3_QUESTION_H
|
||||
#define KNS3_QUESTION_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class Entry;
|
||||
class QuestionPrivate;
|
||||
/**
|
||||
* @short A way to ask a user a question from inside a GUI-less library (like KNewStuffCore)
|
||||
*
|
||||
* Rather than using a message box (which is a UI thing), when you want to ask your user
|
||||
* a question, create an instance of this class and use that instead. The consuming library
|
||||
* (in most cases KNewStuff or KNewStuffQuick) will listen to any question being asked,
|
||||
* and act appropriately (that is, KNewStuff will show a dialog with an appropriate dialog
|
||||
* box, and KNewStuffQuick will either request a question be asked if the developer is using
|
||||
* the plugin directly, or ask the question using an appropriate method for Qt Quick based
|
||||
* applications)
|
||||
*
|
||||
* The following is an example of a question asking the user to select an item from a list.
|
||||
*
|
||||
* @code
|
||||
QStringList choices() << "foo" << "bar";
|
||||
Question question(Question::SelectFromListQuestion);
|
||||
question.setTitle("Pick your option");
|
||||
question.setQuestion("Please select which option you would like");
|
||||
question.setList(choices);
|
||||
question.setEntry(entry);
|
||||
if (question.ask() == Question::OKResponse) {
|
||||
QString theChoice = question.response();
|
||||
}
|
||||
@endcode
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT Question : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Response {
|
||||
InvalidResponse = 0,
|
||||
YesResponse = 1,
|
||||
NoResponse = 2,
|
||||
ContinueResponse = 3,
|
||||
CancelResponse = 4,
|
||||
OKResponse = YesResponse,
|
||||
};
|
||||
Q_ENUM(Response)
|
||||
|
||||
enum QuestionType {
|
||||
YesNoQuestion = 0,
|
||||
ContinueCancelQuestion = 1,
|
||||
InputTextQuestion = 2,
|
||||
SelectFromListQuestion = 3,
|
||||
PasswordQuestion = 4,
|
||||
};
|
||||
Q_ENUM(QuestionType)
|
||||
|
||||
explicit Question(QuestionType = YesNoQuestion, QObject *parent = nullptr);
|
||||
~Question() override;
|
||||
|
||||
Response ask();
|
||||
|
||||
void setQuestionType(QuestionType newType = YesNoQuestion);
|
||||
QuestionType questionType() const;
|
||||
|
||||
void setQuestion(const QString &newQuestion);
|
||||
QString question() const;
|
||||
void setTitle(const QString &newTitle);
|
||||
QString title() const;
|
||||
void setList(const QStringList &newList);
|
||||
QStringList list() const;
|
||||
void setEntry(const Entry &entry);
|
||||
Entry entry() const;
|
||||
|
||||
/**
|
||||
* When the user makes a choice on a question, that is a response. This is the return value in ask().
|
||||
* @param response This will set the response, and mark the question as answered
|
||||
*/
|
||||
void setResponse(Response response);
|
||||
/**
|
||||
* If the user has any way of inputting data to go along with the response above, consider this a part
|
||||
* of the response. As such, you can set, and later get, that response as well. This does NOT mark the
|
||||
* question as answered ( @see setResponse(Response) ).
|
||||
* @param response This sets the string response for the question
|
||||
*/
|
||||
void setResponse(const QString &response);
|
||||
QString response() const;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<QuestionPrivate> d;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // KNS3_QUESTION_H
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
This file is part of KNewStuffCore.
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "questionlistener.h"
|
||||
#include "question.h"
|
||||
#include "questionmanager.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
QuestionListener::QuestionListener(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(QuestionManager::instance(), &QuestionManager::askQuestion, this, &QuestionListener::askQuestion);
|
||||
}
|
||||
|
||||
QuestionListener::~QuestionListener() = default;
|
||||
|
||||
#include "moc_questionlistener.cpp"
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This file is part of KNewStuffCore.
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNS3_QUESTIONLISTENER_H
|
||||
#define KNS3_QUESTIONLISTENER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class Question;
|
||||
/**
|
||||
* @short Implementation-side handler class for questions sent from KNewStuffCore
|
||||
*
|
||||
* When implementing anything on top of KNewStuffCore, you will need to be able
|
||||
* to react to questions asked from inside the framework. This is done by creating
|
||||
* an instance of a QuestionListener, and reacting to any calls to the askQuestion
|
||||
* slot, which you must extend and implement. Two examples of this exist, in the
|
||||
* form of the KNS3::WidgetQuestionListener and KNewStuffQuick::QuickQuestionListener
|
||||
* and should you need to create your own, take inspiration from them.
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT QuestionListener : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit QuestionListener(QObject *parent = nullptr);
|
||||
~QuestionListener() override;
|
||||
|
||||
virtual void askQuestion(Question *question) = 0;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // KNS3_QUESTIONLISTENER_H
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
This file is part of KNewStuffCore.
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "questionmanager.h"
|
||||
|
||||
#include "question.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class QuestionManagerHelper
|
||||
{
|
||||
public:
|
||||
QuestionManagerHelper() = default;
|
||||
~QuestionManagerHelper()
|
||||
{
|
||||
delete q;
|
||||
}
|
||||
QuestionManager *q = nullptr;
|
||||
};
|
||||
Q_GLOBAL_STATIC(QuestionManagerHelper, s_kns3_questionManager)
|
||||
|
||||
QuestionManager *QuestionManager::instance()
|
||||
{
|
||||
if (!s_kns3_questionManager()->q) {
|
||||
s_kns3_questionManager()->q = new QuestionManager;
|
||||
}
|
||||
return s_kns3_questionManager()->q;
|
||||
}
|
||||
|
||||
QuestionManager::QuestionManager()
|
||||
: QObject()
|
||||
{
|
||||
Q_UNUSED(d)
|
||||
}
|
||||
|
||||
QuestionManager::~QuestionManager() = default;
|
||||
}
|
||||
|
||||
#include "moc_questionmanager.cpp"
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
This file is part of KNewStuffCore.
|
||||
SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNS3_QUESTIONMANAGER_H
|
||||
#define KNS3_QUESTIONMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
namespace KNSCore
|
||||
{
|
||||
class Question;
|
||||
class QuestionManagerPrivate;
|
||||
/**
|
||||
* @short The central class handling Question redirection
|
||||
*
|
||||
* This class is used to ensure that KNSCore::Question instances get redirected
|
||||
* to the appropriate KNSCore::QuestionListener instances. It is a very dumb class
|
||||
* which only ensures the listeners have somewhere to listen to, and the
|
||||
* questions have somewhere to ask to be asked.
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT QuestionManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(QuestionManager)
|
||||
public:
|
||||
static QuestionManager *instance();
|
||||
~QuestionManager() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void askQuestion(KNSCore::Question *question);
|
||||
|
||||
private:
|
||||
QuestionManager();
|
||||
const void *d; // Future BIC
|
||||
};
|
||||
}
|
||||
|
||||
#endif // KNS3_QUESTIONMANAGER_H
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "resultsstream.h"
|
||||
#include "enginebase_p.h"
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#include "providerbase_p.h"
|
||||
#include "providercore.h"
|
||||
#include "providercore_p.h"
|
||||
#include "searchrequest.h"
|
||||
#include "searchrequest_p.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::ResultsStreamPrivate
|
||||
{
|
||||
public:
|
||||
QList<QSharedPointer<KNSCore::ProviderCore>> providers;
|
||||
EngineBase const *engine;
|
||||
SearchRequest request;
|
||||
bool finished = false;
|
||||
int queuedFetch = 0;
|
||||
};
|
||||
|
||||
#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
|
||||
ResultsStream::ResultsStream([[maybe_unused]] const Provider::SearchRequest &request, EngineBase *base)
|
||||
: KNSCore::ResultsStream(SearchRequest(), base)
|
||||
{
|
||||
// This ctor should not be used. It is private and we don't use. Nobody else should either. Here for ABI stability.
|
||||
Q_ASSERT(false);
|
||||
qFatal("Do not use private constructors!");
|
||||
}
|
||||
#endif
|
||||
|
||||
ResultsStream::ResultsStream(const SearchRequest &request, EngineBase *base)
|
||||
: d(new ResultsStreamPrivate{
|
||||
.providers = base->d->providerCores.values(),
|
||||
.engine = base,
|
||||
.request = request,
|
||||
})
|
||||
{
|
||||
auto entriesLoaded = [this](const KNSCore::SearchRequest &request, const KNSCore::Entry::List &entries) {
|
||||
if (request.d != d->request.d) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT entriesFound(entries);
|
||||
};
|
||||
|
||||
auto done = [this](const KNSCore::SearchRequest &request) {
|
||||
if (request.d != d->request.d) {
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << this << "Finishing" << sender() << request.d->id;
|
||||
|
||||
auto base = qobject_cast<ProviderBase *>(sender());
|
||||
Q_ASSERT_X(base, Q_FUNC_INFO, "Sender failed to cast to ProviderBase");
|
||||
if (const auto coresRemoved = d->providers.removeIf([base](const auto &core) {
|
||||
return core->d->base == base;
|
||||
});
|
||||
coresRemoved <= 0) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Request finished twice, check your provider" << sender() << d->engine;
|
||||
|
||||
Q_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (d->providers.isEmpty()) {
|
||||
d->finished = true;
|
||||
if (d->queuedFetch > 0) {
|
||||
d->queuedFetch--;
|
||||
fetchMore();
|
||||
return;
|
||||
}
|
||||
|
||||
d->request = {}; // prevent this stream from making more requests
|
||||
d->finished = true;
|
||||
finish();
|
||||
}
|
||||
};
|
||||
auto failed = [this](const KNSCore::SearchRequest &request) {
|
||||
if (request.d == d->request.d) {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
auto seenProviders = d->providers;
|
||||
seenProviders.clear();
|
||||
for (const auto &provider : d->providers) {
|
||||
Q_ASSERT(!seenProviders.contains(provider));
|
||||
seenProviders.append(provider);
|
||||
|
||||
connect(provider->d->base, &ProviderBase::entriesLoaded, this, entriesLoaded);
|
||||
connect(provider->d->base, &ProviderBase::loadingDone, this, done);
|
||||
connect(provider->d->base, &ProviderBase::entryDetailsLoaded, this, [this](const KNSCore::Entry &entry) {
|
||||
if (d->request.d->filter == KNSCore::Filter::ExactEntryId && d->request.d->searchTerm == entry.uniqueId()) {
|
||||
if (entry.isValid()) {
|
||||
Q_EMIT entriesFound({entry});
|
||||
}
|
||||
finish();
|
||||
}
|
||||
});
|
||||
connect(provider->d->base, &ProviderBase::loadingFailed, this, failed);
|
||||
}
|
||||
}
|
||||
|
||||
ResultsStream::~ResultsStream() = default;
|
||||
|
||||
void ResultsStream::fetch()
|
||||
{
|
||||
if (d->finished) {
|
||||
Q_ASSERT_X(false, Q_FUNC_INFO, "Called fetch on an already finished stream. Call fetchMore.");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << this << "fetching" << d->request;
|
||||
if (d->request.d->filter != Filter::Installed) {
|
||||
// when asking for installed entries, never use the cache
|
||||
Entry::List cacheEntries = d->engine->d->cache->requestFromCache(d->request);
|
||||
if (!cacheEntries.isEmpty()) {
|
||||
Q_EMIT entriesFound(cacheEntries);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &providerCore : std::as_const(d->providers)) {
|
||||
auto provider = providerCore->d->base;
|
||||
qDebug() << this << "loading entries from provider" << provider;
|
||||
if (provider->isInitialized()) {
|
||||
QTimer::singleShot(0, this, [this, provider] {
|
||||
provider->loadEntries(d->request);
|
||||
});
|
||||
} else {
|
||||
connect(provider, &KNSCore::ProviderBase::providerInitialized, this, [this, provider] {
|
||||
disconnect(provider, &KNSCore::ProviderBase::providerInitialized, this, nullptr);
|
||||
provider->loadEntries(d->request);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResultsStream::fetchMore()
|
||||
{
|
||||
// fetchMore requires some extra tinkering but this is worthwhile. By offering a fetchMore we can fully encapsulate
|
||||
// a search state so the caller doesn't have to worry about persisting SearchRequests. Instead we'll do it for them.
|
||||
if (!d->finished) {
|
||||
d->queuedFetch++;
|
||||
return;
|
||||
}
|
||||
d->finished = false;
|
||||
const auto nextPage = d->request.d->page + 1;
|
||||
d->request =
|
||||
SearchRequest(d->request.d->sortMode, d->request.d->filter, d->request.d->searchTerm, d->request.d->categories, nextPage, d->request.d->pageSize);
|
||||
d->providers = d->engine->d->providerCores.values();
|
||||
fetch();
|
||||
}
|
||||
|
||||
void ResultsStream::finish()
|
||||
{
|
||||
Q_EMIT finished();
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
#include "moc_resultsstream.cpp"
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef RESULTSSTREAM_H
|
||||
#define RESULTSSTREAM_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "enginebase.h"
|
||||
#include "provider.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class SearchRequest;
|
||||
class ResultsStreamPrivate;
|
||||
/**
|
||||
* The ResultsStream is returned by EngineBase::search. It is used to communicate
|
||||
* the different entries in response to a request using the signal @m entriesFound.
|
||||
*
|
||||
* Initially the stream will communicate the entries part of the page as specified
|
||||
* in the request. Further pages can be requested using @m fetchMore.
|
||||
*
|
||||
* Once we have reached the end of the requested stream, the object shall emit
|
||||
* @m finished and delete itself.
|
||||
*
|
||||
* @since 6.0
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT ResultsStream : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
~ResultsStream() override;
|
||||
|
||||
/// Issues the search, make sure all signals are connected before calling
|
||||
void fetch();
|
||||
|
||||
/// Increments the requested page and issues another search
|
||||
void fetchMore();
|
||||
|
||||
Q_SIGNALS:
|
||||
void entriesFound(const KNSCore::Entry::List &entries);
|
||||
void finished();
|
||||
|
||||
private:
|
||||
friend class EngineBase;
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/// @deprecated since 6.9 Use SearchRequest constructor
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "Use SearchRequest constructor")
|
||||
ResultsStream(const Provider::SearchRequest &request, EngineBase *base);
|
||||
#endif
|
||||
/**
|
||||
* @param request The search request to be issued
|
||||
* @param base The engine issuing the request
|
||||
* @since 6.9
|
||||
*/
|
||||
ResultsStream(const SearchRequest &request, EngineBase *base);
|
||||
void finish();
|
||||
|
||||
std::unique_ptr<ResultsStreamPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // RESULTSSTREAM_H
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#include "searchpreset.h"
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
class KNSCore::SearchPresetPrivate
|
||||
{
|
||||
public:
|
||||
SearchRequest request;
|
||||
QString displayName;
|
||||
QString iconName;
|
||||
SearchPreset::Type type;
|
||||
QString providerId; // not all providers can handle all search requests.
|
||||
};
|
||||
|
||||
KNSCore::SearchPreset::SearchPreset(SearchPresetPrivate *dptr)
|
||||
: d(dptr)
|
||||
{
|
||||
}
|
||||
|
||||
SearchRequest KNSCore::SearchPreset::request() const
|
||||
{
|
||||
return d->request;
|
||||
}
|
||||
|
||||
QString KNSCore::SearchPreset::displayName() const
|
||||
{
|
||||
return d->displayName;
|
||||
}
|
||||
|
||||
QString KNSCore::SearchPreset::iconName() const
|
||||
{
|
||||
return d->iconName;
|
||||
}
|
||||
|
||||
KNSCore::SearchPreset::Type KNSCore::SearchPreset::type() const
|
||||
{
|
||||
return d->type;
|
||||
}
|
||||
|
||||
QString KNSCore::SearchPreset::providerId() const
|
||||
{
|
||||
return d->providerId;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
#include "searchrequest.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class SearchPresetPrivate;
|
||||
|
||||
/**
|
||||
* Describes a search request that may come from the provider.
|
||||
* This is used by the OPDS provider to handle the different urls.
|
||||
* @since 6.9
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT SearchPreset
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief The SearchPresetTypes enum
|
||||
* the preset type enum is a helper to identify the kind of label and icon
|
||||
* the search preset should have if none are found.
|
||||
* @since 6.9
|
||||
*/
|
||||
enum class Type {
|
||||
NoPresetType = 0,
|
||||
GoBack, ///< preset representing the previous search.
|
||||
Root, ///< preset indicating a root directory.
|
||||
Start, ///< preset indicating the first entry.
|
||||
Popular, ///< preset indicating popular items.
|
||||
Featured, ///< preset for featured items.
|
||||
Recommended, ///< preset for recommended. This may be customized by the server per user.
|
||||
Shelf, ///< preset indicating previously acquired items.
|
||||
Subscription, ///< preset indicating items that the user is subscribed to.
|
||||
New, ///< preset indicating new items.
|
||||
FolderUp, ///< preset indicating going up in the search result hierarchy.
|
||||
AllEntries, ///< preset indicating all possible entries, such as a crawlable list. Might be intense to load.
|
||||
};
|
||||
|
||||
[[nodiscard]] SearchRequest request() const;
|
||||
[[nodiscard]] QString displayName() const;
|
||||
[[nodiscard]] QString iconName() const;
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] QString providerId() const; // not all providers can handle all search requests.
|
||||
|
||||
private:
|
||||
friend class OPDSProviderPrivate;
|
||||
SearchPreset(SearchPresetPrivate *dptr);
|
||||
std::shared_ptr<SearchPresetPrivate> d;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
// SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
// SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
|
||||
|
||||
#pragma once
|
||||
#include "searchpreset.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
class SearchPresetPrivate
|
||||
{
|
||||
public:
|
||||
SearchRequest request;
|
||||
QString displayName;
|
||||
QString iconName;
|
||||
SearchPreset::Type type;
|
||||
QString providerId; // not all providers can handle all search requests.
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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 "searchrequest.h"
|
||||
#include "searchrequest_p.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
SearchRequest::SearchRequest(SortMode sortMode_, Filter filter_, const QString &searchTerm_, const QStringList &categories_, int page_, int pageSize_)
|
||||
: d(new SearchRequestPrivate{.sortMode = sortMode_,
|
||||
.filter = filter_,
|
||||
.searchTerm = searchTerm_,
|
||||
.categories = categories_,
|
||||
.page = page_,
|
||||
.pageSize = pageSize_,
|
||||
.id = SearchRequestPrivate::searchRequestId()})
|
||||
{
|
||||
}
|
||||
|
||||
SortMode SearchRequest::sortMode() const
|
||||
{
|
||||
return d->sortMode;
|
||||
}
|
||||
|
||||
Filter SearchRequest::filter() const
|
||||
{
|
||||
return d->filter;
|
||||
}
|
||||
|
||||
QString SearchRequest::searchTerm() const
|
||||
{
|
||||
return d->searchTerm;
|
||||
}
|
||||
|
||||
QStringList SearchRequest::categories() const
|
||||
{
|
||||
return d->categories;
|
||||
}
|
||||
|
||||
int SearchRequest::page() const
|
||||
{
|
||||
return d->page;
|
||||
}
|
||||
|
||||
int SearchRequest::pageSize() const
|
||||
{
|
||||
return d->page;
|
||||
}
|
||||
|
||||
SearchRequest SearchRequest::nextPage() const
|
||||
{
|
||||
return {sortMode(), filter(), searchTerm(), categories(), page() + 1, pageSize()};
|
||||
}
|
||||
|
||||
KNEWSTUFFCORE_EXPORT QDebug KNSCore::operator<<(QDebug dbg, const SearchRequest &search)
|
||||
{
|
||||
QDebugStateSaver saver(dbg);
|
||||
dbg.nospace();
|
||||
dbg << "SearchRequest(";
|
||||
dbg << "id: " << search.d->id << ',';
|
||||
dbg << "searchTerm: " << search.d->searchTerm << ',';
|
||||
dbg << "categories: " << search.d->categories << ',';
|
||||
dbg << "filter: " << search.d->filter << ',';
|
||||
dbg << "page: " << search.d->page << ',';
|
||||
dbg << "pageSize: " << search.d->pageSize;
|
||||
dbg << ')';
|
||||
return dbg;
|
||||
}
|
||||
|
||||
KNSCore::SearchRequest KNSCore::searchRequestFromLegacy(const KNSCore::Provider::SearchRequest &request)
|
||||
{
|
||||
return {[request] {
|
||||
switch (request.sortMode) {
|
||||
case Provider::SortMode::Alphabetical:
|
||||
return SortMode::Alphabetical;
|
||||
case Provider::SortMode::Downloads:
|
||||
return SortMode::Downloads;
|
||||
case Provider::SortMode::Newest:
|
||||
return SortMode::Newest;
|
||||
case Provider::SortMode::Rating:
|
||||
return SortMode::Rating;
|
||||
}
|
||||
Q_ASSERT(false);
|
||||
return SortMode::Rating;
|
||||
}(),
|
||||
[request] {
|
||||
switch (request.filter) {
|
||||
case Provider::Filter::None:
|
||||
return Filter::None;
|
||||
case Provider::Filter::Installed:
|
||||
return Filter::Installed;
|
||||
case Provider::Filter::Updates:
|
||||
return Filter::Updates;
|
||||
case Provider::Filter::ExactEntryId:
|
||||
return Filter::ExactEntryId;
|
||||
}
|
||||
Q_ASSERT(false);
|
||||
return Filter::None;
|
||||
}(),
|
||||
request.searchTerm,
|
||||
request.categories,
|
||||
request.page,
|
||||
request.pageSize};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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 <QStringList>
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
Q_NAMESPACE_EXPORT(KNEWSTUFFCORE_EXPORT)
|
||||
|
||||
struct SearchRequestPrivate;
|
||||
|
||||
/**
|
||||
* @since 6.9
|
||||
*/
|
||||
enum class SortMode {
|
||||
Newest,
|
||||
Alphabetical,
|
||||
Rating,
|
||||
Downloads,
|
||||
};
|
||||
Q_ENUM_NS(SortMode)
|
||||
|
||||
/**
|
||||
* @since 6.9
|
||||
*/
|
||||
enum class Filter {
|
||||
None,
|
||||
Installed,
|
||||
Updates,
|
||||
ExactEntryId,
|
||||
};
|
||||
Q_ENUM_NS(Filter)
|
||||
|
||||
KNEWSTUFFCORE_EXPORT QDebug operator<<(QDebug, const class SearchRequest &);
|
||||
|
||||
/**
|
||||
* @brief A search request
|
||||
* @since 6.9
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT SearchRequest
|
||||
{
|
||||
public:
|
||||
SearchRequest(SortMode sortMode_ = KNSCore::SortMode::Downloads,
|
||||
Filter filter_ = KNSCore::Filter::None,
|
||||
const QString &searchTerm_ = {},
|
||||
const QStringList &categories_ = {},
|
||||
int page_ = -1,
|
||||
int pageSize_ = 20);
|
||||
|
||||
[[nodiscard]] SortMode sortMode() const;
|
||||
[[nodiscard]] Filter filter() const;
|
||||
[[nodiscard]] QString searchTerm() const;
|
||||
[[nodiscard]] QStringList categories() const;
|
||||
[[nodiscard]] int page() const;
|
||||
[[nodiscard]] int pageSize() const;
|
||||
[[nodiscard]] SearchRequest nextPage() const;
|
||||
|
||||
private:
|
||||
friend class ResultsStream;
|
||||
friend class AtticaProvider;
|
||||
friend class AtticaRequester;
|
||||
friend class StaticXmlProvider;
|
||||
friend class OPDSProvider;
|
||||
friend class Cache2;
|
||||
friend QDebug KNSCore::operator<<(QDebug, const SearchRequest &);
|
||||
std::shared_ptr<SearchRequestPrivate> d;
|
||||
};
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 "provider.h"
|
||||
#include "searchrequest.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
|
||||
struct SearchRequestPrivate {
|
||||
KNSCore::SortMode sortMode;
|
||||
KNSCore::Filter filter;
|
||||
QString searchTerm;
|
||||
QStringList categories;
|
||||
int page;
|
||||
int pageSize;
|
||||
quint64 id;
|
||||
|
||||
[[nodiscard]] QString hashForRequest() const
|
||||
{
|
||||
return QString::number((int)sortMode) + QLatin1Char(',') + searchTerm + QLatin1Char(',') + categories.join(QLatin1Char('-')) + QLatin1Char(',')
|
||||
+ QString::number(page) + QLatin1Char(',') + QString::number(pageSize);
|
||||
}
|
||||
|
||||
bool operator==(const SearchRequestPrivate &other) const
|
||||
{
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
[[nodiscard]] static quint64 searchRequestId()
|
||||
{
|
||||
static quint64 id = 0;
|
||||
return id++;
|
||||
}
|
||||
};
|
||||
|
||||
KNSCore::SearchRequest searchRequestFromLegacy(const KNSCore::Provider::SearchRequest &request);
|
||||
|
||||
} // namespace KNSCore
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2018 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "tagsfilterchecker.h"
|
||||
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include <QMap>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class TagsFilterCheckerPrivate
|
||||
{
|
||||
public:
|
||||
TagsFilterCheckerPrivate()
|
||||
{
|
||||
}
|
||||
~TagsFilterCheckerPrivate()
|
||||
{
|
||||
qDeleteAll(validators);
|
||||
}
|
||||
class Validator;
|
||||
// If people start using a LOT of validators (>20ish), we can always change it, but
|
||||
// for now it seems reasonable that QMap is better than QHash here...
|
||||
QMap<QString, Validator *> validators;
|
||||
|
||||
class Validator
|
||||
{
|
||||
public:
|
||||
Validator(const QString &tag, const QString &value)
|
||||
: m_tag(tag)
|
||||
{
|
||||
if (!value.isNull()) {
|
||||
m_acceptedValues << value;
|
||||
}
|
||||
}
|
||||
virtual ~Validator()
|
||||
{
|
||||
}
|
||||
virtual bool filterAccepts(const QString &tag, const QString &value) = 0;
|
||||
|
||||
protected:
|
||||
friend class TagsFilterCheckerPrivate;
|
||||
QString m_tag;
|
||||
QStringList m_acceptedValues;
|
||||
};
|
||||
|
||||
// Will only accept entries which have one of the accepted values set for the tag key
|
||||
class EqualityValidator : public Validator
|
||||
{
|
||||
public:
|
||||
EqualityValidator(const QString &tag, const QString &value)
|
||||
: Validator(tag, value)
|
||||
{
|
||||
}
|
||||
~EqualityValidator() override
|
||||
{
|
||||
}
|
||||
bool filterAccepts(const QString &tag, const QString &value) override
|
||||
{
|
||||
bool result = true;
|
||||
if (tag == m_tag && !m_acceptedValues.contains(value)) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Item excluded by filter on" << m_tag << "because" << value << "was not included in" << m_acceptedValues;
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// Will only accept entries which have none of the values set for the tag key
|
||||
class InequalityValidator : public Validator
|
||||
{
|
||||
public:
|
||||
InequalityValidator(const QString &tag, const QString &value)
|
||||
: Validator(tag, value)
|
||||
{
|
||||
}
|
||||
~InequalityValidator() override
|
||||
{
|
||||
}
|
||||
bool filterAccepts(const QString &tag, const QString &value) override
|
||||
{
|
||||
bool result = true;
|
||||
if (tag == m_tag && m_acceptedValues.contains(value)) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Item excluded by filter on" << m_tag << "because" << value << "was included in" << m_acceptedValues;
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
void addValidator(const QString &filter)
|
||||
{
|
||||
int pos = 0;
|
||||
if ((pos = filter.indexOf(QLatin1String("=="))) > -1) {
|
||||
QString tag = filter.left(pos);
|
||||
QString value = filter.mid(tag.length() + 2);
|
||||
Validator *val = validators.value(tag, nullptr);
|
||||
if (!val) {
|
||||
val = new EqualityValidator(tag, QString());
|
||||
validators.insert(tag, val);
|
||||
}
|
||||
val->m_acceptedValues << value;
|
||||
qCDebug(KNEWSTUFFCORE) << "Created EqualityValidator for tag" << tag << "with value" << value;
|
||||
} else if ((pos = filter.indexOf(QLatin1String("!="))) > -1) {
|
||||
QString tag = filter.left(pos);
|
||||
QString value = filter.mid(tag.length() + 2);
|
||||
Validator *val = validators.value(tag, nullptr);
|
||||
if (!val) {
|
||||
val = new InequalityValidator(tag, QString());
|
||||
validators.insert(tag, val);
|
||||
}
|
||||
val->m_acceptedValues << value;
|
||||
qCDebug(KNEWSTUFFCORE) << "Created InequalityValidator for tag" << tag << "with value" << value;
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "Critical error attempting to create tag filter validators. The filter is defined as" << filter
|
||||
<< "which is not in the accepted formats key==value or key!=value";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TagsFilterChecker::TagsFilterChecker(const QStringList &tagFilter)
|
||||
: d(new TagsFilterCheckerPrivate)
|
||||
{
|
||||
for (const QString &filter : tagFilter) {
|
||||
d->addValidator(filter);
|
||||
}
|
||||
}
|
||||
|
||||
TagsFilterChecker::~TagsFilterChecker() = default;
|
||||
|
||||
bool TagsFilterChecker::filterAccepts(const QStringList &tags)
|
||||
{
|
||||
// if any tag in the content matches any of the tag filters, skip this entry
|
||||
qCDebug(KNEWSTUFFCORE) << "Checking tags list" << tags << "against validators with keys" << d->validators.keys();
|
||||
for (const QString &tag : tags) {
|
||||
if (tag.isEmpty()) {
|
||||
// This happens when you do a split on an empty string (not an empty list, a list with one empty element... because reasons).
|
||||
// Also handy for other things, i guess, though, so let's just catch it here.
|
||||
continue;
|
||||
}
|
||||
QStringList current = tag.split(QLatin1Char('='));
|
||||
if (current.length() > 2) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Critical error attempting to filter tags. Entry has tag defined as" << tag
|
||||
<< "which is not in the format \"key=value\" or \"key\".";
|
||||
return false;
|
||||
} else if (current.length() == 1) {
|
||||
// If the tag is defined simply as a key, we give it the value "1", just to make our filtering work simpler
|
||||
current << QStringLiteral("1");
|
||||
}
|
||||
QMap<QString, TagsFilterCheckerPrivate::Validator *>::const_iterator i = d->validators.constBegin();
|
||||
while (i != d->validators.constEnd()) {
|
||||
if (!i.value()->filterAccepts(current.at(0), current.at(1))) {
|
||||
return false;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// If we have arrived here, nothing has filtered the entry
|
||||
// out (by being either incorrectly tagged or a filter rejecting
|
||||
// it), and consequently it is an acceptable entry.
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2018 Dan Leinir Turthra Jensen <admin@leinir.dk>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNSCORE_TAGSFILTERCHECKER_H
|
||||
#define KNSCORE_TAGSFILTERCHECKER_H
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
#include <QStringList>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class TagsFilterCheckerPrivate;
|
||||
/**
|
||||
* @brief Apply simple filtering logic to a list of tags
|
||||
*
|
||||
* == Examples of specifying tag filters ==
|
||||
* Value for tag "tagname" must be exactly "tagdata":
|
||||
* tagname==tagdata
|
||||
*
|
||||
* Value for tag "tagname" must be different from "tagdata":
|
||||
* tagname!=tagdata
|
||||
*
|
||||
* == Tag filter list ==
|
||||
* A tag filter list is a string list of filters as shown above, and a combination
|
||||
* of which might look like:
|
||||
*
|
||||
* - ghns_excluded!=1
|
||||
* - data##mimetype==application/cbr+zip
|
||||
* - data##mimetype==application/cbr+rar
|
||||
*
|
||||
* which would filter out anything which has ghns_excluded set to 1, and
|
||||
* anything where the value of data##mimetype does not equal either
|
||||
* "application/cbr+zip" or "application/cbr+rar".
|
||||
* Notice in particular the two data##mimetype entries. Use this
|
||||
* for when a tag may have multiple values.
|
||||
*
|
||||
* The value does not current support wildcards. The list should be considered
|
||||
* a binary AND operation (that is, all filter entries must match for the data
|
||||
* entry to be included in the return data)
|
||||
* @since 5.51
|
||||
*/
|
||||
// TODO KF7: privatize this class. it's not used by the outside
|
||||
class KNEWSTUFFCORE_EXPORT TagsFilterChecker
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Constructs an instance of the tags filter checker, prepopulated
|
||||
* with the list of tag filters in the tagFilter parameter.
|
||||
*
|
||||
* @param tagFilter The list of tag filters
|
||||
* @since 5.51
|
||||
*/
|
||||
explicit TagsFilterChecker(const QStringList &tagFilter);
|
||||
~TagsFilterChecker();
|
||||
|
||||
TagsFilterChecker(const TagsFilterChecker &) = delete;
|
||||
TagsFilterChecker &operator=(const TagsFilterChecker &) = delete;
|
||||
|
||||
/**
|
||||
* Check whether the filter list accepts the passed list of tags
|
||||
*
|
||||
* @param tags A list of tags in the form of key=value strings
|
||||
* @return True if the filter accepts the list, false if not
|
||||
* @since 5.51
|
||||
*/
|
||||
bool filterAccepts(const QStringList &tags);
|
||||
|
||||
private:
|
||||
const std::unique_ptr<TagsFilterCheckerPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // KNSCORE_TAGSFILTERCHECKER_H
|
||||
@@ -0,0 +1,474 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "transaction.h"
|
||||
#include "enginebase.h"
|
||||
#include "enginebase_p.h"
|
||||
#include "entry_p.h"
|
||||
#include "providerbase_p.h"
|
||||
#include "providercore.h"
|
||||
#include "providercore_p.h"
|
||||
#include "question.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <KShell>
|
||||
#include <QDir>
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
#include <QVersionNumber>
|
||||
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
using namespace KNSCore;
|
||||
|
||||
namespace
|
||||
{
|
||||
std::optional<int> linkIdFromVersions(const QList<DownloadLinkInformationV2Private> &downloadLinksInformationList)
|
||||
{
|
||||
switch (downloadLinksInformationList.size()) {
|
||||
case 0:
|
||||
return {};
|
||||
case 1:
|
||||
return downloadLinksInformationList.at(0).id;
|
||||
}
|
||||
|
||||
QMap<QVersionNumber, int> infoByVersion;
|
||||
for (const auto &info : downloadLinksInformationList) {
|
||||
const auto number = QVersionNumber::fromString(info.version);
|
||||
if (number.isNull()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Found no valid version number on linkid" << info.id << info.version;
|
||||
continue;
|
||||
}
|
||||
if (infoByVersion.contains(number)) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Encountered version number" << info.version << "more than once. Ignoring duplicates." << info.distributionType;
|
||||
continue;
|
||||
}
|
||||
infoByVersion[number] = info.id;
|
||||
}
|
||||
|
||||
if (infoByVersion.isEmpty()) { // found no valid version
|
||||
return {};
|
||||
}
|
||||
|
||||
return infoByVersion.last(); // map is sorted by keys, highest version is last entry.
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class KNSCore::TransactionPrivate
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] static Transaction *createInstallTransaction(const KNSCore::Entry &_entry, EngineBase *engine, int linkId)
|
||||
{
|
||||
auto ret = new Transaction(_entry, engine);
|
||||
QObject::connect(engine->d->installation, &Installation::signalInstallationError, ret, [ret, _entry](const QString &msg, const KNSCore::Entry &entry) {
|
||||
if (_entry.uniqueId() == entry.uniqueId()) {
|
||||
Q_EMIT ret->signalErrorCode(KNSCore::ErrorCode::InstallationError, msg, {});
|
||||
}
|
||||
});
|
||||
QTimer::singleShot(0, ret, [ret, linkId] {
|
||||
ret->d->installLinkId(linkId);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
TransactionPrivate(const KNSCore::Entry &entry, EngineBase *engine, Transaction *q)
|
||||
: m_engine(engine)
|
||||
, q(q)
|
||||
, subject(entry)
|
||||
{
|
||||
}
|
||||
|
||||
void finish()
|
||||
{
|
||||
m_finished = true;
|
||||
Q_EMIT q->finished();
|
||||
q->deleteLater();
|
||||
}
|
||||
|
||||
int findLinkIdToInstall(KNSCore::Entry &entry)
|
||||
{
|
||||
const auto downloadLinksInformationList = entry.d.constData()->mDownloadLinkInformationList;
|
||||
const auto optionalLinkId = linkIdFromVersions(downloadLinksInformationList);
|
||||
if (optionalLinkId.has_value()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Found linkid by version" << optionalLinkId.value();
|
||||
payloadToIdentify[entry] = QString{};
|
||||
return optionalLinkId.value();
|
||||
}
|
||||
|
||||
if (downloadLinksInformationList.size() == 1 || !entry.payload().isEmpty()) {
|
||||
// If there is only one downloadable item (which also includes a predefined payload name), then we can fairly safely assume that's
|
||||
// what we're wanting to update, meaning we can bypass some of the more expensive operations in downloadLinkLoaded
|
||||
qCDebug(KNEWSTUFFCORE) << "Just the one download link, so let's use that";
|
||||
payloadToIdentify[entry] = QString{};
|
||||
return 1;
|
||||
}
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "Try and identify a download link to use from a total of" << entry.downloadLinkCount();
|
||||
// While this seems silly, the payload gets reset when fetching the new download link information
|
||||
payloadToIdentify[entry] = entry.payload();
|
||||
// Drop a fresh list in place so we've got something to work with when we get the links
|
||||
payloads[entry] = QStringList{};
|
||||
return 1;
|
||||
}
|
||||
|
||||
// linkid may be -1 to denote the latest link id
|
||||
void installLinkId(int linkId)
|
||||
{
|
||||
if (subject.downloadLinkCount() == 0 && subject.payload().isEmpty()) {
|
||||
// Turns out this happens sometimes, so we should deal with that and spit out an error
|
||||
qCDebug(KNEWSTUFFCORE) << "There were no downloadlinks defined in the entry we were just asked to update: " << subject.uniqueId() << "on provider"
|
||||
<< subject.providerId();
|
||||
Q_EMIT q->signalErrorCode(
|
||||
KNSCore::ErrorCode::InstallationError,
|
||||
i18n("Could not perform an installation of the entry %1 as it does not have any downloadable items defined. Please contact the "
|
||||
"author so they can fix this.",
|
||||
subject.name()),
|
||||
subject.uniqueId());
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
KNSCore::Entry entry = subject;
|
||||
if (entry.status() == KNSCore::Entry::Updateable) {
|
||||
entry.setStatus(KNSCore::Entry::Updating);
|
||||
} else {
|
||||
entry.setStatus(KNSCore::Entry::Installing);
|
||||
}
|
||||
Q_EMIT q->signalEntryEvent(entry, Entry::StatusChangedEvent);
|
||||
|
||||
qCDebug(KNEWSTUFFCORE) << "Install " << entry.name() << " from: " << entry.providerId();
|
||||
auto provider = m_engine->d->providerCores.value(entry.providerId());
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(provider->d->base, &ProviderBase::payloadLinkLoaded, q, &Transaction::downloadLinkLoaded);
|
||||
// If linkId is -1, assume we don't know what to update
|
||||
if (linkId == -1) {
|
||||
linkId = findLinkIdToInstall(entry);
|
||||
} else {
|
||||
qCDebug(KNEWSTUFFCORE) << "Link ID already known" << linkId;
|
||||
// If there is no payload to identify, we will assume the payload is already known and just use that
|
||||
payloadToIdentify[entry] = QString{};
|
||||
}
|
||||
|
||||
provider->d->base->loadPayloadLink(entry, linkId);
|
||||
|
||||
m_finished = false;
|
||||
m_engine->updateStatus();
|
||||
}
|
||||
|
||||
EngineBase *const m_engine;
|
||||
Transaction *const q;
|
||||
bool m_finished = false;
|
||||
// Used for updating purposes - we ought to be saving this information, but we also have to deal with old stuff, and so... this will have to do for now
|
||||
// TODO KF6: Installed state needs to move onto a per-downloadlink basis rather than per-entry
|
||||
QMap<Entry, QStringList> payloads;
|
||||
QMap<Entry, QString> payloadToIdentify;
|
||||
const Entry subject;
|
||||
};
|
||||
|
||||
/**
|
||||
* we look for the directory where all the resources got installed.
|
||||
* assuming it was extracted into a directory
|
||||
*/
|
||||
static QDir sharedDir(QStringList dirs, QString rootPath)
|
||||
{
|
||||
// Ensure that rootPath definitely is a clean path with a slash at the end
|
||||
rootPath = QDir::cleanPath(rootPath) + QStringLiteral("/");
|
||||
qCInfo(KNEWSTUFFCORE) << Q_FUNC_INFO << dirs << rootPath;
|
||||
while (!dirs.isEmpty()) {
|
||||
QString thisDir(dirs.takeLast());
|
||||
if (thisDir.endsWith(QStringLiteral("*"))) {
|
||||
qCInfo(KNEWSTUFFCORE) << "Directory entry" << thisDir
|
||||
<< "ends in a *, indicating this was installed from an archive - see Installation::archiveEntries";
|
||||
thisDir.chop(1);
|
||||
}
|
||||
|
||||
const QString currentPath = QDir::cleanPath(thisDir);
|
||||
qCInfo(KNEWSTUFFCORE) << "Current path is" << currentPath;
|
||||
if (!currentPath.startsWith(rootPath)) {
|
||||
qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "does not start with" << rootPath << "and should be ignored";
|
||||
continue;
|
||||
}
|
||||
|
||||
const QFileInfo current(currentPath);
|
||||
qCInfo(KNEWSTUFFCORE) << "Current file info is" << current;
|
||||
if (!current.isDir()) {
|
||||
qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "is not a directory, and should be ignored";
|
||||
continue;
|
||||
}
|
||||
|
||||
const QDir dir(currentPath);
|
||||
if (dir.path() == (rootPath + dir.dirName())) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Found directory" << dir;
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
qCWarning(KNEWSTUFFCORE) << "Failed to locate any shared installed directory in" << dirs << "and this is almost certainly very bad.";
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString getAdoptionCommand(const QString &command, const KNSCore::Entry &entry, Installation *inst)
|
||||
{
|
||||
auto adoption = command;
|
||||
if (adoption.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QLatin1String dirReplace("%d");
|
||||
if (adoption.contains(dirReplace)) {
|
||||
QString installPath = sharedDir(entry.installedFiles(), inst->targetInstallationPath()).path();
|
||||
adoption.replace(dirReplace, KShell::quoteArg(installPath));
|
||||
}
|
||||
|
||||
const QLatin1String fileReplace("%f");
|
||||
if (adoption.contains(fileReplace)) {
|
||||
if (entry.installedFiles().isEmpty()) {
|
||||
qCWarning(KNEWSTUFFCORE) << "no installed files to adopt";
|
||||
return {};
|
||||
} else if (entry.installedFiles().count() != 1) {
|
||||
qCWarning(KNEWSTUFFCORE) << "can only adopt one file, will be using the first" << entry.installedFiles().at(0);
|
||||
}
|
||||
|
||||
adoption.replace(fileReplace, KShell::quoteArg(entry.installedFiles().at(0)));
|
||||
}
|
||||
return adoption;
|
||||
}
|
||||
|
||||
Transaction::Transaction(const KNSCore::Entry &entry, EngineBase *engine)
|
||||
: QObject(engine)
|
||||
, d(new TransactionPrivate(entry, engine, this))
|
||||
{
|
||||
connect(d->m_engine->d->installation, &Installation::signalEntryChanged, this, [this](const KNSCore::Entry &changedEntry) {
|
||||
Q_EMIT signalEntryEvent(changedEntry, Entry::StatusChangedEvent);
|
||||
d->m_engine->d->cache->registerChangedEntry(changedEntry);
|
||||
});
|
||||
connect(d->m_engine->d->installation, &Installation::signalInstallationFailed, this, [this](const QString &message, const KNSCore::Entry &entry) {
|
||||
if (entry == d->subject) {
|
||||
Q_EMIT signalErrorCode(KNSCore::ErrorCode::InstallationError, message, {});
|
||||
d->finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Transaction::~Transaction() = default;
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
Transaction *Transaction::install(EngineBase *engine, const KNSCore::Entry &_entry, int _linkId)
|
||||
{
|
||||
return TransactionPrivate::createInstallTransaction(_entry, engine, _linkId);
|
||||
}
|
||||
#endif
|
||||
|
||||
Transaction *Transaction::installLatest(EngineBase *engine, const KNSCore::Entry &_entry)
|
||||
{
|
||||
return TransactionPrivate::createInstallTransaction(_entry, engine, -1);
|
||||
}
|
||||
|
||||
Transaction *Transaction::installLinkId(EngineBase *engine, const KNSCore::Entry &_entry, quint8 _linkId)
|
||||
{
|
||||
return TransactionPrivate::createInstallTransaction(_entry, engine, _linkId);
|
||||
}
|
||||
|
||||
void Transaction::downloadLinkLoaded(const KNSCore::Entry &entry)
|
||||
{
|
||||
if (entry.status() == KNSCore::Entry::Updating) {
|
||||
if (d->payloadToIdentify[entry].isEmpty()) {
|
||||
// If there's nothing to identify, and we've arrived here, then we know what the payload is
|
||||
qCDebug(KNEWSTUFFCORE) << "If there's nothing to identify, and we've arrived here, then we know what the payload is";
|
||||
d->m_engine->d->installation->install(entry);
|
||||
connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
|
||||
if (entry.uniqueId() == finishedEntry.uniqueId()) {
|
||||
d->finish();
|
||||
}
|
||||
});
|
||||
d->payloadToIdentify.remove(entry);
|
||||
} else if (d->payloads[entry].count() < entry.downloadLinkCount()) {
|
||||
// We've got more to get before we can attempt to identify anything, so fetch the next one...
|
||||
qCDebug(KNEWSTUFFCORE) << "We've got more to get before we can attempt to identify anything, so fetch the next one...";
|
||||
QStringList payloads = d->payloads[entry];
|
||||
payloads << entry.payload();
|
||||
d->payloads[entry] = payloads;
|
||||
const auto &p = d->m_engine->d->providerCores.value(entry.providerId());
|
||||
if (p) {
|
||||
// ok, so this should definitely always work, but... safety first, kids!
|
||||
p->d->base->loadPayloadLink(entry, payloads.count());
|
||||
}
|
||||
} else {
|
||||
// We now have all the links, so let's try and identify the correct one...
|
||||
qCDebug(KNEWSTUFFCORE) << "We now have all the links, so let's try and identify the correct one...";
|
||||
QString identifiedLink;
|
||||
const QString payloadToIdentify = d->payloadToIdentify[entry];
|
||||
const QList<Entry::DownloadLinkInformation> downloadLinks = entry.downloadLinkInformationList();
|
||||
const QStringList &payloads = d->payloads[entry];
|
||||
|
||||
if (payloads.contains(payloadToIdentify)) {
|
||||
// Simplest option, the link hasn't changed at all
|
||||
qCDebug(KNEWSTUFFCORE) << "Simplest option, the link hasn't changed at all";
|
||||
identifiedLink = payloadToIdentify;
|
||||
} else {
|
||||
// Next simplest option, filename is the same but in a different folder
|
||||
qCDebug(KNEWSTUFFCORE) << "Next simplest option, filename is the same but in a different folder";
|
||||
const QString fileName = payloadToIdentify.split(QChar::fromLatin1('/')).last();
|
||||
for (const QString &payload : payloads) {
|
||||
if (payload.endsWith(fileName)) {
|
||||
identifiedLink = payload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...
|
||||
qCDebug(KNEWSTUFFCORE) << "Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...";
|
||||
QStringList payloadNames;
|
||||
for (const Entry::DownloadLinkInformation &downloadLink : downloadLinks) {
|
||||
qCDebug(KNEWSTUFFCORE) << "Download link" << downloadLink.name << downloadLink.id << downloadLink.size << downloadLink.descriptionLink;
|
||||
payloadNames << downloadLink.name;
|
||||
if (downloadLink.name == fileName) {
|
||||
identifiedLink = payloads[payloadNames.count() - 1];
|
||||
qCDebug(KNEWSTUFFCORE) << "Found a suitable download link for" << fileName << "which should match" << identifiedLink;
|
||||
}
|
||||
}
|
||||
|
||||
if (identifiedLink.isEmpty()) {
|
||||
// Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)
|
||||
qCDebug(KNEWSTUFFCORE)
|
||||
<< "Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)";
|
||||
auto question = std::make_unique<Question>(Question::SelectFromListQuestion);
|
||||
question->setTitle(i18n("Pick Update Item"));
|
||||
question->setQuestion(
|
||||
i18n("Please pick the item from the list below which should be used to apply this update. We were unable to identify which item to "
|
||||
"select, based on the original item, which was named %1",
|
||||
fileName));
|
||||
question->setList(payloadNames);
|
||||
if (question->ask() == Question::OKResponse) {
|
||||
identifiedLink = payloads.value(payloadNames.indexOf(question->response()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!identifiedLink.isEmpty()) {
|
||||
KNSCore::Entry theEntry(entry);
|
||||
theEntry.setPayload(identifiedLink);
|
||||
d->m_engine->d->installation->install(theEntry);
|
||||
connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
|
||||
if (entry.uniqueId() == finishedEntry.uniqueId()) {
|
||||
d->finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
qCWarning(KNEWSTUFFCORE) << "We failed to identify a good link for updating" << entry.name() << "and are unable to perform the update";
|
||||
KNSCore::Entry theEntry(entry);
|
||||
theEntry.setStatus(KNSCore::Entry::Updateable);
|
||||
Q_EMIT signalEntryEvent(theEntry, Entry::StatusChangedEvent);
|
||||
Q_EMIT signalErrorCode(ErrorCode::InstallationError,
|
||||
i18n("We failed to identify a good link for updating %1, and are unable to perform the update", entry.name()),
|
||||
{entry.uniqueId()});
|
||||
}
|
||||
// As the serverside data may change before next time this is called, even in the same session,
|
||||
// let's not make assumptions, and just get rid of this
|
||||
d->payloads.remove(entry);
|
||||
d->payloadToIdentify.remove(entry);
|
||||
d->finish();
|
||||
}
|
||||
} else {
|
||||
d->m_engine->d->installation->install(entry);
|
||||
connect(d->m_engine->d->installation, &Installation::signalInstallationFinished, this, [this, entry](const KNSCore::Entry &finishedEntry) {
|
||||
if (entry.uniqueId() == finishedEntry.uniqueId()) {
|
||||
d->finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Transaction *Transaction::uninstall(EngineBase *engine, const KNSCore::Entry &_entry)
|
||||
{
|
||||
auto ret = new Transaction(_entry, engine);
|
||||
const KNSCore::Entry::List list = ret->d->m_engine->d->cache->registryForProvider(_entry.providerId());
|
||||
// we have to use the cached entry here, not the entry from the provider
|
||||
// since that does not contain the list of installed files
|
||||
KNSCore::Entry actualEntryForUninstall;
|
||||
for (const KNSCore::Entry &eInt : list) {
|
||||
if (eInt.uniqueId() == _entry.uniqueId()) {
|
||||
actualEntryForUninstall = eInt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!actualEntryForUninstall.isValid()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "could not find a cached entry with following id:" << _entry.uniqueId() << " -> using the non-cached version";
|
||||
actualEntryForUninstall = _entry;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, ret, [actualEntryForUninstall, _entry, ret] {
|
||||
KNSCore::Entry entry = _entry;
|
||||
entry.setStatus(KNSCore::Entry::Installing);
|
||||
|
||||
Entry actualEntryForUninstall2 = actualEntryForUninstall;
|
||||
actualEntryForUninstall2.setStatus(KNSCore::Entry::Installing);
|
||||
Q_EMIT ret->signalEntryEvent(entry, Entry::StatusChangedEvent);
|
||||
|
||||
// We connect to/forward the relevant signals
|
||||
qCDebug(KNEWSTUFFCORE) << "about to uninstall entry " << entry.uniqueId();
|
||||
ret->d->m_engine->d->installation->uninstall(actualEntryForUninstall2);
|
||||
|
||||
// FIXME: signalEntryEvent to uninstalled already happened in installation.cpp:584
|
||||
// Update the correct entry
|
||||
entry.setStatus(actualEntryForUninstall2.status());
|
||||
Q_EMIT ret->signalEntryEvent(entry, Entry::StatusChangedEvent);
|
||||
|
||||
ret->d->finish();
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Transaction *Transaction::adopt(EngineBase *engine, const Entry &entry)
|
||||
{
|
||||
if (!engine->hasAdoptionCommand()) {
|
||||
qCWarning(KNEWSTUFFCORE) << "no adoption command specified";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ret = new Transaction(entry, engine);
|
||||
const QString command = getAdoptionCommand(engine->d->adoptionCommand, entry, engine->d->installation);
|
||||
|
||||
QTimer::singleShot(0, ret, [command, entry, ret] {
|
||||
QStringList split = KShell::splitArgs(command);
|
||||
QProcess *process = new QProcess(ret);
|
||||
process->setProgram(split.takeFirst());
|
||||
process->setArguments(split);
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
// The debug output is too talkative to be useful
|
||||
env.insert(QStringLiteral("QT_LOGGING_RULES"), QStringLiteral("*.debug=false"));
|
||||
process->setProcessEnvironment(env);
|
||||
|
||||
process->start();
|
||||
|
||||
connect(process, &QProcess::finished, ret, [ret, process, entry, command](int exitCode) {
|
||||
if (exitCode == 0) {
|
||||
Q_EMIT ret->signalEntryEvent(entry, Entry::EntryEvent::AdoptedEvent);
|
||||
|
||||
// Handle error output as warnings if the process hasn't crashed
|
||||
const QString stdErr = QString::fromLocal8Bit(process->readAllStandardError());
|
||||
if (!stdErr.isEmpty()) {
|
||||
Q_EMIT ret->signalMessage(stdErr);
|
||||
}
|
||||
} else {
|
||||
const QString errorMsg = i18n("Failed to adopt '%1'\n%2", entry.name(), QString::fromLocal8Bit(process->readAllStandardError()));
|
||||
Q_EMIT ret->signalErrorCode(KNSCore::ErrorCode::AdoptionError, errorMsg, QVariantList{command});
|
||||
}
|
||||
ret->d->finish();
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Transaction::isFinished() const
|
||||
{
|
||||
return d->m_finished;
|
||||
}
|
||||
|
||||
#include "moc_transaction.cpp"
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez <aleixpol@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_TRANSACTION_H
|
||||
#define KNEWSTUFF3_TRANSACTION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "entry.h"
|
||||
#include "errorcode.h"
|
||||
|
||||
#include "knewstuffcore_export.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class EngineBase;
|
||||
class TransactionPrivate;
|
||||
|
||||
/**
|
||||
* KNewStuff Transaction
|
||||
*
|
||||
* Exposes different actions that can be done on an entry and means to track them to completion.
|
||||
*
|
||||
* To create a Transaction we should call one of the static methods that
|
||||
* represent the different actions we can take. These will return the Transaction
|
||||
* and we can use it to track mesages, the entries' states and eventually its
|
||||
* completion using the @m finished signal.
|
||||
*
|
||||
* The Transaction will delete itself once it has finished.
|
||||
*
|
||||
* @since 6.0
|
||||
*/
|
||||
class KNEWSTUFFCORE_EXPORT Transaction : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
~Transaction() override;
|
||||
|
||||
#if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(6, 9)
|
||||
/**
|
||||
* Performs an install on the given @p entry from the @p engine.
|
||||
*
|
||||
* @param linkId specifies which of the assets we want to see installed.
|
||||
* @returns a Transaction object that we can use to track the progress to completion
|
||||
* @deprecated since 6.9, use installLatest or installLinkId instead
|
||||
*/
|
||||
KNEWSTUFFCORE_DEPRECATED_VERSION(6, 9, "use installLatest or installLinkId instead")
|
||||
static Transaction *install(EngineBase *engine, const Entry &entry, int linkId = 1);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Performs an install on the given @p entry from the @p engine.
|
||||
*
|
||||
* @param linkId specifies which of the assets we want to see installed.
|
||||
* @returns a Transaction object that we can use to track the progress to completion
|
||||
* @since 6.9
|
||||
*/
|
||||
[[nodiscard]] static Transaction *installLinkId(EngineBase *engine, const Entry &entry, quint8 linkId);
|
||||
|
||||
/**
|
||||
* Performs an install of the latest version on the given @p entry from the @p engine.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @returns a Transaction object that we can use to track the progress to completion
|
||||
* @since 6.9
|
||||
*/
|
||||
[[nodiscard]] static Transaction *installLatest(EngineBase *engine, const Entry &entry);
|
||||
|
||||
/**
|
||||
* Uninstalls the given @p entry from the @p engine.
|
||||
*
|
||||
* It reverses the step done when @m install was called.
|
||||
* @returns a Transaction object that we can use to track the progress to completion
|
||||
*/
|
||||
static Transaction *uninstall(EngineBase *engine, const Entry &entry);
|
||||
|
||||
/**
|
||||
* Adopt the @p entry from @p engine using the adoption command.
|
||||
*
|
||||
* For more information, see the documentation about AdoptionCommand from
|
||||
* the knsrc file.
|
||||
*/
|
||||
static Transaction *adopt(EngineBase *engine, const Entry &entry);
|
||||
|
||||
/**
|
||||
* @returns true as soon as the Transaction is completed as it gets ready to
|
||||
* clean itself up
|
||||
*/
|
||||
bool isFinished() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished();
|
||||
|
||||
/**
|
||||
* Provides the @p message to update our users about how the Transaction progressed
|
||||
*/
|
||||
void signalMessage(const QString &message);
|
||||
|
||||
/**
|
||||
* Informs about how the @p entry has changed
|
||||
*
|
||||
* @param event nature of the change
|
||||
*/
|
||||
void signalEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event);
|
||||
|
||||
/**
|
||||
* Fires in the case of any critical or serious errors, such as network or API problems.
|
||||
* @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::Entry::ErrorCode
|
||||
*/
|
||||
void signalErrorCode(KNSCore::ErrorCode::ErrorCode errorCode, const QString &message, const QVariant &metadata);
|
||||
|
||||
private:
|
||||
friend class TransactionPrivate;
|
||||
|
||||
Transaction(const KNSCore::Entry &entry, EngineBase *engine);
|
||||
void downloadLinkLoaded(const KNSCore::Entry &entry);
|
||||
|
||||
std::unique_ptr<TransactionPrivate> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
knewstuff3/xmlloader.cpp.
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#include "xmlloader_p.h"
|
||||
|
||||
#include "jobs/httpjob.h"
|
||||
#include "knewstuffcore_debug.h"
|
||||
|
||||
#include <KConfig>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
void handleData(XmlLoader *q, const QByteArray &data)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "--Xml Loader-START--";
|
||||
qCDebug(KNEWSTUFFCORE) << QString::fromUtf8(data);
|
||||
qCDebug(KNEWSTUFFCORE) << "--Xml Loader-END--";
|
||||
QDomDocument doc;
|
||||
if (doc.setContent(data)) {
|
||||
Q_EMIT q->signalLoaded(doc);
|
||||
} else {
|
||||
Q_EMIT q->signalFailed();
|
||||
}
|
||||
}
|
||||
|
||||
XmlLoader::XmlLoader(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void XmlLoader::load(const QUrl &url)
|
||||
{
|
||||
qCDebug(KNEWSTUFFCORE) << "XmlLoader::load(): url: " << url;
|
||||
// The load call is expected to be asynchronous (to allow for people to connect to signals
|
||||
// after it is called), and so we need to postpone its implementation until the listeners
|
||||
// are actually listening
|
||||
QTimer::singleShot(0, this, [this, url]() {
|
||||
m_jobdata.clear();
|
||||
static const QStringList remoteSchemeOptions{QLatin1String{"http"}, QLatin1String{"https"}, QLatin1String{"ftp"}};
|
||||
if (remoteSchemeOptions.contains(url.scheme())) {
|
||||
HTTPJob *job = HTTPJob::get(url, Reload, JobFlag::HideProgressInfo, this);
|
||||
connect(job, &KJob::result, this, &XmlLoader::slotJobResult);
|
||||
connect(job, &HTTPJob::data, this, &XmlLoader::slotJobData);
|
||||
connect(job, &HTTPJob::httpError, this, &XmlLoader::signalHttpError);
|
||||
Q_EMIT jobStarted(job);
|
||||
} else if (url.isLocalFile()) {
|
||||
QFile localProvider(url.toLocalFile());
|
||||
if (localProvider.open(QFile::ReadOnly)) {
|
||||
m_jobdata = localProvider.readAll();
|
||||
handleData(this, m_jobdata);
|
||||
} else {
|
||||
Q_EMIT signalFailed();
|
||||
}
|
||||
} else {
|
||||
// This is not supported
|
||||
qCDebug(KNEWSTUFFCORE) << "Attempted to load data from unsupported URL:" << url;
|
||||
Q_EMIT signalFailed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void XmlLoader::slotJobData(KJob *, const QByteArray &data)
|
||||
{
|
||||
m_jobdata.append(data);
|
||||
}
|
||||
|
||||
void XmlLoader::slotJobResult(KJob *job)
|
||||
{
|
||||
deleteLater();
|
||||
if (job->error()) {
|
||||
Q_EMIT signalFailed();
|
||||
} else {
|
||||
handleData(this, m_jobdata);
|
||||
}
|
||||
}
|
||||
|
||||
QDomElement addElement(QDomDocument &doc, QDomElement &parent, const QString &tag, const QString &value)
|
||||
{
|
||||
QDomElement n = doc.createElement(tag);
|
||||
n.appendChild(doc.createTextNode(value));
|
||||
parent.appendChild(n);
|
||||
|
||||
return n;
|
||||
}
|
||||
} // end KNS namespace
|
||||
|
||||
#include "moc_xmlloader_p.cpp"
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
knewstuff3/xmlloader.h.
|
||||
SPDX-FileCopyrightText: 2002 Cornelius Schumacher <schumacher@kde.org>
|
||||
SPDX-FileCopyrightText: 2003-2007 Josef Spillner <spillner@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Frederik Gladhorn <gladhorn@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef KNEWSTUFF3_XMLLOADER_P_H
|
||||
#define KNEWSTUFF3_XMLLOADER_P_H
|
||||
|
||||
#include "provider.h"
|
||||
#include "searchrequest.h"
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <qdom.h>
|
||||
|
||||
class KJob;
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
QDomElement addElement(QDomDocument &doc, QDomElement &parent, const QString &tag, const QString &value);
|
||||
|
||||
/**
|
||||
* KNewStuff xml loader.
|
||||
* This class loads an xml document from a qurl and returns the
|
||||
* resulting domdocument once completed.
|
||||
* It should probably not be used directly by the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class XmlLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
explicit XmlLoader(QObject *parent);
|
||||
|
||||
/**
|
||||
* Starts asynchronously loading the xml document from the
|
||||
* specified URL.
|
||||
*
|
||||
* @param url location of the XML file
|
||||
*/
|
||||
void load(const QUrl &url);
|
||||
|
||||
void setFilter(Filter filter)
|
||||
{
|
||||
m_filter = filter;
|
||||
}
|
||||
|
||||
void setSearchTerm(const QString &searchTerm)
|
||||
{
|
||||
m_searchTerm = searchTerm;
|
||||
}
|
||||
|
||||
Filter filter() const
|
||||
{
|
||||
return m_filter;
|
||||
}
|
||||
|
||||
QString searchTerm() const
|
||||
{
|
||||
return m_searchTerm;
|
||||
}
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* Indicates that the list of providers has been successfully loaded.
|
||||
*/
|
||||
void signalLoaded(const QDomDocument &);
|
||||
void signalFailed();
|
||||
/**
|
||||
* Fired in case there is a http error reported
|
||||
* In some instances this is useful information for our users, and we want to make sure we report this centrally
|
||||
* @param status The HTTP status code (fired in cases where it is perceived by QNetworkReply as an error)
|
||||
* @param rawHeaders The raw HTTP headers for the errored-out network request
|
||||
*/
|
||||
void signalHttpError(int status, QList<QNetworkReply::RawHeaderPair> rawHeaders);
|
||||
|
||||
void jobStarted(KJob *);
|
||||
|
||||
protected Q_SLOTS:
|
||||
void slotJobData(KJob *, const QByteArray &);
|
||||
void slotJobResult(KJob *);
|
||||
|
||||
private:
|
||||
QByteArray m_jobdata;
|
||||
Filter m_filter;
|
||||
QString m_searchTerm;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,709 @@
|
||||
/*
|
||||
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 "opdsprovider_p.h"
|
||||
|
||||
#include <KFormat>
|
||||
#include <KLocalizedString>
|
||||
#include <QDate>
|
||||
#include <QIcon>
|
||||
#include <QLocale>
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
#include <syndication/atom/atom.h>
|
||||
#include <syndication/documentsource.h>
|
||||
|
||||
#include <knewstuffcore_debug.h>
|
||||
|
||||
#include "searchpreset.h"
|
||||
#include "searchpreset_p.h"
|
||||
#include "tagsfilterchecker.h"
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
static const QLatin1String OPDS_REL_ACQUISITION{"http://opds-spec.org/acquisition"};
|
||||
static const QLatin1String OPDS_REL_AC_OPEN_ACCESS{"http://opds-spec.org/acquisition/open-access"};
|
||||
static const QLatin1String OPDS_REL_AC_BORROW{"http://opds-spec.org/acquisition/borrow"};
|
||||
static const QLatin1String OPDS_REL_AC_BUY{"http://opds-spec.org/acquisition/buy"};
|
||||
static const QLatin1String OPDS_REL_AC_SUBSCRIBE{"http://opds-spec.org/acquisition/subscribe"};
|
||||
// static const QLatin1String OPDS_REL_AC_SAMPLE{"http://opds-spec.org/acquisition/sample"};
|
||||
static const QLatin1String OPDS_REL_IMAGE{"http://opds-spec.org/image"};
|
||||
static const QLatin1String OPDS_REL_THUMBNAIL{"http://opds-spec.org/image/thumbnail"};
|
||||
static const QLatin1String OPDS_REL_CRAWL{"http://opds-spec.org/crawlable"};
|
||||
// static const QLatin1String OPDS_REL_FACET{"http://opds-spec.org/facet"};
|
||||
static const QLatin1String OPDS_REL_SHELF{"http://opds-spec.org/shelf"};
|
||||
static const QLatin1String OPDS_REL_SORT_NEW{"http://opds-spec.org/sort/new"};
|
||||
static const QLatin1String OPDS_REL_SORT_POPULAR{"http://opds-spec.org/sort/popular"};
|
||||
static const QLatin1String OPDS_REL_FEATURED{"http://opds-spec.org/featured"};
|
||||
static const QLatin1String OPDS_REL_RECOMMENDED{"http://opds-spec.org/recommended"};
|
||||
static const QLatin1String OPDS_REL_SUBSCRIPTIONS{"http://opds-spec.org/subscriptions"};
|
||||
static const QLatin1String OPDS_EL_PRICE{"opds:price"};
|
||||
// static const QLatin1String OPDS_EL_INDIRECT{"opds:indirectAcquisition"};
|
||||
// static const QLatin1String OPDS_ATTR_FACET_GROUP{"opds:facetGroup"};
|
||||
// static const QLatin1String OPDS_ATTR_ACTIVE_FACET{"opds:activeFacet"};
|
||||
|
||||
static const QLatin1String OPDS_ATOM_MT{"application/atom+xml"};
|
||||
static const QLatin1String OPDS_PROFILE{"profile=opds-catalog"};
|
||||
static const QLatin1String OPDS_TYPE_ENTRY{"type=entry"};
|
||||
// static const QLatin1String OPDS_KIND_NAVIGATION{"kind=navigation"};
|
||||
// static const QLatin1String OPDS_KIND_ACQUISITION{"kind=acquisition"};
|
||||
|
||||
static const QLatin1String REL_START{"start"};
|
||||
// static const QLatin1String REL_SUBSECTION{"subsection"};
|
||||
// static const QLatin1String REL_COLLECTION{"collection"};
|
||||
// static const QLatin1String REL_PREVIEW{"preview"};
|
||||
// static const QLatin1String REL_REPLIES{"replies"};
|
||||
// static const QLatin1String REL_RELATED{"related"};
|
||||
// static const QLatin1String REL_PREVIOUS{"previous"};
|
||||
// static const QLatin1String REL_NEXT{"next"};
|
||||
// static const QLatin1String REL_FIRST{"first"};
|
||||
// static const QLatin1String REL_LAST{"last"};
|
||||
static const QLatin1String REL_UP{"up"};
|
||||
static const QLatin1String REL_SELF{"self"};
|
||||
static const QLatin1String REL_ALTERNATE{"alternate"};
|
||||
static const QLatin1String ATTR_CURRENCY_CODE{"currencycode"};
|
||||
// static const QLatin1String FEED_COMPLETE{"fh:complete"};
|
||||
// static const QLatin1String THREAD_COUNT{"count"};
|
||||
|
||||
static const QLatin1String OPENSEARCH_NS{"http://a9.com/-/spec/opensearch/1.1/"};
|
||||
static const QLatin1String OPENSEARCH_MT{"application/opensearchdescription+xml"};
|
||||
static const QLatin1String REL_SEARCH{"search"};
|
||||
|
||||
static const QLatin1String OPENSEARCH_EL_URL{"Url"};
|
||||
static const QLatin1String OPENSEARCH_ATTR_TYPE{"type"};
|
||||
static const QLatin1String OPENSEARCH_ATTR_TEMPLATE{"template"};
|
||||
static const QLatin1String OPENSEARCH_SEARCH_TERMS{"searchTerms"};
|
||||
static const QLatin1String OPENSEARCH_COUNT{"count"};
|
||||
static const QLatin1String OPENSEARCH_START_INDEX{"startIndex"};
|
||||
static const QLatin1String OPENSEARCH_START_PAGE{"startPage"};
|
||||
|
||||
static const QLatin1String HTML_MT{"text/html"};
|
||||
|
||||
static const QLatin1String KEY_MIME_TYPE{"data##mimetype="};
|
||||
static const QLatin1String KEY_URL{"data##url="};
|
||||
static const QLatin1String KEY_LANGUAGE{"data##language="};
|
||||
|
||||
class OPDSProviderPrivate
|
||||
{
|
||||
public:
|
||||
OPDSProviderPrivate(OPDSProvider *qq)
|
||||
: q(qq)
|
||||
, initialized(false)
|
||||
, loadingExtraDetails(false)
|
||||
{
|
||||
}
|
||||
OPDSProvider *q;
|
||||
QString providerId;
|
||||
QString providerName;
|
||||
QUrl iconUrl;
|
||||
bool initialized;
|
||||
|
||||
/***
|
||||
* OPDS catalogs consist of many small atom feeds. This variable
|
||||
* tracks which atom feed to load.
|
||||
*/
|
||||
QUrl currentUrl;
|
||||
// partial url identifying the self. This is necessary to resolve relative links.
|
||||
QString selfUrl;
|
||||
|
||||
QDateTime currentTime;
|
||||
bool loadingExtraDetails;
|
||||
|
||||
XmlLoader *xmlLoader;
|
||||
|
||||
Entry::List cachedEntries;
|
||||
SearchRequest currentRequest;
|
||||
|
||||
QUrl openSearchDocumentURL;
|
||||
QString openSearchTemplate;
|
||||
|
||||
// Generate an opensearch string.
|
||||
QUrl openSearchStringForRequest(const KNSCore::SearchRequest &request)
|
||||
{
|
||||
{
|
||||
QUrl searchUrl = QUrl(openSearchTemplate);
|
||||
|
||||
QUrlQuery templateQuery(searchUrl);
|
||||
QUrlQuery query;
|
||||
|
||||
for (QPair<QString, QString> key : templateQuery.queryItems()) {
|
||||
if (key.second.contains(OPENSEARCH_SEARCH_TERMS)) {
|
||||
query.addQueryItem(key.first, request.searchTerm());
|
||||
} else if (key.second.contains(OPENSEARCH_COUNT)) {
|
||||
query.addQueryItem(key.first, QString::number(request.pageSize()));
|
||||
} else if (key.second.contains(OPENSEARCH_START_PAGE)) {
|
||||
query.addQueryItem(key.first, QString::number(request.page()));
|
||||
} else if (key.second.contains(OPENSEARCH_START_INDEX)) {
|
||||
query.addQueryItem(key.first, QString::number(request.page() * request.pageSize()));
|
||||
}
|
||||
}
|
||||
searchUrl.setQuery(query);
|
||||
return searchUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle URL resolving.
|
||||
QUrl fixRelativeUrl(QString urlPart)
|
||||
{
|
||||
QUrl query = QUrl(urlPart);
|
||||
if (query.isRelative()) {
|
||||
if (selfUrl.isEmpty() || QUrl(selfUrl).isRelative()) {
|
||||
return currentUrl.resolved(query);
|
||||
} else {
|
||||
return QUrl(selfUrl).resolved(query);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
Entry::List installedEntries() const {{Entry::List entries;
|
||||
for (const Entry &entry : std::as_const(cachedEntries)) {
|
||||
if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
|
||||
entries.append(entry);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
};
|
||||
|
||||
void slotLoadingFailed()
|
||||
{
|
||||
qCWarning(KNEWSTUFFCORE) << "OPDS Loading failed for" << currentUrl;
|
||||
Q_EMIT q->loadingFailed(currentRequest);
|
||||
};
|
||||
|
||||
// Parse the opensearch configuration document.
|
||||
// https://github.com/dewitt/opensearch
|
||||
void parseOpenSearchDocument(const QDomDocument &doc){{openSearchTemplate = QString();
|
||||
if (doc.documentElement().attribute(QStringLiteral("xmlns")) != OPENSEARCH_NS) {
|
||||
qCWarning(KNEWSTUFFCORE) << "Opensearch link does not point at document with opensearch namespace" << openSearchDocumentURL;
|
||||
return;
|
||||
}
|
||||
QDomElement el = doc.documentElement().firstChildElement(OPENSEARCH_EL_URL);
|
||||
while (!el.isNull()) {
|
||||
if (el.attribute(OPENSEARCH_ATTR_TYPE).contains(OPDS_ATOM_MT)) {
|
||||
if (openSearchTemplate.isEmpty() || el.attribute(OPENSEARCH_ATTR_TYPE).contains(OPDS_PROFILE)) {
|
||||
openSearchTemplate = el.attribute(OPENSEARCH_ATTR_TEMPLATE);
|
||||
}
|
||||
}
|
||||
|
||||
el = el.nextSiblingElement(OPENSEARCH_EL_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
/**
|
||||
* @brief parseFeedData
|
||||
* The main parsing function of this provider. Receives a QDomDocument
|
||||
* and parses that with Syndication's atom reader.
|
||||
* @param doc
|
||||
*/
|
||||
void parseFeedData(const QDomDocument &doc)
|
||||
{
|
||||
Syndication::DocumentSource source(doc.toByteArray(), currentUrl.toString());
|
||||
Syndication::Atom::Parser parser;
|
||||
Syndication::Atom::FeedDocumentPtr feedDoc = parser.parse(source).staticCast<Syndication::Atom::FeedDocument>();
|
||||
|
||||
QString fullEntryMimeType = QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(";"));
|
||||
|
||||
if (!feedDoc->isValid()) {
|
||||
qCWarning(KNEWSTUFFCORE) << "OPDS Feed at" << currentUrl << "not valid";
|
||||
Q_EMIT q->loadingFailed(currentRequest);
|
||||
return;
|
||||
}
|
||||
if (!feedDoc->title().isEmpty()) {
|
||||
providerName = feedDoc->title();
|
||||
}
|
||||
if (!feedDoc->icon().isEmpty()) {
|
||||
iconUrl = QUrl(fixRelativeUrl(feedDoc->icon()));
|
||||
}
|
||||
|
||||
Entry::List entries;
|
||||
QList<SearchPreset> presets;
|
||||
|
||||
{
|
||||
SearchRequest request(SortMode::Downloads, Filter::None, providerId);
|
||||
SearchPreset preset(new SearchPresetPrivate{
|
||||
.request = request,
|
||||
.displayName = {},
|
||||
.iconName = {},
|
||||
.type = SearchPreset::Type::Start,
|
||||
.providerId = providerId,
|
||||
});
|
||||
presets.append(preset);
|
||||
}
|
||||
|
||||
// find the self link first!
|
||||
selfUrl.clear();
|
||||
for (auto link : feedDoc->links()) {
|
||||
if (link.rel().contains(REL_SELF)) {
|
||||
selfUrl = link.href();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto link : feedDoc->links()) {
|
||||
// There will be a number of links toplevel, amongst which probably a lot of sortorder and navigation links.
|
||||
if (link.rel() == REL_SEARCH && link.type() == OPENSEARCH_MT) {
|
||||
std::function<void(Syndication::Atom::Link)> osdUrlLoader;
|
||||
osdUrlLoader = [this, &osdUrlLoader](Syndication::Atom::Link theLink) {
|
||||
openSearchDocumentURL = fixRelativeUrl(theLink.href());
|
||||
xmlLoader = new XmlLoader(q);
|
||||
|
||||
QObject::connect(xmlLoader, &XmlLoader::signalLoaded, q, [this](const QDomDocument &doc) {
|
||||
q->d->parseOpenSearchDocument(doc);
|
||||
});
|
||||
QObject::connect(xmlLoader, &XmlLoader::signalFailed, q, [this]() {
|
||||
qCWarning(KNEWSTUFFCORE) << "OpenSearch XML Document Loading failed" << openSearchDocumentURL;
|
||||
});
|
||||
QObject::connect(
|
||||
xmlLoader,
|
||||
&XmlLoader::signalHttpError,
|
||||
q,
|
||||
[this, &osdUrlLoader, theLink](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) { // clazy:exclude=lambda-in-connect
|
||||
if (status == 503) { // Temporarily Unavailable
|
||||
QDateTime retryAfter;
|
||||
static const QByteArray retryAfterKey{"Retry-After"};
|
||||
for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) {
|
||||
if (headerPair.first == retryAfterKey) {
|
||||
// Retry-After is not a known header, so we need to do a bit of running around to make that work
|
||||
// Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
|
||||
// So, simple workaround, just pass it through a dummy request and get a formatted date out (the
|
||||
// cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
|
||||
QNetworkRequest dummyRequest;
|
||||
dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
|
||||
retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// clazy:exclude=lambda-in-connect
|
||||
QTimer::singleShot(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), q, [&osdUrlLoader, theLink]() {
|
||||
osdUrlLoader(theLink);
|
||||
});
|
||||
// if it's a matter of a human moment's worth of seconds, just reload
|
||||
if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) {
|
||||
// more than that, spit out TryAgainLaterError to let the user know what we're doing with their time
|
||||
static const KFormat formatter;
|
||||
Q_EMIT q->signalErrorCode(
|
||||
KNSCore::ErrorCode::TryAgainLaterError,
|
||||
i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
|
||||
formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
|
||||
{retryAfter});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xmlLoader->load(openSearchDocumentURL);
|
||||
};
|
||||
} else if (link.type().contains(OPDS_PROFILE) && link.rel() != REL_SELF) {
|
||||
SearchRequest request(SortMode::Downloads, Filter::None, fixRelativeUrl(link.href()).toString());
|
||||
SearchPreset preset(new SearchPresetPrivate{
|
||||
.request = request,
|
||||
.displayName = link.title().isEmpty() ? link.rel() : link.title(),
|
||||
.iconName = {},
|
||||
.type =
|
||||
[&link] {
|
||||
if (link.rel() == REL_START) {
|
||||
return SearchPreset::Type::Root;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_FEATURED) {
|
||||
return SearchPreset::Type::Featured;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_SHELF) {
|
||||
return SearchPreset::Type::Shelf;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_SORT_NEW) {
|
||||
return SearchPreset::Type::New;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_SORT_POPULAR) {
|
||||
return SearchPreset::Type::Popular;
|
||||
}
|
||||
if (link.rel() == REL_UP) {
|
||||
return SearchPreset::Type::FolderUp;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_CRAWL) {
|
||||
return SearchPreset::Type::AllEntries;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_RECOMMENDED) {
|
||||
return SearchPreset::Type::Recommended;
|
||||
}
|
||||
if (link.rel() == OPDS_REL_SUBSCRIPTIONS) {
|
||||
return SearchPreset::Type::Subscription;
|
||||
}
|
||||
return SearchPreset::Type::NoPresetType;
|
||||
}(),
|
||||
.providerId = providerId,
|
||||
});
|
||||
presets.append(preset);
|
||||
}
|
||||
}
|
||||
TagsFilterChecker downloadTagChecker(q->downloadTagFilter());
|
||||
TagsFilterChecker entryTagChecker(q->tagFilter());
|
||||
|
||||
for (int i = 0; i < feedDoc->entries().size(); i++) {
|
||||
Syndication::Atom::Entry feedEntry = feedDoc->entries().at(i);
|
||||
|
||||
Entry entry;
|
||||
entry.setName(feedEntry.title());
|
||||
entry.setProviderId(providerId);
|
||||
entry.setUniqueId(feedEntry.id());
|
||||
|
||||
entry.setStatus(KNSCore::Entry::Invalid);
|
||||
for (const Entry &cachedEntry : std::as_const(cachedEntries)) {
|
||||
if (entry.uniqueId() == cachedEntry.uniqueId()) {
|
||||
entry = cachedEntry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a bit of a pickle: atom feeds can have multiple categories.
|
||||
// but these categories are not specifically tags...
|
||||
QStringList entryTags;
|
||||
for (int j = 0; j < feedEntry.categories().size(); j++) {
|
||||
QString tag = feedEntry.categories().at(j).label();
|
||||
if (tag.isEmpty()) {
|
||||
tag = feedEntry.categories().at(j).term();
|
||||
}
|
||||
entryTags.append(tag);
|
||||
}
|
||||
if (entryTagChecker.filterAccepts(entryTags)) {
|
||||
entry.setTags(entryTags);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
// Same issue with author...
|
||||
for (int j = 0; j < feedEntry.authors().size(); j++) {
|
||||
Author author;
|
||||
Syndication::Atom::Person person = feedEntry.authors().at(j);
|
||||
author.setId(person.uri());
|
||||
author.setName(person.name());
|
||||
author.setEmail(person.email());
|
||||
author.setHomepage(person.uri());
|
||||
entry.setAuthor(author);
|
||||
}
|
||||
entry.setLicense(feedEntry.rights());
|
||||
if (feedEntry.content().isEscapedHTML()) {
|
||||
entry.setSummary(feedEntry.content().childNodesAsXML());
|
||||
} else {
|
||||
entry.setSummary(feedEntry.content().asString());
|
||||
}
|
||||
entry.setShortSummary(feedEntry.summary());
|
||||
|
||||
int counterThumbnails = 0;
|
||||
int counterImages = 0;
|
||||
QString groupEntryUrl;
|
||||
for (int j = 0; j < feedEntry.links().size(); j++) {
|
||||
Syndication::Atom::Link link = feedEntry.links().at(j);
|
||||
|
||||
KNSCore::Entry::DownloadLinkInformation download;
|
||||
download.id = entry.downloadLinkCount() + 1;
|
||||
// Linkrelations can have multiple values, expressed as something like... rel="me nofollow alternate".
|
||||
QStringList linkRelation = link.rel().split(QStringLiteral(" "));
|
||||
|
||||
QStringList tags;
|
||||
tags.append(KEY_MIME_TYPE + link.type());
|
||||
if (!link.hrefLanguage().isEmpty()) {
|
||||
tags.append(KEY_LANGUAGE + link.hrefLanguage());
|
||||
}
|
||||
QString linkUrl = fixRelativeUrl(link.href()).toString();
|
||||
tags.append(KEY_URL + linkUrl);
|
||||
download.name = link.title();
|
||||
download.size = link.length() / 1000;
|
||||
download.tags = tags;
|
||||
download.isDownloadtypeLink = false;
|
||||
|
||||
if (link.rel().startsWith(OPDS_REL_ACQUISITION)) {
|
||||
if (link.title().isEmpty()) {
|
||||
QStringList l;
|
||||
l.append(link.type());
|
||||
l.append(QStringLiteral("(") + link.rel().split(QStringLiteral("/")).last() + QStringLiteral(")"));
|
||||
download.name = l.join(QStringLiteral(" "));
|
||||
}
|
||||
|
||||
if (!downloadTagChecker.filterAccepts(download.tags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linkRelation.contains(OPDS_REL_AC_BORROW) || linkRelation.contains(OPDS_REL_AC_SUBSCRIBE) || linkRelation.contains(OPDS_REL_AC_BUY)) {
|
||||
// TODO we don't support borrow, buy and subscribe right now, requires authentication.
|
||||
continue;
|
||||
|
||||
} else if (linkRelation.contains(OPDS_REL_ACQUISITION) || linkRelation.contains(OPDS_REL_AC_OPEN_ACCESS)) {
|
||||
download.isDownloadtypeLink = true;
|
||||
|
||||
if (entry.status() != KNSCore::Entry::Installed && entry.status() != KNSCore::Entry::Updateable) {
|
||||
entry.setStatus(KNSCore::Entry::Downloadable);
|
||||
}
|
||||
|
||||
entry.setEntryType(Entry::CatalogEntry);
|
||||
}
|
||||
// TODO, support preview relation, but this requires we show that an entry is otherwise paid for in the UI.
|
||||
|
||||
for (QDomElement el : feedEntry.elementsByTagName(OPDS_EL_PRICE)) {
|
||||
QLocale locale;
|
||||
download.priceAmount = locale.toCurrencyString(el.text().toFloat(), el.attribute(ATTR_CURRENCY_CODE));
|
||||
}
|
||||
// There's an 'opds:indirectaquistition' element that gives extra metadata about bundles.
|
||||
entry.appendDownloadLinkInformation(download);
|
||||
|
||||
} else if (link.rel().startsWith(OPDS_REL_IMAGE)) {
|
||||
if (link.rel() == OPDS_REL_THUMBNAIL) {
|
||||
entry.setPreviewUrl(linkUrl, KNSCore::Entry::PreviewType(counterThumbnails));
|
||||
counterThumbnails += 1;
|
||||
} else {
|
||||
entry.setPreviewUrl(linkUrl, KNSCore::Entry::PreviewType(counterImages + 3));
|
||||
counterImages += 1;
|
||||
}
|
||||
|
||||
} else {
|
||||
// This could be anything from a more info link, to navigation links, to links to the outside world.
|
||||
// Todo: think of using link rel's 'replies', 'payment'(donation) and 'version-history'.
|
||||
|
||||
if (link.type().startsWith(OPDS_ATOM_MT)) {
|
||||
if (link.type() == fullEntryMimeType) {
|
||||
entry.appendDownloadLinkInformation(download);
|
||||
} else {
|
||||
groupEntryUrl = linkUrl;
|
||||
}
|
||||
|
||||
} else if (link.type() == HTML_MT && linkRelation.contains(REL_ALTERNATE)) {
|
||||
entry.setHomepage(QUrl(linkUrl));
|
||||
|
||||
} else if (downloadTagChecker.filterAccepts(download.tags)) {
|
||||
entry.appendDownloadLinkInformation(download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Todo:
|
||||
// feedEntry.elementsByTagName( dc:terms:issued ) is the official initial release date.
|
||||
// published is the released date of the opds catalog item, updated for the opds catalog item update.
|
||||
// maybe we should make sure to also check dc:terms:modified?
|
||||
// QDateTime date = QDateTime::fromSecsSinceEpoch(feedEntry.published());
|
||||
|
||||
QDateTime date = QDateTime::fromSecsSinceEpoch(feedEntry.updated());
|
||||
|
||||
if (entry.releaseDate().isNull()) {
|
||||
entry.setReleaseDate(date.date());
|
||||
}
|
||||
|
||||
if (entry.status() != KNSCore::Entry::Invalid) {
|
||||
entry.setPayload(QString());
|
||||
// Gutenberg doesn't do versioning in the opds, so it's update value is unreliable,
|
||||
// even though openlib and standard do use it properly. We'll instead doublecheck that
|
||||
// the new time is larger than 6min since we requested the feed.
|
||||
if (date.secsTo(currentTime) > 360) {
|
||||
if (entry.releaseDate() < date.date()) {
|
||||
entry.setUpdateReleaseDate(date.date());
|
||||
if (entry.status() == KNSCore::Entry::Installed) {
|
||||
entry.setStatus(KNSCore::Entry::Updateable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (counterThumbnails == 0) {
|
||||
// fallback.
|
||||
if (!feedDoc->icon().isEmpty()) {
|
||||
entry.setPreviewUrl(fixRelativeUrl(feedDoc->icon()).toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.downloadLinkCount() == 0) {
|
||||
if (groupEntryUrl.isEmpty()) {
|
||||
continue;
|
||||
} else {
|
||||
entry.setEntryType(Entry::GroupEntry);
|
||||
entry.setPayload(groupEntryUrl);
|
||||
}
|
||||
}
|
||||
|
||||
entries.append(entry);
|
||||
}
|
||||
|
||||
if (loadingExtraDetails) {
|
||||
Q_EMIT q->entryDetailsLoaded(entries.first());
|
||||
loadingExtraDetails = false;
|
||||
} else {
|
||||
Q_EMIT q->entriesLoaded(currentRequest, entries);
|
||||
}
|
||||
Q_EMIT q->searchPresetsLoaded(presets);
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
OPDSProvider::OPDSProvider()
|
||||
: d(new OPDSProviderPrivate(this))
|
||||
{
|
||||
}
|
||||
|
||||
OPDSProvider::~OPDSProvider() = default;
|
||||
|
||||
QString OPDSProvider::id() const
|
||||
{
|
||||
return d->providerId;
|
||||
}
|
||||
|
||||
QString OPDSProvider::name() const
|
||||
{
|
||||
return d->providerName;
|
||||
}
|
||||
|
||||
QUrl OPDSProvider::icon() const
|
||||
{
|
||||
return d->iconUrl;
|
||||
}
|
||||
|
||||
void OPDSProvider::loadEntries(const KNSCore::SearchRequest &request)
|
||||
{
|
||||
d->currentRequest = request;
|
||||
|
||||
if (request.filter() == Filter::Installed) {
|
||||
Q_EMIT entriesLoaded(request, d->installedEntries());
|
||||
Q_EMIT loadingDone(request);
|
||||
return;
|
||||
} else if (request.filter() == Filter::ExactEntryId) {
|
||||
for (Entry entry : d->cachedEntries) {
|
||||
if (entry.uniqueId() == request.searchTerm()) {
|
||||
loadEntryDetails(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (QUrl(request.searchTerm()).scheme().startsWith(QStringLiteral("http"))) {
|
||||
d->currentUrl = QUrl(request.searchTerm());
|
||||
} else if (!d->openSearchTemplate.isEmpty() && !request.searchTerm().isEmpty()) {
|
||||
// We should check if there's an opensearch implementation, and see if we can funnel search
|
||||
// requests to that.
|
||||
d->currentUrl = d->openSearchStringForRequest(request);
|
||||
}
|
||||
|
||||
// TODO request: check if entries is above pagesize*index, otherwise load next page.
|
||||
|
||||
QUrl url = d->currentUrl;
|
||||
if (!url.isEmpty()) {
|
||||
qCDebug(KNEWSTUFFCORE) << "requesting url" << url;
|
||||
d->xmlLoader = new XmlLoader(this);
|
||||
d->currentTime = QDateTime::currentDateTime();
|
||||
d->loadingExtraDetails = false;
|
||||
connect(d->xmlLoader, &XmlLoader::signalLoaded, this, [this](const QDomDocument &doc) {
|
||||
d->parseFeedData(doc);
|
||||
});
|
||||
connect(d->xmlLoader, &XmlLoader::signalFailed, this, [this]() {
|
||||
d->slotLoadingFailed();
|
||||
});
|
||||
d->xmlLoader->load(url);
|
||||
} else {
|
||||
Q_EMIT loadingFailed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OPDSProvider::loadEntryDetails(const Entry &entry)
|
||||
{
|
||||
QUrl url;
|
||||
QString entryMimeType = QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(";"));
|
||||
for (auto link : entry.downloadLinkInformationList()) {
|
||||
if (link.tags.contains(KEY_MIME_TYPE + entryMimeType)) {
|
||||
for (QString string : link.tags) {
|
||||
if (string.startsWith(KEY_URL)) {
|
||||
url = QUrl(string.split(QStringLiteral("=")).last());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!url.isEmpty()) {
|
||||
d->xmlLoader = new XmlLoader(this);
|
||||
d->currentTime = QDateTime::currentDateTime();
|
||||
d->loadingExtraDetails = true;
|
||||
connect(d->xmlLoader, &XmlLoader::signalLoaded, this, [this](const QDomDocument &doc) {
|
||||
d->parseFeedData(doc);
|
||||
});
|
||||
connect(d->xmlLoader, &XmlLoader::signalFailed, this, [this]() {
|
||||
d->slotLoadingFailed();
|
||||
});
|
||||
d->xmlLoader->load(url);
|
||||
}
|
||||
}
|
||||
|
||||
void OPDSProvider::loadPayloadLink(const KNSCore::Entry &entry, int linkNumber)
|
||||
{
|
||||
KNSCore::Entry copy = entry;
|
||||
for (auto downloadInfo : entry.downloadLinkInformationList()) {
|
||||
if (downloadInfo.id == linkNumber) {
|
||||
for (QString string : downloadInfo.tags) {
|
||||
if (string.startsWith(KEY_URL)) {
|
||||
copy.setPayload(string.split(QStringLiteral("=")).last());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Q_EMIT payloadLinkLoaded(copy);
|
||||
}
|
||||
|
||||
bool OPDSProvider::setProviderXML(const QDomElement &xmldata)
|
||||
{
|
||||
if (xmldata.tagName() != QLatin1String("provider")) {
|
||||
return false;
|
||||
}
|
||||
d->providerId = xmldata.attribute(QStringLiteral("downloadurl"));
|
||||
|
||||
QUrl iconurl(xmldata.attribute(QStringLiteral("icon")));
|
||||
if (!iconurl.isValid()) {
|
||||
iconurl = QUrl::fromLocalFile(xmldata.attribute(QStringLiteral("icon")));
|
||||
}
|
||||
d->iconUrl = iconurl;
|
||||
|
||||
QDomNode n;
|
||||
for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) {
|
||||
QDomElement e = n.toElement();
|
||||
if (e.tagName() == QLatin1String("title")) {
|
||||
d->providerName = e.text().trimmed();
|
||||
}
|
||||
}
|
||||
|
||||
d->currentUrl = QUrl(d->providerId);
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
d->initialized = true;
|
||||
Q_EMIT providerInitialized(this);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OPDSProvider::isInitialized() const
|
||||
{
|
||||
return d->initialized;
|
||||
}
|
||||
|
||||
void OPDSProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries)
|
||||
{
|
||||
d->cachedEntries = cachedEntries;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString OPDSProvider::version()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] QUrl OPDSProvider::website()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] QUrl OPDSProvider::host()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] QString OPDSProvider::contactEmail()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool OPDSProvider::supportsSsl()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_opdsprovider_p.cpp"
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 OPDSPROVIDER_H
|
||||
#define OPDSPROVIDER_H
|
||||
|
||||
#include "providerbase_p.h"
|
||||
#include "xmlloader_p.h"
|
||||
#include <QMap>
|
||||
#include <memory>
|
||||
|
||||
/**
|
||||
* OPDS provider.
|
||||
*
|
||||
* The OPDS provider loads OPDS feeds:
|
||||
* https://specs.opds.io/opds-1.2
|
||||
*
|
||||
* These feeds are most common with online book providers, but the format itself is agnostic.
|
||||
* For loading feeds, these, as with other providers, need to have a KNSRC file pointed
|
||||
* at a Provider.xml, with the "type" element containing "opds" as text.
|
||||
*
|
||||
* Supports:
|
||||
* - Loads a given feed, it's images, and loads it's download links.
|
||||
* - Opensearch for the search, if available.
|
||||
* - Should load full entries, if possible.
|
||||
* - Navigation feed entries can be selected.
|
||||
*
|
||||
* TODO:
|
||||
* - We need a better handling of non-free items (requires authentication).
|
||||
* - entry navigation links are not supported.
|
||||
* - pagination support (together with the navigation links)
|
||||
* - No Sorting
|
||||
*
|
||||
* Would-be-nice, but requires a lot of rewiring in knewstuff:
|
||||
* - We could get authenticated feeds going by using basic http authentication(in spec), or have bearer token uris (oauth bearcaps).
|
||||
* - Autodiscovery or protocol based discovery of opds catalogs, this does not gel with the provider xml system used by knewstuff.
|
||||
*
|
||||
* @since 5.83
|
||||
*/
|
||||
|
||||
namespace KNSCore
|
||||
{
|
||||
class OPDSProviderPrivate;
|
||||
class OPDSProvider : public ProviderBase
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
OPDSProvider();
|
||||
~OPDSProvider() override;
|
||||
|
||||
// Unique ID, url of the feed.
|
||||
QString id() const override;
|
||||
|
||||
// Name of the feed.
|
||||
QString name() const override;
|
||||
|
||||
// Feed icon
|
||||
QUrl icon() const override;
|
||||
|
||||
[[nodiscard]] QString version() override;
|
||||
[[nodiscard]] QUrl website() override;
|
||||
[[nodiscard]] QUrl host() override;
|
||||
[[nodiscard]] QString contactEmail() override;
|
||||
[[nodiscard]] bool supportsSsl() override;
|
||||
|
||||
void loadEntries(const KNSCore::SearchRequest &request) override;
|
||||
void loadEntryDetails(const KNSCore::Entry &entry) override;
|
||||
void loadPayloadLink(const KNSCore::Entry &entry, int linkNumber) override;
|
||||
|
||||
bool setProviderXML(const QDomElement &xmldata) override;
|
||||
bool isInitialized() const override;
|
||||
void setCachedEntries(const KNSCore::Entry::List &cachedEntries) override;
|
||||
|
||||
const std::unique_ptr<OPDSProviderPrivate> d;
|
||||
|
||||
Q_DISABLE_COPY(OPDSProvider)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OPDSPROVIDER_H
|
||||
@@ -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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user