cmodlistview_moc.cpp 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644
  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 "modstatemodel.h"
  19. #include "modstateitemmodel_moc.h"
  20. #include "modstatecontroller.h"
  21. #include "cdownloadmanager_moc.h"
  22. #include "chroniclesextractor.h"
  23. #include "../settingsView/csettingsview_moc.h"
  24. #include "../vcmiqt/launcherdirs.h"
  25. #include "../vcmiqt/jsonutils.h"
  26. #include "../helper.h"
  27. #include "../../lib/CConfigHandler.h"
  28. #include "../../lib/VCMIDirs.h"
  29. #include "../../lib/filesystem/Filesystem.h"
  30. #include "../../lib/filesystem/CZipLoader.h"
  31. #include "../../lib/json/JsonUtils.h"
  32. #include "../../lib/modding/CModVersion.h"
  33. #include "../../lib/modding/ModDescription.h"
  34. #include "../../lib/texts/CGeneralTextHandler.h"
  35. #include "../../lib/texts/Languages.h"
  36. #include "../vcmiqt/launcherdirs.h"
  37. #include <future>
  38. void CModListView::setupModModel()
  39. {
  40. static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json";
  41. const auto &cachedRepositoryData = JsonUtils::jsonFromFile(repositoryCachePath);
  42. modStateModel = std::make_shared<ModStateModel>();
  43. if (!cachedRepositoryData.isNull())
  44. modStateModel->setRepositoryData(cachedRepositoryData);
  45. modModel = new ModStateItemModel(modStateModel, this);
  46. manager = std::make_unique<ModStateController>(modStateModel);
  47. }
  48. void CModListView::changeEvent(QEvent *event)
  49. {
  50. if(event->type() == QEvent::LanguageChange)
  51. {
  52. ui->retranslateUi(this);
  53. modModel->reloadViewModel();
  54. }
  55. QWidget::changeEvent(event);
  56. }
  57. void CModListView::setupFilterModel()
  58. {
  59. filterModel = new CModFilterModel(modModel, this);
  60. filterModel->setFilterKeyColumn(-1); // filter across all columns
  61. filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); // to make it more user-friendly
  62. filterModel->setDynamicSortFilter(true);
  63. }
  64. void CModListView::setupModsView()
  65. {
  66. ui->allModsView->setModel(filterModel);
  67. // input data is not sorted - sort it before display
  68. ui->allModsView->sortByColumn(ModFields::STARS, Qt::AscendingOrder);
  69. ui->allModsView->header()->setSectionResizeMode(ModFields::STATUS_ENABLED, QHeaderView::Fixed);
  70. ui->allModsView->header()->setSectionResizeMode(ModFields::STATUS_UPDATE, QHeaderView::Fixed);
  71. ui->allModsView->header()->setSectionResizeMode(ModFields::STARS, QHeaderView::Fixed);
  72. QSettings s = CLauncherDirs::getSettings(Ui::appName);
  73. auto state = s.value("AllModsView/State").toByteArray();
  74. if(!state.isNull()) //read last saved settings
  75. {
  76. ui->allModsView->header()->restoreState(state);
  77. }
  78. else //default //TODO: default high-DPI scaling
  79. {
  80. ui->allModsView->setColumnWidth(ModFields::NAME, 220);
  81. ui->allModsView->setColumnWidth(ModFields::TYPE, 75);
  82. }
  83. ui->allModsView->resizeColumnToContents(ModFields::STATUS_ENABLED);
  84. ui->allModsView->resizeColumnToContents(ModFields::STATUS_UPDATE);
  85. ui->allModsView->resizeColumnToContents(ModFields::STARS);
  86. ui->allModsView->setUniformRowHeights(true);
  87. ui->allModsView->setContextMenuPolicy(Qt::CustomContextMenu);
  88. connect(ui->allModsView, SIGNAL(customContextMenuRequested(const QPoint &)),
  89. this, SLOT(onCustomContextMenu(const QPoint &)));
  90. connect(ui->allModsView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex&,const QModelIndex&)),
  91. this, SLOT(modSelected(const QModelIndex&,const QModelIndex&)));
  92. connect(filterModel, SIGNAL(modelReset()),
  93. this, SLOT(modelReset()));
  94. connect(modModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
  95. this, SLOT(dataChanged(QModelIndex,QModelIndex)));
  96. }
  97. CModListView::CModListView(QWidget * parent)
  98. : QWidget(parent)
  99. , ui(new Ui::CModListView)
  100. {
  101. ui->setupUi(this);
  102. ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"});
  103. ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"});
  104. ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"});
  105. ui->updateButton->setIcon(QIcon{":/icons/mod-update.png"});
  106. ui->installButton->setIcon(QIcon{":/icons/mod-download.png"});
  107. ui->splitter->setStyleSheet("QSplitter::handle {background: palette('window');}");
  108. disableModInfo();
  109. setupModModel();
  110. setupFilterModel();
  111. setupModsView();
  112. ui->progressWidget->setVisible(false);
  113. dlManager = nullptr;
  114. modModel->reloadViewModel();
  115. if(settings["launcher"]["autoCheckRepositories"].Bool())
  116. loadRepositories();
  117. #ifdef VCMI_MOBILE
  118. for(auto * scrollWidget : {
  119. (QAbstractItemView*)ui->allModsView,
  120. (QAbstractItemView*)ui->screenshotsList})
  121. {
  122. Helper::enableScrollBySwiping(scrollWidget);
  123. scrollWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
  124. scrollWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  125. }
  126. #endif
  127. }
  128. void CModListView::reload(const QString & modToSelect)
  129. {
  130. modStateModel->reloadLocalState();
  131. modModel->reloadViewModel();
  132. if (!modToSelect.isEmpty())
  133. {
  134. QModelIndexList matches = modModel->match(modModel->index(0, 0, QModelIndex()), ModRoles::ModNameRole, modToSelect, 1, Qt::MatchExactly | Qt::MatchRecursive);
  135. if (!matches.isEmpty())
  136. ui->allModsView->setCurrentIndex(filterModel->mapFromSource(matches.first()));
  137. }
  138. }
  139. void CModListView::loadRepositories()
  140. {
  141. accumulatedRepositoryData.clear();
  142. QStringList repositories;
  143. if (settings["launcher"]["defaultRepositoryEnabled"].Bool())
  144. repositories.push_back(QString::fromStdString(settings["launcher"]["defaultRepositoryURL"].String()));
  145. if (settings["launcher"]["extraRepositoryEnabled"].Bool())
  146. repositories.push_back(QString::fromStdString(settings["launcher"]["extraRepositoryURL"].String()));
  147. for(const auto & entry : repositories)
  148. {
  149. if (entry.isEmpty())
  150. continue;
  151. // URL must be encoded to something else to get rid of symbols illegal in file names
  152. auto hashed = QCryptographicHash::hash(entry.toUtf8(), QCryptographicHash::Md5);
  153. auto hashedStr = QString::fromUtf8(hashed.toHex());
  154. downloadFile(hashedStr + ".json", entry, tr("mods repository index"));
  155. }
  156. }
  157. CModListView::~CModListView()
  158. {
  159. QSettings s = CLauncherDirs::getSettings(Ui::appName);
  160. s.setValue("AllModsView/State", ui->allModsView->header()->saveState());
  161. delete ui;
  162. }
  163. static QString replaceIfNotEmpty(QVariant value, QString pattern)
  164. {
  165. if(value.canConvert<QString>())
  166. {
  167. if (value.toString().isEmpty())
  168. return "";
  169. else
  170. return pattern.arg(value.toString());
  171. }
  172. if(value.canConvert<QStringList>())
  173. {
  174. if (value.toStringList().isEmpty())
  175. return "";
  176. else
  177. return pattern.arg(value.toStringList().join(", "));
  178. }
  179. // all valid types of data should have been filtered by code above
  180. assert(!value.isValid());
  181. return "";
  182. }
  183. static QString replaceIfNotEmpty(QStringList value, QString pattern)
  184. {
  185. if(!value.empty())
  186. return pattern.arg(value.join(", "));
  187. return "";
  188. }
  189. QString CModListView::genChangelogText(const ModState & mod)
  190. {
  191. QString headerTemplate = "### %1:";
  192. QString entryBegin = "\n\n";
  193. QString entryEnd = "\n";
  194. QString entryLine = "- %1\n";
  195. QString result;
  196. QMap<QString, QStringList> changelog = mod.getChangelog();
  197. QList<QString> versions = changelog.keys();
  198. std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater)
  199. {
  200. return CModVersion::fromString(lesser.toStdString()) < CModVersion::fromString(greater.toStdString());
  201. });
  202. std::reverse(versions.begin(), versions.end());
  203. for(const auto & version : versions)
  204. {
  205. result += headerTemplate.arg(version);
  206. result += entryBegin;
  207. for(const auto & line : changelog.value(version))
  208. result += entryLine.arg(line);
  209. result += entryEnd;
  210. }
  211. return result;
  212. }
  213. QStringList CModListView::getModNames(QString queryingModID, QStringList input)
  214. {
  215. QStringList result;
  216. auto queryingMod = modStateModel->getMod(queryingModID);
  217. for(const auto & modID : input)
  218. {
  219. if (modStateModel->isModExists(modID) && modStateModel->getMod(modID).isHidden())
  220. continue;
  221. QString parentModID = modStateModel->getTopParent(modID);
  222. QString displayName;
  223. if (modStateModel->isSubmod(modID) && queryingMod.getParentID() != parentModID )
  224. {
  225. // show in form "parent mod (submod)"
  226. QString parentDisplayName = parentModID;
  227. QString submodDisplayName = modID;
  228. if (modStateModel->isModExists(parentModID))
  229. parentDisplayName = modStateModel->getMod(parentModID).getName();
  230. if (modStateModel->isModExists(modID))
  231. submodDisplayName = modStateModel->getMod(modID).getName();
  232. displayName = QString("%1 (%2)").arg(submodDisplayName, parentDisplayName);
  233. }
  234. else
  235. {
  236. // show simply as mod name
  237. displayName = modID;
  238. if (modStateModel->isModExists(modID))
  239. displayName = modStateModel->getMod(modID).getName();
  240. }
  241. result += displayName;
  242. }
  243. return result;
  244. }
  245. QString CModListView::genModInfoText(const ModState & mod)
  246. {
  247. QString modNameTemplate = "# %1\n\n";
  248. QString lineTemplate = "**%1**: %2\n\n";
  249. QString urlTemplate = "**%1**: [%3](%2)\n\n";
  250. QString textTemplate = "**%1**: \n\n%2\n\n";
  251. QString listTemplate = "**%1**: %2\n\n";
  252. QString noteTemplate = "**%1**\n\n";
  253. QString incompatibleString = "**%1**: " + tr("Mod is incompatible") + "\n\n";
  254. QString supportedVersions = "**%1**: %2 %3 %4\n\n";
  255. QString result;
  256. QTextDocument description;
  257. description.setMarkdown(mod.getDescription());
  258. QString cleanDescription = description.toMarkdown();
  259. if (cleanDescription.isEmpty())
  260. cleanDescription = description.toPlainText();
  261. result += replaceIfNotEmpty(mod.getName(), modNameTemplate);
  262. result += cleanDescription;
  263. result += "\n\n"; // to get some empty space
  264. if (mod.isUpdateAvailable())
  265. {
  266. result += replaceIfNotEmpty(mod.getInstalledVersion(), lineTemplate.arg(tr("Installed version")));
  267. result += replaceIfNotEmpty(mod.getRepositoryVersion(), lineTemplate.arg(tr("Latest version")));
  268. }
  269. else
  270. {
  271. if (mod.isInstalled())
  272. result += replaceIfNotEmpty(mod.getInstalledVersion(), lineTemplate.arg(tr("Installed version")));
  273. else
  274. result += replaceIfNotEmpty(mod.getRepositoryVersion(), lineTemplate.arg(tr("Latest version")));
  275. }
  276. if (mod.isInstalled())
  277. result += replaceIfNotEmpty(modStateModel->getInstalledModSizeFormatted(mod.getID()), lineTemplate.arg(tr("Size")));
  278. if((!mod.isInstalled() || mod.isUpdateAvailable()) && !mod.getDownloadSizeFormatted().isEmpty())
  279. result += replaceIfNotEmpty(mod.getDownloadSizeFormatted(), lineTemplate.arg(tr("Download size")));
  280. result += replaceIfNotEmpty(mod.getAuthors(), lineTemplate.arg(tr("Authors")));
  281. if(!mod.getLicenseName().isEmpty())
  282. result += urlTemplate.arg(tr("License")).arg(mod.getLicenseUrl()).arg(mod.getLicenseName());
  283. if(!mod.getContact().isEmpty())
  284. result += lineTemplate.arg(tr("Contact")).arg(mod.getContact());
  285. if(!mod.getDownloadUrl().isEmpty())
  286. result += urlTemplate.arg(tr("Git-Repository")).arg(getRepoUrl(mod)).arg(getRepoUrl(mod));
  287. if(mod.getGithubStars() != -1)
  288. result += replaceIfNotEmpty(mod.getGithubStars(), lineTemplate.arg(tr("GitHub-Stars")));
  289. //compatibility info
  290. if(!mod.isCompatible())
  291. {
  292. auto compatibilityInfo = mod.getCompatibleVersionRange();
  293. auto minStr = compatibilityInfo.first;
  294. auto maxStr = compatibilityInfo.second;
  295. result += incompatibleString.arg(tr("Compatibility"));
  296. if(minStr == maxStr)
  297. result += supportedVersions.arg(tr("Required VCMI version"), minStr, "", "");
  298. else
  299. {
  300. if(minStr.isEmpty() || maxStr.isEmpty())
  301. {
  302. if(minStr.isEmpty())
  303. result += supportedVersions.arg(tr("Supported VCMI version"), maxStr, ", ", tr("please upgrade mod"));
  304. else
  305. result += supportedVersions.arg(tr("Required VCMI version"), minStr, " ", tr("or newer"));
  306. }
  307. else
  308. result += supportedVersions.arg(tr("Supported VCMI versions"), minStr, " - ", maxStr);
  309. }
  310. }
  311. QVariant baseLanguageVariant = mod.getBaseLanguage();
  312. QString baseLanguageID = baseLanguageVariant.isValid() ? baseLanguageVariant.toString() : "english";
  313. QStringList supportedLanguages = mod.getSupportedLanguages();
  314. if(supportedLanguages.size() > 1)
  315. {
  316. QStringList supportedLanguagesTranslated;
  317. for (const auto & languageID : supportedLanguages)
  318. supportedLanguagesTranslated += QApplication::translate("Language", Languages::getLanguageOptions(languageID.toStdString()).nameEnglish.c_str());
  319. result += replaceIfNotEmpty(supportedLanguagesTranslated, lineTemplate.arg(tr("Languages")));
  320. }
  321. QStringList conflicts = mod.getConflicts();
  322. for (const auto & otherMod : modStateModel->getAllMods())
  323. {
  324. QStringList otherConflicts = modStateModel->getMod(otherMod).getConflicts();
  325. if (otherConflicts.contains(mod.getID()) && !conflicts.contains(otherMod))
  326. conflicts.push_back(otherMod);
  327. }
  328. result += replaceIfNotEmpty(getModNames(mod.getID(), mod.getDependencies()), lineTemplate.arg(tr("Required mods")));
  329. result += replaceIfNotEmpty(getModNames(mod.getID(), conflicts), lineTemplate.arg(tr("Conflicting mods")));
  330. QString translationMismatch = tr("This mod cannot be enabled because it translates into a different language.");
  331. QString notInstalledDeps = tr("This mod can not be enabled because the following dependencies are not present");
  332. QString unavailableDeps = tr("This mod can not be installed because the following dependencies are not present");
  333. QString thisIsSubmod = tr("This is a submod and it cannot be installed or uninstalled separately from its parent mod");
  334. QString notes;
  335. QStringList notInstalledDependencies = this->getModsToInstall(mod.getID());
  336. QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies);
  337. if (mod.isInstalled())
  338. notes += replaceIfNotEmpty(getModNames(mod.getID(), notInstalledDependencies), listTemplate.arg(notInstalledDeps));
  339. else
  340. notes += replaceIfNotEmpty(getModNames(mod.getID(), unavailableDependencies), listTemplate.arg(unavailableDeps));
  341. if(mod.isSubmod())
  342. notes += noteTemplate.arg(thisIsSubmod);
  343. if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString())
  344. notes += noteTemplate.arg(translationMismatch);
  345. if(notes.size())
  346. result += textTemplate.arg(tr("Notes")).arg(notes);
  347. return result;
  348. }
  349. QString CModListView::getRepoUrl(const ModState & mod)
  350. {
  351. QUrl url(mod.getDownloadUrl());
  352. QString repoUrl = QString("%1://%2/%3/%4")
  353. .arg(url.scheme())
  354. .arg(url.host())
  355. .arg(url.path().split('/')[1])
  356. .arg(url.path().split('/')[2]);
  357. return repoUrl;
  358. }
  359. void CModListView::disableModInfo()
  360. {
  361. ui->disableButton->setVisible(false);
  362. ui->enableButton->setVisible(false);
  363. ui->installButton->setVisible(false);
  364. ui->uninstallButton->setVisible(false);
  365. ui->updateButton->setVisible(false);
  366. }
  367. auto CModListView::buttonEnabledState(QString modName, ModState & mod)
  368. {
  369. struct result {
  370. bool disableVisible;
  371. bool enableVisible;
  372. bool installVisible;
  373. bool uninstallVisible;
  374. bool updateVisible;
  375. bool directoryVisible;
  376. bool repositoryVisible;
  377. bool disableEnabled;
  378. bool enableEnabled;
  379. bool installEnabled;
  380. bool uninstallEnabled;
  381. bool updateEnabled;
  382. bool directoryEnabled;
  383. bool repositoryEnabled;
  384. } res;
  385. QStringList notInstalledDependencies = getModsToInstall(modName);
  386. QStringList unavailableDependencies = findUnavailableMods(notInstalledDependencies);
  387. bool translationMismatch = mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString();
  388. bool modIsBeingDownloaded = enqueuedModDownloads.contains(mod.getID());
  389. res.disableVisible = modStateModel->isModInstalled(mod.getID()) && modStateModel->isModEnabled(mod.getID());
  390. res.enableVisible = modStateModel->isModInstalled(mod.getID()) && !modStateModel->isModEnabled(mod.getID());
  391. res.installVisible = mod.isAvailable() && !mod.isSubmod();
  392. res.uninstallVisible = mod.isInstalled() && !mod.isSubmod();
  393. res.updateVisible = mod.isUpdateAvailable();
  394. #ifndef VCMI_MOBILE
  395. res.directoryVisible = mod.isInstalled();
  396. #else
  397. res.directoryVisible = false;
  398. #endif
  399. res.repositoryVisible = !mod.getDownloadUrl().isEmpty();
  400. // Block buttons if action is not allowed at this time
  401. res.disableEnabled = true;
  402. res.enableEnabled = notInstalledDependencies.empty() && !translationMismatch;
  403. res.installEnabled = unavailableDependencies.empty() && !modIsBeingDownloaded;
  404. res.uninstallEnabled = true;
  405. res.updateEnabled = unavailableDependencies.empty() && !modIsBeingDownloaded;
  406. res.directoryEnabled = true;
  407. res.repositoryEnabled = true;
  408. return res;
  409. }
  410. void CModListView::onCustomContextMenu(const QPoint &point)
  411. {
  412. QModelIndex index = ui->allModsView->indexAt(point);
  413. if(!index.isValid())
  414. return;
  415. const auto modName = index.data(ModRoles::ModNameRole).toString();
  416. auto mod = modStateModel->getMod(modName);
  417. auto contextMenu = new QMenu(tr("Context menu"), this);
  418. QList<QAction*> actions;
  419. auto addContextEntry = [this, &contextMenu, &actions, mod](bool visible, bool enabled, QIcon icon, QString name, std::function<void(ModState)> function){
  420. if(!visible)
  421. return;
  422. actions.append(new QAction(name, this));
  423. connect(actions.back(), &QAction::triggered, this, [mod, function](){ function(mod); });
  424. contextMenu->addAction(actions.back());
  425. actions.back()->setEnabled(enabled);
  426. actions.back()->setIcon(icon);
  427. };
  428. auto state = buttonEnabledState(modName, mod);
  429. addContextEntry(
  430. state.disableVisible, state.disableEnabled, QIcon{":/icons/mod-disabled.png"},
  431. tr("Disable"),
  432. [this](ModState mod){ disableModByName(mod.getID()); }
  433. );
  434. addContextEntry(
  435. state.enableVisible, state.enableEnabled, QIcon{":/icons/mod-enabled.png"},
  436. tr("Enable"),
  437. [this](ModState mod){ enableModByName(mod.getID());
  438. });
  439. addContextEntry(
  440. state.installVisible, state.installEnabled, QIcon{":/icons/mod-download.png"},
  441. tr("Install"),
  442. [this](ModState mod){ doInstallMod(mod.getID()); }
  443. );
  444. addContextEntry(
  445. state.uninstallVisible, state.uninstallEnabled, QIcon{":/icons/mod-delete.png"},
  446. tr("Uninstall"),
  447. [this](ModState mod){ doUninstallMod(mod.getID()); }
  448. );
  449. addContextEntry(
  450. state.updateVisible, state.updateEnabled, QIcon{":/icons/mod-update.png"},
  451. tr("Update"),
  452. [this](ModState mod){ doUpdateMod(mod.getID()); }
  453. );
  454. addContextEntry(
  455. state.directoryVisible, state.directoryEnabled, QIcon{":/icons/menu-mods.png"},
  456. tr("Open directory"),
  457. [this](ModState mod){ openModDictionary(mod.getID()); }
  458. );
  459. addContextEntry(
  460. state.repositoryVisible, state.repositoryEnabled, QIcon{":/icons/about-project.png"},
  461. tr("Open repository"),
  462. [this](ModState mod){
  463. QString repoUrl = getRepoUrl(mod);
  464. QDesktopServices::openUrl(repoUrl);
  465. }
  466. );
  467. contextMenu->exec(ui->allModsView->viewport()->mapToGlobal(point));
  468. }
  469. void CModListView::dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight)
  470. {
  471. selectMod(ui->allModsView->currentIndex());
  472. }
  473. static void scrollTextBrowserToTop(QTextBrowser* browser)
  474. {
  475. QTimer::singleShot(0, browser, [browser]()
  476. {
  477. browser->moveCursor(QTextCursor::Start);
  478. browser->verticalScrollBar()->setValue(0);
  479. });
  480. }
  481. void CModListView::selectMod(const QModelIndex & index)
  482. {
  483. ui->tabWidget->setCurrentIndex(0);
  484. if(!index.isValid())
  485. {
  486. disableModInfo();
  487. }
  488. else
  489. {
  490. const auto modName = index.data(ModRoles::ModNameRole).toString();
  491. auto mod = modStateModel->getMod(modName);
  492. ui->tabWidget->setTabEnabled(1, !mod.getChangelog().isEmpty());
  493. ui->tabWidget->setTabEnabled(2, !mod.getScreenshots().isEmpty());
  494. ui->modInfoBrowser->document()->setMarkdown(genModInfoText(mod), QTextDocument::MarkdownFeature::MarkdownNoHTML);
  495. ui->changelogBrowser->document()->setMarkdown(genChangelogText(mod), QTextDocument::MarkdownFeature::MarkdownNoHTML);
  496. Helper::enableScrollBySwiping(ui->modInfoBrowser);
  497. Helper::enableScrollBySwiping(ui->changelogBrowser);
  498. scrollTextBrowserToTop(ui->modInfoBrowser);
  499. scrollTextBrowserToTop(ui->changelogBrowser);
  500. auto state = buttonEnabledState(modName, mod);
  501. ui->disableButton->setVisible(state.disableVisible);
  502. ui->enableButton->setVisible(state.enableVisible);
  503. ui->installButton->setVisible(state.installVisible);
  504. ui->uninstallButton->setVisible(state.uninstallVisible);
  505. ui->updateButton->setVisible(state.updateVisible);
  506. // Block buttons if action is not allowed at this time
  507. ui->disableButton->setEnabled(state.disableEnabled);
  508. ui->enableButton->setEnabled(state.enableEnabled);
  509. ui->installButton->setEnabled(state.installEnabled);
  510. ui->uninstallButton->setEnabled(state.uninstallEnabled);
  511. ui->updateButton->setEnabled(state.updateEnabled);
  512. loadScreenshots();
  513. }
  514. }
  515. void CModListView::modSelected(const QModelIndex & current, const QModelIndex &)
  516. {
  517. selectMod(current);
  518. }
  519. void CModListView::on_allModsView_activated(const QModelIndex & index)
  520. {
  521. selectMod(index);
  522. loadScreenshots();
  523. }
  524. void CModListView::on_lineEdit_textChanged(const QString & arg1)
  525. {
  526. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  527. auto baseStr = QRegularExpression::wildcardToRegularExpression(arg1, QRegularExpression::UnanchoredWildcardConversion);
  528. #else
  529. auto baseStr = QRegularExpression::wildcardToRegularExpression(arg1);
  530. //Hack due to lack QRegularExpression::UnanchoredWildcardConversion in Qt5
  531. baseStr.chop(3);
  532. baseStr.remove(0,5);
  533. #endif
  534. QRegularExpression regExp{baseStr, QRegularExpression::CaseInsensitiveOption};
  535. filterModel->setFilterRegularExpression(regExp);
  536. }
  537. void CModListView::on_comboBox_currentIndexChanged(int index)
  538. {
  539. auto enumIndex = static_cast<ModFilterMask>(index);
  540. filterModel->setTypeFilter(enumIndex);
  541. }
  542. QStringList CModListView::findUnavailableMods(QStringList candidates)
  543. {
  544. QStringList invalidMods;
  545. for(QString modName : candidates)
  546. {
  547. if(!modStateModel->isModExists(modName))
  548. invalidMods.push_back(modName);
  549. }
  550. return invalidMods;
  551. }
  552. void CModListView::on_enableButton_clicked()
  553. {
  554. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  555. enableModByName(modName);
  556. checkManagerErrors();
  557. }
  558. void CModListView::enableModByName(QString modName)
  559. {
  560. manager->enableMods({modName});
  561. modModel->modChanged(modName);
  562. }
  563. void CModListView::on_disableButton_clicked()
  564. {
  565. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  566. disableModByName(modName);
  567. checkManagerErrors();
  568. }
  569. void CModListView::disableModByName(QString modName)
  570. {
  571. manager->disableMod(modName);
  572. modModel->modChanged(modName);
  573. }
  574. QStringList CModListView::getModsToInstall(QString mod)
  575. {
  576. QStringList result;
  577. QStringList candidates;
  578. QStringList processed;
  579. candidates.push_back(mod);
  580. while (!candidates.empty())
  581. {
  582. QString potentialToInstall = candidates.back();
  583. candidates.pop_back();
  584. processed.push_back(potentialToInstall);
  585. if (modStateModel->isSubmod(potentialToInstall))
  586. {
  587. QString topParent = modStateModel->getTopParent(potentialToInstall);
  588. if (modStateModel->isModInstalled(topParent))
  589. {
  590. if (modStateModel->isModUpdateAvailable(topParent))
  591. potentialToInstall = modStateModel->getTopParent(potentialToInstall);
  592. // else - potentially broken mod that depends on non-existing submod
  593. }
  594. else
  595. potentialToInstall = modStateModel->getTopParent(potentialToInstall);
  596. }
  597. if (!modStateModel->isModInstalled(potentialToInstall))
  598. result.push_back(potentialToInstall);
  599. if (modStateModel->isModExists(potentialToInstall))
  600. {
  601. QStringList dependencies = modStateModel->getMod(potentialToInstall).getDependencies();
  602. for (const auto & dependency : dependencies)
  603. {
  604. if (!processed.contains(dependency) && !candidates.contains(dependency))
  605. candidates.push_back(dependency);
  606. }
  607. }
  608. }
  609. result.removeDuplicates();
  610. return result;
  611. }
  612. void CModListView::on_updateButton_clicked()
  613. {
  614. QModelIndex selectedMod = ui->allModsView->currentIndex();
  615. if (!selectedMod.isValid())
  616. {
  617. logGlobal->error("Update failed! Invalid index selected but update button is not locked!");
  618. return;
  619. }
  620. QString modName = selectedMod.data(ModRoles::ModNameRole).toString();
  621. if (modName.isEmpty())
  622. {
  623. logGlobal->error("Update failed! Model index is valid but mod name is empty!");
  624. return;
  625. }
  626. doUpdateMod(modName);
  627. ui->updateButton->setEnabled(false);
  628. }
  629. void CModListView::doUpdateMod(const QString & modName)
  630. {
  631. auto targetMod = modStateModel->getMod(modName);
  632. if(targetMod.isUpdateAvailable())
  633. downloadMod(targetMod);
  634. for(const auto & name : getModsToInstall(modName))
  635. {
  636. auto mod = modStateModel->getMod(name);
  637. // update required mod, install missing (can be new dependency)
  638. if(mod.isUpdateAvailable() || !mod.isInstalled())
  639. downloadMod(mod);
  640. }
  641. }
  642. void CModListView::openModDictionary(const QString & modName)
  643. {
  644. QString tmp = modName;
  645. tmp.replace(".", "/Mods/");
  646. ResourcePath resID(std::string("Mods/") + tmp.toStdString(), EResType::DIRECTORY);
  647. // Get location of the mod, in case-insensitive way
  648. QString modDir = pathToQString(*CResourceHandler::get()->getResourceName(resID));
  649. Helper::revealDirectoryInFileBrowser(modDir);
  650. }
  651. void CModListView::on_uninstallButton_clicked()
  652. {
  653. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  654. doUninstallMod(modName);
  655. checkManagerErrors();
  656. }
  657. void CModListView::on_installButton_clicked()
  658. {
  659. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  660. doInstallMod(modName);
  661. ui->installButton->setEnabled(false);
  662. }
  663. void CModListView::downloadMod(const ModState & mod)
  664. {
  665. if (enqueuedModDownloads.contains(mod.getID()))
  666. return;
  667. enqueuedModDownloads.push_back(mod.getID());
  668. downloadFile(mod.getID() + ".zip", mod.getDownloadUrl(), mod.getName(), mod.getDownloadSizeBytes());
  669. }
  670. void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes)
  671. {
  672. if(!dlManager)
  673. {
  674. dlManager = new CDownloadManager();
  675. ui->progressWidget->setVisible(true);
  676. connect(dlManager, SIGNAL(downloadProgress(qint64,qint64)),
  677. this, SLOT(downloadProgress(qint64,qint64)));
  678. connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)),
  679. this, SLOT(downloadFinished(QStringList,QStringList,QStringList)));
  680. connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)),
  681. this, SLOT(extractionProgress(qint64,qint64)));
  682. connect(modModel, &ModStateItemModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged);
  683. const auto progressBarFormat = tr("Downloading %1. %p% (%v MB out of %m MB) finished").arg(description);
  684. ui->progressBar->setFormat(progressBarFormat);
  685. }
  686. Helper::keepScreenOn(true);
  687. dlManager->downloadFile(url, file, sizeBytes);
  688. }
  689. void CModListView::downloadProgress(qint64 current, qint64 max)
  690. {
  691. // display progress, in megabytes
  692. ui->progressBar->setVisible(true);
  693. ui->progressBar->setMaximum(max / (1024 * 1024));
  694. ui->progressBar->setValue(current / (1024 * 1024));
  695. }
  696. void CModListView::extractionProgress(qint64 current, qint64 max)
  697. {
  698. // display progress, in extracted files
  699. ui->progressBar->setVisible(true);
  700. ui->progressBar->setMaximum(max);
  701. ui->progressBar->setValue(current);
  702. }
  703. void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors)
  704. {
  705. QString title = tr("Download failed");
  706. QString firstLine = tr("Unable to download all files.\n\nEncountered errors:\n\n");
  707. QString lastLine = tr("\n\nInstall successfully downloaded?");
  708. bool doInstallFiles = false;
  709. // if all files were d/loaded there should be no errors. And on failure there must be an error
  710. assert(failedFiles.empty() == errors.empty());
  711. if(savedFiles.empty())
  712. {
  713. // no successfully downloaded mods
  714. QMessageBox::warning(this, title, firstLine + errors.join("\n"), QMessageBox::Ok, QMessageBox::Ok);
  715. }
  716. else if(!failedFiles.empty())
  717. {
  718. // some mods were not downloaded
  719. int result = QMessageBox::warning (this, title, firstLine + errors.join("\n") + lastLine,
  720. QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
  721. if(result == QMessageBox::Yes)
  722. doInstallFiles = true;
  723. }
  724. else
  725. {
  726. // everything OK
  727. doInstallFiles = true;
  728. }
  729. enqueuedModDownloads.clear();
  730. dlManager->deleteLater();
  731. dlManager = nullptr;
  732. ui->progressBar->setMaximum(0);
  733. ui->progressBar->setValue(0);
  734. if(doInstallFiles)
  735. installFiles(savedFiles);
  736. Helper::keepScreenOn(false);
  737. hideProgressBar();
  738. }
  739. void CModListView::hideProgressBar()
  740. {
  741. if(dlManager == nullptr) // it was not recreated meanwhile
  742. {
  743. ui->progressWidget->setVisible(false);
  744. ui->progressBar->setMaximum(0);
  745. ui->progressBar->setValue(0);
  746. }
  747. }
  748. void CModListView::installFiles(QStringList files)
  749. {
  750. QStringList mods;
  751. QStringList maps;
  752. QStringList images;
  753. QStringList exe;
  754. bool repositoryFilesEnqueued = false;
  755. // TODO: some better way to separate zip's with mods and downloaded repository files
  756. for(QString filename : files)
  757. {
  758. QString realFilename = Helper::getRealPath(filename);
  759. if(realFilename.endsWith(".zip", Qt::CaseInsensitive))
  760. {
  761. try {
  762. // TODO: there is some weird crash on Android where this constructor fails to open file
  763. ZipArchive archive(qstringToPath(realFilename));
  764. auto fileList = archive.listFiles();
  765. bool hasModJson = false;
  766. bool hasMaps = false;
  767. for (const auto& file : fileList)
  768. {
  769. QString lower = QString::fromStdString(file).toLower();
  770. // Check for mod.json anywhere in archive
  771. if (lower.endsWith("mod.json"))
  772. hasModJson = true;
  773. // Check for map files anywhere
  774. if (lower.endsWith(".h3m") || lower.endsWith(".h3c") || lower.endsWith(".vmap") || lower.endsWith(".vcmp"))
  775. hasMaps = true;
  776. }
  777. if (hasModJson)
  778. mods.push_back(filename);
  779. else if (hasMaps)
  780. maps.push_back(filename);
  781. else
  782. mods.push_back(filename);
  783. }
  784. catch (const std::runtime_error & e)
  785. {
  786. QMessageBox::warning(this, tr("Import failed"), tr("Failed to install file %1.\nReason: %2.\nPlease report this issue to developers").arg(filename).arg(QString::fromStdString(e.what())));
  787. }
  788. }
  789. else if(realFilename.endsWith(".h3m", Qt::CaseInsensitive) || realFilename.endsWith(".h3c", Qt::CaseInsensitive) || realFilename.endsWith(".vmap", Qt::CaseInsensitive) || realFilename.endsWith(".vcmp", Qt::CaseInsensitive))
  790. maps.push_back(filename);
  791. if(realFilename.endsWith(".exe", Qt::CaseInsensitive))
  792. exe.push_back(filename);
  793. else if(realFilename.endsWith(".json", Qt::CaseInsensitive))
  794. {
  795. //download and merge additional files
  796. JsonNode repoData = JsonUtils::jsonFromFile(filename);
  797. if(repoData["name"].isNull())
  798. {
  799. // MODS COMPATIBILITY: in 1.6, repository list contains mod list directly, in 1.7 it is located in 'availableMods' node
  800. const auto & availableRepositoryMods = repoData["availableMods"].isNull() ? repoData : repoData["availableMods"];
  801. // This is main repository index. Download all referenced mods
  802. for(const auto & [modName, modJson] : availableRepositoryMods.Struct())
  803. {
  804. auto modNameLower = boost::algorithm::to_lower_copy(modName);
  805. auto modJsonUrl = modJson["mod"];
  806. auto modDescriptionUrl = modJson["descriptionURL"];
  807. if(!modJsonUrl.isNull())
  808. {
  809. downloadFile(QString::fromStdString(modName + ".json"), QString::fromStdString(modJsonUrl.String()), tr("mods repository index"));
  810. repositoryFilesEnqueued = true;
  811. }
  812. if(!modDescriptionUrl.isNull())
  813. {
  814. downloadFile(QString::fromStdString(modName + ".md"), QString::fromStdString(modDescriptionUrl.String()), tr("mods repository index"));
  815. repositoryFilesEnqueued = true;
  816. }
  817. accumulatedRepositoryData[modNameLower] = modJson;
  818. }
  819. }
  820. else
  821. {
  822. // This is json of a single mod. Extract name of mod and add it to repo
  823. auto modName = QFileInfo(filename).baseName().toStdString();
  824. auto modNameLower = boost::algorithm::to_lower_copy(modName);
  825. JsonUtils::merge(accumulatedRepositoryData[modNameLower], repoData);
  826. }
  827. }
  828. else if(realFilename.endsWith(".png", Qt::CaseInsensitive))
  829. images.push_back(filename);
  830. else if(realFilename.endsWith(".md", Qt::CaseInsensitive))
  831. {
  832. // This is description of a single mod. Extract name of mod and add it to index
  833. auto modName = QFileInfo(filename).baseName().toStdString();
  834. auto modNameLower = boost::algorithm::to_lower_copy(modName);
  835. QFile file(realFilename);
  836. if(file.open(QFile::ReadOnly))
  837. {
  838. const auto data = file.readAll();
  839. std::string modDescriptions(data.data(), data.size());
  840. ModDescription::mergeModDescriptions(accumulatedRepositoryData[modNameLower], modDescriptions);
  841. }
  842. else
  843. logGlobal->error("Failed to open file %s. Reason: %s", qUtf8Printable(filename), qUtf8Printable(file.errorString()));
  844. }
  845. }
  846. if (!accumulatedRepositoryData.isNull() && !repositoryFilesEnqueued)
  847. {
  848. logGlobal->info("Installing repository: started");
  849. manager->setRepositoryData(accumulatedRepositoryData);
  850. modModel->reloadViewModel();
  851. accumulatedRepositoryData.clear();
  852. static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json";
  853. JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData());
  854. logGlobal->info("Installing repository: ended");
  855. }
  856. if(!mods.empty())
  857. {
  858. logGlobal->info("Installing mods: started");
  859. installMods(mods);
  860. logGlobal->info("Installing mods: ended");
  861. }
  862. if(!maps.empty())
  863. {
  864. logGlobal->info("Installing maps: started");
  865. installMaps(maps);
  866. logGlobal->info("Installing maps: ended");
  867. }
  868. if(!exe.empty())
  869. {
  870. logGlobal->info("Installing chronicles: started");
  871. ui->progressBar->setFormat(tr("Installing Heroes Chronicles"));
  872. ui->progressWidget->setVisible(true);
  873. ui->abortButton->setEnabled(false);
  874. float prog = 0.0;
  875. Helper::keepScreenOn(true);
  876. auto futureExtract = std::async(std::launch::async, [this, exe, &prog]()
  877. {
  878. ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
  879. return ce.installChronicles(exe);
  880. });
  881. while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
  882. {
  883. extractionProgress(static_cast<int>(prog * 1000.f), 1000);
  884. qApp->processEvents();
  885. }
  886. const auto extractResult = futureExtract.get();
  887. Helper::keepScreenOn(false);
  888. hideProgressBar();
  889. ui->abortButton->setEnabled(true);
  890. ui->progressWidget->setVisible(false);
  891. //update
  892. reload("chronicles");
  893. if(modStateModel->isModExists("chronicles"))
  894. enableModByName("chronicles");
  895. logGlobal->info("Installing chronicles: ended");
  896. if(extractResult & ChroniclesExtractor::ChroniclesInstallResultMask::ExtractError)
  897. QMessageBox::critical(this, {}, tr("Extracting error!"));
  898. if(extractResult & ChroniclesExtractor::ChroniclesInstallResultMask::InvalidFile)
  899. QMessageBox::critical(this, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!"));
  900. }
  901. if(!images.empty())
  902. loadScreenshots();
  903. }
  904. void CModListView::installMods(QStringList archives)
  905. {
  906. QStringList modNames;
  907. QStringList modsToEnable;
  908. for(QString archive : archives)
  909. {
  910. // get basename out of full file name
  911. // remove path remove extension
  912. QString modName = archive.section('/', -1, -1).section('.', 0, 0);
  913. modNames.push_back(modName);
  914. }
  915. if (!activatingPreset.isEmpty())
  916. {
  917. modStateModel->activatePreset(activatingPreset);
  918. activatingPreset.clear();
  919. }
  920. // uninstall old version of mod, if installed
  921. for(QString mod : modNames)
  922. {
  923. if(modStateModel->isModExists(mod) && modStateModel->getMod(mod).isInstalled())
  924. {
  925. logGlobal->info("Uninstalling old version of mod '%s'", mod.toStdString());
  926. if (modStateModel->isModEnabled(mod))
  927. modsToEnable.push_back(mod);
  928. doUninstallMod(mod, true);
  929. }
  930. else
  931. {
  932. // installation of previously not present mod -> enable it
  933. modsToEnable.push_back(mod);
  934. }
  935. }
  936. QString lastInstalled;
  937. for(int i = 0; i < modNames.size(); i++)
  938. {
  939. logGlobal->info("Installing mod '%s'", modNames[i].toStdString());
  940. QString modDisplayName = modNames[i];
  941. if (modStateModel->isModExists(modNames[i]))
  942. modDisplayName = modStateModel->getMod(modNames[i]).getName();
  943. ui->progressBar->setFormat(tr("Installing mod %1").arg(modDisplayName));
  944. manager->installMod(modNames[i], archives[i]);
  945. if (i == modNames.size() - 1 && modStateModel->isModExists(modNames[i]))
  946. lastInstalled = modStateModel->getMod(modNames[i]).getID();
  947. }
  948. reload(lastInstalled);
  949. if (!modsToEnable.empty())
  950. {
  951. manager->enableMods(modsToEnable);
  952. }
  953. checkManagerErrors();
  954. for(QString archive : archives)
  955. {
  956. logGlobal->info("Erasing archive '%s'", archive.toStdString());
  957. QFile::remove(archive);
  958. }
  959. }
  960. void CModListView::installMaps(QStringList maps)
  961. {
  962. const auto destDir = CLauncherDirs::mapsPath() + QChar{'/'};
  963. int successCount = 0;
  964. QStringList failedMaps;
  965. // Pre-scan maps to count total conflicts (used for Yes to All/No to All)
  966. int conflictCount = 0;
  967. for (const QString& map : maps)
  968. {
  969. if (map.endsWith(".zip", Qt::CaseInsensitive))
  970. {
  971. ZipArchive archive(qstringToPath(map));
  972. for (const auto& file : archive.listFiles())
  973. {
  974. QString name = QString::fromStdString(file);
  975. if (name.endsWith(".h3m", Qt::CaseInsensitive) || name.endsWith(".h3c", Qt::CaseInsensitive) ||
  976. name.endsWith(".vmap", Qt::CaseInsensitive) || name.endsWith(".vcmp", Qt::CaseInsensitive))
  977. {
  978. if (QFile::exists(destDir + name))
  979. conflictCount++;
  980. }
  981. }
  982. }
  983. else
  984. {
  985. QString srcPath = Helper::getRealPath(map);
  986. QString fileName = QFileInfo(srcPath).fileName();
  987. QString destFile = destDir + fileName;
  988. if (QFile::exists(destFile))
  989. conflictCount++;
  990. }
  991. }
  992. bool applyToAll = false;
  993. bool overwriteAll = false;
  994. auto askOverwrite = [&](const QString& name) -> bool {
  995. if (applyToAll)
  996. return overwriteAll;
  997. QMessageBox msgBox(this);
  998. msgBox.setIcon(QMessageBox::Question);
  999. msgBox.setWindowTitle(tr("Map exists"));
  1000. msgBox.setText(tr("Map '%1' already exists. Do you want to overwrite it?").arg(name));
  1001. QPushButton* yes = msgBox.addButton(QMessageBox::Yes);
  1002. msgBox.addButton(QMessageBox::No);
  1003. QPushButton* yesAll = nullptr;
  1004. QPushButton* noAll = nullptr;
  1005. if (conflictCount > 1)
  1006. {
  1007. yesAll = msgBox.addButton(tr("Yes to All"), QMessageBox::YesRole);
  1008. noAll = msgBox.addButton(tr("No to All"), QMessageBox::NoRole);
  1009. }
  1010. msgBox.exec();
  1011. QAbstractButton* clicked = msgBox.clickedButton();
  1012. if (clicked == yes)
  1013. return true;
  1014. if (clicked == yesAll)
  1015. {
  1016. applyToAll = true;
  1017. overwriteAll = true;
  1018. return true;
  1019. }
  1020. if (clicked == noAll)
  1021. {
  1022. applyToAll = true;
  1023. overwriteAll = false;
  1024. return false;
  1025. }
  1026. return false;
  1027. };
  1028. // Process each map file and archive
  1029. for (const QString& map : maps)
  1030. {
  1031. if (map.endsWith(".zip", Qt::CaseInsensitive))
  1032. {
  1033. // ZIP archive
  1034. ZipArchive archive(qstringToPath(map));
  1035. for (const auto& file : archive.listFiles())
  1036. {
  1037. QString name = QString::fromStdString(file);
  1038. if (!(name.endsWith(".h3m", Qt::CaseInsensitive) || name.endsWith(".h3c", Qt::CaseInsensitive) ||
  1039. name.endsWith(".vmap", Qt::CaseInsensitive) || name.endsWith(".vcmp", Qt::CaseInsensitive)))
  1040. continue;
  1041. QString destFile = destDir + name;
  1042. logGlobal->info("Importing map '%s' from ZIP '%s'", name.toStdString(), map.toStdString());
  1043. if (QFile::exists(destFile))
  1044. {
  1045. if (!askOverwrite(name))
  1046. {
  1047. logGlobal->info("Skipped map '%s'", name.toStdString());
  1048. continue;
  1049. }
  1050. QFile::remove(destFile);
  1051. }
  1052. if (archive.extract(qstringToPath(destDir), file))
  1053. successCount++;
  1054. else
  1055. {
  1056. logGlobal->warn("Failed to extract map '%s'", name.toStdString());
  1057. failedMaps.push_back(name);
  1058. }
  1059. }
  1060. }
  1061. else
  1062. {
  1063. // Single map file
  1064. QString srcPath = Helper::getRealPath(map);
  1065. QString fileName = QFileInfo(srcPath).fileName();
  1066. QString destFile = destDir + fileName;
  1067. logGlobal->info("Importing map '%s'", srcPath.toStdString());
  1068. if (QFile::exists(destFile))
  1069. {
  1070. if (!askOverwrite(fileName))
  1071. {
  1072. logGlobal->info("Skipped map '%s'", fileName.toStdString());
  1073. continue;
  1074. }
  1075. QFile::remove(destFile);
  1076. }
  1077. if (Helper::performNativeCopy(map, destFile))
  1078. successCount++;
  1079. else
  1080. {
  1081. logGlobal->warn("Failed to copy map '%s'", fileName.toStdString());
  1082. failedMaps.push_back(fileName);
  1083. }
  1084. }
  1085. }
  1086. if (successCount > 0)
  1087. QMessageBox::information(this, tr("Import complete"), tr("%n map(s) successfully imported.", "", successCount));
  1088. if (!failedMaps.isEmpty())
  1089. QMessageBox::warning(this, tr("Import failed"), tr("Failed to import the following maps:\n%1").arg(failedMaps.join("\n")));
  1090. }
  1091. void CModListView::on_refreshButton_clicked()
  1092. {
  1093. loadRepositories();
  1094. }
  1095. void CModListView::on_abortButton_clicked()
  1096. {
  1097. delete dlManager;
  1098. dlManager = nullptr;
  1099. Helper::keepScreenOn(false);
  1100. hideProgressBar();
  1101. }
  1102. void CModListView::modelReset()
  1103. {
  1104. ui->allModsView->setCurrentIndex(filterModel->rowCount() > 0 ? filterModel->index(0, 0) : QModelIndex());
  1105. }
  1106. void CModListView::checkManagerErrors()
  1107. {
  1108. QString errors = manager->getErrors().join('\n');
  1109. if(errors.size() != 0)
  1110. {
  1111. QString title = tr("Operation failed");
  1112. QString description = tr("Encountered errors:\n") + errors;
  1113. QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok);
  1114. }
  1115. }
  1116. void CModListView::on_tabWidget_currentChanged(int index)
  1117. {
  1118. loadScreenshots();
  1119. }
  1120. void CModListView::loadScreenshots()
  1121. {
  1122. if(ui->tabWidget->currentIndex() != 2)
  1123. return;
  1124. assert(ui->allModsView->currentIndex().isValid());
  1125. if (!ui->allModsView->currentIndex().isValid())
  1126. return;
  1127. ui->screenshotsList->clear();
  1128. QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
  1129. assert(modStateModel->isModExists(modName)); //should be filtered out by check above
  1130. const auto localScreenshotsPath = QString{QLatin1String{"%1/%2/screenshots"}}.arg(CLauncherDirs::modsPath(), modName);
  1131. for(QString url : modStateModel->getMod(modName).getScreenshots())
  1132. {
  1133. // URL must be encoded to something else to get rid of symbols illegal in file names
  1134. const auto hashed = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5);
  1135. const auto fileName = QString{QLatin1String{"%1.png"}}.arg(QLatin1String{hashed.toHex()});
  1136. const auto originalFileName = QUrl{url}.fileName(QUrl::FullyDecoded);
  1137. const QStringList fullPaths = {
  1138. QString{QLatin1String{"%1/%2"}}.arg(localScreenshotsPath, originalFileName),
  1139. QString{QLatin1String{"%1/%2"}}.arg(localScreenshotsPath, fileName),
  1140. QString{QLatin1String{"%1/%2"}}.arg(CLauncherDirs::downloadsPath(), fileName)
  1141. };
  1142. QPixmap pixmap;
  1143. QString loadedPath;
  1144. for(const auto & fullPath : fullPaths)
  1145. {
  1146. pixmap.load(fullPath);
  1147. if(!pixmap.isNull())
  1148. {
  1149. loadedPath = fullPath;
  1150. break;
  1151. }
  1152. }
  1153. if(pixmap.isNull())
  1154. {
  1155. // image file not exists or corrupted - try to redownload
  1156. downloadFile(fileName, url, tr("screenshots"));
  1157. }
  1158. else
  1159. {
  1160. // managed to load cached image
  1161. QIcon icon(pixmap);
  1162. auto * item = new QListWidgetItem(icon, QString(tr("Screenshot %1")).arg(ui->screenshotsList->count() + 1));
  1163. item->setData(Qt::UserRole, loadedPath);
  1164. ui->screenshotsList->addItem(item);
  1165. }
  1166. }
  1167. }
  1168. void CModListView::on_screenshotsList_clicked(const QModelIndex & index)
  1169. {
  1170. if(index.isValid())
  1171. {
  1172. QStringList imagePaths;
  1173. for(int i = 0; i < ui->screenshotsList->count(); ++i)
  1174. {
  1175. auto * item = ui->screenshotsList->item(i);
  1176. const auto path = item->data(Qt::UserRole).toString();
  1177. if(!path.isEmpty())
  1178. imagePaths.push_back(path);
  1179. }
  1180. if(!imagePaths.empty())
  1181. ImageViewer::showImages(imagePaths, index.row(), this);
  1182. }
  1183. }
  1184. void CModListView::doInstallMod(const QString & modName)
  1185. {
  1186. for(const auto & name : getModsToInstall(modName))
  1187. {
  1188. auto mod = modStateModel->getMod(name);
  1189. if(mod.isAvailable())
  1190. downloadMod(mod);
  1191. else if(!modStateModel->isModEnabled(name))
  1192. enableModByName(name);
  1193. }
  1194. }
  1195. void CModListView::doUninstallMod(const QString & modName, bool silent)
  1196. {
  1197. if(!modStateModel->isModExists(modName) || !modStateModel->getMod(modName).isInstalled())
  1198. return;
  1199. if(!silent)
  1200. {
  1201. int result = QMessageBox::question(this, tr("Uninstall mod"), tr("Are you sure you want to uninstall %1?").arg(modStateModel->getMod(modName).getName()), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
  1202. if(result != QMessageBox::Yes)
  1203. return;
  1204. }
  1205. if(modStateModel->isModEnabled(modName))
  1206. manager->disableMod(modName);
  1207. manager->uninstallMod(modName);
  1208. reload(modName);
  1209. }
  1210. bool CModListView::isModAvailable(const QString & modName)
  1211. {
  1212. return modStateModel->isModExists(modName) && !modStateModel->isModInstalled(modName);
  1213. }
  1214. bool CModListView::isModEnabled(const QString & modName)
  1215. {
  1216. return modStateModel->isModEnabled(modName);
  1217. }
  1218. bool CModListView::isModInstalled(const QString & modName)
  1219. {
  1220. if(!modStateModel->isModExists(modName))
  1221. return false;
  1222. auto mod = modStateModel->getMod(modName);
  1223. return mod.isInstalled();
  1224. }
  1225. QStringList CModListView::getInstalledChronicles()
  1226. {
  1227. QStringList result;
  1228. for(const auto & modName : modStateModel->getAllMods())
  1229. {
  1230. auto mod = modStateModel->getMod(modName);
  1231. if (!mod.isInstalled())
  1232. continue;
  1233. if (mod.getTopParentID() != "chronicles")
  1234. continue;
  1235. result += modName;
  1236. }
  1237. return result;
  1238. }
  1239. bool CModListView::isInstalledHd()
  1240. {
  1241. for(const auto & modName : modStateModel->getAllMods())
  1242. {
  1243. auto mod = modStateModel->getMod(modName);
  1244. if (!mod.isInstalled())
  1245. continue;
  1246. if (mod.getID() == "hd-edition")
  1247. return true;
  1248. }
  1249. return false;
  1250. }
  1251. QStringList CModListView::getUpdateableMods()
  1252. {
  1253. QStringList result;
  1254. for(const auto & modName : modStateModel->getAllMods())
  1255. {
  1256. auto mod = modStateModel->getMod(modName);
  1257. if (!mod.isUpdateAvailable())
  1258. continue;
  1259. QStringList notInstalledDependencies = getModsToInstall(mod.getID());
  1260. QStringList unavailableDependencies = findUnavailableMods(notInstalledDependencies);
  1261. if (unavailableDependencies.empty())
  1262. result.push_back(modName);
  1263. }
  1264. return result;
  1265. }
  1266. QString CModListView::getTranslationModName(const QString & language)
  1267. {
  1268. for(const auto & modName : modStateModel->getAllMods())
  1269. {
  1270. auto mod = modStateModel->getMod(modName);
  1271. if (!mod.isTranslation())
  1272. continue;
  1273. if (mod.getBaseLanguage() != language)
  1274. continue;
  1275. return modName;
  1276. }
  1277. return QString();
  1278. }
  1279. void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
  1280. {
  1281. if(!index.isValid())
  1282. return;
  1283. auto modName = index.data(ModRoles::ModNameRole).toString();
  1284. auto mod = modStateModel->getMod(modName);
  1285. QStringList notInstalledDependencies = this->getModsToInstall(mod.getID());
  1286. QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies);
  1287. if(unavailableDependencies.empty() && mod.isAvailable() && !mod.isSubmod())
  1288. {
  1289. on_installButton_clicked();
  1290. return;
  1291. }
  1292. if(unavailableDependencies.empty() && mod.isUpdateAvailable() && index.column() == ModFields::STATUS_UPDATE)
  1293. {
  1294. on_updateButton_clicked();
  1295. return;
  1296. }
  1297. if(index.column() == ModFields::NAME)
  1298. {
  1299. if(ui->allModsView->isExpanded(index))
  1300. ui->allModsView->collapse(index);
  1301. else
  1302. ui->allModsView->expand(index);
  1303. return;
  1304. }
  1305. if(notInstalledDependencies.empty() && !modStateModel->isModEnabled(modName))
  1306. {
  1307. on_enableButton_clicked();
  1308. return;
  1309. }
  1310. if(modStateModel->isModEnabled(modName))
  1311. {
  1312. on_disableButton_clicked();
  1313. return;
  1314. }
  1315. }
  1316. void CModListView::createNewPreset(const QString & presetName)
  1317. {
  1318. modStateModel->createNewPreset(presetName);
  1319. }
  1320. void CModListView::deletePreset(const QString & presetName)
  1321. {
  1322. modStateModel->deletePreset(presetName);
  1323. }
  1324. void CModListView::activatePreset(const QString & presetName)
  1325. {
  1326. modStateModel->activatePreset(presetName);
  1327. reload();
  1328. }
  1329. void CModListView::renamePreset(const QString & oldPresetName, const QString & newPresetName)
  1330. {
  1331. modStateModel->renamePreset(oldPresetName, newPresetName);
  1332. }
  1333. QStringList CModListView::getAllPresets() const
  1334. {
  1335. return modStateModel->getAllPresets();
  1336. }
  1337. QString CModListView::getActivePreset() const
  1338. {
  1339. return modStateModel->getActivePreset();
  1340. }
  1341. JsonNode CModListView::exportCurrentPreset() const
  1342. {
  1343. return modStateModel->exportCurrentPreset();
  1344. }
  1345. void CModListView::importPreset(const JsonNode & data)
  1346. {
  1347. const auto & [presetName, modList] = modStateModel->importPreset(data);
  1348. if (modList.empty())
  1349. {
  1350. modStateModel->activatePreset(presetName);
  1351. modStateModel->reloadLocalState();
  1352. }
  1353. else
  1354. {
  1355. activatingPreset = presetName;
  1356. for (const auto & modID : modList)
  1357. {
  1358. if (modStateModel->isModExists(modID))
  1359. doInstallMod(modID);
  1360. }
  1361. }
  1362. }