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