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 <QTimer>
17
18#include "categoriesmodel.h"
19#include "quickquestionlistener.h"
20#include "searchpresetmodel.h"
21
22class EnginePrivate
23{
24public:
25 bool isValid = false;
26 CategoriesModel *categoriesModel = nullptr;
27 SearchPresetModel *searchPresetModel = nullptr;
28 QString configFile;
29 QTimer searchTimer;
30 Engine::BusyState busyState;
31 QString busyMessage;
32 // the current request from providers
33 KNSCore::Provider::SearchRequest currentRequest;
34 KNSCore::Provider::SearchRequest storedRequest;
35 // the page that is currently displayed, so it is not requested repeatedly
36 int currentPage = -1;
37
38 // when requesting entries from a provider, how many to ask for
39 int pageSize = 20;
40
41 int numDataJobs = 0;
42 int numPictureJobs = 0;
43 int numInstallJobs = 0;
44};
45
46Engine::Engine(QObject *parent)
47 : KNSCore::EngineBase(parent)
48 , d(new EnginePrivate)
49{
50 const auto setBusy = [this](Engine::BusyState state, const QString &msg) {
51 setBusyState(state);
52 d->busyMessage = msg;
53 };
54 setBusy(BusyOperation::Initializing, i18n("Loading data")); // For the user this should be the same as initializing
55
56 KNewStuffQuick::QuickQuestionListener::instance();
57 d->categoriesModel = new CategoriesModel(this);
58 connect(sender: d->categoriesModel, signal: &QAbstractListModel::modelReset, context: this, slot: &Engine::categoriesChanged);
59 d->searchPresetModel = new SearchPresetModel(this);
60 connect(sender: d->searchPresetModel, signal: &QAbstractListModel::modelReset, context: this, slot: &Engine::searchPresetModelChanged);
61
62 d->searchTimer.setSingleShot(true);
63 d->searchTimer.setInterval(1000);
64 connect(sender: &d->searchTimer, signal: &QTimer::timeout, context: this, slot: &Engine::reloadEntries);
65 connect(sender: installation(), signal: &KNSCore::Installation::signalInstallationFinished, context: this, slot: [this]() {
66 --d->numInstallJobs;
67 updateStatus();
68 });
69 connect(sender: installation(), signal: &KNSCore::Installation::signalInstallationFailed, context: this, slot: [this](const QString &message) {
70 --d->numInstallJobs;
71 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::InstallationError, message, metadata: QVariant());
72 });
73 connect(sender: this, signal: &EngineBase::signalProvidersLoaded, context: this, slot: &Engine::updateStatus);
74 connect(sender: this, signal: &EngineBase::signalProvidersLoaded, context: this, slot: [this]() {
75 d->currentRequest.categories = EngineBase::categories();
76 });
77
78 connect(sender: this,
79 signal: &KNSCore::EngineBase::signalErrorCode,
80 context: this,
81 slot: [setBusy, this](const KNSCore::ErrorCode::ErrorCode &error, const QString &message, const QVariant &metadata) {
82 Q_EMIT errorCode(errorCode: error, message, metadata);
83 if (error == KNSCore::ErrorCode::ProviderError || error == KNSCore::ErrorCode::ConfigFileError) {
84 // This means loading the config or providers file failed entirely and we cannot complete the
85 // initialisation. It also means the engine is done loading, but that nothing will
86 // work, and we need to inform the user of this.
87 setBusy({}, QString());
88 }
89
90 // Emit the signal later, currently QML is not connected to the slot
91 if (error == KNSCore::ErrorCode::ConfigFileError) {
92 QTimer::singleShot(interval: 0, slot: [this, error, message, metadata]() {
93 Q_EMIT errorCode(errorCode: error, message, metadata);
94 });
95 }
96 });
97
98 connect(sender: this, signal: &Engine::signalEntryEvent, context: this, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
99 // Just forward the event but not do anything more
100 Q_EMIT entryEvent(entry, event);
101 });
102 //
103 // And finally, let's just make sure we don't miss out the various things here getting changed
104 // In other words, when we're asked to reset the view, actually do that
105 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::categoriesFilterChanged);
106 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::filterChanged);
107 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::sortOrderChanged);
108 connect(sender: this, signal: &Engine::signalResetView, context: this, slot: &Engine::searchTermChanged);
109}
110
111bool Engine::init(const QString &configfile)
112{
113 const bool valid = EngineBase::init(configfile);
114 if (valid) {
115 connect(sender: this, signal: &Engine::signalEntryEvent, context: cache().data(), slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) {
116 if (event == KNSCore::Entry::StatusChangedEvent) {
117 cache()->registerChangedEntry(entry);
118 }
119 });
120 const auto slotEntryChanged = [this](const KNSCore::Entry &entry) {
121 Q_EMIT signalEntryEvent(entry, event: KNSCore::Entry::StatusChangedEvent);
122 };
123 connect(sender: installation(), signal: &KNSCore::Installation::signalEntryChanged, context: this, slot: slotEntryChanged);
124 connect(sender: cache().data(), signal: &KNSCore::Cache::entryChanged, context: this, slot: slotEntryChanged);
125 }
126 return valid;
127}
128void Engine::updateStatus()
129{
130 QString busyMessage;
131 BusyState state;
132 if (d->numPictureJobs > 0) {
133 // If it is loading previews or data is irrelevant for the user
134 busyMessage = i18n("Loading data");
135 state |= BusyOperation::LoadingPreview;
136 }
137 if (d->numInstallJobs > 0) {
138 busyMessage = i18n("Installing");
139 state |= BusyOperation::InstallingEntry;
140 }
141 if (d->numDataJobs > 0) {
142 busyMessage = i18n("Loading data");
143 state |= BusyOperation::LoadingData;
144 }
145 d->busyMessage = busyMessage;
146 setBusyState(state);
147}
148
149bool Engine::needsLazyLoadSpinner()
150{
151 return d->numDataJobs > 0 || d->numPictureJobs;
152}
153
154Engine::~Engine() = default;
155
156void Engine::setBusyState(BusyState state)
157{
158 d->busyState = state;
159 Q_EMIT busyStateChanged();
160}
161Engine::BusyState Engine::busyState() const
162{
163 return d->busyState;
164}
165QString Engine::busyMessage() const
166{
167 return d->busyMessage;
168}
169
170QString Engine::configFile() const
171{
172 return d->configFile;
173}
174
175void Engine::setConfigFile(const QString &newFile)
176{
177 if (d->configFile != newFile) {
178 d->configFile = newFile;
179 Q_EMIT configFileChanged();
180
181 if (KNewStuffQuick::Settings::instance()->allowedByKiosk()) {
182 d->isValid = init(configfile: newFile);
183 Q_EMIT categoriesFilterChanged();
184 Q_EMIT filterChanged();
185 Q_EMIT sortOrderChanged();
186 Q_EMIT searchTermChanged();
187 } else {
188 // 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
189 // nothing the user can do to fix it, and we just tell them so they're not wondering what's wrong)
190 Q_EMIT errorCode(
191 errorCode: KNSCore::ErrorCode::ConfigFileError,
192 i18nc("An informational message which is shown to inform the user they are not authorized to use GetHotNewStuff functionality",
193 "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."),
194 metadata: QVariant());
195 }
196 }
197}
198
199QObject *Engine::categories() const
200{
201 return d->categoriesModel;
202}
203
204QStringList Engine::categoriesFilter() const
205{
206 return d->currentRequest.categories;
207}
208
209void Engine::setCategoriesFilter(const QStringList &newCategoriesFilter)
210{
211 if (d->currentRequest.categories != newCategoriesFilter) {
212 d->currentRequest.categories = newCategoriesFilter;
213 reloadEntries();
214 Q_EMIT categoriesFilterChanged();
215 }
216}
217
218KNSCore::Provider::Filter Engine::filter() const
219{
220 return d->currentRequest.filter;
221}
222
223void Engine::setFilter(KNSCore::Provider::Filter newFilter)
224{
225 if (d->currentRequest.filter != newFilter) {
226 d->currentRequest.filter = newFilter;
227 reloadEntries();
228 Q_EMIT filterChanged();
229 }
230}
231
232KNSCore::Provider::SortMode Engine::sortOrder() const
233{
234 return d->currentRequest.sortMode;
235}
236
237void Engine::setSortOrder(KNSCore::Provider::SortMode mode)
238{
239 if (d->currentRequest.sortMode != mode) {
240 d->currentRequest.sortMode = mode;
241 reloadEntries();
242 Q_EMIT sortOrderChanged();
243 }
244}
245
246QString Engine::searchTerm() const
247{
248 return d->currentRequest.searchTerm;
249}
250
251void Engine::setSearchTerm(const QString &searchTerm)
252{
253 if (d->isValid && d->currentRequest.searchTerm != searchTerm) {
254 d->currentRequest.searchTerm = searchTerm;
255 Q_EMIT searchTermChanged();
256 }
257 KNSCore::Entry::List cacheEntries = cache()->requestFromCache(d->currentRequest);
258 if (!cacheEntries.isEmpty()) {
259 reloadEntries();
260 } else {
261 d->searchTimer.start();
262 }
263}
264
265QObject *Engine::searchPresetModel() const
266{
267 return d->searchPresetModel;
268}
269
270bool Engine::isValid()
271{
272 return d->isValid;
273}
274
275void Engine::updateEntryContents(const KNSCore::Entry &entry)
276{
277 const auto provider = EngineBase::provider(providerId: entry.providerId());
278 if (provider.isNull() || !provider->isInitialized()) {
279 qCWarning(KNEWSTUFFQUICK) << "Provider was not found or is not initialized" << provider << entry.providerId();
280 return;
281 }
282 provider->loadEntryDetails(entry);
283}
284
285void Engine::reloadEntries()
286{
287 Q_EMIT signalResetView();
288 d->currentPage = -1;
289 d->currentRequest.page = 0;
290 d->numDataJobs = 0;
291
292 const auto providersList = EngineBase::providers();
293 for (const QSharedPointer<KNSCore::Provider> &p : providersList) {
294 if (p->isInitialized()) {
295 if (d->currentRequest.filter == KNSCore::Provider::Installed || d->currentRequest.filter == KNSCore::Provider::Updates) {
296 // when asking for installed entries, never use the cache
297 p->loadEntries(request: d->currentRequest);
298 } else {
299 // take entries from cache until there are no more
300 KNSCore::Entry::List cacheEntries;
301 KNSCore::Entry::List lastCache = cache()->requestFromCache(d->currentRequest);
302 while (!lastCache.isEmpty()) {
303 qCDebug(KNEWSTUFFQUICK) << "From cache";
304 cacheEntries << lastCache;
305
306 d->currentPage = d->currentRequest.page;
307 ++d->currentRequest.page;
308 lastCache = cache()->requestFromCache(d->currentRequest);
309 }
310
311 // Since the cache has no more pages, reset the request's page
312 if (d->currentPage >= 0) {
313 d->currentRequest.page = d->currentPage;
314 }
315
316 if (!cacheEntries.isEmpty()) {
317 Q_EMIT signalEntriesLoaded(entries: cacheEntries);
318 } else {
319 qCDebug(KNEWSTUFFQUICK) << "From provider";
320 p->loadEntries(request: d->currentRequest);
321
322 ++d->numDataJobs;
323 updateStatus();
324 }
325 }
326 }
327 }
328}
329void Engine::addProvider(QSharedPointer<KNSCore::Provider> provider)
330{
331 EngineBase::addProvider(provider);
332 connect(sender: provider.data(), signal: &KNSCore::Provider::loadingFinished, context: this, slot: [this](const auto &request, const auto &entries) {
333 d->currentPage = qMax<int>(request.page, d->currentPage);
334 qCDebug(KNEWSTUFFQUICK) << "loaded page " << request.page << "current page" << d->currentPage << "count:" << entries.count();
335
336 if (request.filter != KNSCore::Provider::Updates) {
337 cache()->insertRequest(request, entries);
338 }
339 Q_EMIT signalEntriesLoaded(entries);
340
341 --d->numDataJobs;
342 updateStatus();
343 });
344 connect(sender: provider.data(), signal: &KNSCore::Provider::entryDetailsLoaded, context: this, slot: [this](const auto &entry) {
345 --d->numDataJobs;
346 updateStatus();
347 Q_EMIT signalEntryEvent(entry, event: KNSCore::Entry::DetailsLoadedEvent);
348 });
349}
350
351void Engine::loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type)
352{
353 qCDebug(KNEWSTUFFQUICK) << "START preview: " << entry.name() << type;
354 auto l = new KNSCore::ImageLoader(entry, type, this);
355 connect(sender: l, signal: &KNSCore::ImageLoader::signalPreviewLoaded, context: this, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type) {
356 qCDebug(KNEWSTUFFQUICK) << "FINISH preview: " << entry.name() << type;
357 Q_EMIT signalEntryPreviewLoaded(entry, type);
358 --d->numPictureJobs;
359 updateStatus();
360 });
361 connect(sender: l, signal: &KNSCore::ImageLoader::signalError, context: this, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type, const QString &errorText) {
362 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ImageError, message: errorText, metadata: QVariantList() << entry.name() << type);
363 qCDebug(KNEWSTUFFQUICK) << "ERROR preview: " << errorText << entry.name() << type;
364 --d->numPictureJobs;
365 updateStatus();
366 });
367 l->start();
368 ++d->numPictureJobs;
369 updateStatus();
370}
371
372void Engine::adoptEntry(const KNSCore::Entry &entry)
373{
374 registerTransaction(transactions: KNSCore::Transaction::adopt(engine: this, entry));
375}
376void Engine::install(const KNSCore::Entry &entry, int linkId)
377{
378 auto transaction = KNSCore::Transaction::install(engine: this, entry, linkId);
379 registerTransaction(transactions: transaction);
380 if (!transaction->isFinished()) {
381 ++d->numInstallJobs;
382 }
383}
384void Engine::uninstall(const KNSCore::Entry &entry)
385{
386 registerTransaction(transactions: KNSCore::Transaction::uninstall(engine: this, entry));
387}
388void Engine::registerTransaction(KNSCore::Transaction *transaction)
389{
390 connect(sender: transaction, signal: &KNSCore::Transaction::signalErrorCode, context: this, slot: &EngineBase::signalErrorCode);
391 connect(sender: transaction, signal: &KNSCore::Transaction::signalMessage, context: this, slot: &EngineBase::signalMessage);
392 connect(sender: transaction, signal: &KNSCore::Transaction::signalEntryEvent, context: this, slot: &Engine::signalEntryEvent);
393}
394
395void Engine::requestMoreData()
396{
397 qCDebug(KNEWSTUFFQUICK) << "Get more data! current page: " << d->currentPage << " requested: " << d->currentRequest.page;
398
399 if (d->currentPage < d->currentRequest.page) {
400 return;
401 }
402
403 d->currentRequest.page++;
404 doRequest();
405}
406void Engine::doRequest()
407{
408 const auto providersList = providers();
409 for (const QSharedPointer<KNSCore::Provider> &p : providersList) {
410 if (p->isInitialized()) {
411 p->loadEntries(request: d->currentRequest);
412 ++d->numDataJobs;
413 updateStatus();
414 }
415 }
416}
417
418void Engine::revalidateCacheEntries()
419{
420 // This gets called from QML, because in QtQuick we reuse the engine, BUG: 417985
421 // We can't handle this in the cache, because it can't access the configuration of the engine
422 if (cache()) {
423 const auto providersList = providers();
424 for (const auto &provider : providersList) {
425 if (provider && provider->isInitialized()) {
426 const KNSCore::Entry::List cacheBefore = cache()->registryForProvider(providerId: provider->id());
427 cache()->removeDeletedEntries();
428 const KNSCore::Entry::List cacheAfter = cache()->registryForProvider(providerId: provider->id());
429 // If the user has deleted them in the background we have to update the state to deleted
430 for (const auto &oldCachedEntry : cacheBefore) {
431 if (!cacheAfter.contains(t: oldCachedEntry)) {
432 KNSCore::Entry removedEntry = oldCachedEntry;
433 removedEntry.setEntryDeleted();
434 Q_EMIT signalEntryEvent(entry: removedEntry, event: KNSCore::Entry::StatusChangedEvent);
435 }
436 }
437 }
438 }
439 }
440}
441
442void Engine::restoreSearch()
443{
444 d->searchTimer.stop();
445 d->currentRequest = d->storedRequest;
446 if (cache()) {
447 KNSCore::Entry::List cacheEntries = cache()->requestFromCache(d->currentRequest);
448 if (!cacheEntries.isEmpty()) {
449 reloadEntries();
450 } else {
451 d->searchTimer.start();
452 }
453 } else {
454 qCWarning(KNEWSTUFFQUICK) << "Attempted to call restoreSearch() without a correctly initialized engine. You will likely get unexpected behaviour.";
455 }
456}
457
458void Engine::storeSearch()
459{
460 d->storedRequest = d->currentRequest;
461}
462
463#include "moc_quickengine.cpp"
464

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