1/*
2 SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "quickengine.h"
8#include "cache.h"
9#include "errorcode.h"
10#include "imageloader_p.h"
11#include "installation_p.h"
12#include "knewstuffquick_debug.h"
13#include "quicksettings.h"
14
15#include <KLocalizedString>
16#include <QQmlInfo>
17#include <QTimer>
18
19#include "categoriesmodel.h"
20#include "quickquestionlistener.h"
21#include "searchpresetmodel.h"
22
23#include "../core/enginebase_p.h"
24#include "../core/providerbase_p.h"
25#include "../core/providercore.h"
26#include "../core/providercore_p.h"
27
28// Could be made :public EngineBasePrivate so we don't have two distinct d pointers
29class EnginePrivate
30{
31public:
32 bool isValid = false;
33 CategoriesModel *categoriesModel = nullptr;
34 SearchPresetModel *searchPresetModel = nullptr;
35 QString configFile;
36 QTimer searchTimer;
37 Engine::BusyState busyState;
38 QString busyMessage;
39 // the current request from providers
40 KNSCore::SearchRequest currentRequest;
41 KNSCore::SearchRequest storedRequest;
42 // the page that is currently displayed, so it is not requested repeatedly
43 int currentPage = -1;
44
45 // when requesting entries from a provider, how many to ask for
46 int pageSize = 20;
47
48 int numDataJobs = 0;
49 int numPictureJobs = 0;
50 int numInstallJobs = 0;
51};
52
53Engine::Engine(QObject *parent)
54 : KNSCore::EngineBase(parent)
55 , d(new EnginePrivate)
56 , dd(KNSCore::EngineBase::d.get())
57{
58 connect(sender: this, signal: &KNSCore::EngineBase::providerAdded, context: this, slot: [this](auto core) {
59 connect(core->d->base, &KNSCore::ProviderBase::entriesLoaded, this, [this](const auto &request, const auto &entries) {
60 d->currentPage = qMax<int>(request.page(), d->currentPage);
61 qCDebug(KNEWSTUFFQUICK) << "loaded page " << request.page() << "current page" << d->currentPage << "count:" << entries.count();
62
63 if (request.filter() != KNSCore::Filter::Updates) {
64 dd->cache->insertRequest(request, entries);
65 }
66 Q_EMIT signalEntriesLoaded(entries);
67
68 --d->numDataJobs;
69 updateStatus();
70 });
71 connect(core->d->base, &KNSCore::ProviderBase::entryDetailsLoaded, this, [this](const auto &entry) {
72 --d->numDataJobs;
73 updateStatus();
74 Q_EMIT signalEntryEvent(entry, event: KNSCore::Entry::DetailsLoadedEvent);
75 });
76 });
77
78 const auto setBusy = [this](Engine::BusyState state, const QString &msg) {
79 setBusyState(state);
80 d->busyMessage = msg;
81 };
82 setBusy(BusyOperation::Initializing, i18n("Loading data")); // For the user this should be the same as initializing
83
84 KNewStuffQuick::QuickQuestionListener::instance();
85 d->categoriesModel = new CategoriesModel(this);
86 connect(sender: d->categoriesModel, signal: &QAbstractListModel::modelReset, context: this, slot: &Engine::categoriesChanged);
87 d->searchPresetModel = new SearchPresetModel(this);
88 connect(sender: d->searchPresetModel, signal: &QAbstractListModel::modelReset, context: this, slot: &Engine::searchPresetModelChanged);
89
90 d->searchTimer.setSingleShot(true);
91 d->searchTimer.setInterval(1000);
92 connect(sender: &d->searchTimer, signal: &QTimer::timeout, context: this, slot: &Engine::reloadEntries);
93 connect(sender: installation(), signal: &KNSCore::Installation::signalInstallationFinished, context: this, slot: [this]() {
94 --d->numInstallJobs;
95 updateStatus();
96 });
97 connect(sender: installation(), signal: &KNSCore::Installation::signalInstallationFailed, context: this, slot: [this](const QString &message) {
98 --d->numInstallJobs;
99 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::InstallationError, message, metadata: QVariant());
100 });
101 connect(sender: this, signal: &EngineBase::signalProvidersLoaded, context: this, slot: &Engine::updateStatus);
102 connect(sender: this, signal: &EngineBase::signalProvidersLoaded, context: this, slot: [this]() {
103 d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
104 d->currentRequest.filter(),
105 d->currentRequest.searchTerm(),
106 EngineBase::categories(),
107 d->currentRequest.page(),
108 d->currentRequest.pageSize());
109 });
110
111 connect(sender: this,
112 signal: &KNSCore::EngineBase::signalErrorCode,
113 context: this,
114 slot: [setBusy, this](const KNSCore::ErrorCode::ErrorCode &error, const QString &message, const QVariant &metadata) {
115 Q_EMIT errorCode(errorCode: error, message, metadata);
116 if (error == KNSCore::ErrorCode::ProviderError || error == KNSCore::ErrorCode::ConfigFileError) {
117 // This means loading the config or providers file failed entirely and we cannot complete the
118 // initialisation. It also means the engine is done loading, but that nothing will
119 // work, and we need to inform the user of this.
120 setBusy({}, QString());
121 }
122
123 // Emit the signal later, currently QML is not connected to the slot
124 if (error == KNSCore::ErrorCode::ConfigFileError) {
125 QTimer::singleShot(interval: 0, slot: [this, error, message, metadata]() {
126 Q_EMIT errorCode(errorCode: error, message, metadata);
127 });
128 }
129 });
130
131 connect(sender: this, signal: &Engine::signalEntryEvent, context: this, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
132 // Just forward the event but not do anything more
133 Q_EMIT entryEvent(entry, event);
134 });
135 //
136 // And finally, let's just make sure we don't miss out the various things here getting changed
137 // In other words, when we're asked to reset the view, actually do that
138 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::categoriesFilterChanged);
139 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::filterChanged);
140 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::sortOrderChanged);
141 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::searchTermChanged);
142}
143
144bool Engine::init(const QString &configfile)
145{
146 const bool valid = EngineBase::init(configfile);
147 if (valid) {
148 connect(sender: this, signal: &Engine::signalEntryEvent, context: dd->cache.get(), slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
149 if (event == KNSCore::Entry::StatusChangedEvent) {
150 dd->cache->registerChangedEntry(entry);
151 }
152 });
153 const auto slotEntryChanged = [this](const KNSCore::Entry &entry) {
154 Q_EMIT signalEntryEvent(entry, event: KNSCore::Entry::StatusChangedEvent);
155 };
156 // Don't connect KNSCore::Installation::signalEntryChanged as is already forwarded to
157 // Transaction, which in turn is forwarded to our slotEntryChanged, so avoids a double emission
158 connect(sender: dd->cache.get(), signal: &KNSCore::Cache2::entryChanged, context: this, slot: slotEntryChanged);
159 }
160 return valid;
161}
162void Engine::updateStatus()
163{
164 QString busyMessage;
165 BusyState state;
166 if (d->numPictureJobs > 0) {
167 // If it is loading previews or data is irrelevant for the user
168 busyMessage = i18n("Loading data");
169 state |= BusyOperation::LoadingPreview;
170 }
171 if (d->numInstallJobs > 0) {
172 busyMessage = i18n("Installing");
173 state |= BusyOperation::InstallingEntry;
174 }
175 if (d->numDataJobs > 0) {
176 busyMessage = i18n("Loading data");
177 state |= BusyOperation::LoadingData;
178 }
179 d->busyMessage = busyMessage;
180 setBusyState(state);
181}
182
183bool Engine::needsLazyLoadSpinner()
184{
185 return d->numDataJobs > 0 || d->numPictureJobs;
186}
187
188Engine::~Engine() = default;
189
190void Engine::setBusyState(BusyState state)
191{
192 d->busyState = state;
193 Q_EMIT busyStateChanged();
194}
195Engine::BusyState Engine::busyState() const
196{
197 return d->busyState;
198}
199QString Engine::busyMessage() const
200{
201 return d->busyMessage;
202}
203
204QString Engine::configFile() const
205{
206 return d->configFile;
207}
208
209void Engine::setConfigFile(const QString &newFile)
210{
211 if (d->configFile != newFile) {
212 d->configFile = newFile;
213 Q_EMIT configFileChanged();
214
215 if (KNewStuffQuick::Settings::instance()->allowedByKiosk()) {
216 d->isValid = init(configfile: newFile);
217 Q_EMIT categoriesFilterChanged();
218 Q_EMIT filterChanged();
219 Q_EMIT sortOrderChanged();
220 Q_EMIT searchTermChanged();
221 } else {
222 // This is not an error message in the proper sense, and the message is not intended to look like an error (as there is really
223 // nothing the user can do to fix it, and we just tell them so they're not wondering what's wrong)
224 Q_EMIT errorCode(
225 errorCode: KNSCore::ErrorCode::ConfigFileError,
226 i18nc("An informational message which is shown to inform the user they are not authorized to use GetHotNewStuff functionality",
227 "You are not authorized to Get Hot New Stuff. If you think this is in error, please contact the person in charge of your permissions."),
228 metadata: QVariant());
229 }
230 }
231}
232
233CategoriesModel *Engine::categories() const
234{
235 return d->categoriesModel;
236}
237
238QStringList Engine::categoriesFilter() const
239{
240 return d->currentRequest.categories();
241}
242
243void Engine::setCategoriesFilter(const QStringList &newCategoriesFilter)
244{
245 if (d->currentRequest.categories() != newCategoriesFilter) {
246 d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
247 d->currentRequest.filter(),
248 d->currentRequest.searchTerm(),
249 newCategoriesFilter,
250 d->currentRequest.page(),
251 d->currentRequest.pageSize());
252 reloadEntries();
253 Q_EMIT categoriesFilterChanged();
254 }
255}
256
257#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
258KNSCore::Provider::Filter Engine::filter() const
259{
260 return [filter = filter2()] {
261 switch (filter) {
262 case KNSCore::Filter::None:
263 return KNSCore::Provider::None;
264 case KNSCore::Filter::Installed:
265 return KNSCore::Provider::Installed;
266 case KNSCore::Filter::Updates:
267 return KNSCore::Provider::Updates;
268 case KNSCore::Filter::ExactEntryId:
269 return KNSCore::Provider::ExactEntryId;
270 }
271 return KNSCore::Provider::None;
272 }();
273}
274#endif
275
276#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
277void Engine::setFilter(KNSCore::Provider::Filter newFilter_)
278{
279 setFilter2([newFilter_] {
280 switch (newFilter_) {
281 case KNSCore::Provider::None:
282 return KNSCore::Filter::None;
283 case KNSCore::Provider::Installed:
284 return KNSCore::Filter::Installed;
285 case KNSCore::Provider::Updates:
286 return KNSCore::Filter::Updates;
287 case KNSCore::Provider::ExactEntryId:
288 return KNSCore::Filter::ExactEntryId;
289 }
290 return KNSCore::Filter::None;
291 }());
292}
293#endif
294
295KNSCore::Filter Engine::filter2() const
296{
297 return d->currentRequest.filter();
298}
299
300void Engine::setFilter2(KNSCore::Filter newFilter)
301{
302 if (d->currentRequest.filter() != newFilter) {
303 d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
304 newFilter,
305 d->currentRequest.searchTerm(),
306 d->currentRequest.categories(),
307 d->currentRequest.page(),
308 d->currentRequest.pageSize());
309 reloadEntries();
310 Q_EMIT filterChanged();
311 }
312}
313
314#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
315KNSCore::Provider::SortMode Engine::sortOrder() const
316{
317 return [mode = sortOrder2()] {
318 switch (mode) {
319 case KNSCore::SortMode::Newest:
320 return KNSCore::Provider::Newest;
321 case KNSCore::SortMode::Alphabetical:
322 return KNSCore::Provider::Alphabetical;
323 case KNSCore::SortMode::Rating:
324 return KNSCore::Provider::Rating;
325 case KNSCore::SortMode::Downloads:
326 return KNSCore::Provider::Downloads;
327 }
328 return KNSCore::Provider::Rating;
329 }();
330}
331#endif
332
333#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
334void Engine::setSortOrder(KNSCore::Provider::SortMode mode_)
335{
336 setSortOrder2([mode_] {
337 switch (mode_) {
338 case KNSCore::Provider::Newest:
339 return KNSCore::SortMode::Newest;
340 case KNSCore::Provider::Alphabetical:
341 return KNSCore::SortMode::Alphabetical;
342 case KNSCore::Provider::Rating:
343 return KNSCore::SortMode::Rating;
344 case KNSCore::Provider::Downloads:
345 return KNSCore::SortMode::Downloads;
346 }
347 return KNSCore::SortMode::Rating;
348 }());
349}
350#endif
351
352KNSCore::SortMode Engine::sortOrder2() const
353{
354 return d->currentRequest.sortMode();
355}
356
357void Engine::setSortOrder2(KNSCore::SortMode mode)
358{
359 if (d->currentRequest.sortMode() != mode) {
360 d->currentRequest = KNSCore::SearchRequest(mode,
361 d->currentRequest.filter(),
362 d->currentRequest.searchTerm(),
363 d->currentRequest.categories(),
364 d->currentRequest.page(),
365 d->currentRequest.pageSize());
366 reloadEntries();
367 Q_EMIT sortOrderChanged();
368 }
369}
370
371QString Engine::searchTerm() const
372{
373 return d->currentRequest.searchTerm();
374}
375
376void Engine::setSearchTerm(const QString &searchTerm)
377{
378 if (d->isValid && d->currentRequest.searchTerm() != searchTerm) {
379 d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
380 d->currentRequest.filter(),
381 searchTerm,
382 d->currentRequest.categories(),
383 d->currentRequest.page(),
384 d->currentRequest.pageSize());
385 Q_EMIT searchTermChanged();
386 }
387 KNSCore::Entry::List cacheEntries = dd->cache->requestFromCache(d->currentRequest);
388 if (!cacheEntries.isEmpty()) {
389 reloadEntries();
390 } else {
391 d->searchTimer.start();
392 }
393}
394
395SearchPresetModel *Engine::searchPresetModel() const
396{
397 return d->searchPresetModel;
398}
399
400bool Engine::isValid()
401{
402 return d->isValid;
403}
404
405void Engine::updateEntryContents(const KNSCore::Entry &entry)
406{
407 const auto core = dd->providerCores.value(key: entry.providerId());
408 if (!core) {
409 qCWarning(KNEWSTUFFQUICK) << "Provider was not found" << entry.providerId();
410 return;
411 }
412
413 const auto base = core->d->base;
414 if (!base->isInitialized()) {
415 qCWarning(KNEWSTUFFQUICK) << "Provider was not initialized" << base << entry.providerId();
416 return;
417 }
418
419 base->loadEntryDetails(entry);
420}
421
422void Engine::reloadEntries()
423{
424 Q_EMIT signalResetView();
425 d->currentPage = -1;
426 d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
427 d->currentRequest.filter(),
428 d->currentRequest.searchTerm(),
429 d->currentRequest.categories(),
430 0,
431 d->currentRequest.pageSize());
432 d->numDataJobs = 0;
433
434 const auto providersList = dd->providerCores;
435 for (const auto &core : providersList) {
436 const auto &base = core->d->base;
437 if (base->isInitialized()) {
438 if (d->currentRequest.filter() == KNSCore::Filter::Installed || d->currentRequest.filter() == KNSCore::Filter::Updates) {
439 // when asking for installed entries, never use the cache
440 base->loadEntries(request: d->currentRequest);
441 } else {
442 // take entries from cache until there are no more
443 KNSCore::Entry::List cacheEntries;
444 KNSCore::Entry::List lastCache = dd->cache->requestFromCache(d->currentRequest);
445 while (!lastCache.isEmpty()) {
446 qCDebug(KNEWSTUFFQUICK) << "From cache";
447 cacheEntries << lastCache;
448
449 d->currentPage = d->currentRequest.page();
450 d->currentRequest = d->currentRequest.nextPage();
451 lastCache = dd->cache->requestFromCache(d->currentRequest);
452 }
453
454 // Since the cache has no more pages, reset the request's page
455 if (d->currentPage >= 0) {
456 d->currentRequest = KNSCore::SearchRequest(d->currentRequest.sortMode(),
457 d->currentRequest.filter(),
458 d->currentRequest.searchTerm(),
459 d->currentRequest.categories(),
460 d->currentPage,
461 d->currentRequest.pageSize());
462 }
463
464 if (!cacheEntries.isEmpty()) {
465 Q_EMIT signalEntriesLoaded(entries: cacheEntries);
466 } else {
467 qCDebug(KNEWSTUFFQUICK) << "From provider";
468 base->loadEntries(request: d->currentRequest);
469
470 ++d->numDataJobs;
471 updateStatus();
472 }
473 }
474 }
475 }
476}
477
478void Engine::loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type)
479{
480 qCDebug(KNEWSTUFFQUICK) << "START preview: " << entry.name() << type;
481 auto l = new KNSCore::ImageLoader(entry, type, this);
482 connect(sender: l, signal: &KNSCore::ImageLoader::signalPreviewLoaded, context: this, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type) {
483 qCDebug(KNEWSTUFFQUICK) << "FINISH preview: " << entry.name() << type;
484 Q_EMIT signalEntryPreviewLoaded(entry, type);
485 --d->numPictureJobs;
486 updateStatus();
487 });
488 connect(sender: l, signal: &KNSCore::ImageLoader::signalError, context: this, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type, const QString &errorText) {
489 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ImageError, message: errorText, metadata: QVariantList() << entry.name() << type);
490 qCDebug(KNEWSTUFFQUICK) << "ERROR preview: " << errorText << entry.name() << type;
491 --d->numPictureJobs;
492 updateStatus();
493 });
494 l->start();
495 ++d->numPictureJobs;
496 updateStatus();
497}
498
499void Engine::adoptEntry(const KNSCore::Entry &entry)
500{
501 registerTransaction(transactions: KNSCore::Transaction::adopt(engine: this, entry));
502}
503
504#if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(6, 9)
505void Engine::install(const KNSCore::Entry &entry, int linkId)
506{
507 qmlWarning(me: this) << "org.kde.newstuff.core.Engine.install is deprecated. Use installLinkId or installLatest";
508 auto transaction = KNSCore::Transaction::install(engine: this, entry, linkId);
509 registerTransaction(transactions: transaction);
510 if (!transaction->isFinished()) {
511 ++d->numInstallJobs;
512 }
513}
514#endif
515
516void Engine::installLinkId(const KNSCore::Entry &entry, quint8 linkId)
517{
518 auto transaction = KNSCore::Transaction::installLinkId(engine: this, entry, linkId);
519 registerTransaction(transactions: transaction);
520 if (!transaction->isFinished()) {
521 ++d->numInstallJobs;
522 }
523}
524
525void Engine::installLatest(const KNSCore::Entry &entry)
526{
527 auto transaction = KNSCore::Transaction::installLatest(engine: this, entry);
528 registerTransaction(transactions: transaction);
529 if (!transaction->isFinished()) {
530 ++d->numInstallJobs;
531 }
532}
533
534void Engine::uninstall(const KNSCore::Entry &entry)
535{
536 registerTransaction(transactions: KNSCore::Transaction::uninstall(engine: this, entry));
537}
538void Engine::registerTransaction(KNSCore::Transaction *transaction)
539{
540 connect(sender: transaction, signal: &KNSCore::Transaction::signalErrorCode, context: this, slot: &EngineBase::signalErrorCode);
541 connect(sender: transaction, signal: &KNSCore::Transaction::signalMessage, context: this, slot: &EngineBase::signalMessage);
542 connect(sender: transaction, signal: &KNSCore::Transaction::signalEntryEvent, context: this, slot: &Engine::signalEntryEvent);
543}
544
545void Engine::requestMoreData()
546{
547 qCDebug(KNEWSTUFFQUICK) << "Get more data! current page: " << d->currentPage << " requested: " << d->currentRequest.page();
548
549 if (d->currentPage < d->currentRequest.page()) {
550 return;
551 }
552
553 d->currentRequest = d->currentRequest.nextPage();
554 doRequest();
555}
556
557void Engine::doRequest()
558{
559 const auto cores = dd->providerCores;
560 for (const auto &core : cores) {
561 const auto &base = core->d->base;
562 if (base->isInitialized()) {
563 base->loadEntries(request: d->currentRequest);
564 ++d->numDataJobs;
565 updateStatus();
566 }
567 }
568}
569
570void Engine::revalidateCacheEntries()
571{
572 // This gets called from QML, because in QtQuick we reuse the engine, BUG: 417985
573 // We can't handle this in the cache, because it can't access the configuration of the engine
574 if (dd->cache) {
575 const auto cores = dd->providerCores;
576 for (const auto &core : cores) {
577 const auto &base = core->d->base;
578 if (base && base->isInitialized()) {
579 const KNSCore::Entry::List cacheBefore = dd->cache->registryForProvider(providerId: base->id());
580 dd->cache->removeDeletedEntries();
581 const KNSCore::Entry::List cacheAfter = dd->cache->registryForProvider(providerId: base->id());
582 // If the user has deleted them in the background we have to update the state to deleted
583 for (const auto &oldCachedEntry : cacheBefore) {
584 if (!cacheAfter.contains(t: oldCachedEntry)) {
585 KNSCore::Entry removedEntry = oldCachedEntry;
586 removedEntry.setEntryDeleted();
587 Q_EMIT signalEntryEvent(entry: removedEntry, event: KNSCore::Entry::StatusChangedEvent);
588 }
589 }
590 }
591 }
592 }
593}
594
595void Engine::restoreSearch()
596{
597 d->searchTimer.stop();
598 d->currentRequest = d->storedRequest;
599 if (dd->cache) {
600 KNSCore::Entry::List cacheEntries = dd->cache->requestFromCache(d->currentRequest);
601 if (!cacheEntries.isEmpty()) {
602 reloadEntries();
603 } else {
604 d->searchTimer.start();
605 }
606 } else {
607 qCWarning(KNEWSTUFFQUICK) << "Attempted to call restoreSearch() without a correctly initialized engine. You will likely get unexpected behaviour.";
608 }
609}
610
611void Engine::storeSearch()
612{
613 d->storedRequest = d->currentRequest;
614}
615
616#include "moc_quickengine.cpp"
617

source code of knewstuff/src/qtquick/quickengine.cpp