cmodlistview_moc.cpp 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  1. /*
  2. * cmodlistview_moc.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "cmodlistview_moc.h"
  12. #include "ui_cmodlistview_moc.h"
  13. #include "imageviewer_moc.h"
  14. #include "../mainwindow_moc.h"
  15. #include <QJsonArray>
  16. #include <QCryptographicHash>
  17. #include <QRegularExpression>
  18. #include "cmodlistmodel_moc.h"
  19. #include "cmodmanager.h"
  20. #include "cdownloadmanager_moc.h"
  21. #include "../settingsView/csettingsview_moc.h"
  22. #include "../launcherdirs.h"
  23. #include "../jsonutils.h"
  24. #include "../helper.h"
  25. #include "../../lib/VCMIDirs.h"
  26. #include "../../lib/CConfigHandler.h"
  27. #include "../../lib/Languages.h"
  28. #include "../../lib/modding/CModVersion.h"
  29. static double mbToBytes(double mb)
  30. {
  31. return mb * 1024 * 1024;
  32. }
  33. void CModListView::setupModModel()
  34. {
  35. modModel = new CModListModel(this);
  36. manager = std::make_unique<CModManager>(modModel);
  37. }
  38. void CModListView::changeEvent(QEvent *event)
  39. {
  40. if(event->type() == QEvent::LanguageChange)
  41. {
  42. ui->retranslateUi(this);
  43. modModel->reloadRepositories();
  44. }
  45. QWidget::changeEvent(event);
  46. }
  47. void CModListView::dragEnterEvent(QDragEnterEvent* event)
  48. {
  49. if(event->mimeData()->hasUrls())
  50. for(const auto & url : event->mimeData()->urls())
  51. for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json"}))
  52. if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
  53. {
  54. event->acceptProposedAction();
  55. return;
  56. }
  57. }
  58. void CModListView::dropEvent(QDropEvent* event)
  59. {
  60. const QMimeData* mimeData = event->mimeData();
  61. if(mimeData->hasUrls())
  62. {
  63. const QList<QUrl> urlList = mimeData->urls();
  64. for (const auto & url : urlList)
  65. manualInstallFile(url);
  66. }
  67. }
  68. void CModListView::setupFilterModel()
  69. {
  70. filterModel = new CModFilterModel(modModel, this);
  71. filterModel->setFilterKeyColumn(-1); // filter across all columns
  72. filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); // to make it more user-friendly
  73. filterModel->setDynamicSortFilter(true);
  74. }
  75. void CModListView::setupModsView()
  76. {
  77. ui->allModsView->setModel(filterModel);
  78. // input data is not sorted - sort it before display
  79. ui->allModsView->sortByColumn(ModFields::TYPE, Qt::AscendingOrder);
  80. ui->allModsView->header()->setSectionResizeMode(ModFields::STATUS_ENABLED, QHeaderView::Fixed);
  81. ui->allModsView->header()->setSectionResizeMode(ModFields::STATUS_UPDATE, QHeaderView::Fixed);
  82. QSettings s(Ui::teamName, Ui::appName);
  83. auto state = s.value("AllModsView/State").toByteArray();
  84. if(!state.isNull()) //read last saved settings
  85. {
  86. ui->allModsView->header()->restoreState(state);
  87. }
  88. else //default //TODO: default high-DPI scaling
  89. {
  90. ui->allModsView->setColumnWidth(ModFields::NAME, 185);
  91. ui->allModsView->setColumnWidth(ModFields::TYPE, 75);
  92. ui->allModsView->setColumnWidth(ModFields::VERSION, 60);
  93. }
  94. ui->allModsView->resizeColumnToContents(ModFields::STATUS_ENABLED);
  95. ui->allModsView->resizeColumnToContents(ModFields::STATUS_UPDATE);
  96. ui->allModsView->setUniformRowHeights(true);
  97. connect(ui->allModsView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex&,const QModelIndex&)),
  98. this, SLOT(modSelected(const QModelIndex&,const QModelIndex&)));
  99. connect(filterModel, SIGNAL(modelReset()),
  100. this, SLOT(modelReset()));
  101. connect(modModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
  102. this, SLOT(dataChanged(QModelIndex,QModelIndex)));
  103. }
  104. CModListView::CModListView(QWidget * parent)
  105. : QWidget(parent)
  106. , ui(new Ui::CModListView)
  107. {
  108. ui->setupUi(this);
  109. setAcceptDrops(true);
  110. setupModModel();
  111. setupFilterModel();
  112. setupModsView();
  113. ui->progressWidget->setVisible(false);
  114. dlManager = nullptr;
  115. if(settings["launcher"]["autoCheckRepositories"].Bool())
  116. {
  117. loadRepositories();
  118. }
  119. else
  120. {
  121. manager->resetRepositories();
  122. }
  123. #ifdef Q_OS_IOS
  124. for(auto * scrollWidget : {
  125. (QAbstractItemView*)ui->allModsView,
  126. (QAbstractItemView*)ui->screenshotsList})
  127. {
  128. QScroller::grabGesture(scrollWidget, QScroller::LeftMouseButtonGesture);
  129. scrollWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
  130. scrollWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  131. }
  132. #endif
  133. }
  134. void CModListView::loadRepositories()
  135. {
  136. manager->resetRepositories();
  137. QStringList repositories;
  138. if (settings["launcher"]["defaultRepositoryEnabled"].Bool())
  139. repositories.push_back(QString::fromStdString(settings["launcher"]["defaultRepositoryURL"].String()));
  140. if (settings["launcher"]["extraRepositoryEnabled"].Bool())
  141. repositories.push_back(QString::fromStdString(settings["launcher"]["extraRepositoryURL"].String()));
  142. for(auto entry : repositories)
  143. {
  144. if (entry.isEmpty())
  145. continue;
  146. // URL must be encoded to something else to get rid of symbols illegal in file names
  147. auto hashed = QCryptographicHash::hash(entry.toUtf8(), QCryptographicHash::Md5);
  148. auto hashedStr = QString::fromUtf8(hashed.toHex());
  149. downloadFile(hashedStr + ".json", entry, "repository index");
  150. }
  151. }
  152. CModListView::~CModListView()
  153. {
  154. QSettings s(Ui::teamName, Ui::appName);
  155. s.setValue("AllModsView/State", ui->allModsView->header()->saveState());
  156. delete ui;
  157. }
  158. static QString replaceIfNotEmpty(QVariant value, QString pattern)
  159. {
  160. if(value.canConvert<QStringList>())
  161. return pattern.arg(value.toStringList().join(", "));
  162. if(value.canConvert<QString>())
  163. return pattern.arg(value.toString());
  164. // all valid types of data should have been filtered by code above
  165. assert(!value.isValid());
  166. return "";
  167. }
  168. static QString replaceIfNotEmpty(QStringList value, QString pattern)
  169. {
  170. if(!value.empty())
  171. return pattern.arg(value.join(", "));
  172. return "";
  173. }
  174. QString CModListView::genChangelogText(CModEntry & mod)
  175. {
  176. QString headerTemplate = "<p><span style=\" font-weight:600;\">%1: </span></p>";
  177. QString entryBegin = "<p align=\"justify\"><ul>";
  178. QString entryEnd = "</ul></p>";
  179. QString entryLine = "<li>%1</li>";
  180. //QString versionSeparator = "<hr/>";
  181. QString result;
  182. QVariantMap changelog = mod.getValue("changelog").toMap();
  183. QList<QString> versions = changelog.keys();
  184. std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater)
  185. {
  186. return CModVersion::fromString(lesser.toStdString()) < CModVersion::fromString(greater.toStdString());
  187. });
  188. std::reverse(versions.begin(), versions.end());
  189. for(auto & version : versions)
  190. {
  191. result += headerTemplate.arg(version);
  192. result += entryBegin;
  193. for(auto & line : changelog.value(version).toStringList())
  194. result += entryLine.arg(line);
  195. result += entryEnd;
  196. }
  197. return result;
  198. }
  199. QStringList CModListView::getModNames(QStringList input)
  200. {
  201. QStringList result;
  202. for(const auto & modID : input)
  203. {
  204. auto mod = modModel->getMod(modID.toLower());
  205. QString modName = mod.getValue("name").toString();
  206. if (modName.isEmpty())
  207. result += modID.toLower();
  208. else
  209. result += modName;
  210. }
  211. return result;
  212. }
  213. QString CModListView::genModInfoText(CModEntry & mod)
  214. {
  215. QString prefix = "<p><span style=\" font-weight:600;\">%1: </span>"; // shared prefix
  216. QString redPrefix = "<p><span style=\" font-weight:600; color:red\">%1: </span>"; // shared prefix
  217. QString lineTemplate = prefix + "%2</p>";
  218. QString urlTemplate = prefix + "<a href=\"%2\">%3</a></p>";
  219. QString textTemplate = prefix + "</p><p align=\"justify\">%2</p>";
  220. QString listTemplate = "<p align=\"justify\">%1: %2</p>";
  221. QString noteTemplate = "<p align=\"justify\">%1</p>";
  222. QString incompatibleString = redPrefix + tr("Mod is incompatible") + "</p>";
  223. QString supportedVersions = redPrefix + "%2 %3 %4</p>";
  224. QString result;
  225. result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg(tr("Mod name")));
  226. result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version")));
  227. result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version")));
  228. if(mod.getValue("localSizeBytes").isValid())
  229. result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size")));
  230. if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("downloadSize").isValid())
  231. result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("downloadSize").toDouble())), lineTemplate.arg(tr("Download size")));
  232. result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors")));
  233. if(mod.getValue("licenseURL").isValid())
  234. result += urlTemplate.arg(tr("License")).arg(mod.getValue("licenseURL").toString()).arg(mod.getValue("licenseName").toString());
  235. if(mod.getValue("contact").isValid())
  236. result += urlTemplate.arg(tr("Contact")).arg(mod.getValue("contact").toString()).arg(mod.getValue("contact").toString());
  237. //compatibility info
  238. if(!mod.isCompatible())
  239. {
  240. auto compatibilityInfo = mod.getValue("compatibility").toMap();
  241. auto minStr = compatibilityInfo.value("min").toString();
  242. auto maxStr = compatibilityInfo.value("max").toString();
  243. result += incompatibleString.arg(tr("Compatibility"));
  244. if(minStr == maxStr)
  245. result += supportedVersions.arg(tr("Required VCMI version"), minStr, "", "");
  246. else
  247. {
  248. if(minStr.isEmpty() || maxStr.isEmpty())
  249. {
  250. if(minStr.isEmpty())
  251. result += supportedVersions.arg(tr("Supported VCMI version"), maxStr, ", ", "please upgrade mod");
  252. else
  253. result += supportedVersions.arg(tr("Required VCMI version"), minStr, " ", "or above");
  254. }
  255. else
  256. result += supportedVersions.arg(tr("Supported VCMI versions"), minStr, " - ", maxStr);
  257. }
  258. }
  259. QStringList supportedLanguages;
  260. QVariant baseLanguageVariant = mod.getBaseValue("language");
  261. QString baseLanguageID = baseLanguageVariant.isValid() ? baseLanguageVariant.toString() : "english";
  262. bool needToShowSupportedLanguages = false;
  263. for(const auto & language : Languages::getLanguageList())
  264. {
  265. if (!language.hasTranslation)
  266. continue;
  267. QString languageID = QString::fromStdString(language.identifier);
  268. if (languageID != baseLanguageID && !mod.getValue(languageID).isValid())
  269. continue;
  270. if (languageID != baseLanguageID)
  271. needToShowSupportedLanguages = true;
  272. supportedLanguages += QApplication::translate("Language", language.nameEnglish.c_str());
  273. }
  274. if(needToShowSupportedLanguages)
  275. result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages")));
  276. result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods")));
  277. result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods")));
  278. result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description")));
  279. result += "<p></p>"; // to get some empty space
  280. QString unknownDeps = tr("This mod can not be installed or enabled because the following dependencies are not present");
  281. QString blockingMods = tr("This mod can not be enabled because the following mods are incompatible with it");
  282. QString hasActiveDependentMods = tr("This mod cannot be disabled because it is required by the following mods");
  283. QString hasDependentMods = tr("This mod cannot be uninstalled or updated because it is required by the following mods");
  284. QString thisIsSubmod = tr("This is a submod and it cannot be installed or uninstalled separately from its parent mod");
  285. QString notes;
  286. notes += replaceIfNotEmpty(getModNames(findInvalidDependencies(mod.getName())), listTemplate.arg(unknownDeps));
  287. notes += replaceIfNotEmpty(getModNames(findBlockingMods(mod.getName())), listTemplate.arg(blockingMods));
  288. if(mod.isEnabled())
  289. notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), true)), listTemplate.arg(hasActiveDependentMods));
  290. if(mod.isInstalled())
  291. notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), false)), listTemplate.arg(hasDependentMods));
  292. if(mod.isSubmod())
  293. notes += noteTemplate.arg(thisIsSubmod);
  294. if(notes.size())
  295. result += textTemplate.arg(tr("Notes")).arg(notes);
  296. return result;
  297. }
  298. void CModListView::disableModInfo()
  299. {
  300. ui->disableButton->setVisible(false);
  301. ui->enableButton->setVisible(false);
  302. ui->installButton->setVisible(false);
  303. ui->uninstallButton->setVisible(false);
  304. ui->updateButton->setVisible(false);
  305. }
  306. void CModListView::dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight)
  307. {
  308. selectMod(ui->allModsView->currentIndex());
  309. }
  310. void CModListView::selectMod(const QModelIndex & index)
  311. {
  312. if(!index.isValid())
  313. {
  314. disableModInfo();
  315. }
  316. else
  317. {
  318. auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString());
  319. ui->modInfoBrowser->setHtml(genModInfoText(mod));
  320. ui->changelogBrowser->setHtml(genChangelogText(mod));
  321. bool hasInvalidDeps = !findInvalidDependencies(index.data(ModRoles::ModNameRole).toString()).empty();
  322. bool hasBlockingMods = !findBlockingMods(index.data(ModRoles::ModNameRole).toString()).empty();
  323. bool hasDependentMods = !findDependentMods(index.data(ModRoles::ModNameRole).toString(), true).empty();
  324. ui->disableButton->setVisible(mod.isEnabled());
  325. ui->enableButton->setVisible(mod.isDisabled());
  326. ui->installButton->setVisible(mod.isAvailable() && !mod.isSubmod());
  327. ui->uninstallButton->setVisible(mod.isInstalled() && !mod.isSubmod());
  328. ui->updateButton->setVisible(mod.isUpdateable());
  329. // Block buttons if action is not allowed at this time
  330. // TODO: automate handling of some of these cases instead of forcing player
  331. // to resolve all conflicts manually.
  332. ui->disableButton->setEnabled(!hasDependentMods && !mod.isEssential());
  333. ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps);
  334. ui->installButton->setEnabled(!hasInvalidDeps);
  335. ui->uninstallButton->setEnabled(!hasDependentMods && !mod.isEssential());
  336. ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods);
  337. loadScreenshots();
  338. }
  339. }
  340. void CModListView::modSelected(const QModelIndex & current, const QModelIndex &)
  341. {
  342. selectMod(current);
  343. }
  344. void CModListView::on_allModsView_activated(const QModelIndex & index)
  345. {
  346. selectMod(index);
  347. loadScreenshots();
  348. }
  349. void CModListView::on_lineEdit_textChanged(const QString & arg1)
  350. {
  351. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  352. auto baseStr = QRegularExpression::wildcardToRegularExpression(arg1, QRegularExpression::UnanchoredWildcardConversion);
  353. #else
  354. auto baseStr = QRegularExpression::wildcardToRegularExpression(arg1);
  355. //Hack due to lack QRegularExpression::UnanchoredWildcardConversion in Qt5
  356. baseStr.chop(3);
  357. baseStr.remove(0,5);
  358. #endif
  359. QRegularExpression regExp{baseStr, QRegularExpression::CaseInsensitiveOption};
  360. filterModel->setFilterRegularExpression(regExp);
  361. }
  362. void CModListView::on_comboBox_currentIndexChanged(int index)
  363. {
  364. switch(index)
  365. {
  366. case 0:
  367. filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE);
  368. break;
  369. case 1:
  370. filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED);
  371. break;
  372. case 2:
  373. filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED);
  374. break;
  375. case 3:
  376. filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE);
  377. break;
  378. case 4:
  379. filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
  380. break;
  381. case 5:
  382. filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
  383. break;
  384. }
  385. }
  386. QStringList CModListView::findInvalidDependencies(QString mod)
  387. {
  388. QStringList ret;
  389. for(QString requirement : modModel->getRequirements(mod))
  390. {
  391. if(!modModel->hasMod(requirement) && !modModel->hasMod(requirement.split(QChar('.'))[0]))
  392. ret += requirement;
  393. }
  394. return ret;
  395. }
  396. QStringList CModListView::findBlockingMods(QString modUnderTest)
  397. {
  398. QStringList ret;
  399. auto required = modModel->getRequirements(modUnderTest);
  400. for(QString name : modModel->getModList())
  401. {
  402. auto mod = modModel->getMod(name);
  403. if(mod.isEnabled())
  404. {
  405. // one of enabled mods have requirement (or this mod) marked as conflict
  406. for(auto conflict : mod.getConflicts())
  407. {
  408. if(required.contains(conflict))
  409. ret.push_back(name);
  410. }
  411. }
  412. }
  413. return ret;
  414. }
  415. QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled)
  416. {
  417. QStringList ret;
  418. for(QString modName : modModel->getModList())
  419. {
  420. auto current = modModel->getMod(modName);
  421. if(!current.isInstalled() || !current.isVisible())
  422. continue;
  423. if(current.getDependencies().contains(mod, Qt::CaseInsensitive))
  424. {
  425. if(!(current.isDisabled() && excludeDisabled))
  426. ret += modName;
  427. }
  428. }
  429. return ret;
  430. }
  431. void CModListView::on_enableButton_clicked()
  432. {
  433. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  434. enableModByName(modName);
  435. checkManagerErrors();
  436. }
  437. void CModListView::enableModByName(QString modName)
  438. {
  439. assert(findBlockingMods(modName).empty());
  440. assert(findInvalidDependencies(modName).empty());
  441. for(auto & name : modModel->getRequirements(modName))
  442. {
  443. if(modModel->getMod(name).isDisabled())
  444. manager->enableMod(name);
  445. }
  446. emit modsChanged();
  447. }
  448. void CModListView::on_disableButton_clicked()
  449. {
  450. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  451. disableModByName(modName);
  452. checkManagerErrors();
  453. }
  454. void CModListView::disableModByName(QString modName)
  455. {
  456. if(modModel->hasMod(modName) && modModel->getMod(modName).isEnabled())
  457. manager->disableMod(modName);
  458. emit modsChanged();
  459. }
  460. void CModListView::on_updateButton_clicked()
  461. {
  462. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  463. assert(findInvalidDependencies(modName).empty());
  464. for(auto & name : modModel->getRequirements(modName))
  465. {
  466. auto mod = modModel->getMod(name);
  467. // update required mod, install missing (can be new dependency)
  468. if(mod.isUpdateable() || !mod.isInstalled())
  469. downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble()));
  470. }
  471. }
  472. void CModListView::on_uninstallButton_clicked()
  473. {
  474. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  475. // NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it?
  476. if(modModel->hasMod(modName) && modModel->getMod(modName).isInstalled())
  477. {
  478. if(modModel->getMod(modName).isEnabled())
  479. manager->disableMod(modName);
  480. manager->uninstallMod(modName);
  481. }
  482. emit modsChanged();
  483. checkManagerErrors();
  484. }
  485. void CModListView::on_installButton_clicked()
  486. {
  487. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  488. assert(findInvalidDependencies(modName).empty());
  489. for(auto & name : modModel->getRequirements(modName))
  490. {
  491. auto mod = modModel->getMod(name);
  492. if(!mod.isInstalled())
  493. downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble()));
  494. else if(!mod.isEnabled())
  495. enableModByName(name);
  496. }
  497. for(auto & name : modModel->getMod(modName).getConflicts())
  498. {
  499. auto mod = modModel->getMod(name);
  500. if(mod.isEnabled())
  501. {
  502. //TODO: consider reverse dependencies disabling
  503. //TODO: consider if it may be possible for subdependencies to block disabling conflicting mod?
  504. //TODO: consider if it may be possible to get subconflicts that will block disabling conflicting mod?
  505. disableModByName(name);
  506. }
  507. }
  508. }
  509. void CModListView::on_installFromFileButton_clicked()
  510. {
  511. QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json);;" + tr("Maps") + " (*.h3m *.vmap);;" + tr("Campaigns") + " (*.h3c *.vcmp);;" + tr("Configs") + " (*.json);;" + tr("Mods") + " (*.zip)";
  512. QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns) to install..."), QDir::homePath(), filter);
  513. for (const auto & file : files)
  514. {
  515. QUrl url = QUrl::fromLocalFile(file);
  516. manualInstallFile(url);
  517. }
  518. }
  519. void CModListView::manualInstallFile(QUrl url)
  520. {
  521. QString urlStr = url.toString();
  522. QString fileName = url.fileName();
  523. if(urlStr.endsWith(".zip", Qt::CaseInsensitive))
  524. downloadFile(fileName.toLower()
  525. // mod name currently comes from zip file -> remove suffixes from github zip download
  526. .replace(QRegularExpression("-[0-9a-f]{40}"), "")
  527. .replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip")
  528. .replace("-main.zip", ".zip")
  529. , urlStr, "mods", 0);
  530. else if(urlStr.endsWith(".json", Qt::CaseInsensitive))
  531. {
  532. QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
  533. QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check
  534. if(!configFile.empty())
  535. {
  536. auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  537. if(dialogResult == QMessageBox::Yes)
  538. {
  539. const auto configFilePath = configDir.filePath(configFile[0]);
  540. QFile::remove(configFilePath);
  541. QFile::copy(url.toLocalFile(), configFilePath);
  542. // reload settings
  543. Helper::loadSettings();
  544. for(auto widget : qApp->allWidgets())
  545. if(auto settingsView = qobject_cast<CSettingsView *>(widget))
  546. settingsView->loadSettings();
  547. manager->loadMods();
  548. manager->loadModSettings();
  549. }
  550. }
  551. }
  552. else
  553. downloadFile(fileName, urlStr, "mods", 0);
  554. }
  555. void CModListView::downloadFile(QString file, QString url, QString description, qint64 size)
  556. {
  557. if(!dlManager)
  558. {
  559. dlManager = new CDownloadManager();
  560. ui->progressWidget->setVisible(true);
  561. connect(dlManager, SIGNAL(downloadProgress(qint64,qint64)),
  562. this, SLOT(downloadProgress(qint64,qint64)));
  563. connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)),
  564. this, SLOT(downloadFinished(QStringList,QStringList,QStringList)));
  565. connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)),
  566. this, SLOT(extractionProgress(qint64,qint64)));
  567. connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged);
  568. QString progressBarFormat = tr("Downloading %s%. %p% (%v MB out of %m MB) finished");
  569. progressBarFormat.replace("%s%", description);
  570. ui->progressBar->setFormat(progressBarFormat);
  571. }
  572. dlManager->downloadFile(QUrl(url), file, size);
  573. }
  574. void CModListView::downloadProgress(qint64 current, qint64 max)
  575. {
  576. // display progress, in megabytes
  577. ui->progressBar->setVisible(true);
  578. ui->progressBar->setMaximum(max / (1024 * 1024));
  579. ui->progressBar->setValue(current / (1024 * 1024));
  580. }
  581. void CModListView::extractionProgress(qint64 current, qint64 max)
  582. {
  583. // display progress, in extracted files
  584. ui->progressBar->setVisible(true);
  585. ui->progressBar->setMaximum(max);
  586. ui->progressBar->setValue(current);
  587. }
  588. void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors)
  589. {
  590. QString title = tr("Download failed");
  591. QString firstLine = tr("Unable to download all files.\n\nEncountered errors:\n\n");
  592. QString lastLine = tr("\n\nInstall successfully downloaded?");
  593. bool doInstallFiles = false;
  594. // if all files were d/loaded there should be no errors. And on failure there must be an error
  595. assert(failedFiles.empty() == errors.empty());
  596. if(savedFiles.empty())
  597. {
  598. // no successfully downloaded mods
  599. QMessageBox::warning(this, title, firstLine + errors.join("\n"), QMessageBox::Ok, QMessageBox::Ok);
  600. }
  601. else if(!failedFiles.empty())
  602. {
  603. // some mods were not downloaded
  604. int result = QMessageBox::warning (this, title, firstLine + errors.join("\n") + lastLine,
  605. QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
  606. if(result == QMessageBox::Yes)
  607. doInstallFiles = true;
  608. }
  609. else
  610. {
  611. // everything OK
  612. doInstallFiles = true;
  613. }
  614. dlManager->deleteLater();
  615. dlManager = nullptr;
  616. ui->progressBar->setMaximum(0);
  617. ui->progressBar->setValue(0);
  618. if(doInstallFiles)
  619. installFiles(savedFiles);
  620. hideProgressBar();
  621. emit modsChanged();
  622. }
  623. void CModListView::hideProgressBar()
  624. {
  625. if(dlManager == nullptr) // it was not recreated meanwhile
  626. {
  627. ui->progressWidget->setVisible(false);
  628. ui->progressBar->setMaximum(0);
  629. ui->progressBar->setValue(0);
  630. }
  631. }
  632. void CModListView::installFiles(QStringList files)
  633. {
  634. QStringList mods;
  635. QStringList maps;
  636. QStringList images;
  637. QVector<QVariantMap> repositories;
  638. // TODO: some better way to separate zip's with mods and downloaded repository files
  639. for(QString filename : files)
  640. {
  641. if(filename.endsWith(".zip", Qt::CaseInsensitive))
  642. mods.push_back(filename);
  643. else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive))
  644. maps.push_back(filename);
  645. else if(filename.endsWith(".json", Qt::CaseInsensitive))
  646. {
  647. //download and merge additional files
  648. auto repoData = JsonUtils::JsonFromFile(filename).toMap();
  649. if(repoData.value("name").isNull())
  650. {
  651. for(const auto & key : repoData.keys())
  652. {
  653. auto modjson = repoData[key].toMap().value("mod");
  654. if(!modjson.isNull())
  655. {
  656. downloadFile(key + ".json", modjson.toString(), "repository index");
  657. }
  658. }
  659. }
  660. else
  661. {
  662. auto modn = QFileInfo(filename).baseName();
  663. QVariantMap temp;
  664. temp[modn] = repoData;
  665. repoData = temp;
  666. }
  667. repositories.push_back(repoData);
  668. }
  669. else if(filename.endsWith(".png", Qt::CaseInsensitive))
  670. images.push_back(filename);
  671. }
  672. if (!repositories.empty())
  673. manager->loadRepositories(repositories);
  674. if(!mods.empty())
  675. installMods(mods);
  676. if(!maps.empty())
  677. installMaps(maps);
  678. if(!images.empty())
  679. loadScreenshots();
  680. }
  681. void CModListView::installMods(QStringList archives)
  682. {
  683. QStringList modNames;
  684. for(QString archive : archives)
  685. {
  686. // get basename out of full file name
  687. // remove path remove extension
  688. QString modName = archive.section('/', -1, -1).section('.', 0, 0);
  689. modNames.push_back(modName);
  690. }
  691. QStringList modsToEnable;
  692. // disable mod(s), to properly recalculate dependencies, if changed
  693. for(QString mod : boost::adaptors::reverse(modNames))
  694. {
  695. CModEntry entry = modModel->getMod(mod);
  696. if(entry.isInstalled())
  697. {
  698. // enable mod if installed and enabled
  699. if(entry.isEnabled())
  700. modsToEnable.push_back(mod);
  701. }
  702. else
  703. {
  704. // enable mod if m
  705. if(settings["launcher"]["enableInstalledMods"].Bool())
  706. modsToEnable.push_back(mod);
  707. }
  708. }
  709. // uninstall old version of mod, if installed
  710. for(QString mod : boost::adaptors::reverse(modNames))
  711. {
  712. if(modModel->getMod(mod).isInstalled())
  713. manager->uninstallMod(mod);
  714. }
  715. for(int i = 0; i < modNames.size(); i++)
  716. {
  717. ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i]));
  718. manager->installMod(modNames[i], archives[i]);
  719. }
  720. std::function<void(QString)> enableMod;
  721. enableMod = [&](QString modName)
  722. {
  723. auto mod = modModel->getMod(modName);
  724. if(mod.isInstalled() && !mod.getValue("keepDisabled").toBool())
  725. {
  726. if(mod.isDisabled() && manager->enableMod(modName))
  727. {
  728. for(QString child : modModel->getChildren(modName))
  729. enableMod(child);
  730. }
  731. }
  732. };
  733. for(QString mod : modsToEnable)
  734. {
  735. enableMod(mod);
  736. }
  737. checkManagerErrors();
  738. for(QString archive : archives)
  739. QFile::remove(archive);
  740. }
  741. void CModListView::installMaps(QStringList maps)
  742. {
  743. const auto destDir = CLauncherDirs::mapsPath() + QChar{'/'};
  744. for(QString map : maps)
  745. {
  746. QFile(map).rename(destDir + map.section('/', -1, -1));
  747. }
  748. }
  749. void CModListView::on_refreshButton_clicked()
  750. {
  751. loadRepositories();
  752. }
  753. void CModListView::on_pushButton_clicked()
  754. {
  755. delete dlManager;
  756. dlManager = nullptr;
  757. hideProgressBar();
  758. }
  759. void CModListView::modelReset()
  760. {
  761. selectMod(filterModel->rowCount() > 0 ? filterModel->index(0, 0) : QModelIndex());
  762. }
  763. void CModListView::checkManagerErrors()
  764. {
  765. QString errors = manager->getErrors().join('\n');
  766. if(errors.size() != 0)
  767. {
  768. QString title = tr("Operation failed");
  769. QString description = tr("Encountered errors:\n") + errors;
  770. QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok);
  771. }
  772. }
  773. void CModListView::on_tabWidget_currentChanged(int index)
  774. {
  775. loadScreenshots();
  776. }
  777. void CModListView::loadScreenshots()
  778. {
  779. if(ui->tabWidget->currentIndex() == 2)
  780. {
  781. ui->screenshotsList->clear();
  782. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  783. assert(modModel->hasMod(modName)); //should be filtered out by check above
  784. for(QString url : modModel->getMod(modName).getValue("screenshots").toStringList())
  785. {
  786. // URL must be encoded to something else to get rid of symbols illegal in file names
  787. const auto hashed = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5);
  788. const auto fileName = QString{QLatin1String{"%1.png"}}.arg(QLatin1String{hashed.toHex()});
  789. const auto fullPath = QString{QLatin1String{"%1/%2"}}.arg(CLauncherDirs::downloadsPath(), fileName);
  790. QPixmap pixmap(fullPath);
  791. if(pixmap.isNull())
  792. {
  793. // image file not exists or corrupted - try to redownload
  794. downloadFile(fileName, url, "screenshots");
  795. }
  796. else
  797. {
  798. // managed to load cached image
  799. QIcon icon(pixmap);
  800. auto * item = new QListWidgetItem(icon, QString(tr("Screenshot %1")).arg(ui->screenshotsList->count() + 1));
  801. ui->screenshotsList->addItem(item);
  802. }
  803. }
  804. }
  805. }
  806. void CModListView::on_screenshotsList_clicked(const QModelIndex & index)
  807. {
  808. if(index.isValid())
  809. {
  810. QIcon icon = ui->screenshotsList->item(index.row())->icon();
  811. auto pixmap = icon.pixmap(icon.availableSizes()[0]);
  812. ImageViewer::showPixmap(pixmap, this);
  813. }
  814. }
  815. const CModList & CModListView::getModList() const
  816. {
  817. assert(modModel);
  818. return *modModel;
  819. }
  820. void CModListView::doInstallMod(const QString & modName)
  821. {
  822. assert(findInvalidDependencies(modName).empty());
  823. for(auto & name : modModel->getRequirements(modName))
  824. {
  825. auto mod = modModel->getMod(name);
  826. if(!mod.isInstalled())
  827. downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble()));
  828. }
  829. }
  830. bool CModListView::isModAvailable(const QString & modName)
  831. {
  832. auto mod = modModel->getMod(modName);
  833. return mod.isAvailable();
  834. }
  835. bool CModListView::isModEnabled(const QString & modName)
  836. {
  837. auto mod = modModel->getMod(modName);
  838. return mod.isEnabled();
  839. }
  840. QString CModListView::getTranslationModName(const QString & language)
  841. {
  842. for(const auto & modName : modModel->getModList())
  843. {
  844. auto mod = modModel->getMod(modName);
  845. if (!mod.isTranslation())
  846. continue;
  847. if (mod.getBaseValue("language").toString() != language)
  848. continue;
  849. return modName;
  850. }
  851. return QString();
  852. }
  853. void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
  854. {
  855. if(!index.isValid())
  856. return;
  857. auto modName = index.data(ModRoles::ModNameRole).toString();
  858. auto mod = modModel->getMod(modName);
  859. bool hasInvalidDeps = !findInvalidDependencies(modName).empty();
  860. bool hasBlockingMods = !findBlockingMods(modName).empty();
  861. bool hasDependentMods = !findDependentMods(modName, true).empty();
  862. if(!hasInvalidDeps && mod.isAvailable() && !mod.isSubmod())
  863. {
  864. on_installButton_clicked();
  865. return;
  866. }
  867. if(!hasInvalidDeps && !hasDependentMods && mod.isUpdateable() && index.column() == ModFields::STATUS_UPDATE)
  868. {
  869. on_updateButton_clicked();
  870. return;
  871. }
  872. if(index.column() == ModFields::NAME)
  873. {
  874. if(ui->allModsView->isExpanded(index))
  875. ui->allModsView->collapse(index);
  876. else
  877. ui->allModsView->expand(index);
  878. return;
  879. }
  880. if(!hasBlockingMods && !hasInvalidDeps && mod.isDisabled())
  881. {
  882. on_enableButton_clicked();
  883. return;
  884. }
  885. if(!hasDependentMods && !mod.isEssential() && mod.isEnabled())
  886. {
  887. on_disableButton_clicked();
  888. return;
  889. }
  890. }