DownloadQueue.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. #include <iostream>
  2. #include <string>
  3. #include <cpp3ds/System/I18n.hpp>
  4. #include <TweenEngine/Tween.h>
  5. #include <cpp3ds/System/Sleep.hpp>
  6. #include <cpp3ds/System/Err.hpp>
  7. #include <cpp3ds/System/FileSystem.hpp>
  8. #include <fstream>
  9. #include <rapidjson/ostreamwrapper.h>
  10. #include <rapidjson/writer.h>
  11. #include <cpp3ds/System/FileInputStream.hpp>
  12. #include <cpp3ds/System/Lock.hpp>
  13. #include <cmath>
  14. #include "DownloadQueue.hpp"
  15. #include "Notification.hpp"
  16. #include "AppList.hpp"
  17. #include "TitleKeys.hpp"
  18. #include "InstalledList.hpp"
  19. #include "Config.hpp"
  20. namespace FreeShop {
  21. DownloadItem::DownloadItem(std::shared_ptr<AppItem> appItem, Download *download, Installer *installer)
  22. : appItem(appItem)
  23. , download(download)
  24. , installer(installer)
  25. {
  26. }
  27. DownloadItem::~DownloadItem()
  28. {
  29. if (installer)
  30. delete installer;
  31. if (download)
  32. delete download;
  33. }
  34. DownloadQueue::DownloadQueue()
  35. : m_threadRefresh(&DownloadQueue::refresh, this)
  36. , m_refreshEnd(false)
  37. , m_size(320.f, 0.f)
  38. {
  39. m_soundBufferFinish.loadFromFile("sounds/chime.ogg");
  40. m_soundFinish.setBuffer(m_soundBufferFinish);
  41. load();
  42. m_threadRefresh.launch();
  43. }
  44. DownloadQueue::~DownloadQueue()
  45. {
  46. m_refreshEnd = true;
  47. cpp3ds::Lock lock(m_mutexRefresh);
  48. m_downloads.clear();
  49. }
  50. void DownloadQueue::addDownload(std::shared_ptr<AppItem> app, cpp3ds::Uint64 titleId, DownloadCompleteCallback callback, int contentIndex, float progress)
  51. {
  52. cpp3ds::Lock lock(m_mutexRefresh);
  53. if (titleId == 0)
  54. {
  55. if (app->isInstalled()) // Don't allow reinstalling without deleting
  56. return;
  57. titleId = app->getTitleId();
  58. }
  59. cpp3ds::String title = app->getTitle();
  60. cpp3ds::Uint32 type = titleId >> 32;
  61. if (type != TitleKeys::Game)
  62. {
  63. if (type == TitleKeys::Update)
  64. title = _("[Update] %s", title.toAnsiString().c_str());
  65. else if (type == TitleKeys::Demo)
  66. title = _("[Demo] %s", title.toAnsiString().c_str());
  67. else if (type == TitleKeys::DLC)
  68. title = _("[DLC] %s", title.toAnsiString().c_str());
  69. }
  70. std::string url = _("http://ccs.cdn.c.shop.nintendowifi.net/ccs/download/%016llX/tmd", titleId);
  71. Download* download = new Download(url);
  72. download->fillFromAppItem(app);
  73. download->m_textTitle.setString(title);
  74. download->setPosition(0.f, 240.f);
  75. download->setRetryCount(6);
  76. download->setTimeout(cpp3ds::seconds(Config::get(Config::DownloadTimeout).GetFloat()));
  77. download->setBufferSize(1024 * Config::get(Config::DownloadBufferSize).GetUint());
  78. download->setSendTopCallback([this](Download *d){
  79. sendTop(d);
  80. });
  81. std::vector<char> buf; // Store fetched files (only used for TMD atm)
  82. cpp3ds::Clock clock;
  83. float count = 0;
  84. int fileIndex = 0; // Keep track of the multiple files, first is usually the TMD with rest being contents
  85. size_t fileSize = 0; // Size of each file (as obtain by http Content-Length)
  86. Installer *installer = new Installer(titleId, contentIndex);
  87. cpp3ds::Uint64 titleFileSize = 0;
  88. cpp3ds::Uint64 totalProcessed;
  89. // Is resuming from saved queue
  90. bool isResuming = contentIndex >= 0;
  91. // TMD values
  92. cpp3ds::Uint16 contentCount;
  93. cpp3ds::Uint16 titleVersion;
  94. std::vector<cpp3ds::Uint16> contentIndices;
  95. download->setDataCallback([=](const void* data, size_t len, size_t processed, const cpp3ds::Http::Response& response) mutable
  96. {
  97. // This condition should be true when download first starts.
  98. if (len == processed) {
  99. std::string length = response.getField("Content-Length");
  100. if (!length.empty())
  101. fileSize = strtoul(length.c_str(), 0, 10);
  102. }
  103. const char *bufdata = reinterpret_cast<const char*>(data);
  104. if (fileIndex == 0)
  105. {
  106. buf.insert(buf.end(), bufdata, bufdata + len);
  107. if (processed == fileSize && fileSize != 0)
  108. {
  109. static int dataOffsets[6] = {0x240, 0x140, 0x80, 0x240, 0x140, 0x80};
  110. char sigType = buf[0x3];
  111. titleVersion = *(cpp3ds::Uint16*)&buf[dataOffsets[sigType] + 0x9C];
  112. contentCount = __builtin_bswap16(*(cpp3ds::Uint16*)&buf[dataOffsets[sigType] + 0x9E]);
  113. if (isResuming)
  114. installer->resume();
  115. bool foundIndex = false; // For resuming via contentIndex arg
  116. for (int i = 0; i < contentCount; ++i)
  117. {
  118. char *contentChunk = &buf[dataOffsets[sigType] + 0x9C4 + (i * 0x30)];
  119. cpp3ds::Uint32 contentId = __builtin_bswap32(*(cpp3ds::Uint32*)&contentChunk[0]);
  120. cpp3ds::Uint16 contentIdx = __builtin_bswap16(*(cpp3ds::Uint16*)&contentChunk[4]);
  121. cpp3ds::Uint64 contentSize = __builtin_bswap64(*(cpp3ds::Uint64*)&contentChunk[8]);
  122. titleFileSize += contentSize;
  123. contentIndices.push_back(contentIdx);
  124. if (contentIdx == contentIndex)
  125. {
  126. foundIndex = true;
  127. fileIndex = i + 1;
  128. }
  129. if (!isResuming || foundIndex)
  130. download->pushUrl(_("http://ccs.cdn.c.shop.nintendowifi.net/ccs/download/%016llX/%08lX", titleId, contentId), (contentIdx == contentIndex) ? installer->getCurrentContentPosition() : 0);
  131. }
  132. totalProcessed = progress * titleFileSize;
  133. if (!isResuming)
  134. {
  135. // Check for cancel at each stage in case it changes
  136. if (!download->isCanceled())
  137. {
  138. download->setProgressMessage(_("Installing ticket..."));
  139. if (!installer->installTicket(titleVersion))
  140. return false;
  141. }
  142. if (!download->isCanceled() && type == TitleKeys::Game && !app->getSeed().empty())
  143. {
  144. download->setProgressMessage(_("Installing seed..."));
  145. if (!installer->installSeed(&app->getSeed()[0]))
  146. return false;
  147. }
  148. if (!download->isCanceled())
  149. {
  150. if (!installer->start(true))
  151. return false;
  152. download->setProgressMessage(_("Installing TMD..."));
  153. if (!installer->installTmd(&buf[0], dataOffsets[sigType] + 0x9C4 + (contentCount * 0x30)))
  154. return false;
  155. if (!installer->finalizeTmd())
  156. return false;
  157. }
  158. }
  159. buf.clear();
  160. if (!isResuming)
  161. fileIndex++;
  162. }
  163. }
  164. else // is a Content file
  165. {
  166. // Conditions indicate download issue (e.g. internet is down)
  167. // with either an empty length or one not 64-byte aligned
  168. #ifdef _3DS
  169. if (len == 0 || len % 64 > 0)
  170. {
  171. cpp3ds::Lock lock(m_mutexRefresh);
  172. download->suspend();
  173. installer->suspend();
  174. return true;
  175. }
  176. #endif
  177. int oldIndex = installer->getCurrentContentIndex();
  178. if (!installer->installContent(data, len, contentIndices[fileIndex-1]))
  179. {
  180. if (download->getStatus() == Download::Suspended)
  181. return true;
  182. return false;
  183. }
  184. // Save index change to help recover queue from crash
  185. if (oldIndex != installer->getCurrentContentIndex())
  186. save();
  187. totalProcessed += len;
  188. download->setProgress(static_cast<double>(totalProcessed) / titleFileSize);
  189. if (processed == fileSize && fileSize != 0)
  190. {
  191. if (!installer->finalizeContent())
  192. return false;
  193. if (fileIndex == 1 && type == TitleKeys::DLC)
  194. {
  195. download->setProgressMessage(_("Importing content..."));
  196. if (!installer->importContents(contentIndices.size() - 1, &contentIndices[1]))
  197. return false;
  198. }
  199. fileIndex++;
  200. }
  201. }
  202. // Handle status message and counters
  203. count += len;
  204. float speed = count / clock.getElapsedTime().asSeconds() / 1024.f;
  205. int secsRemaining = (titleFileSize - totalProcessed) / 1024 / speed;
  206. if (fileIndex <= contentCount)
  207. download->setProgressMessage(_("Installing %d/%d... %.1f%% (%.0f KB/s) %dm %02ds",
  208. fileIndex, contentCount,
  209. download->getProgress() * 100.f,
  210. speed, secsRemaining / 60, secsRemaining % 60));
  211. if (clock.getElapsedTime() > cpp3ds::seconds(5.f))
  212. {
  213. count = 0;
  214. clock.restart();
  215. }
  216. return true;
  217. });
  218. download->setFinishCallback([=](bool canceled, bool failed) mutable
  219. {
  220. bool succeeded = false;
  221. switch (download->getStatus())
  222. {
  223. case Download::Suspended:
  224. download->setProgressMessage(_("Suspended"));
  225. return;
  226. case Download::Finished:
  227. if (installer->commit())
  228. {
  229. Notification::spawn(_("Completed: %s", app->getTitle().toAnsiString().c_str()));
  230. download->setProgressMessage(_("Installed"));
  231. succeeded = true;
  232. break;
  233. }
  234. download->m_status = Download::Failed;
  235. // Fall through
  236. case Download::Failed:
  237. Notification::spawn(_("Failed: %s", app->getTitle().toAnsiString().c_str()));
  238. installer->abort();
  239. switch (installer->getErrorCode()) {
  240. case 0: // Failed due to internet problem and not from Installer
  241. download->setProgressMessage(_("Failed: Internet problem"));
  242. break;
  243. case 0xC8A08035: download->setProgressMessage(_("Not enough space on NAND")); break;
  244. case 0xC86044D2: download->setProgressMessage(_("Not enough space on SD")); break;
  245. case 0xD8E0806A: download->setProgressMessage(_("Wrong title key")); break;
  246. default:
  247. download->setProgressMessage(installer->getErrorString());
  248. }
  249. break;
  250. case Download::Canceled:
  251. break;
  252. }
  253. download->setProgress(1.f);
  254. // Reset CanSendTop state
  255. for (auto& download : m_downloads)
  256. download->download->m_canSendTop = true;
  257. // Refresh installed list to add recent install
  258. if (succeeded)
  259. InstalledList::getInstance().refresh();
  260. // Play sound if queue is finished
  261. if (getActiveCount() == 0 && !canceled)
  262. if (Config::get(Config::PlaySoundAfterDownload).GetBool())
  263. m_soundFinish.play(3);
  264. if (callback)
  265. callback(succeeded);
  266. });
  267. if (progress > 0.f)
  268. {
  269. download->setProgress(progress);
  270. download->setProgressMessage(_("Suspended"));
  271. download->m_status = Download::Suspended;
  272. }
  273. else
  274. download->setProgressMessage(_("Queued"));
  275. std::unique_ptr<DownloadItem> downloadItem(new DownloadItem(app, download, installer));
  276. m_downloads.emplace_back(std::move(downloadItem));
  277. realign();
  278. }
  279. void DownloadQueue::draw(cpp3ds::RenderTarget &target, cpp3ds::RenderStates states) const
  280. {
  281. states.transform *= getTransform();
  282. states.scissor = cpp3ds::UintRect(0, 30, 320, 210);
  283. for (auto& item : m_downloads)
  284. {
  285. static float top = 30.f - item->download->getSize().y;
  286. float posY = item->download->getPosition().y + m_scrollPos;
  287. if (posY > top && posY < 240.f)
  288. target.draw(*item->download, states);
  289. }
  290. }
  291. void DownloadQueue::restartDownload(Download *download)
  292. {
  293. for (auto& item : m_downloads)
  294. if (item->download == download && item->download->getStatus() == Download::Failed)
  295. {
  296. item->download->m_markedForDelete = true;
  297. addDownload(item->appItem, item->installer->getTitleId());
  298. break;
  299. }
  300. }
  301. bool DownloadQueue::isDownloading(std::shared_ptr<AppItem> app)
  302. {
  303. for (auto& item : m_downloads)
  304. if (item->appItem == app)
  305. {
  306. auto status = item->download->getStatus();
  307. if (status == Download::Queued || status == Download::Downloading || status == Download::Suspended)
  308. return true;
  309. }
  310. return false;
  311. }
  312. bool DownloadQueue::isDownloading(cpp3ds::Uint64 titleId)
  313. {
  314. for (auto& item : m_downloads)
  315. if (item->installer->getTitleId() == titleId)
  316. {
  317. auto status = item->download->getStatus();
  318. if (status == Download::Queued || status == Download::Downloading || status == Download::Suspended)
  319. return true;
  320. }
  321. return false;
  322. }
  323. bool DownloadQueue::processEvent(const cpp3ds::Event &event)
  324. {
  325. for (auto& item : m_downloads)
  326. {
  327. static float top = 30.f - item->download->getSize().y;
  328. float posY = item->download->getPosition().y + m_scrollPos;
  329. if (posY > top && posY < 240.f)
  330. item->download->processEvent(event);
  331. }
  332. return true;
  333. }
  334. void DownloadQueue::realign()
  335. {
  336. bool processedFirstItem = false;
  337. m_size.y = 0.f;
  338. for (int i = 0; i < m_downloads.size(); ++i)
  339. {
  340. Download *download = m_downloads[i]->download;
  341. Download::Status status = download->getStatus();
  342. if (download->markedForDelete())
  343. continue;
  344. if (processedFirstItem)
  345. {
  346. download->m_canSendTop = (status == Download::Queued || status == Download::Suspended || status == Download::Downloading);
  347. }
  348. else if (status == Download::Queued || status == Download::Suspended || status == Download::Downloading)
  349. {
  350. processedFirstItem = true;
  351. download->m_canSendTop = false;
  352. }
  353. TweenEngine::Tween::to(*download, util3ds::TweenSprite::POSITION_Y, 0.2f)
  354. .target(33.f + i * download->getSize().y)
  355. .start(m_tweenManager);
  356. m_size.y += download->getSize().y;
  357. }
  358. updateScrollSize();
  359. save();
  360. }
  361. void DownloadQueue::update(float delta)
  362. {
  363. // Remove downloads marked for delete
  364. {
  365. bool changed = false;
  366. cpp3ds::Lock lock(m_mutexRefresh);
  367. for (auto it = m_downloads.begin(); it != m_downloads.end();)
  368. {
  369. Download *download = it->get()->download;
  370. if (download->markedForDelete())
  371. {
  372. if (download->getStatus() == Download::Suspended)
  373. it->get()->installer->abort();
  374. m_downloads.erase(it);
  375. changed = true;
  376. }
  377. else
  378. ++it;
  379. }
  380. if (changed)
  381. realign();
  382. }
  383. m_tweenManager.update(delta);
  384. }
  385. size_t DownloadQueue::getCount()
  386. {
  387. return m_downloads.size();
  388. }
  389. size_t DownloadQueue::getActiveCount()
  390. {
  391. size_t count = 0;
  392. for (auto& item : m_downloads)
  393. if (item->download->getProgress() < 1.f)
  394. ++count;
  395. return count;
  396. }
  397. DownloadQueue &DownloadQueue::getInstance()
  398. {
  399. static DownloadQueue downloadQueue;
  400. return downloadQueue;
  401. }
  402. void DownloadQueue::sendTop(Download *download)
  403. {
  404. cpp3ds::Lock lock(m_mutexRefresh);
  405. auto iterTopDownload = m_downloads.end();
  406. auto iterBottomDownload = m_downloads.end();
  407. for (auto it = m_downloads.begin(); it != m_downloads.end(); ++it)
  408. {
  409. if (iterTopDownload == m_downloads.end())
  410. {
  411. Download::Status status = it->get()->download->getStatus();
  412. if (status == Download::Downloading || status == Download::Queued || status == Download::Suspended)
  413. iterTopDownload = it;
  414. }
  415. else if (iterBottomDownload == m_downloads.end())
  416. {
  417. if (it->get()->download == download)
  418. iterBottomDownload = it;
  419. }
  420. }
  421. if (iterTopDownload != m_downloads.end() && iterBottomDownload != m_downloads.end())
  422. {
  423. std::rotate(iterTopDownload, iterBottomDownload, iterBottomDownload + 1);
  424. realign();
  425. m_clockRefresh.restart();
  426. }
  427. }
  428. void DownloadQueue::refresh()
  429. {
  430. while (!m_refreshEnd)
  431. {
  432. {
  433. cpp3ds::Lock lock(m_mutexRefresh);
  434. DownloadItem *activeDownload = nullptr;
  435. DownloadItem *firstQueued = nullptr;
  436. bool activeNeedsSuspension = false;
  437. for (auto& item : m_downloads)
  438. {
  439. Download::Status status = item->download->getStatus();
  440. if (!firstQueued && (status == Download::Queued || status == Download::Suspended))
  441. {
  442. firstQueued = item.get();
  443. }
  444. else if (!activeDownload && status == Download::Downloading)
  445. {
  446. activeDownload = item.get();
  447. if (firstQueued)
  448. activeNeedsSuspension = true;
  449. }
  450. }
  451. if ((!activeDownload && firstQueued) || activeNeedsSuspension)
  452. {
  453. if (activeNeedsSuspension)
  454. {
  455. activeDownload->download->suspend();
  456. activeDownload->installer->suspend();
  457. }
  458. firstQueued->installer->resume();
  459. firstQueued->download->resume();
  460. realign();
  461. }
  462. }
  463. m_clockRefresh.restart();
  464. while (m_clockRefresh.getElapsedTime() < cpp3ds::seconds(1.f))
  465. cpp3ds::sleep(cpp3ds::milliseconds(200));
  466. }
  467. }
  468. void DownloadQueue::suspend()
  469. {
  470. cpp3ds::Lock lock(m_mutexRefresh);
  471. m_refreshEnd = true;
  472. for (auto& item : m_downloads)
  473. {
  474. item->download->suspend();
  475. item->installer->suspend();
  476. }
  477. }
  478. void DownloadQueue::resume()
  479. {
  480. m_refreshEnd = false;
  481. m_threadRefresh.launch();
  482. }
  483. void DownloadQueue::save()
  484. {
  485. rapidjson::Document json;
  486. std::string filepath = cpp3ds::FileSystem::getFilePath(FREESHOP_DIR "/queue.json");
  487. std::ofstream file(filepath);
  488. rapidjson::OStreamWrapper osw(file);
  489. rapidjson::Writer<rapidjson::OStreamWrapper> writer(osw);
  490. std::vector<std::string> subTitleIds; // Strings need to be in memory for json allocator write
  491. json.SetArray();
  492. for (auto& item : m_downloads)
  493. {
  494. Download::Status status = item->download->getStatus();
  495. if (status != Download::Downloading && status != Download::Queued && status != Download::Suspended)
  496. continue;
  497. rapidjson::Value obj(rapidjson::kObjectType);
  498. cpp3ds::Uint32 titleType = item->installer->getTitleId() >> 32;
  499. subTitleIds.push_back(_("%016llX", item->installer->getTitleId()).toAnsiString());
  500. obj.AddMember("title_id", rapidjson::StringRef(item->appItem->getTitleIdStr().c_str()), json.GetAllocator());
  501. obj.AddMember("subtitle_id", rapidjson::StringRef(subTitleIds.back().c_str()), json.GetAllocator());
  502. // TODO: Figure out how to properly resume DLC installation
  503. if (titleType == TitleKeys::DLC) {
  504. obj.AddMember("content_index", rapidjson::Value().SetInt(-1), json.GetAllocator());
  505. obj.AddMember("progress", rapidjson::Value().SetFloat(0.f), json.GetAllocator());
  506. } else {
  507. obj.AddMember("content_index", rapidjson::Value().SetInt(status == Download::Queued ? -1 : item->installer->getCurrentContentIndex()), json.GetAllocator());
  508. obj.AddMember("progress", rapidjson::Value().SetFloat(item->download->getProgress()), json.GetAllocator());
  509. }
  510. json.PushBack(obj, json.GetAllocator());
  511. }
  512. json.Accept(writer);
  513. }
  514. void DownloadQueue::load()
  515. {
  516. uint32_t pendingTitleCountSD = 0;
  517. uint32_t pendingTitleCountNAND = 0;
  518. std::vector<uint64_t> pendingTitleIds;
  519. #ifndef EMULATION
  520. Result res = 0;
  521. if (R_SUCCEEDED(res = AM_GetPendingTitleCount(&pendingTitleCountSD, MEDIATYPE_SD, AM_STATUS_MASK_INSTALLING)))
  522. if (R_SUCCEEDED(res = AM_GetPendingTitleCount(&pendingTitleCountNAND, MEDIATYPE_NAND, AM_STATUS_MASK_INSTALLING)))
  523. {
  524. pendingTitleIds.resize(pendingTitleCountSD + pendingTitleCountNAND);
  525. res = AM_GetPendingTitleList(nullptr, pendingTitleCountSD, MEDIATYPE_SD, AM_STATUS_MASK_INSTALLING, &pendingTitleIds[0]);
  526. res = AM_GetPendingTitleList(nullptr, pendingTitleCountNAND, MEDIATYPE_NAND, AM_STATUS_MASK_INSTALLING, &pendingTitleIds[pendingTitleCountSD]);
  527. }
  528. #endif
  529. cpp3ds::FileInputStream file;
  530. if (file.open(FREESHOP_DIR "/queue.json"))
  531. {
  532. rapidjson::Document json;
  533. std::string jsonStr;
  534. jsonStr.resize(file.getSize());
  535. file.read(&jsonStr[0], jsonStr.size());
  536. json.Parse(jsonStr.c_str());
  537. if (!json.HasParseError())
  538. {
  539. auto &list = AppList::getInstance().getList();
  540. for (auto it = json.Begin(); it != json.End(); ++it)
  541. {
  542. auto item = it->GetObject();
  543. std::string strTitleId = item["title_id"].GetString();
  544. std::string strSubTitleId = item["subtitle_id"].GetString();
  545. int contentIndex = item["content_index"].GetInt();
  546. float progress = item["progress"].GetFloat();
  547. uint64_t titleId = strtoull(strTitleId.c_str(), 0, 16);
  548. uint64_t subTitleId = strtoull(strSubTitleId.c_str(), 0, 16);
  549. #ifdef _3DS
  550. for (auto pendingTitleId : pendingTitleIds)
  551. if (contentIndex == -1 || pendingTitleId == titleId || pendingTitleId == subTitleId)
  552. {
  553. for (auto& app : list)
  554. if (app->getAppItem()->getTitleId() == titleId)
  555. addDownload(app->getAppItem(), subTitleId, nullptr, contentIndex, progress);
  556. break;
  557. }
  558. #else
  559. for (auto& app : list)
  560. if (app->getAppItem()->getTitleId() == titleId)
  561. addDownload(app->getAppItem(), subTitleId, nullptr, contentIndex, progress);
  562. #endif
  563. }
  564. }
  565. }
  566. }
  567. void DownloadQueue::setScroll(float position)
  568. {
  569. m_scrollPos = position;
  570. setPosition(0.f, std::round(position));
  571. }
  572. float DownloadQueue::getScroll()
  573. {
  574. return m_scrollPos;
  575. }
  576. const cpp3ds::Vector2f &DownloadQueue::getScrollSize()
  577. {
  578. return m_size;
  579. }
  580. } // namespace FreeShop