| 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 "itemsmodel.h" |
| 11 | #include "quickengine.h" |
| 12 | |
| 13 | #include <KShell> |
| 14 | #include <QProcess> |
| 15 | |
| 16 | class ItemsModelPrivate |
| 17 | { |
| 18 | public: |
| 19 | ItemsModelPrivate(ItemsModel *qq) |
| 20 | : q(qq) |
| 21 | , model(nullptr) |
| 22 | , engine(nullptr) |
| 23 | { |
| 24 | } |
| 25 | ItemsModel *q; |
| 26 | KNSCore::ItemsModel *model; |
| 27 | Engine *engine; |
| 28 | |
| 29 | QHash<QString, KNSCore::CommentsModel *> ; |
| 30 | |
| 31 | bool initModel() |
| 32 | { |
| 33 | if (model) { |
| 34 | return true; |
| 35 | } |
| 36 | if (!engine) { |
| 37 | return false; |
| 38 | } |
| 39 | model = new KNSCore::ItemsModel(engine, q); |
| 40 | |
| 41 | q->connect(sender: engine, signal: &Engine::signalProvidersLoaded, context: engine, slot: &Engine::reloadEntries); |
| 42 | // Entries have been fetched and should be shown: |
| 43 | q->connect(sender: engine, signal: &Engine::signalEntriesLoaded, context: model, slot: [this](const KNSCore::Entry::List &entries) { |
| 44 | model->slotEntriesLoaded(entries); |
| 45 | }); |
| 46 | q->connect(sender: engine, signal: &Engine::entryEvent, context: model, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) { |
| 47 | if (event == KNSCore::Entry::DetailsLoadedEvent && engine->filter() != KNSCore::Provider::Installed |
| 48 | && engine->filter() != KNSCore::Provider::Updates) { |
| 49 | model->slotEntriesLoaded(entries: KNSCore::Entry::List{entry}); |
| 50 | } |
| 51 | }); |
| 52 | |
| 53 | // Check if we need intermediate states |
| 54 | q->connect(sender: engine, signal: &Engine::entryEvent, context: q, slot: [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) { |
| 55 | onEntryEvent(entry, event); |
| 56 | }); |
| 57 | q->connect(sender: engine, signal: &Engine::signalResetView, context: model, slot: &KNSCore::ItemsModel::clearEntries); |
| 58 | |
| 59 | q->connect(sender: model, signal: &KNSCore::ItemsModel::loadPreview, context: engine, slot: &Engine::loadPreview); |
| 60 | q->connect(sender: engine, signal: &Engine::entryPreviewLoaded, context: model, slot: &KNSCore::ItemsModel::slotEntryPreviewLoaded); |
| 61 | |
| 62 | q->connect(sender: model, signal: &KNSCore::ItemsModel::rowsInserted, context: q, slot: &ItemsModel::rowsInserted); |
| 63 | q->connect(sender: model, signal: &KNSCore::ItemsModel::rowsRemoved, context: q, slot: &ItemsModel::rowsRemoved); |
| 64 | q->connect(sender: model, signal: &KNSCore::ItemsModel::dataChanged, context: q, slot: &ItemsModel::dataChanged); |
| 65 | q->connect(sender: model, signal: &KNSCore::ItemsModel::modelAboutToBeReset, context: q, slot: &ItemsModel::modelAboutToBeReset); |
| 66 | q->connect(sender: model, signal: &KNSCore::ItemsModel::modelReset, context: q, slot: &ItemsModel::modelReset); |
| 67 | return true; |
| 68 | } |
| 69 | |
| 70 | void onEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) |
| 71 | { |
| 72 | if (event == KNSCore::Entry::StatusChangedEvent) { |
| 73 | model->slotEntryChanged(entry); |
| 74 | Q_EMIT q->entryChanged(entry); |
| 75 | |
| 76 | // If we update/uninstall an entry we have to update the UI, see BUG: 425135 |
| 77 | if (engine->filter() == KNSCore::Provider::Updates && entry.status() != KNSCore::Entry::Updateable && entry.status() != KNSCore::Entry::Updating) { |
| 78 | model->removeEntry(entry); |
| 79 | } else if (engine->filter() == KNSCore::Provider::Installed && entry.status() == KNSCore::Entry::Deleted) { |
| 80 | model->removeEntry(entry); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | if (event == KNSCore::Entry::DetailsLoadedEvent) { |
| 85 | model->slotEntryChanged(entry); |
| 86 | Q_EMIT q->entryChanged(entry); |
| 87 | } |
| 88 | } |
| 89 | }; |
| 90 | |
| 91 | ItemsModel::ItemsModel(QObject *parent) |
| 92 | : QAbstractListModel(parent) |
| 93 | , d(new ItemsModelPrivate(this)) |
| 94 | { |
| 95 | } |
| 96 | |
| 97 | ItemsModel::~ItemsModel() = default; |
| 98 | |
| 99 | QHash<int, QByteArray> ItemsModel::roleNames() const |
| 100 | { |
| 101 | static const QHash<int, QByteArray> roles = QHash<int, QByteArray>{ |
| 102 | {Qt::DisplayRole, "display" }, |
| 103 | {NameRole, "name" }, |
| 104 | {UniqueIdRole, "uniqueId" }, |
| 105 | {CategoryRole, "category" }, |
| 106 | {HomepageRole, "homepage" }, |
| 107 | {AuthorRole, "author" }, |
| 108 | {LicenseRole, "license" }, |
| 109 | {ShortSummaryRole, "shortSummary" }, |
| 110 | {SummaryRole, "summary" }, |
| 111 | {ChangelogRole, "changelog" }, |
| 112 | {VersionRole, "version" }, |
| 113 | {ReleaseDateRole, "releaseDate" }, |
| 114 | {UpdateVersionRole, "updateVersion" }, |
| 115 | {UpdateReleaseDateRole, "updateReleaseDate" }, |
| 116 | {PayloadRole, "payload" }, |
| 117 | {Qt::DecorationRole, "decoration" }, |
| 118 | {PreviewsSmallRole, "previewsSmall" }, |
| 119 | {PreviewsRole, "previews" }, |
| 120 | {InstalledFilesRole, "installedFiles" }, |
| 121 | {UnInstalledFilesRole, "uninstalledFiles" }, |
| 122 | {RatingRole, "rating" }, |
| 123 | {NumberOfCommentsRole, "numberOfComments" }, |
| 124 | {DownloadCountRole, "downloadCount" }, |
| 125 | {NumberFansRole, "numberFans" }, |
| 126 | {NumberKnowledgebaseEntriesRole, "numberKnowledgebaseEntries" }, |
| 127 | {KnowledgebaseLinkRole, "knowledgebaseLink" }, |
| 128 | {DownloadLinksRole, "downloadLinks" }, |
| 129 | {DonationLinkRole, "donationLink" }, |
| 130 | {ProviderIdRole, "providerId" }, |
| 131 | {SourceRole, "source" }, |
| 132 | {EntryRole, "entry" }, |
| 133 | }; |
| 134 | return roles; |
| 135 | } |
| 136 | |
| 137 | int ItemsModel::rowCount(const QModelIndex &parent) const |
| 138 | { |
| 139 | if (parent.isValid()) { |
| 140 | return 0; |
| 141 | } |
| 142 | if (d->initModel()) { |
| 143 | return d->model->rowCount(parent: QModelIndex()); |
| 144 | } |
| 145 | return 0; |
| 146 | } |
| 147 | |
| 148 | QVariant ItemsModel::data(const QModelIndex &index, int role) const |
| 149 | { |
| 150 | if (index.isValid() && d->initModel()) { |
| 151 | KNSCore::Entry entry = d->model->data(index: d->model->index(row: index.row()), role: Qt::UserRole).value<KNSCore::Entry>(); |
| 152 | switch (role) { |
| 153 | case NameRole: |
| 154 | case Qt::DisplayRole: |
| 155 | return entry.name(); |
| 156 | case EntryRole: |
| 157 | return QVariant::fromValue(value: entry); |
| 158 | case UniqueIdRole: |
| 159 | return entry.uniqueId(); |
| 160 | case CategoryRole: |
| 161 | return entry.category(); |
| 162 | case HomepageRole: |
| 163 | return entry.homepage(); |
| 164 | break; |
| 165 | case AuthorRole: { |
| 166 | KNSCore::Author author = entry.author(); |
| 167 | QVariantMap returnAuthor; |
| 168 | returnAuthor[QStringLiteral("id" )] = author.id(); |
| 169 | returnAuthor[QStringLiteral("name" )] = author.name(); |
| 170 | returnAuthor[QStringLiteral("email" )] = author.email(); |
| 171 | returnAuthor[QStringLiteral("homepage" )] = author.homepage(); |
| 172 | returnAuthor[QStringLiteral("jabber" )] = author.jabber(); |
| 173 | returnAuthor[QStringLiteral("avatarUrl" )] = author.avatarUrl(); |
| 174 | returnAuthor[QStringLiteral("description" )] = author.description(); |
| 175 | return returnAuthor; |
| 176 | } break; |
| 177 | case LicenseRole: |
| 178 | return entry.license(); |
| 179 | case ShortSummaryRole: |
| 180 | return entry.shortSummary(); |
| 181 | case SummaryRole: |
| 182 | return entry.summary(); |
| 183 | case ChangelogRole: |
| 184 | return entry.changelog(); |
| 185 | case VersionRole: |
| 186 | return entry.version(); |
| 187 | case ReleaseDateRole: |
| 188 | return entry.releaseDate(); |
| 189 | case UpdateVersionRole: |
| 190 | return entry.updateVersion(); |
| 191 | case UpdateReleaseDateRole: |
| 192 | return entry.updateReleaseDate(); |
| 193 | case PayloadRole: |
| 194 | return entry.payload(); |
| 195 | case Qt::DecorationRole: |
| 196 | return entry.previewUrl(type: KNSCore::Entry::PreviewSmall1); |
| 197 | case PreviewsSmallRole: { |
| 198 | QStringList previews; |
| 199 | previews << entry.previewUrl(type: KNSCore::Entry::PreviewSmall1); |
| 200 | previews << entry.previewUrl(type: KNSCore::Entry::PreviewSmall2); |
| 201 | previews << entry.previewUrl(type: KNSCore::Entry::PreviewSmall3); |
| 202 | while (!previews.isEmpty() && previews.last().isEmpty()) { |
| 203 | previews.takeLast(); |
| 204 | } |
| 205 | return previews; |
| 206 | } |
| 207 | case PreviewsRole: { |
| 208 | QStringList previews; |
| 209 | previews << entry.previewUrl(type: KNSCore::Entry::PreviewBig1); |
| 210 | previews << entry.previewUrl(type: KNSCore::Entry::PreviewBig2); |
| 211 | previews << entry.previewUrl(type: KNSCore::Entry::PreviewBig3); |
| 212 | while (!previews.isEmpty() && previews.last().isEmpty()) { |
| 213 | previews.takeLast(); |
| 214 | } |
| 215 | return previews; |
| 216 | } |
| 217 | case InstalledFilesRole: |
| 218 | return entry.installedFiles(); |
| 219 | case UnInstalledFilesRole: |
| 220 | return entry.uninstalledFiles(); |
| 221 | case RatingRole: |
| 222 | return entry.rating(); |
| 223 | case NumberOfCommentsRole: |
| 224 | return entry.numberOfComments(); |
| 225 | case DownloadCountRole: |
| 226 | return entry.downloadCount(); |
| 227 | case NumberFansRole: |
| 228 | return entry.numberFans(); |
| 229 | case NumberKnowledgebaseEntriesRole: |
| 230 | return entry.numberKnowledgebaseEntries(); |
| 231 | case KnowledgebaseLinkRole: |
| 232 | return entry.knowledgebaseLink(); |
| 233 | case DownloadLinksRole: { |
| 234 | // This would be good to cache... but it also needs marking as dirty, somehow... |
| 235 | const QList<KNSCore::Entry::DownloadLinkInformation> dllinks = entry.downloadLinkInformationList(); |
| 236 | QVariantList list; |
| 237 | for (const KNSCore::Entry::DownloadLinkInformation &link : dllinks) { |
| 238 | list.append(t: QVariant::fromValue(value: DownloadLinkInfo(link))); |
| 239 | } |
| 240 | if (list.isEmpty() && !entry.payload().isEmpty()) { |
| 241 | KNSCore::Entry::DownloadLinkInformation data; |
| 242 | data.descriptionLink = entry.payload(); |
| 243 | list.append(t: QVariant::fromValue(value: DownloadLinkInfo(data))); |
| 244 | } |
| 245 | return QVariant::fromValue(value: list); |
| 246 | } |
| 247 | case DonationLinkRole: |
| 248 | return entry.donationLink(); |
| 249 | case ProviderIdRole: |
| 250 | return entry.providerId(); |
| 251 | case SourceRole: { |
| 252 | KNSCore::Entry::Source src = entry.source(); |
| 253 | switch (src) { |
| 254 | case KNSCore::Entry::Cache: |
| 255 | return QStringLiteral("Cache" ); |
| 256 | case KNSCore::Entry::Online: |
| 257 | return QStringLiteral("Online" ); |
| 258 | case KNSCore::Entry::Registry: |
| 259 | return QStringLiteral("Registry" ); |
| 260 | default: |
| 261 | return QStringLiteral("Unknown source - shouldn't be possible" ); |
| 262 | } |
| 263 | } |
| 264 | case CommentsModelRole: { |
| 265 | KNSCore::CommentsModel *{nullptr}; |
| 266 | if (!d->commentsModels.contains(key: entry.uniqueId())) { |
| 267 | commentsModel = new KNSCore::CommentsModel(d->engine); |
| 268 | commentsModel->setEntry(entry); |
| 269 | d->commentsModels[entry.uniqueId()] = commentsModel; |
| 270 | } else { |
| 271 | commentsModel = d->commentsModels[entry.uniqueId()]; |
| 272 | } |
| 273 | return QVariant::fromValue(value: commentsModel); |
| 274 | } |
| 275 | default: |
| 276 | return QStringLiteral("Unknown role" ); |
| 277 | } |
| 278 | } |
| 279 | return QVariant(); |
| 280 | } |
| 281 | |
| 282 | bool ItemsModel::canFetchMore(const QModelIndex &parent) const |
| 283 | { |
| 284 | return !parent.isValid() && d->engine && d->engine->categoriesMetadata().count() > 0; |
| 285 | } |
| 286 | |
| 287 | void ItemsModel::fetchMore(const QModelIndex &parent) |
| 288 | { |
| 289 | if (parent.isValid() || !d->engine) { |
| 290 | return; |
| 291 | } |
| 292 | d->engine->requestMoreData(); |
| 293 | } |
| 294 | |
| 295 | Engine *ItemsModel::engine() const |
| 296 | { |
| 297 | return d->engine; |
| 298 | } |
| 299 | |
| 300 | void ItemsModel::setEngine(Engine *newEngine) |
| 301 | { |
| 302 | if (d->engine != newEngine) { |
| 303 | beginResetModel(); |
| 304 | d->engine = newEngine; |
| 305 | if (d->model) { |
| 306 | d->model->deleteLater(); |
| 307 | d->model = nullptr; |
| 308 | } |
| 309 | Q_EMIT engineChanged(); |
| 310 | endResetModel(); |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | int ItemsModel::indexOfEntryId(const QString &providerId, const QString &entryId) |
| 315 | { |
| 316 | int idx{-1}; |
| 317 | if (d->engine && d->model) { |
| 318 | for (int i = 0; i < rowCount(); ++i) { |
| 319 | KNSCore::Entry testEntry = d->model->data(index: d->model->index(row: i), role: Qt::UserRole).value<KNSCore::Entry>(); |
| 320 | if (providerId == testEntry.providerId() && entryId == testEntry.uniqueId()) { |
| 321 | idx = i; |
| 322 | break; |
| 323 | } |
| 324 | } |
| 325 | } |
| 326 | return idx; |
| 327 | } |
| 328 | |
| 329 | #include "moc_quickitemsmodel.cpp" |
| 330 | |