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 | |
22 | class EnginePrivate |
23 | { |
24 | public: |
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 | |
46 | Engine::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 | |
111 | bool 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 | } |
128 | void 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 | |
149 | bool Engine::needsLazyLoadSpinner() |
150 | { |
151 | return d->numDataJobs > 0 || d->numPictureJobs; |
152 | } |
153 | |
154 | Engine::~Engine() = default; |
155 | |
156 | void Engine::setBusyState(BusyState state) |
157 | { |
158 | d->busyState = state; |
159 | Q_EMIT busyStateChanged(); |
160 | } |
161 | Engine::BusyState Engine::busyState() const |
162 | { |
163 | return d->busyState; |
164 | } |
165 | QString Engine::busyMessage() const |
166 | { |
167 | return d->busyMessage; |
168 | } |
169 | |
170 | QString Engine::configFile() const |
171 | { |
172 | return d->configFile; |
173 | } |
174 | |
175 | void 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 | |
199 | QObject *Engine::categories() const |
200 | { |
201 | return d->categoriesModel; |
202 | } |
203 | |
204 | QStringList Engine::categoriesFilter() const |
205 | { |
206 | return d->currentRequest.categories; |
207 | } |
208 | |
209 | void 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 | |
218 | KNSCore::Provider::Filter Engine::filter() const |
219 | { |
220 | return d->currentRequest.filter; |
221 | } |
222 | |
223 | void 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 | |
232 | KNSCore::Provider::SortMode Engine::sortOrder() const |
233 | { |
234 | return d->currentRequest.sortMode; |
235 | } |
236 | |
237 | void 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 | |
246 | QString Engine::searchTerm() const |
247 | { |
248 | return d->currentRequest.searchTerm; |
249 | } |
250 | |
251 | void 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 | |
265 | QObject *Engine::searchPresetModel() const |
266 | { |
267 | return d->searchPresetModel; |
268 | } |
269 | |
270 | bool Engine::isValid() |
271 | { |
272 | return d->isValid; |
273 | } |
274 | |
275 | void 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 | |
285 | void 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 | } |
329 | void 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 | |
351 | void 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 | |
372 | void Engine::adoptEntry(const KNSCore::Entry &entry) |
373 | { |
374 | registerTransaction(transactions: KNSCore::Transaction::adopt(engine: this, entry)); |
375 | } |
376 | void 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 | } |
384 | void Engine::uninstall(const KNSCore::Entry &entry) |
385 | { |
386 | registerTransaction(transactions: KNSCore::Transaction::uninstall(engine: this, entry)); |
387 | } |
388 | void 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 | |
395 | void 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 | } |
406 | void 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 | |
418 | void 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 | |
442 | void 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 | |
458 | void Engine::storeSearch() |
459 | { |
460 | d->storedRequest = d->currentRequest; |
461 | } |
462 | |
463 | #include "moc_quickengine.cpp" |
464 | |