unifiedsearchresultslistmodel.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. /*
  2. * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 2 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  11. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. * for more details.
  13. */
  14. #include "unifiedsearchresultslistmodel.h"
  15. #include "account.h"
  16. #include "accountstate.h"
  17. #include "guiutility.h"
  18. #include "folderman.h"
  19. #include "networkjobs.h"
  20. #include <algorithm>
  21. #include <QAbstractListModel>
  22. #include <QDesktopServices>
  23. namespace {
  24. QString imagePlaceholderUrlForProviderId(const QString &providerId, const bool darkMode)
  25. {
  26. const auto colorIconPath = darkMode ? QStringLiteral(":/client/theme/white/") : QStringLiteral(":/client/theme/black/");
  27. if (providerId.contains(QStringLiteral("message"), Qt::CaseInsensitive)
  28. || providerId.contains(QStringLiteral("talk"), Qt::CaseInsensitive)) {
  29. return colorIconPath % QStringLiteral("wizard-talk.svg");
  30. } else if (providerId.contains(QStringLiteral("file"), Qt::CaseInsensitive)) {
  31. return colorIconPath % QStringLiteral("edit.svg");
  32. } else if (providerId.contains(QStringLiteral("deck"), Qt::CaseInsensitive)) {
  33. return colorIconPath % QStringLiteral("deck.svg");
  34. } else if (providerId.contains(QStringLiteral("calendar"), Qt::CaseInsensitive)) {
  35. return colorIconPath % QStringLiteral("calendar.svg");
  36. } else if (providerId.contains(QStringLiteral("mail"), Qt::CaseInsensitive)) {
  37. return colorIconPath % QStringLiteral("email.svg");
  38. } else if (providerId.contains(QStringLiteral("comment"), Qt::CaseInsensitive)) {
  39. return colorIconPath % QStringLiteral("comment.svg");
  40. }
  41. return colorIconPath % QStringLiteral("change.svg");
  42. }
  43. QString localIconPathFromIconPrefix(const QString &iconNameWithPrefix, const bool darkMode)
  44. {
  45. const auto colorIconPath = darkMode ? QStringLiteral(":/client/theme/white/") : QStringLiteral(":/client/theme/black/");
  46. if (iconNameWithPrefix.contains(QStringLiteral("message"), Qt::CaseInsensitive)
  47. || iconNameWithPrefix.contains(QStringLiteral("talk"), Qt::CaseInsensitive)) {
  48. return colorIconPath % QStringLiteral("wizard-talk.svg");
  49. } else if (iconNameWithPrefix.contains(QStringLiteral("folder"), Qt::CaseInsensitive)) {
  50. return colorIconPath % QStringLiteral("folder.svg");
  51. } else if (iconNameWithPrefix.contains(QStringLiteral("deck"), Qt::CaseInsensitive)) {
  52. return colorIconPath % QStringLiteral("deck.svg");
  53. } else if (iconNameWithPrefix.contains(QStringLiteral("contacts"), Qt::CaseInsensitive)) {
  54. return colorIconPath % QStringLiteral("wizard-groupware.svg");
  55. } else if (iconNameWithPrefix.contains(QStringLiteral("calendar"), Qt::CaseInsensitive)) {
  56. return colorIconPath % QStringLiteral("calendar.svg");
  57. } else if (iconNameWithPrefix.contains(QStringLiteral("mail"), Qt::CaseInsensitive)) {
  58. return colorIconPath % QStringLiteral("email.svg");
  59. }
  60. return colorIconPath % QStringLiteral("change.svg");
  61. }
  62. QString iconUrlForDefaultIconName(const QString &defaultIconName, const bool darkMode)
  63. {
  64. const QUrl urlForIcon{defaultIconName};
  65. if (urlForIcon.isValid() && !urlForIcon.scheme().isEmpty()) {
  66. return defaultIconName;
  67. }
  68. const auto colorIconPath = darkMode ? QStringLiteral(":/client/theme/white/") : QStringLiteral(":/client/theme/black/");
  69. if (defaultIconName.startsWith(QStringLiteral("icon-"))) {
  70. const auto parts = defaultIconName.split(QLatin1Char('-'));
  71. if (parts.size() > 1) {
  72. const QString blackOrWhiteIconFilePath = colorIconPath + parts[1] + QStringLiteral(".svg");
  73. if (QFile::exists(blackOrWhiteIconFilePath)) {
  74. return blackOrWhiteIconFilePath;
  75. }
  76. const QString iconFilePath = QStringLiteral(":/client/theme/") + parts[1] + QStringLiteral(".svg");
  77. if (QFile::exists(iconFilePath)) {
  78. return iconFilePath;
  79. }
  80. }
  81. const auto iconNameFromIconPrefix = localIconPathFromIconPrefix(defaultIconName, darkMode);
  82. if (!iconNameFromIconPrefix.isEmpty()) {
  83. return iconNameFromIconPrefix;
  84. }
  85. }
  86. return colorIconPath % QStringLiteral("change.svg");
  87. }
  88. QString generateUrlForThumbnail(const QString &thumbnailUrl, const QUrl &serverUrl)
  89. {
  90. auto serverUrlCopy = serverUrl;
  91. auto thumbnailUrlCopy = thumbnailUrl;
  92. if (thumbnailUrlCopy.startsWith(QLatin1Char('/')) || thumbnailUrlCopy.startsWith(QLatin1Char('\\'))) {
  93. // relative image resource URL, just needs some concatenation with current server URL
  94. // some icons may contain parameters after (?)
  95. const QStringList thumbnailUrlCopySplitted = thumbnailUrlCopy.contains(QLatin1Char('?'))
  96. ? thumbnailUrlCopy.split(QLatin1Char('?'), Qt::SkipEmptyParts)
  97. : QStringList{thumbnailUrlCopy};
  98. Q_ASSERT(!thumbnailUrlCopySplitted.isEmpty());
  99. serverUrlCopy.setPath(thumbnailUrlCopySplitted[0]);
  100. thumbnailUrlCopy = serverUrlCopy.toString();
  101. if (thumbnailUrlCopySplitted.size() > 1) {
  102. thumbnailUrlCopy += QLatin1Char('?') + thumbnailUrlCopySplitted[1];
  103. }
  104. }
  105. return thumbnailUrlCopy;
  106. }
  107. QString generateUrlForIcon(const QString &fallbackIcon, const QUrl &serverUrl, const bool darkMode)
  108. {
  109. auto serverUrlCopy = serverUrl;
  110. auto fallbackIconCopy = fallbackIcon;
  111. if (fallbackIconCopy.startsWith(QLatin1Char('/')) || fallbackIconCopy.startsWith(QLatin1Char('\\'))) {
  112. // relative image resource URL, just needs some concatenation with current server URL
  113. // some icons may contain parameters after (?)
  114. const QStringList fallbackIconPathSplitted =
  115. fallbackIconCopy.contains(QLatin1Char('?')) ? fallbackIconCopy.split(QLatin1Char('?')) : QStringList{fallbackIconCopy};
  116. Q_ASSERT(!fallbackIconPathSplitted.isEmpty());
  117. serverUrlCopy.setPath(fallbackIconPathSplitted[0]);
  118. fallbackIconCopy = serverUrlCopy.toString();
  119. if (fallbackIconPathSplitted.size() > 1) {
  120. fallbackIconCopy += QLatin1Char('?') + fallbackIconPathSplitted[1];
  121. }
  122. } else if (!fallbackIconCopy.isEmpty()) {
  123. // could be one of names for standard icons (e.g. icon-mail)
  124. const auto defaultIconUrl = iconUrlForDefaultIconName(fallbackIconCopy, darkMode);
  125. if (!defaultIconUrl.isEmpty()) {
  126. fallbackIconCopy = defaultIconUrl;
  127. }
  128. }
  129. return fallbackIconCopy;
  130. }
  131. // Return image URL and whether it is a thumbnail or not
  132. std::pair<QString, bool> iconsFromThumbnailAndFallbackIcon(const QString &thumbnailUrl, const QString &fallbackIcon, const QUrl &serverUrl, const bool darkMode)
  133. {
  134. if (thumbnailUrl.isEmpty() && fallbackIcon.isEmpty()) {
  135. return {};
  136. }
  137. if (serverUrl.isEmpty()) {
  138. const QStringList listImages = {thumbnailUrl, fallbackIcon};
  139. return {listImages.join(QLatin1Char(';')), false};
  140. }
  141. const auto urlForThumbnail = generateUrlForThumbnail(thumbnailUrl, serverUrl);
  142. const auto urlForFallbackIcon = generateUrlForIcon(fallbackIcon, serverUrl, darkMode);
  143. qDebug() << "SEARCH" << urlForThumbnail << urlForFallbackIcon;
  144. if (urlForThumbnail.isEmpty() && !urlForFallbackIcon.isEmpty()) {
  145. return {urlForFallbackIcon, false};
  146. }
  147. if (!urlForThumbnail.isEmpty() && urlForFallbackIcon.isEmpty()) {
  148. return {urlForThumbnail, true};
  149. }
  150. const QStringList listImages{urlForThumbnail, urlForFallbackIcon};
  151. return {listImages.join(QLatin1Char(';')), true};
  152. }
  153. constexpr int searchTermEditingFinishedSearchStartDelay = 800;
  154. // server-side bug of returning the cursor > 0 and isPaginated == 'true', using '5' as it is done on Android client's end now
  155. constexpr int minimumEntresNumberToShowLoadMore = 5;
  156. }
  157. namespace OCC {
  158. Q_LOGGING_CATEGORY(lcUnifiedSearch, "nextcloud.gui.unifiedsearch", QtInfoMsg)
  159. UnifiedSearchResultsListModel::UnifiedSearchResultsListModel(AccountState *accountState, QObject *parent)
  160. : QAbstractListModel(parent)
  161. , _accountState(accountState)
  162. {
  163. }
  164. QVariant UnifiedSearchResultsListModel::data(const QModelIndex &index, int role) const
  165. {
  166. Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
  167. switch (role) {
  168. case ProviderNameRole:
  169. return _results.at(index.row())._providerName;
  170. case ProviderIdRole:
  171. return _results.at(index.row())._providerId;
  172. case DarkImagePlaceholderRole:
  173. return imagePlaceholderUrlForProviderId(_results.at(index.row())._providerId, true);
  174. case LightImagePlaceholderRole:
  175. return imagePlaceholderUrlForProviderId(_results.at(index.row())._providerId, false);
  176. case DarkIconsRole:
  177. return _results.at(index.row())._darkIcons;
  178. case LightIconsRole:
  179. return _results.at(index.row())._lightIcons;
  180. case DarkIconsIsThumbnailRole:
  181. return _results.at(index.row())._darkIconsIsThumbnail;
  182. case LightIconsIsThumbnailRole:
  183. return _results.at(index.row())._lightIconsIsThumbnail;
  184. case TitleRole:
  185. return _results.at(index.row())._title;
  186. case SublineRole:
  187. return _results.at(index.row())._subline;
  188. case ResourceUrlRole:
  189. return _results.at(index.row())._resourceUrl;
  190. case RoundedRole:
  191. return _results.at(index.row())._isRounded;
  192. case TypeRole:
  193. return _results.at(index.row())._type;
  194. case TypeAsStringRole:
  195. return UnifiedSearchResult::typeAsString(_results.at(index.row())._type);
  196. }
  197. return {};
  198. }
  199. int UnifiedSearchResultsListModel::rowCount(const QModelIndex &parent) const
  200. {
  201. if (parent.isValid()) {
  202. return 0;
  203. }
  204. return _results.size();
  205. }
  206. QHash<int, QByteArray> UnifiedSearchResultsListModel::roleNames() const
  207. {
  208. auto roles = QAbstractListModel::roleNames();
  209. roles[ProviderNameRole] = "providerName";
  210. roles[ProviderIdRole] = "providerId";
  211. roles[DarkIconsRole] = "darkIcons";
  212. roles[LightIconsRole] = "lightIcons";
  213. roles[DarkIconsIsThumbnailRole] = "darkIconsIsThumbnail";
  214. roles[LightIconsIsThumbnailRole] = "lightIconsIsThumbnail";
  215. roles[DarkImagePlaceholderRole] = "darkImagePlaceholder";
  216. roles[LightImagePlaceholderRole] = "lightImagePlaceholder";
  217. roles[TitleRole] = "resultTitle";
  218. roles[SublineRole] = "subline";
  219. roles[ResourceUrlRole] = "resourceUrlRole";
  220. roles[TypeRole] = "type";
  221. roles[TypeAsStringRole] = "typeAsString";
  222. roles[RoundedRole] = "isRounded";
  223. return roles;
  224. }
  225. QString UnifiedSearchResultsListModel::searchTerm() const
  226. {
  227. return _searchTerm;
  228. }
  229. QString UnifiedSearchResultsListModel::errorString() const
  230. {
  231. return _errorString;
  232. }
  233. QString UnifiedSearchResultsListModel::currentFetchMoreInProgressProviderId() const
  234. {
  235. return _currentFetchMoreInProgressProviderId;
  236. }
  237. bool UnifiedSearchResultsListModel::waitingForSearchTermEditEnd() const
  238. {
  239. return _waitingForSearchTermEditEnd;
  240. }
  241. void UnifiedSearchResultsListModel::setSearchTerm(const QString &term)
  242. {
  243. if (term == _searchTerm) {
  244. return;
  245. }
  246. _searchTerm = term;
  247. emit searchTermChanged();
  248. if (!_errorString.isEmpty()) {
  249. _errorString.clear();
  250. emit errorStringChanged();
  251. }
  252. disconnectAndClearSearchJobs();
  253. clearCurrentFetchMoreInProgressProviderId();
  254. disconnect(&_unifiedSearchTextEditingFinishedTimer, &QTimer::timeout, this,
  255. &UnifiedSearchResultsListModel::slotSearchTermEditingFinished);
  256. if (_unifiedSearchTextEditingFinishedTimer.isActive()) {
  257. _unifiedSearchTextEditingFinishedTimer.stop();
  258. _waitingForSearchTermEditEnd = false;
  259. emit waitingForSearchTermEditEndChanged();
  260. }
  261. if (!_searchTerm.isEmpty()) {
  262. _unifiedSearchTextEditingFinishedTimer.setInterval(searchTermEditingFinishedSearchStartDelay);
  263. connect(&_unifiedSearchTextEditingFinishedTimer, &QTimer::timeout, this,
  264. &UnifiedSearchResultsListModel::slotSearchTermEditingFinished);
  265. _unifiedSearchTextEditingFinishedTimer.start();
  266. _waitingForSearchTermEditEnd = true;
  267. emit waitingForSearchTermEditEndChanged();
  268. }
  269. if (!_results.isEmpty()) {
  270. beginResetModel();
  271. _results.clear();
  272. endResetModel();
  273. }
  274. }
  275. bool UnifiedSearchResultsListModel::isSearchInProgress() const
  276. {
  277. return !_searchJobConnections.isEmpty();
  278. }
  279. void UnifiedSearchResultsListModel::resultClicked(const QString &providerId, const QUrl &resourceUrl) const
  280. {
  281. const QUrlQuery urlQuery{resourceUrl};
  282. const auto dir = urlQuery.queryItemValue(QStringLiteral("dir"), QUrl::ComponentFormattingOption::FullyDecoded);
  283. const auto fileName =
  284. urlQuery.queryItemValue(QStringLiteral("scrollto"), QUrl::ComponentFormattingOption::FullyDecoded);
  285. if (providerId.contains(QStringLiteral("file"), Qt::CaseInsensitive) && !dir.isEmpty() && !fileName.isEmpty()) {
  286. if (!_accountState || !_accountState->account()) {
  287. return;
  288. }
  289. const QString relativePath = dir + QLatin1Char('/') + fileName;
  290. const auto localFiles =
  291. FolderMan::instance()->findFileInLocalFolders(QFileInfo(relativePath).path(), _accountState->account());
  292. if (!localFiles.isEmpty()) {
  293. qCInfo(lcUnifiedSearch) << "Opening file:" << localFiles.constFirst();
  294. QDesktopServices::openUrl(QUrl::fromLocalFile(localFiles.constFirst()));
  295. return;
  296. }
  297. }
  298. Utility::openBrowser(resourceUrl);
  299. }
  300. void UnifiedSearchResultsListModel::fetchMoreTriggerClicked(const QString &providerId)
  301. {
  302. if (isSearchInProgress() || !_currentFetchMoreInProgressProviderId.isEmpty()) {
  303. return;
  304. }
  305. const auto providerInfo = _providers.value(providerId, {});
  306. if (!providerInfo._id.isEmpty() && providerInfo._id == providerId && providerInfo._isPaginated) {
  307. // Load more items
  308. _currentFetchMoreInProgressProviderId = providerId;
  309. emit currentFetchMoreInProgressProviderIdChanged();
  310. startSearchForProvider(providerId, providerInfo._cursor);
  311. }
  312. }
  313. void UnifiedSearchResultsListModel::slotSearchTermEditingFinished()
  314. {
  315. _waitingForSearchTermEditEnd = false;
  316. emit waitingForSearchTermEditEndChanged();
  317. disconnect(&_unifiedSearchTextEditingFinishedTimer, &QTimer::timeout, this,
  318. &UnifiedSearchResultsListModel::slotSearchTermEditingFinished);
  319. if (!_accountState || !_accountState->account()) {
  320. qCCritical(lcUnifiedSearch) << QString("Account state is invalid. Could not start search!");
  321. return;
  322. }
  323. if (_providers.isEmpty()) {
  324. auto job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/search/providers"));
  325. QObject::connect(job, &JsonApiJob::jsonReceived, this, &UnifiedSearchResultsListModel::slotFetchProvidersFinished);
  326. job->start();
  327. } else {
  328. startSearch();
  329. }
  330. }
  331. void UnifiedSearchResultsListModel::slotFetchProvidersFinished(const QJsonDocument &json, int statusCode)
  332. {
  333. const auto job = qobject_cast<JsonApiJob *>(sender());
  334. if (!job) {
  335. qCCritical(lcUnifiedSearch) << QString("Failed to fetch providers.").arg(_searchTerm);
  336. _errorString += tr("Failed to fetch providers.") + QLatin1Char('\n');
  337. emit errorStringChanged();
  338. return;
  339. }
  340. if (statusCode != 200) {
  341. qCCritical(lcUnifiedSearch) << QString("%1: Failed to fetch search providers for '%2'. Error: %3")
  342. .arg(statusCode)
  343. .arg(_searchTerm)
  344. .arg(job->errorString());
  345. _errorString +=
  346. tr("Failed to fetch search providers for '%1'. Error: %2").arg(_searchTerm).arg(job->errorString())
  347. + QLatin1Char('\n');
  348. emit errorStringChanged();
  349. return;
  350. }
  351. const auto providerList =
  352. json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toVariant().toList();
  353. for (const auto &provider : providerList) {
  354. const auto providerMap = provider.toMap();
  355. const auto id = providerMap[QStringLiteral("id")].toString();
  356. const auto name = providerMap[QStringLiteral("name")].toString();
  357. if (!name.isEmpty() && id != QStringLiteral("talk-message-current")) {
  358. UnifiedSearchProvider newProvider;
  359. newProvider._name = name;
  360. newProvider._id = id;
  361. newProvider._order = providerMap[QStringLiteral("order")].toInt();
  362. _providers.insert(newProvider._id, newProvider);
  363. }
  364. }
  365. if (!_providers.empty()) {
  366. startSearch();
  367. }
  368. }
  369. void UnifiedSearchResultsListModel::slotSearchForProviderFinished(const QJsonDocument &json, int statusCode)
  370. {
  371. Q_ASSERT(_accountState && _accountState->account());
  372. const auto job = qobject_cast<JsonApiJob *>(sender());
  373. if (!job) {
  374. qCCritical(lcUnifiedSearch) << QString("Search has failed for '%2'.").arg(_searchTerm);
  375. _errorString += tr("Search has failed for '%2'.").arg(_searchTerm) + QLatin1Char('\n');
  376. emit errorStringChanged();
  377. return;
  378. }
  379. const auto providerId = job->property("providerId").toString();
  380. if (providerId.isEmpty()) {
  381. return;
  382. }
  383. if (!_searchJobConnections.isEmpty()) {
  384. _searchJobConnections.remove(providerId);
  385. if (_searchJobConnections.isEmpty()) {
  386. emit isSearchInProgressChanged();
  387. }
  388. }
  389. if (providerId == _currentFetchMoreInProgressProviderId) {
  390. clearCurrentFetchMoreInProgressProviderId();
  391. }
  392. if (statusCode != 200) {
  393. qCCritical(lcUnifiedSearch) << QString("%1: Search has failed for '%2'. Error: %3")
  394. .arg(statusCode)
  395. .arg(_searchTerm)
  396. .arg(job->errorString());
  397. _errorString +=
  398. tr("Search has failed for '%1'. Error: %2").arg(_searchTerm).arg(job->errorString()) + QLatin1Char('\n');
  399. emit errorStringChanged();
  400. return;
  401. }
  402. const auto data = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toObject();
  403. if (!data.isEmpty()) {
  404. parseResultsForProvider(data, providerId, job->property("appendResults").toBool());
  405. }
  406. }
  407. void UnifiedSearchResultsListModel::startSearch()
  408. {
  409. Q_ASSERT(_accountState && _accountState->account());
  410. disconnectAndClearSearchJobs();
  411. if (!_accountState || !_accountState->account()) {
  412. return;
  413. }
  414. if (!_results.isEmpty()) {
  415. beginResetModel();
  416. _results.clear();
  417. endResetModel();
  418. }
  419. for (const auto &provider : qAsConst(_providers)) {
  420. startSearchForProvider(provider._id);
  421. }
  422. }
  423. void UnifiedSearchResultsListModel::startSearchForProvider(const QString &providerId, qint32 cursor)
  424. {
  425. Q_ASSERT(_accountState && _accountState->account());
  426. if (!_accountState || !_accountState->account()) {
  427. return;
  428. }
  429. auto job = new JsonApiJob(_accountState->account(),
  430. QLatin1String("ocs/v2.php/search/providers/%1/search").arg(providerId));
  431. QUrlQuery params;
  432. params.addQueryItem(QStringLiteral("term"), _searchTerm);
  433. if (cursor > 0) {
  434. params.addQueryItem(QStringLiteral("cursor"), QString::number(cursor));
  435. job->setProperty("appendResults", true);
  436. }
  437. job->setProperty("providerId", providerId);
  438. job->addQueryParams(params);
  439. const auto wasSearchInProgress = isSearchInProgress();
  440. _searchJobConnections.insert(providerId,
  441. QObject::connect(
  442. job, &JsonApiJob::jsonReceived, this, &UnifiedSearchResultsListModel::slotSearchForProviderFinished));
  443. if (isSearchInProgress() && !wasSearchInProgress) {
  444. emit isSearchInProgressChanged();
  445. }
  446. job->start();
  447. }
  448. void UnifiedSearchResultsListModel::parseResultsForProvider(const QJsonObject &data, const QString &providerId, bool fetchedMore)
  449. {
  450. const auto cursor = data.value(QStringLiteral("cursor")).toInt();
  451. const auto entries = data.value(QStringLiteral("entries")).toVariant().toList();
  452. auto &provider = _providers[providerId];
  453. if (provider._id.isEmpty() && fetchedMore) {
  454. _providers.remove(providerId);
  455. return;
  456. }
  457. if (entries.isEmpty()) {
  458. // we may have received false pagination information from the server, such as, we expect more
  459. // results available via pagination, but, there are no more left, so, we need to stop paginating for
  460. // this provider
  461. provider._isPaginated = false;
  462. if (fetchedMore) {
  463. removeFetchMoreTrigger(provider._id);
  464. }
  465. return;
  466. }
  467. provider._isPaginated = data.value(QStringLiteral("isPaginated")).toBool();
  468. provider._cursor = cursor;
  469. if (provider._pageSize == -1) {
  470. provider._pageSize = cursor;
  471. }
  472. if ((provider._pageSize != -1 && entries.size() < provider._pageSize)
  473. || entries.size() < minimumEntresNumberToShowLoadMore) {
  474. // for some providers we are still getting a non-null cursor and isPaginated true even thought
  475. // there are no more results to paginate
  476. provider._isPaginated = false;
  477. }
  478. QVector<UnifiedSearchResult> newEntries;
  479. for (const auto &entry : entries) {
  480. const auto entryMap = entry.toMap();
  481. if (entryMap.isEmpty()) {
  482. continue;
  483. }
  484. UnifiedSearchResult result;
  485. result._providerId = provider._id;
  486. result._order = provider._order;
  487. result._providerName = provider._name;
  488. result._isRounded = entryMap.value(QStringLiteral("rounded")).toBool();
  489. result._title = entryMap.value(QStringLiteral("title")).toString();
  490. result._subline = entryMap.value(QStringLiteral("subline")).toString();
  491. const auto resourceUrl = entryMap.value(QStringLiteral("resourceUrl")).toUrl();
  492. const auto accountUrl = (_accountState && _accountState->account()) ? _accountState->account()->url() : QUrl();
  493. result._resourceUrl = openableResourceUrl(resourceUrl, accountUrl);
  494. const auto darkIconsData = iconsFromThumbnailAndFallbackIcon(entryMap.value(QStringLiteral("thumbnailUrl")).toString(),
  495. entryMap.value(QStringLiteral("icon")).toString(), accountUrl, true);
  496. const auto lightIconsData = iconsFromThumbnailAndFallbackIcon(entryMap.value(QStringLiteral("thumbnailUrl")).toString(),
  497. entryMap.value(QStringLiteral("icon")).toString(), accountUrl, false);
  498. result._darkIcons = darkIconsData.first;
  499. result._lightIcons = lightIconsData.first;
  500. result._darkIconsIsThumbnail = darkIconsData.second;
  501. result._lightIconsIsThumbnail = lightIconsData.second;
  502. newEntries.push_back(result);
  503. }
  504. if (fetchedMore) {
  505. appendResultsToProvider(newEntries, provider);
  506. } else {
  507. appendResults(newEntries, provider);
  508. }
  509. }
  510. QUrl UnifiedSearchResultsListModel::openableResourceUrl(const QUrl &resourceUrl, const QUrl &accountUrl)
  511. {
  512. if (!resourceUrl.isRelative()) {
  513. return resourceUrl;
  514. }
  515. QUrl finalResourceUrl(accountUrl);
  516. finalResourceUrl.setPath(resourceUrl.toString());
  517. return finalResourceUrl;
  518. }
  519. void UnifiedSearchResultsListModel::appendResults(QVector<UnifiedSearchResult> results, const UnifiedSearchProvider &provider)
  520. {
  521. if (provider._cursor > 0 && provider._isPaginated) {
  522. UnifiedSearchResult fetchMoreTrigger;
  523. fetchMoreTrigger._providerId = provider._id;
  524. fetchMoreTrigger._providerName = provider._name;
  525. fetchMoreTrigger._order = provider._order;
  526. fetchMoreTrigger._type = UnifiedSearchResult::Type::FetchMoreTrigger;
  527. results.push_back(fetchMoreTrigger);
  528. }
  529. if (_results.isEmpty()) {
  530. beginInsertRows({}, 0, results.size() - 1);
  531. _results = results;
  532. endInsertRows();
  533. return;
  534. }
  535. // insertion is done with sorting (first -> by order, then -> by name)
  536. const auto itToInsertTo = std::find_if(std::begin(_results), std::end(_results),
  537. [&provider](const UnifiedSearchResult &current) {
  538. // insert before other results of higher order when possible
  539. if (current._order > provider._order) {
  540. return true;
  541. } else {
  542. if (current._order == provider._order) {
  543. // insert before results of higher QString value when possible
  544. return current._providerName > provider._name;
  545. }
  546. return false;
  547. }
  548. });
  549. const auto first = static_cast<int>(std::distance(std::begin(_results), itToInsertTo));
  550. const auto last = first + results.size() - 1;
  551. beginInsertRows({}, first, last);
  552. std::copy(std::begin(results), std::end(results), std::inserter(_results, itToInsertTo));
  553. endInsertRows();
  554. }
  555. void UnifiedSearchResultsListModel::appendResultsToProvider(const QVector<UnifiedSearchResult> &results, const UnifiedSearchProvider &provider)
  556. {
  557. if (results.isEmpty()) {
  558. return;
  559. }
  560. const auto providerId = provider._id;
  561. /* we need to find the last result that is not a fetch-more-trigger or category-separator for the current
  562. provider */
  563. const auto itLastResultForProviderReverse =
  564. std::find_if(std::rbegin(_results), std::rend(_results), [&providerId](const UnifiedSearchResult &result) {
  565. return result._providerId == providerId && result._type == UnifiedSearchResult::Type::Default;
  566. });
  567. if (itLastResultForProviderReverse != std::rend(_results)) {
  568. // #1 Insert rows
  569. // convert reverse_iterator to iterator
  570. const auto itLastResultForProvider = (itLastResultForProviderReverse + 1).base();
  571. const auto first = static_cast<int>(std::distance(std::begin(_results), itLastResultForProvider + 1));
  572. const auto last = first + results.size() - 1;
  573. beginInsertRows({}, first, last);
  574. std::copy(std::begin(results), std::end(results), std::inserter(_results, itLastResultForProvider + 1));
  575. endInsertRows();
  576. // #2 Remove the FetchMoreTrigger item if there are no more results to load for this provider
  577. if (!provider._isPaginated) {
  578. removeFetchMoreTrigger(providerId);
  579. }
  580. }
  581. }
  582. void UnifiedSearchResultsListModel::removeFetchMoreTrigger(const QString &providerId)
  583. {
  584. const auto itFetchMoreTriggerForProviderReverse = std::find_if(
  585. std::rbegin(_results),
  586. std::rend(_results),
  587. [providerId](const UnifiedSearchResult &result) {
  588. return result._providerId == providerId && result._type == UnifiedSearchResult::Type::FetchMoreTrigger;
  589. });
  590. if (itFetchMoreTriggerForProviderReverse != std::rend(_results)) {
  591. // convert reverse_iterator to iterator
  592. const auto itFetchMoreTriggerForProvider = (itFetchMoreTriggerForProviderReverse + 1).base();
  593. if (itFetchMoreTriggerForProvider != std::end(_results)
  594. && itFetchMoreTriggerForProvider != std::begin(_results)) {
  595. const auto eraseIndex = static_cast<int>(std::distance(std::begin(_results), itFetchMoreTriggerForProvider));
  596. Q_ASSERT(eraseIndex >= 0 && eraseIndex < static_cast<int>(_results.size()));
  597. beginRemoveRows({}, eraseIndex, eraseIndex);
  598. _results.erase(itFetchMoreTriggerForProvider);
  599. endRemoveRows();
  600. }
  601. }
  602. }
  603. void UnifiedSearchResultsListModel::disconnectAndClearSearchJobs()
  604. {
  605. for (const auto &connection : qAsConst(_searchJobConnections)) {
  606. if (connection) {
  607. QObject::disconnect(connection);
  608. }
  609. }
  610. if (!_searchJobConnections.isEmpty()) {
  611. _searchJobConnections.clear();
  612. emit isSearchInProgressChanged();
  613. }
  614. }
  615. void UnifiedSearchResultsListModel::clearCurrentFetchMoreInProgressProviderId()
  616. {
  617. if (!_currentFetchMoreInProgressProviderId.isEmpty()) {
  618. _currentFetchMoreInProgressProviderId.clear();
  619. emit currentFetchMoreInProgressProviderIdChanged();
  620. }
  621. }
  622. }