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 "quickitemsmodel.h"
8 #include "core/commentsmodel.h"
9 #include "downloadlinkinfo.h"
10 #include "engine.h"
11 #include "itemsmodel.h"
12 #include "quickengine.h"
13
14 #include <KShell>
15 #include <QProcess>
16
17 class ItemsModel::Private
18 {
19 public:
Private(ItemsModel * qq)20 Private(ItemsModel *qq)
21 : q(qq)
22 , model(nullptr)
23 , engine(nullptr)
24 , coreEngine(nullptr)
25 {
26 }
~Private()27 ~Private()
28 {
29 }
30 ItemsModel *q;
31 KNSCore::ItemsModel *model;
32 Engine *engine;
33 KNSCore::Engine *coreEngine;
34
35 QHash<QString, KNSCore::CommentsModel *> commentsModels;
36
37 bool isLoadingData{false};
38
initModel()39 bool initModel()
40 {
41 if (model) {
42 return true;
43 }
44 if (!coreEngine) {
45 return false;
46 }
47 model = new KNSCore::ItemsModel(coreEngine, q);
48 q->connect(coreEngine, &KNSCore::Engine::busyStateChanged, q, [=]() {
49 // If we install/update an entry the spinner should be hidden, BUG: 422047
50 const KNSCore::Engine::BusyState state = coreEngine->busyState();
51 const bool busy = state && !state.testFlag(KNSCore::Engine::BusyOperation::InstallingEntry);
52 if (isLoadingData != busy) {
53 isLoadingData = busy;
54 Q_EMIT q->isLoadingDataChanged();
55 }
56 });
57
58 q->connect(coreEngine, &KNSCore::Engine::signalProvidersLoaded, coreEngine, &KNSCore::Engine::reloadEntries);
59 // Entries have been fetched and should be shown:
60 q->connect(coreEngine, &KNSCore::Engine::signalEntriesLoaded, model, [this](const KNSCore::EntryInternal::List &entries) {
61 if (coreEngine->filter() != KNSCore::Provider::Updates) {
62 model->slotEntriesLoaded(entries);
63 }
64 });
65 q->connect(coreEngine,
66 &KNSCore::Engine::signalEntryEvent,
67 model,
68 [this](const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event) {
69 if (event == KNSCore::EntryInternal::DetailsLoadedEvent && coreEngine->filter() != KNSCore::Provider::Updates) {
70 model->slotEntriesLoaded(KNSCore::EntryInternal::List{entry});
71 }
72 });
73 q->connect(coreEngine, &KNSCore::Engine::signalUpdateableEntriesLoaded, model, [this](const KNSCore::EntryInternal::List &entries) {
74 if (coreEngine->filter() == KNSCore::Provider::Updates) {
75 model->slotEntriesLoaded(entries);
76 }
77 });
78
79 q->connect(coreEngine, &KNSCore::Engine::signalEntryEvent, q, [this](const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event) {
80 onEntryEvent(entry, event);
81 });
82 q->connect(coreEngine, &KNSCore::Engine::signalResetView, model, &KNSCore::ItemsModel::clearEntries);
83 q->connect(coreEngine, &KNSCore::Engine::signalEntryPreviewLoaded, model, &KNSCore::ItemsModel::slotEntryPreviewLoaded);
84
85 q->connect(model, &KNSCore::ItemsModel::rowsInserted, q, &ItemsModel::rowsInserted);
86 q->connect(model, &KNSCore::ItemsModel::rowsRemoved, q, &ItemsModel::rowsRemoved);
87 q->connect(model, &KNSCore::ItemsModel::dataChanged, q, &ItemsModel::dataChanged);
88 q->connect(model, &KNSCore::ItemsModel::modelReset, q, &ItemsModel::modelReset);
89 return true;
90 }
91
onEntryEvent(const KNSCore::EntryInternal & entry,KNSCore::EntryInternal::EntryEvent event)92 void onEntryEvent(const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::EntryEvent event)
93 {
94 if (event == KNSCore::EntryInternal::StatusChangedEvent) {
95 model->slotEntryChanged(entry);
96 Q_EMIT q->entryChanged(model->row(entry));
97
98 // If we update/uninstall an entry we have to update the UI, see BUG: 425135
99 if (coreEngine->filter() == KNSCore::Provider::Updates && entry.status() != KNS3::Entry::Updateable && entry.status() != KNS3::Entry::Updating) {
100 model->removeEntry(entry);
101 } else if (coreEngine->filter() == KNSCore::Provider::Installed && entry.status() == KNS3::Entry::Deleted) {
102 model->removeEntry(entry);
103 }
104 }
105 }
106 };
107
ItemsModel(QObject * parent)108 ItemsModel::ItemsModel(QObject *parent)
109 : QAbstractListModel(parent)
110 , d(new Private(this))
111 {
112 }
113
~ItemsModel()114 ItemsModel::~ItemsModel()
115 {
116 delete d;
117 }
118
roleNames() const119 QHash<int, QByteArray> ItemsModel::roleNames() const
120 {
121 // clang-format off
122 static const QHash<int, QByteArray> roles = QHash<int, QByteArray>{
123 {Qt::DisplayRole, "display"},
124 {NameRole, "name"},
125 {UniqueIdRole, "uniqueId"},
126 {CategoryRole, "category"},
127 {HomepageRole, "homepage"},
128 {AuthorRole, "author"},
129 {LicenseRole, "license"},
130 {ShortSummaryRole, "shortSummary"},
131 {SummaryRole, "summary"},
132 {ChangelogRole, "changelog"},
133 {VersionRole, "version"},
134 {ReleaseDateRole, "releaseDate"},
135 {UpdateVersionRole, "updateVersion"},
136 {UpdateReleaseDateRole, "updateReleaseDate"},
137 {PayloadRole, "payload"},
138 {Qt::DecorationRole, "decoration"},
139 {PreviewsSmallRole, "previewsSmall"},
140 {PreviewsRole, "previews"},
141 {InstalledFilesRole, "installedFiles"},
142 {UnInstalledFilesRole, "uninstalledFiles"},
143 {RatingRole, "rating"},
144 {NumberOfCommentsRole, "numberOfComments"},
145 {DownloadCountRole, "downloadCount"},
146 {NumberFansRole, "numberFans"},
147 {NumberKnowledgebaseEntriesRole, "numberKnowledgebaseEntries"},
148 {KnowledgebaseLinkRole, "knowledgebaseLink"},
149 {DownloadLinksRole, "downloadLinks"},
150 {DonationLinkRole, "donationLink"},
151 {ProviderIdRole, "providerId"},
152 {SourceRole, "source"},
153 {StatusRole, "status"},
154 {EntryTypeRole, "entryType"},
155 };
156 // clang-format on
157 return roles;
158 }
159
rowCount(const QModelIndex & parent) const160 int ItemsModel::rowCount(const QModelIndex &parent) const
161 {
162 if (parent.isValid()) {
163 return 0;
164 }
165 if (d->initModel()) {
166 return d->model->rowCount(QModelIndex());
167 }
168 return 0;
169 }
170
data(const QModelIndex & index,int role) const171 QVariant ItemsModel::data(const QModelIndex &index, int role) const
172 {
173 QVariant data;
174 if (index.isValid() && d->initModel()) {
175 KNSCore::EntryInternal entry = d->model->data(d->model->index(index.row()), Qt::UserRole).value<KNSCore::EntryInternal>();
176 switch (role) {
177 case NameRole:
178 case Qt::DisplayRole:
179 data.setValue<QString>(entry.name());
180 break;
181 case UniqueIdRole:
182 data.setValue<QString>(entry.uniqueId());
183 break;
184 case CategoryRole:
185 data.setValue<QString>(entry.category());
186 break;
187 case HomepageRole:
188 data.setValue<QUrl>(entry.homepage());
189 break;
190 case AuthorRole: {
191 KNSCore::Author author = entry.author();
192 QVariantMap returnAuthor;
193 returnAuthor[QStringLiteral("id")] = author.id();
194 returnAuthor[QStringLiteral("name")] = author.name();
195 returnAuthor[QStringLiteral("email")] = author.email();
196 returnAuthor[QStringLiteral("homepage")] = author.homepage();
197 returnAuthor[QStringLiteral("jabber")] = author.jabber();
198 returnAuthor[QStringLiteral("avatarUrl")] = author.avatarUrl();
199 returnAuthor[QStringLiteral("description")] = author.description();
200 data.setValue<>(returnAuthor);
201 } break;
202 case LicenseRole:
203 data.setValue<QString>(entry.license());
204 break;
205 case ShortSummaryRole:
206 data.setValue<QString>(entry.shortSummary());
207 break;
208 case SummaryRole:
209 data.setValue<QString>(entry.summary());
210 break;
211 case ChangelogRole:
212 data.setValue<QString>(entry.changelog());
213 break;
214 case VersionRole:
215 data.setValue<QString>(entry.version());
216 break;
217 case ReleaseDateRole:
218 data.setValue<QDate>(entry.releaseDate());
219 break;
220 case UpdateVersionRole:
221 data.setValue<QString>(entry.updateVersion());
222 break;
223 case UpdateReleaseDateRole:
224 data.setValue<QDate>(entry.updateReleaseDate());
225 break;
226 case PayloadRole:
227 data.setValue<QString>(entry.payload());
228 break;
229 case Qt::DecorationRole:
230 data.setValue<QString>(entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1));
231 break;
232 case PreviewsSmallRole: {
233 QStringList previews;
234 previews << entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1);
235 previews << entry.previewUrl(KNSCore::EntryInternal::PreviewSmall2);
236 previews << entry.previewUrl(KNSCore::EntryInternal::PreviewSmall3);
237 while (!previews.isEmpty() && previews.last().isEmpty()) {
238 previews.takeLast();
239 }
240 data.setValue<QStringList>(previews);
241 } break;
242 case PreviewsRole: {
243 QStringList previews;
244 previews << entry.previewUrl(KNSCore::EntryInternal::PreviewBig1);
245 previews << entry.previewUrl(KNSCore::EntryInternal::PreviewBig2);
246 previews << entry.previewUrl(KNSCore::EntryInternal::PreviewBig3);
247 while (!previews.isEmpty() && previews.last().isEmpty()) {
248 previews.takeLast();
249 }
250 data.setValue<QStringList>(previews);
251 } break;
252 case InstalledFilesRole:
253 data.setValue<QStringList>(entry.installedFiles());
254 break;
255 case UnInstalledFilesRole:
256 data.setValue<QStringList>(entry.uninstalledFiles());
257 break;
258 case RatingRole:
259 data.setValue<int>(entry.rating());
260 break;
261 case NumberOfCommentsRole:
262 data.setValue<int>(entry.numberOfComments());
263 break;
264 case DownloadCountRole:
265 data.setValue<int>(entry.downloadCount());
266 break;
267 case NumberFansRole:
268 data.setValue<int>(entry.numberFans());
269 break;
270 case NumberKnowledgebaseEntriesRole:
271 data.setValue<int>(entry.numberKnowledgebaseEntries());
272 break;
273 case KnowledgebaseLinkRole:
274 data.setValue<QString>(entry.knowledgebaseLink());
275 break;
276 case DownloadLinksRole: {
277 // This would be good to cache... but it also needs marking as dirty, somehow...
278 const QList<KNSCore::EntryInternal::DownloadLinkInformation> dllinks = entry.downloadLinkInformationList();
279 QObjectList list;
280 for (const KNSCore::EntryInternal::DownloadLinkInformation &link : dllinks) {
281 DownloadLinkInfo *info = new DownloadLinkInfo();
282 info->setData(link);
283 list.append(info);
284 }
285 if (list.isEmpty() && !entry.payload().isEmpty()) {
286 DownloadLinkInfo *info = new DownloadLinkInfo();
287 KNSCore::EntryInternal::DownloadLinkInformation data;
288 data.descriptionLink = entry.payload();
289 info->setData(data);
290 list.append(info);
291 }
292 data.setValue<QObjectList>(list);
293 } break;
294 case DonationLinkRole:
295 data.setValue<QString>(entry.donationLink());
296 break;
297 case ProviderIdRole:
298 data.setValue<QString>(entry.providerId());
299 break;
300 case SourceRole: {
301 KNSCore::EntryInternal::Source src = entry.source();
302 switch (src) {
303 case KNSCore::EntryInternal::Cache:
304 data.setValue<QString>(QStringLiteral("Cache"));
305 break;
306 case KNSCore::EntryInternal::Online:
307 data.setValue<QString>(QStringLiteral("Online"));
308 break;
309 case KNSCore::EntryInternal::Registry:
310 data.setValue<QString>(QStringLiteral("Registry"));
311 break;
312 default:
313 data.setValue<QString>(QStringLiteral("Unknown source - shouldn't be possible"));
314 break;
315 }
316 } break;
317 case StatusRole: {
318 KNS3::Entry::Status status = entry.status();
319 switch (status) {
320 case KNS3::Entry::Downloadable:
321 data.setValue<ItemsModel::ItemStatus>(ItemsModel::DownloadableStatus);
322 break;
323 case KNS3::Entry::Installed:
324 data.setValue<ItemsModel::ItemStatus>(ItemsModel::InstalledStatus);
325 break;
326 case KNS3::Entry::Updateable:
327 data.setValue<ItemsModel::ItemStatus>(ItemsModel::UpdateableStatus);
328 break;
329 case KNS3::Entry::Deleted:
330 data.setValue<ItemsModel::ItemStatus>(ItemsModel::DeletedStatus);
331 break;
332 case KNS3::Entry::Installing:
333 data.setValue<ItemsModel::ItemStatus>(ItemsModel::InstallingStatus);
334 break;
335 case KNS3::Entry::Updating:
336 data.setValue<ItemsModel::ItemStatus>(ItemsModel::UpdatingStatus);
337 break;
338 case KNS3::Entry::Invalid:
339 default:
340 data.setValue<ItemsModel::ItemStatus>(ItemsModel::InvalidStatus);
341 break;
342 }
343 } break;
344 case CommentsModelRole: {
345 KNSCore::CommentsModel *commentsModel{nullptr};
346 if (!d->commentsModels.contains(entry.uniqueId())) {
347 commentsModel = d->coreEngine->commentsForEntry(entry);
348 d->commentsModels[entry.uniqueId()] = commentsModel;
349 } else {
350 commentsModel = d->commentsModels[entry.uniqueId()];
351 }
352 data.setValue<QObject *>(commentsModel);
353 } break;
354 case EntryTypeRole: {
355 KNSCore::EntryInternal::EntryType type = entry.entryType();
356 if (type == KNSCore::EntryInternal::GroupEntry) {
357 data.setValue<ItemsModel::EntryType>(ItemsModel::GroupEntry);
358 } else {
359 data.setValue<ItemsModel::EntryType>(ItemsModel::CatalogEntry);
360 }
361 } break;
362 default:
363 data.setValue<QString>(QStringLiteral("Unknown role"));
364 break;
365 }
366 }
367 return data;
368 }
369
canFetchMore(const QModelIndex & parent) const370 bool ItemsModel::canFetchMore(const QModelIndex &parent) const
371 {
372 return !parent.isValid() && d->coreEngine && d->coreEngine->categoriesMetadata().count() > 0;
373 }
374
fetchMore(const QModelIndex & parent)375 void ItemsModel::fetchMore(const QModelIndex &parent)
376 {
377 if (parent.isValid() || !d->coreEngine) {
378 return;
379 }
380 d->coreEngine->requestMoreData();
381 }
382
engine() const383 QObject *ItemsModel::engine() const
384 {
385 return d->engine;
386 }
387
setEngine(QObject * newEngine)388 void ItemsModel::setEngine(QObject *newEngine)
389 {
390 if (d->engine != newEngine) {
391 beginResetModel();
392 d->engine = qobject_cast<Engine *>(newEngine);
393 d->model->deleteLater();
394 d->model = nullptr;
395 d->coreEngine = nullptr;
396 if (d->engine) {
397 d->coreEngine = qobject_cast<KNSCore::Engine *>(d->engine->engine());
398 }
399 connect(d->engine, &Engine::engineChanged, this, [this]() {
400 beginResetModel();
401 d->model->deleteLater();
402 d->model = nullptr;
403 d->coreEngine = qobject_cast<KNSCore::Engine *>(d->engine->engine());
404 endResetModel();
405 });
406 Q_EMIT engineChanged();
407 endResetModel();
408 }
409 }
410
indexOfEntryId(const QString & providerId,const QString & entryId)411 int ItemsModel::indexOfEntryId(const QString &providerId, const QString &entryId)
412 {
413 int idx{-1};
414 if (d->coreEngine && d->model) {
415 for (int i = 0; i < rowCount(); ++i) {
416 KNSCore::EntryInternal testEntry = d->model->data(d->model->index(i), Qt::UserRole).value<KNSCore::EntryInternal>();
417 if (providerId == QUrl(testEntry.providerId()).host() && entryId == testEntry.uniqueId()) {
418 idx = i;
419 break;
420 }
421 }
422 }
423 return idx;
424 }
425
isLoadingData() const426 bool ItemsModel::isLoadingData() const
427 {
428 return d->isLoadingData;
429 }
430
installItem(int index,int linkId)431 void ItemsModel::installItem(int index, int linkId)
432 {
433 if (d->coreEngine) {
434 KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value<KNSCore::EntryInternal>();
435 if (entry.isValid()) {
436 d->coreEngine->install(entry, linkId);
437 }
438 }
439 }
440
updateItem(int index)441 void ItemsModel::updateItem(int index)
442 {
443 installItem(index, AutoDetectLinkId);
444 }
445
uninstallItem(int index)446 void ItemsModel::uninstallItem(int index)
447 {
448 if (d->coreEngine) {
449 KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value<KNSCore::EntryInternal>();
450 if (entry.isValid()) {
451 d->coreEngine->uninstall(entry);
452 }
453 }
454 }
455
adoptItem(int index)456 void ItemsModel::adoptItem(int index)
457 {
458 if (d->coreEngine) {
459 KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value<KNSCore::EntryInternal>();
460 if (entry.isValid()) {
461 d->coreEngine->adoptEntry(entry);
462 }
463 }
464 }
465