diff options
Diffstat (limited to 'launcher/ui/pages')
32 files changed, 1202 insertions, 1454 deletions
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 1edba499..6e1e2183 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -161,10 +161,11 @@ void AccountListPage::on_actionAddMicrosoft_triggered() CustomMessageBox::selectable( this, tr("Microsoft Accounts not available"), + //: %1 refers to the launcher itself tr( - "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated PolyMC.\n\n" - "Please update both your operating system and PolyMC." - ), + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated %1.\n\n" + "Please update both your operating system and %1." + ).arg(BuildConfig.LAUNCHER_NAME), QMessageBox::Warning )->exec(); return; diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 3eb4bd59..f0616db1 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -95,6 +96,7 @@ void JavaPage::applySettings() // Java Settings s->set("JavaPath", ui->javaPathTextBox->text()); s->set("JvmArgs", ui->jvmArgsTextBox->text()); + s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); } void JavaPage::loadSettings() @@ -118,6 +120,7 @@ void JavaPage::loadSettings() // Java Settings ui->javaPathTextBox->setText(s->get("JavaPath").toString()); ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); + ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); } void JavaPage::on_javaDetectBtn_clicked() diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index b67e9994..d27b200f 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -222,6 +222,22 @@ </property> </widget> </item> + <item row="4" column="1"> + <widget class="QCheckBox" name="skipCompatibilityCheckbox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string> + </property> + <property name="text"> + <string>Skip Java compatibility checks</string> + </property> + </widget> + </item> </layout> </widget> </item> diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 6f7e1cc7..42ad5ae3 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (c) 2022 dada513 <dada513@protonmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -134,13 +135,31 @@ void LauncherPage::on_instDirBrowseBtn_clicked() warning.setInformativeText( tr("Do you really want to use this path? " "Selecting \"No\" will close this and not alter your instance path.")); - warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); - if (result == QMessageBox::Yes) + if (result == QMessageBox::Ok) { ui->instDirTextBox->setText(cooked_dir); } } + else if(APPLICATION->isFlatpak() && raw_dir.startsWith("/run/user")) + { + QMessageBox warning; + warning.setText(tr("You're trying to specify an instance folder " + "which was granted temporaily via Flatpak.\n" + "This is known to cause problems. " + "After a restart the launcher might break, " + "because it will no longer have access to that directory.\n\n" + "Granting PolyMC access to it via Flatseal is recommended.")); + warning.setInformativeText( + tr("Do you want to proceed anyway?")); + warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + int result = warning.exec(); + if (result == QMessageBox::Ok) + { + ui->instDirTextBox->setText(cooked_dir); + } + } else { ui->instDirTextBox->setText(cooked_dir); diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 9abae425..f49f5a92 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -94,6 +94,7 @@ void MinecraftPage::applySettings() // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); + s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); } void MinecraftPage::loadSettings() @@ -113,6 +114,7 @@ void MinecraftPage::loadSettings() ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); + ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index a28b1f59..c18ab34b 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -173,13 +173,23 @@ <item> <widget class="QCheckBox" name="closeAfterLaunchCheck"> <property name="toolTip"> - <string><html><head/><body><p>PolyMC will automatically reopen when the game crashes or exits.</p></body></html></string> + <string><html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html></string> </property> <property name="text"> - <string>Close PolyMC after game window opens</string> + <string>Close the launcher after game window opens</string> </property> </widget> </item> + <item> + <widget class="QCheckBox" name="quitAfterGameStopCheck"> + <property name="toolTip"> + <string><html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html></string> + </property> + <property name="text"> + <string>Quit the launcher after game window closes</string> + </property> + </widget> + </item> </layout> </widget> </item> diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index e68a7124..a48c4d69 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -165,10 +165,12 @@ void InstanceSettingsPage::applySettings() if (javaInstall) { m_settings->set("JavaPath", ui->javaPathTextBox->text()); + m_settings->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); } else { m_settings->reset("JavaPath"); + m_settings->reset("IgnoreJavaCompatibility"); } // Java arguments @@ -177,7 +179,6 @@ void InstanceSettingsPage::applySettings() if(javaArgs) { m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); - JavaCommon::checkJVMArgs(m_settings->get("JvmArgs").toString(), this->parentWidget()); } else { @@ -286,6 +287,7 @@ void InstanceSettingsPage::loadSettings() ui->javaSettingsGroupBox->setChecked(overrideLocation); ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); + ui->skipCompatibilityCheckbox->setChecked(m_settings->get("IgnoreJavaCompatibility").toBool()); ui->javaArgumentsGroupBox->setChecked(overrideArgs); ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString()); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 729f8e2a..5db2d147 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -85,6 +85,16 @@ </property> </widget> </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="skipCompatibilityCheckbox"> + <property name="toolTip"> + <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string> + </property> + <property name="text"> + <string>Skip Java compatibility checks</string> + </property> + </widget> + </item> </layout> </widget> </item> diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 599f0e11..8113fe85 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -56,8 +56,11 @@ #include "minecraft/VersionFilterData.h" #include "minecraft/PackProfile.h" +#include "modplatform/ModAPI.h" + #include "Version.h" #include "ui/dialogs/ProgressDialog.h" +#include "tasks/SequentialTask.h" namespace { // FIXME: wasteful @@ -387,32 +390,31 @@ void ModFolderPage::on_actionInstall_mods_triggered() if(m_inst->typeName() != "Minecraft"){ return; //this is a null instance or a legacy instance } - bool hasFabric = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - bool hasForge = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.minecraftforge").isEmpty(); - if (!hasFabric && !hasForge) { + auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); + if (profile->getModLoader() == ModAPI::Unspecified) { QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); return; } ModDownloadDialog mdownload(m_mods, this, m_inst); - if(mdownload.exec()) { - for(auto task : mdownload.getTasks()){ - connect(task, &Task::failed, [this, task](QString reason) { - task->deleteLater(); - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - }); - connect(task, &Task::succeeded, [this, task]() { - QStringList warnings = task->warnings(); - if (warnings.count()) { - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), - QMessageBox::Warning)->show(); - } - task->deleteLater(); - }); - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(task); - m_mods->update(); + if (mdownload.exec()) { + SequentialTask* tasks = new SequentialTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } + tasks->deleteLater(); + }); + + for (auto task : mdownload.getTasks()) { + tasks->addTask(task); } + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + m_mods->update(); } } diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 97c6fe8f..23e2367b 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -242,6 +243,9 @@ void VersionPage::updateVersionControls() bool supportsFabric = minecraftVersion >= Version("1.14"); ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); + bool supportsQuilt = minecraftVersion >= Version("1.14"); + ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt); + bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); @@ -391,7 +395,7 @@ void VersionPage::on_actionChange_version_triggered() return; } VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); - if (uid == "net.fabricmc.intermediary") + if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); @@ -422,7 +426,7 @@ void VersionPage::on_actionDownload_All_triggered() { CustomMessageBox::selectable( this, tr("Error"), - tr("PolyMC cannot download Minecraft or update instances unless you have at least " + tr("Cannot download Minecraft or update instances unless you have at least " "one account added.\nPlease add your Mojang or Minecraft account."), QMessageBox::Warning)->show(); return; @@ -497,6 +501,33 @@ void VersionPage::on_actionInstall_Fabric_triggered() } } +void VersionPage::on_actionInstall_Quilt_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this); + vselect.setEmptyString(tr("No Quilt Loader versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + void VersionPage::on_actionAdd_Empty_triggered() { NewComponentDialog compdialog(QString(), QString(), this); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 2d37af43..979311fc 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -73,6 +73,7 @@ private slots: void on_actionChange_version_triggered(); void on_actionInstall_Forge_triggered(); void on_actionInstall_Fabric_triggered(); + void on_actionInstall_Quilt_triggered(); void on_actionAdd_Empty_triggered(); void on_actionInstall_LiteLoader_triggered(); void on_actionReload_triggered(); diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index a4990ff3..489f7218 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -107,6 +107,7 @@ <addaction name="separator"/> <addaction name="actionInstall_Forge"/> <addaction name="actionInstall_Fabric"/> + <addaction name="actionInstall_Quilt"/> <addaction name="actionInstall_LiteLoader"/> <addaction name="actionInstall_mods"/> <addaction name="separator"/> @@ -192,6 +193,14 @@ <string>Install the Fabric Loader package.</string> </property> </action> + <action name="actionInstall_Quilt"> + <property name="text"> + <string>Install Quilt</string> + </property> + <property name="toolTip"> + <string>Install the Quilt Loader package.</string> + </property> + </action> <action name="actionInstall_LiteLoader"> <property name="text"> <string>Install LiteLoader</string> diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp new file mode 100644 index 00000000..f75d2847 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -0,0 +1,231 @@ +#include "ModModel.h" + +#include "BuildConfig.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/dialogs/ModDownloadDialog.h" + +#include <QMessageBox> + +namespace ModPlatform { + +ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} + +auto ListModel::debugName() const -> QString +{ + return m_parent->debugName(); +} + +/******** Make data requests ********/ + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) return; + if (nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +auto ListModel::data(const QModelIndex& index, int role) const -> QVariant +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } + + ModPlatform::IndexedPack pack = modpacks.at(pos); + if (role == Qt::DisplayRole) { + return pack.name; + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } else if (role == Qt::UserRole) { + QVariant v; + v.setValue(pack); + return v; + } + + return {}; +} + +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) +{ + auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); + + m_parent->apiProvider()->getVersions(this, + { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); +} + +void ListModel::performPaginatedSearch() +{ + auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); + + m_parent->apiProvider()->searchMods(this, + { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions().at(0) }); +} + +void ListModel::searchWithTerm(const QString& term, const int sort) +{ + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } + currentSearchTerm = term; + currentSort = sort; + if (jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache() + ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } + + MetaEntryPtr entry = + APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); + emit logoLoaded(logo, QIcon(fullPath)); + if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); + emit logoFailed(logo); + }); + + job->start(); + m_loadingLogos.append(logo); +} + +/******** Request callbacks ********/ + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::searchRequestFinished(QJsonDocument& doc) +{ + jobPtr.reset(); + + QList<ModPlatform::IndexedPack> newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack pack; + try { + loadIndexedPack(pack, packObj); + newList.append(pack); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + continue; + } + } + + if (packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::searchRequestFailed(QString reason) +{ + if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + //: %1 refers to the launcher itself + QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); + // self-destruct + (dynamic_cast<ModDownloadDialog*>((dynamic_cast<ModPage*>(parent()))->parentWidget()))->reject(); + } + jobPtr.reset(); + + if (searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) +{ + auto& current = m_parent->getCurrent(); + if (addonId != current.addonId) { return; } + + QJsonArray arr = doc.array(); + try { + loadIndexedPackVersions(current, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); + } + + m_parent->updateModVersions(); +} + +} // namespace ModPlatform + +/******** Helpers ********/ + +auto ModPlatform::ListModel::getMineVersions() const -> QList<QString> +{ + return { (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance)) + ->getPackProfile() + ->getComponentVersion("net.minecraft") }; +} diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h new file mode 100644 index 00000000..dbadbeee --- /dev/null +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -0,0 +1,84 @@ +#pragma once + +#include <QAbstractListModel> + +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "net/NetJob.h" + +class ModPage; + +namespace ModPlatform { + +using LogoMap = QMap<QString, QIcon>; +using LogoCallback = std::function<void (QString)>; + +class ListModel : public QAbstractListModel { + Q_OBJECT + + public: + ListModel(ModPage* parent); + ~ListModel() override = default; + + inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; + + auto debugName() const -> QString; + + /* Retrieve information from the model at a given index with the given role */ + auto data(const QModelIndex& index, int role) const -> QVariant override; + + inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + + /* Ask the API for more information */ + void fetchMore(const QModelIndex& parent) override; + void searchWithTerm(const QString& term, const int sort); + void requestModVersions(const ModPlatform::IndexedPack& current); + + virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; + + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + + public slots: + void searchRequestFinished(QJsonDocument& doc); + void searchRequestFailed(QString reason); + + void versionRequestSucceeded(QJsonDocument doc, QString addonId); + + protected slots: + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void performPaginatedSearch(); + + protected: + virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; + virtual auto getSorts() const -> const char** = 0; + + void requestLogo(QString file, QString url); + + inline auto getMineVersions() const -> QList<QString>; + + protected: + ModPage* m_parent; + + QList<ModPlatform::IndexedPack> modpacks; + + LogoMap m_logoMap; + QMap<QString, LogoCallback> waitingCallbacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + + NetJob::Ptr jobPtr; +}; +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp new file mode 100644 index 00000000..eabd8379 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -0,0 +1,171 @@ +#include "ModPage.h" +#include "ui_ModPage.h" + +#include <QKeyEvent> + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/dialogs/ModDownloadDialog.h" + +ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) + : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog), api(api) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + +} + +ModPage::~ModPage() +{ + delete ui; +} + + +/******** Qt things ********/ + +void ModPage::openedImpl() +{ + updateSelectionButton(); + triggerSearch(); +} + +auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + auto* keyEvent = dynamic_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void ModPage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if (!first.isValid()) { return; } + + current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + + if (!current.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { return author.name; } + return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "<br>" + tr(" by ") + authorStrs.join(", "); + } + text += "<br><br>"; + + ui->packDescription->setHtml(text + current.description); + + if (!current.versionsLoaded) { + qDebug() << QString("Loading %1 mod versions").arg(debugName()); + + ui->modSelectionButton->setText(tr("Loading versions...")); + ui->modSelectionButton->setEnabled(false); + + listModel->requestModVersions(current); + } else { + for (int i = 0; i < current.versions.size(); i++) { + ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); + } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } + + updateSelectionButton(); + } +} + +void ModPage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + selectedVersion = -1; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toInt(); + updateSelectionButton(); +} + +void ModPage::onModSelected() +{ + auto& version = current.versions[selectedVersion]; + if (dialog->isModSelected(current.name, version.fileName)) { + dialog->removeSelectedMod(current.name); + } else { + dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); + } + + updateSelectionButton(); +} + + +/******** Make changes to the UI ********/ + +void ModPage::retranslate() +{ + ui->retranslateUi(this); +} + +void ModPage::updateModVersions() +{ + auto packProfile = (dynamic_cast<MinecraftInstance*>(m_instance))->getPackProfile(); + + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + + QString loaderString = ModAPI::getModLoaderString(packProfile->getModLoader()); + + for (int i = 0; i < current.versions.size(); i++) { + auto version = current.versions[i]; + //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. + if (!validateVersion(version, mcVersion, loaderString)) { + continue; + } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); + } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); } + + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); + updateSelectionButton(); +} + + +void ModPage::updateSelectionButton() +{ + if (!isOpened || selectedVersion < 0) { + ui->modSelectionButton->setEnabled(false); + return; + } + + ui->modSelectionButton->setEnabled(true); + auto& version = current.versions[selectedVersion]; + if (!dialog->isModSelected(current.name, version.fileName)) { + ui->modSelectionButton->setText(tr("Select mod for download")); + } else { + ui->modSelectionButton->setText(tr("Deselect mod for download")); + } +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h new file mode 100644 index 00000000..0cd13f37 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QWidget> + +#include "Application.h" +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModModel.h" + +class ModDownloadDialog; + +namespace Ui { +class ModPage; +} + +/* This page handles most logic related to browsing and selecting mods to download. */ +class ModPage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); + ~ModPage() override; + + /* Affects what the user sees */ + auto displayName() const -> QString override = 0; + auto icon() const -> QIcon override = 0; + auto id() const -> QString override = 0; + auto helpPage() const -> QString override = 0; + + /* Used internally */ + virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString = 0; + + + void retranslate() override; + + auto shouldDisplay() const -> bool override = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0; + + auto apiProvider() const -> const ModAPI* { return api.get(); }; + + auto getCurrent() -> ModPlatform::IndexedPack& { return current; } + void updateModVersions(); + + void openedImpl() override; + auto eventFilter(QObject* watched, QEvent* event) -> bool override; + + BaseInstance* m_instance; + + protected: + void updateSelectionButton(); + + protected slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + void onModSelected(); + + protected: + Ui::ModPage* ui = nullptr; + ModDownloadDialog* dialog = nullptr; + ModPlatform::ListModel* listModel = nullptr; + ModPlatform::IndexedPack current; + + std::unique_ptr<ModAPI> api; + + int selectedVersion = -1; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/ModPage.ui index 6c709825..508f1bac 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>ModrinthPage</class> - <widget class="QWidget" name="ModrinthPage"> + <class>ModPage</class> + <widget class="QWidget" name="ModPage"> <property name="geometry"> <rect> <x>0</x> diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index e8afba5a..905fb2dd 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,273 +1,25 @@ #include "FlameModModel.h" -#include "Application.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" -#include "FlameModPage.h" -#include <Json.h> - -#include <MMCStrings.h> -#include <Version.h> - -#include <QtMath> +#include "modplatform/flame/FlameModIndex.h" namespace FlameMod { -ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent) -{ -} - -ListModel::~ListModel() -{ -} +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; -int ListModel::rowCount(const QModelIndex &parent) const +void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { - return modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} - -QVariant ListModel::data(const QModelIndex &index, int role) const -{ - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - - } - return pack.description; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logoName)) - { - return (m_logoMap.value(pack.logoName)); - } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -void ListModel::logoLoaded(QString logo, QIcon out) -{ - m_loadingLogos.removeAll(logo); - m_logoMap.insert(logo, out); - for(int i = 0; i < modpacks.size(); i++) { - if(modpacks[i].logoName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); + FlameMod::loadIndexedPack(m, obj); } -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); } -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if(nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} -const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"}; - -void ListModel::performPaginatedSearch() -{ - - QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - auto netJob = new NetJob("Flame::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" - "gameId=432&" - "categoryId=0&" - "sectionId=6&" - - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sort=%3&" - "modLoaderType=%4&" - "gameVersion=%5" - ) - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType - .arg(mcVersion); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} - -void ListModel::searchWithTerm(const QString &term, const int sort) -{ - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { - return; - } - currentSearchTerm = term; - currentSort = sort; - if(jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); -} - -void ListModel::searchRequestFinished() -{ - jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - - QList<FlameMod::IndexedPack> newList; - auto packs = doc.array(); - for(auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - FlameMod::IndexedPack pack; - try - { - FlameMod::loadIndexedPack(pack, packObj); - newList.append(pack); - } - catch(const JSONValidationError &e) - { - qWarning() << "Error while loading mod from Flame: " << e.cause(); - continue; - } - } - if(packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -void ListModel::searchRequestFailed(QString reason) -{ - jobPtr.reset(); - - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); - } else { - searchState = Finished; - } -} - + return obj.array(); } +} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 0c1cb95e..707c1bb1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -1,79 +1,25 @@ #pragma once -#include <RWStorage.h> - -#include <QAbstractListModel> -#include <QSortFilterProxyModel> -#include <QThreadPool> -#include <QIcon> -#include <QStyledItemDelegate> -#include <QList> -#include <QString> -#include <QStringList> -#include <QMetaType> - -#include <functional> -#include <net/NetJob.h> - -#include <modplatform/flame/FlamePackIndex.h> -#include "modplatform/flame/FlameModIndex.h" -#include "BaseInstance.h" #include "FlameModPage.h" namespace FlameMod { - -typedef QMap<QString, QIcon> LogoMap; -typedef std::function<void(QString)> LogoCallback; - -class ListModel : public QAbstractListModel -{ +class ListModel : public ModPlatform::ListModel { Q_OBJECT -public: - ListModel(FlameModPage *parent); - virtual ~ListModel(); - - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; - - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString &term, const int sort); - -private slots: - void performPaginatedSearch(); - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void searchRequestFinished(); - void searchRequestFailed(QString reason); + public: + ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} + ~ListModel() override = default; -private: - void requestLogo(QString file, QString url); + private: + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; -private: - QList<IndexedPack> modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap<QString, LogoCallback> waitingCallbacks; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; - NetJob::Ptr jobPtr; - QByteArray response; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + static const char* sorts[6]; + inline auto getSorts() const -> const char** override { return sorts; }; }; -} +} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index d1641729..864ae8e6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -34,224 +34,40 @@ */ #include "FlameModPage.h" -#include "ui_FlameModPage.h" +#include "ui_ModPage.h" -#include <QKeyEvent> - -#include "Application.h" #include "FlameModModel.h" -#include "InstanceImportTask.h" -#include "Json.h" -#include "ModDownloadTask.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" -FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), - dialog(dialog) { - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, - &FlameModPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - listModel = new FlameMod::ListModel(this); - ui->packView->setModel(listModel); - - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( - Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - - // index is used to set the sorting with the flame api - ui->sortByBox->addItem(tr("Sort by Featured")); - ui->sortByBox->addItem(tr("Sort by Popularity")); - ui->sortByBox->addItem(tr("Sort by last updated")); - ui->sortByBox->addItem(tr("Sort by Name")); - ui->sortByBox->addItem(tr("Sort by Author")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, - this, &FlameModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, - &FlameModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, - &FlameModPage::onModSelected); -} - -FlameModPage::~FlameModPage() { delete ui; } - -bool FlameModPage::eventFilter(QObject *watched, QEvent *event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - -bool FlameModPage::shouldDisplay() const { return true; } - -void FlameModPage::retranslate() +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) + : ModPage(dialog, instance, new FlameAPI()) { - ui->retranslateUi(this); -} - -void FlameModPage::openedImpl() { - updateSelectionButton(); - triggerSearch(); -} - -void FlameModPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), - ui->sortByBox->currentIndex()); -} - -void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { - ui->versionSelectionBox->clear(); - - if (!first.isValid()) { - return; - } - - current = listModel->data(first, Qt::UserRole).value<FlameMod::IndexedPack>(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - if (!current.authors.empty()) { - auto authorToStr = [](FlameMod::ModpackAuthor &author) { - if (author.url.isEmpty()) { - return author.name; - } - return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto &author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "<br>" + tr(" by ") + authorStrs.join(", "); - } - text += "<br><br>"; - - ui->packDescription->setHtml(text + current.description); - - if (!current.versionsLoaded) { - qDebug() << "Loading flame mod versions"; - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - auto netJob = - new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), - APPLICATION->network()); - auto response = new QByteArray(); - int addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files") - .arg(addonId), - response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { - if(addonId != current.addonId){ - return; //wrong request - } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame at " - << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - QJsonArray arr = doc.array(); - try { - FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), - m_instance); - } catch (const JSONValidationError &e) { - qDebug() << *response; - qWarning() << "Error while reading Flame mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = - (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) - ? "fabric" - : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion)) { - continue; - } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); - }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - netJob->start(); - } else { - for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, - QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - updateSelectionButton(); - } -} - -void FlameModPage::updateSelectionButton() { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto &version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } + listModel = new FlameMod::ListModel(this); + ui->packView->setModel(listModel); + + // index is used to set the sorting with the flame api + ui->sortByBox->addItem(tr("Sort by Featured")); + ui->sortByBox->addItem(tr("Sort by Popularity")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by Name")); + ui->sortByBox->addItem(tr("Sort by Author")); + ui->sortByBox->addItem(tr("Sort by Downloads")); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -void FlameModPage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool +{ + (void) loaderVer; + return ver.mcVersion.contains(mineVer); } -void FlameModPage::onModSelected() { - auto &version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - dialog->addSelectedMod(current.name, - new ModDownloadTask(version.downloadUrl, - version.fileName, dialog->mods)); - } - - updateSelectionButton(); -} +// I don't know why, but doing this on the parent class makes it so that +// other mod providers start loading before being selected, at least with +// my Qt, so we need to implement this in every derived class... +auto FlameModPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 2a6ade85..dc58fd7f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -35,70 +35,26 @@ #pragma once -#include <QWidget> +#include "ui/pages/modplatform/ModPage.h" -#include "ui/pages/BasePage.h" -#include <Application.h> -#include "tasks/Task.h" -#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/flame/FlameAPI.h" -namespace Ui -{ -class FlameModPage; -} - -class ModDownloadDialog; - -namespace FlameMod { - class ListModel; -} - -class FlameModPage : public QWidget, public BasePage -{ +class FlameModPage : public ModPage { Q_OBJECT -public: - explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance); - virtual ~FlameModPage(); - virtual QString displayName() const override - { - return "CurseForge"; - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("flame"); - } - virtual QString id() const override - { - return "curseforge"; - } - virtual QString helpPage() const override - { - return "Flame-platform"; - } - virtual bool shouldDisplay() const override; - void retranslate() override; - - void openedImpl() override; - - bool eventFilter(QObject * watched, QEvent * event) override; - - BaseInstance *m_instance; + public: + explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); + ~FlameModPage() override = default; -private: - void updateSelectionButton(); + inline auto displayName() const -> QString override { return "CurseForge"; } + inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); } + inline auto id() const -> QString override { return "curseforge"; } + inline auto helpPage() const -> QString override { return "Flame-platform"; } -private slots: - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); + inline auto debugName() const -> QString override { return "Flame"; } + inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; -private: - Ui::FlameModPage *ui = nullptr; - ModDownloadDialog* dialog = nullptr; - FlameMod::ListModel* listModel = nullptr; - FlameMod::IndexedPack current; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; - int selectedVersion = -1; + auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui deleted file mode 100644 index 25cb2571..00000000 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui +++ /dev/null @@ -1,97 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>FlameModPage</class> - <widget class="QWidget" name="FlameModPage"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>837</width> - <height>685</height> - </rect> - </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="2" column="0" colspan="2"> - <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0,0,0" columnminimumwidth="0,0,0"> - <item row="1" column="2"> - <widget class="QComboBox" name="versionSelectionBox"/> - </item> - <item row="1" column="0"> - <widget class="QComboBox" name="sortByBox"/> - </item> - <item row="1" column="1"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Version selected:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="2" column="2"> - <widget class="QPushButton" name="modSelectionButton"> - <property name="text"> - <string>Select mod for download</string> - </property> - </widget> - </item> - </layout> - </item> - <item row="0" column="0"> - <widget class="QLineEdit" name="searchEdit"> - <property name="placeholderText"> - <string>Search and filter...</string> - </property> - </widget> - </item> - <item row="1" column="0" colspan="2"> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="1" column="0"> - <widget class="QListView" name="packView"> - <property name="horizontalScrollBarPolicy"> - <enum>Qt::ScrollBarAlwaysOff</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="iconSize"> - <size> - <width>48</width> - <height>48</height> - </size> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QTextBrowser" name="packDescription"> - <property name="openExternalLinks"> - <bool>true</bool> - </property> - <property name="openLinks"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </item> - <item row="0" column="1"> - <widget class="QPushButton" name="searchButton"> - <property name="text"> - <string>Search</string> - </property> - </widget> - </item> - </layout> - </widget> - <tabstops> - <tabstop>searchEdit</tabstop> - <tabstop>searchButton</tabstop> - <tabstop>packView</tabstop> - <tabstop>packDescription</tabstop> - <tabstop>sortByBox</tabstop> - <tabstop>versionSelectionBox</tabstop> - </tabstops> - <resources/> - <connections/> -</ui> diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 5a18830a..b788860a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,276 +1,43 @@ -#include "ModrinthModel.h" -#include "Application.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" -#include "ModrinthPage.h" -#include "ui/dialogs/ModDownloadDialog.h" -#include <Json.h> - -#include <MMCStrings.h> -#include <Version.h> +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ -#include <QtMath> -#include <QMessageBox> +#include "ModrinthModel.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" namespace Modrinth { -ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent) -{ -} - -ListModel::~ListModel() -{ -} +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; -int ListModel::rowCount(const QModelIndex &parent) const +void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { - return modpacks.size(); + Modrinth::loadIndexedPack(m, obj); } -int ListModel::columnCount(const QModelIndex &parent) const +void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - return 1; + Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); } -QVariant ListModel::data(const QModelIndex &index, int role) const +auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - - } - return pack.description; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logoName)) - { - return (m_logoMap.value(pack.logoName)); - } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -void ListModel::logoLoaded(QString logo, QIcon out) -{ - m_loadingLogos.removeAll(logo); - m_logoMap.insert(logo, out); - for(int i = 0; i < modpacks.size(); i++) { - if(modpacks[i].logoName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) -{ - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } -} - -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const -{ - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if(nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} -const char* sorts[5]{"relevance","downloads","follows","updated","newest"}; - -void ListModel::performPaginatedSearch() -{ - - QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - auto netJob = new NetJob("Modrinth::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://api.modrinth.com/v2/search?" - "offset=%1&" - "limit=25&" - "query=%2&" - "index=%3&" - "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]" - ) - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? "fabric" : "forge") - .arg(mcVersion); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} - -void ListModel::searchWithTerm(const QString &term, const int sort) -{ - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { - return; - } - currentSearchTerm = term; - currentSort = sort; - if(jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); -} - -void Modrinth::ListModel::searchRequestFinished() -{ - jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - - QList<Modrinth::IndexedPack> newList; - auto packs = doc.object().value("hits").toArray(); - for(auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - Modrinth::IndexedPack pack; - try - { - Modrinth::loadIndexedPack(pack, packObj); - newList.append(pack); - } - catch(const JSONValidationError &e) - { - qWarning() << "Error while loading mod from Modrinth: " << e.cause(); - continue; - } - } - if(packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -void Modrinth::ListModel::searchRequestFailed(QString reason) -{ - if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){ - //409 Gone, notify user to update - QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!")); - //self-destruct - ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject(); - } - jobPtr.reset(); - - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); - } else { - searchState = Finished; - } -} - + return obj.object().value("hits").toArray(); } +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 53f1f134..45a6090a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,79 +1,25 @@ #pragma once -#include <RWStorage.h> - -#include <QAbstractListModel> -#include <QSortFilterProxyModel> -#include <QThreadPool> -#include <QIcon> -#include <QStyledItemDelegate> -#include <QList> -#include <QString> -#include <QStringList> -#include <QMetaType> - -#include <functional> -#include <net/NetJob.h> - -#include <modplatform/flame/FlamePackIndex.h> -#include "modplatform/modrinth/ModrinthPackIndex.h" -#include "BaseInstance.h" #include "ModrinthPage.h" namespace Modrinth { - -typedef QMap<QString, QIcon> LogoMap; -typedef std::function<void(QString)> LogoCallback; - -class ListModel : public QAbstractListModel -{ +class ListModel : public ModPlatform::ListModel { Q_OBJECT -public: - ListModel(ModrinthPage *parent); - virtual ~ListModel(); - - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; - - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString &term, const int sort); - -private slots: - void performPaginatedSearch(); - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void searchRequestFinished(); - void searchRequestFailed(QString reason); - -private: - void requestLogo(QString file, QString url); + public: + ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){}; + ~ListModel() override = default; -private: - QList<IndexedPack> modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap<QString, LogoCallback> waitingCallbacks; + private: + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; - NetJob::Ptr jobPtr; - QByteArray response; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + static const char* sorts[5]; + inline auto getSorts() const -> const char** override { return sorts; }; }; -} +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 82340448..ddaf96e2 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -34,211 +34,38 @@ */ #include "ModrinthPage.h" -#include "ui_ModrinthPage.h" +#include "ui_ModPage.h" -#include <QKeyEvent> - -#include "Application.h" -#include "InstanceImportTask.h" -#include "Json.h" -#include "ModDownloadTask.h" #include "ModrinthModel.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" -ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), - dialog(dialog) { - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, - &ModrinthPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - listModel = new Modrinth::ListModel(this); - ui->packView->setModel(listModel); - - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( - Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - - // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevance")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by last updated")); - ui->sortByBox->addItem(tr("Sort by newest")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, - this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, - &ModrinthPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, - &ModrinthPage::onModSelected); -} - -ModrinthPage::~ModrinthPage() { delete ui; } - -bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - -bool ModrinthPage::shouldDisplay() const { return true; } - -void ModrinthPage::retranslate() { - ui->retranslateUi(this); -} - -void ModrinthPage::openedImpl() { - updateSelectionButton(); - triggerSearch(); -} - -void ModrinthPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), - ui->sortByBox->currentIndex()); -} - -void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { - ui->versionSelectionBox->clear(); - - if (!first.isValid()) { - return; - } - - current = listModel->data(first, Qt::UserRole).value<Modrinth::IndexedPack>(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; - text += "<br>" + tr(" by ") + "<a href=\"" + current.author.url + "\">" + - current.author.name + "</a><br><br>"; - ui->packDescription->setHtml(text + current.description); - - if (!current.versionsLoaded) { - qDebug() << "Loading Modrinth mod versions"; - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - auto netJob = - new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), - APPLICATION->network()); - auto response = new QByteArray(); - QString addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), - response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { - if(addonId != current.addonId){ - return; - } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " - << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - QJsonArray arr = doc.array(); - try { - Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), - m_instance); - } catch (const JSONValidationError &e) { - qDebug() << *response; - qWarning() << "Error while reading Modrinth mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = - (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) - ? "fabric" - : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion) || - !version.loaders.contains(loaderString)) { - continue; - } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); - }); - - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); - } else { - for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, - QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No valid version found."), - QVariant(-1)); - } - - updateSelectionButton(); - } -} - -void ModrinthPage::updateSelectionButton() { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto &version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } +ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) + : ModPage(dialog, instance, new ModrinthAPI()) +{ + listModel = new Modrinth::ListModel(this); + ui->packView->setModel(listModel); + + // index is used to set the sorting with the modrinth api + ui->sortByBox->addItem(tr("Sort by Relevence")); + ui->sortByBox->addItem(tr("Sort by Downloads")); + ui->sortByBox->addItem(tr("Sort by Follows")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by newest")); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's contructor... + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } -void ModrinthPage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); +auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool +{ + return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer); } -void ModrinthPage::onModSelected() { - auto &version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - dialog->addSelectedMod(current.name, - new ModDownloadTask(version.downloadUrl, - version.fileName, dialog->mods)); - } - - updateSelectionButton(); -} +// I don't know why, but doing this on the parent class makes it so that +// other mod providers start loading before being selected, at least with +// my Qt, so we need to implement this in every derived class... +auto ModrinthPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 92955d62..aa5ed793 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -35,70 +35,26 @@ #pragma once -#include <QWidget> +#include "ui/pages/modplatform/ModPage.h" -#include "ui/pages/BasePage.h" -#include <Application.h> -#include "tasks/Task.h" -#include "modplatform/modrinth/ModrinthPackIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" -namespace Ui -{ -class ModrinthPage; -} - -class ModDownloadDialog; - -namespace Modrinth { - class ListModel; -} - -class ModrinthPage : public QWidget, public BasePage -{ +class ModrinthPage : public ModPage { Q_OBJECT -public: - explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance); - virtual ~ModrinthPage(); - virtual QString displayName() const override - { - return "Modrinth"; - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("modrinth"); - } - virtual QString id() const override - { - return "modrinth"; - } - virtual QString helpPage() const override - { - return "Modrinth-platform"; - } - virtual bool shouldDisplay() const override; - void retranslate() override; - - void openedImpl() override; - - bool eventFilter(QObject * watched, QEvent * event) override; - - BaseInstance *m_instance; + public: + explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); + ~ModrinthPage() override = default; -private: - void updateSelectionButton(); + inline auto displayName() const -> QString override { return "Modrinth"; } + inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } + inline auto id() const -> QString override { return "modrinth"; } + inline auto helpPage() const -> QString override { return "Modrinth-platform"; } -private slots: - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); + inline auto debugName() const -> QString override { return "Modrinth"; } + inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; -private: - Ui::ModrinthPage *ui = nullptr; - ModDownloadDialog* dialog = nullptr; - Modrinth::ListModel* listModel = nullptr; - Modrinth::IndexedPack current; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; - int selectedVersion = -1; + auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 50fd75e8..cd2ea8e1 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -1,22 +1,43 @@ -/* Copyright 2020-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once #include <QList> #include <QString> +#include <QVector> namespace Technic { struct Modpack { @@ -36,6 +57,11 @@ struct Modpack { QString websiteUrl; QString author; QString description; + QString currentVersion; + + bool versionsLoaded = false; + QString recommended; + QVector<QString> versions; }; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 0167f746..9c9d1e75 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -1,20 +1,41 @@ -/* Copyright 2020-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "TechnicModel.h" #include "Application.h" +#include "BuildConfig.h" #include "Json.h" #include <QIcon> @@ -94,13 +115,24 @@ void Technic::ListModel::performSearch() NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = "https://api.technicpack.net/trending?build=multimc"; + searchUrl = QString("%1trending?build=%2") + .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); + searchMode = List; } - else - { + else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=%2") + .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; + } + else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; + } + else { searchUrl = QString( - "https://api.technicpack.net/search?build=multimc&q=%1" - ).arg(currentSearchTerm); + "%1search?build=%2&q=%3" + ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); + searchMode = List; } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; @@ -125,26 +157,58 @@ void Technic::ListModel::searchRequestFinished() QList<Modpack> newList; try { auto root = Json::requireObject(doc); - auto objs = Json::requireArray(root, "modpacks"); - for (auto technicPack: objs) { - Modpack pack; - auto technicPackObject = Json::requireObject(technicPack); - pack.name = Json::requireString(technicPackObject, "name"); - pack.slug = Json::requireString(technicPackObject, "slug"); - if (pack.slug == "vanilla") - continue; - - auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); - if(rawURL == "null") { - pack.logoUrl = "null"; - pack.logoName = "null"; + + switch (searchMode) { + case List: { + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + break; } - else { - pack.logoUrl = rawURL; - pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + case Single: { + if (root.contains("error")) { + // Invalid API url + break; + } + + Modpack pack; + pack.name = Json::requireString(root, "displayName"); + pack.slug = Json::requireString(root, "name"); + + if (root.contains("icon")) { + auto iconObj = Json::requireObject(root, "icon"); + auto iconUrl = Json::requireString(iconObj, "url"); + + pack.logoUrl = iconUrl; + pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + + pack.broken = false; + newList.append(pack); + break; } - pack.broken = false; - newList.append(pack); } } catch (const JSONValidationError &err) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index e80e6e7c..5eea124c 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -1,16 +1,36 @@ -/* Copyright 2020-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021 Jamie Mansfield <jmansfield@cadixdev.org> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -63,6 +83,10 @@ private: ResetRequested, Finished } searchState = None; + enum SearchMode { + List, + Single, + } searchMode = List; NetJob::Ptr jobPtr; QByteArray response; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index c3807269..b8c1e00a 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (c) 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,12 +40,14 @@ #include "ui/dialogs/NewInstanceDialog.h" +#include "BuildConfig.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" #include "Json.h" #include "Application.h" +#include "modplatform/technic/SolderPackManifest.h" TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) @@ -55,7 +57,9 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -98,13 +102,14 @@ void TechnicPage::triggerSearch() { void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) { + ui->versionSelectionBox->clear(); + if(!first.isValid()) { if(isOpened) { dialog->setSuggestedPack(); } - //ui->frame->clear(); return; } @@ -137,17 +142,19 @@ void TechnicPage::suggestCurrent() } NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>(); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); + QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] { + jobPtr.reset(); + if (current.slug != slug) { return; } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonObject obj = doc.object(); if(parse_error.error != QJsonParseError::NoError) { @@ -189,10 +196,14 @@ void TechnicPage::suggestCurrent() current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); current.metadataLoaded = true; + metadataLoaded(); }); - netJob->start(); + + jobPtr = netJob; + jobPtr->start(); } // expects current.metadataLoaded to be true @@ -202,25 +213,119 @@ void TechnicPage::metadataLoaded() QString name = current.name; if (current.websiteUrl.isEmpty()) - // This allows injecting HTML here. - text = name; + text = name.toHtmlEscaped(); else - // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML. - text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; + text = "<a href=\"" + current.websiteUrl.toHtmlEscaped() + "\">" + name.toHtmlEscaped() + "</a>"; + if (!current.author.isEmpty()) { - // This allows injecting HTML here - text += tr(" by ") + current.author; + text += "<br>" + tr(" by ") + current.author.toHtmlEscaped(); + } + + text += "<br><br>"; + + ui->packDescription->setHtml(text + current.description); + + // Strip trailing forward-slashes from Solder URL's + if (current.isSolder) { + while (current.url.endsWith('/')) current.url.chop(1); + } + + // Display versions from Solder + if (!current.isSolder) { + // If the pack isn't a Solder pack, it only has the single version + ui->versionSelectionBox->addItem(current.currentVersion); + } + else if (current.versionsLoaded) { + // reverse foreach, so that the newest versions are first + for (auto i = current.versions.size(); i--;) { + ui->versionSelectionBox->addItem(current.versions.at(i)); + } + ui->versionSelectionBox->setCurrentText(current.recommended); + } + else { + // For now, until the versions are pulled from the Solder instance, display the current + // version so we can display something quicker + ui->versionSelectionBox->addItem(current.currentVersion); + + auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto url = QString("%1/modpack/%2").arg(current.url, current.slug); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + + jobPtr = netJob; + jobPtr->start(); + } + + selectVersion(); +} + +void TechnicPage::selectVersion() { + if (!isOpened) { + return; + } + if (current.broken) { + dialog->setSuggestedPack(); + return; } - ui->frame->setModText(text); - ui->frame->setModDescription(current.description); if (!current.isSolder) { - dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { - while (current.url.endsWith('/')) current.url.chop(1); - dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion)); + } +} + +void TechnicPage::onSolderLoaded() { + jobPtr.reset(); + + auto fallback = [this]() { + current.versionsLoaded = true; + + current.versions.clear(); + current.versions.append(current.currentVersion); + }; + + current.versions.clear(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + fallback(); + return; + } + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError& err) { + qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); + fallback(); + return; + } + + current.versionsLoaded = true; + current.recommended = pack.recommended; + current.versions.append(pack.builds); + + // Finally, let's reload :) + ui->versionSelectionBox->clear(); + metadataLoaded(); +} + +void TechnicPage::onVersionSelectionChanged(QString data) { + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; } + + selectedVersion = data; + selectVersion(); } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index bf4baa58..f4a3b61d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (c) 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,6 +39,7 @@ #include "ui/pages/BasePage.h" #include <Application.h> +#include "net/NetJob.h" #include "tasks/Task.h" #include "TechnicData.h" @@ -86,14 +87,22 @@ public: private: void suggestCurrent(); void metadataLoaded(); + void selectVersion(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onSolderLoaded(); + void onVersionSelectionChanged(QString data); private: Ui::TechnicPage *ui = nullptr; NewInstanceDialog* dialog = nullptr; Technic::ListModel* model = nullptr; + Technic::Modpack current; + QString selectedVersion; + + NetJob::Ptr jobPtr; + QByteArray response; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index 62ab6154..ca6a9b7e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -10,86 +10,76 @@ <height>405</height> </rect> </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QWidget" name="widget" native="true"> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QLineEdit" name="searchEdit"> - <property name="placeholderText"> - <string>Search and filter...</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="searchButton"> - <property name="text"> - <string>Search</string> - </property> - </widget> - </item> - </layout> - </widget> + <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="2"> + <widget class="QComboBox" name="versionSelectionBox"/> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Version selected:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Preferred</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>1</width> + <height>1</height> + </size> + </property> + </spacer> + </item> + </layout> </item> - <item> - <widget class="QListView" name="packView"> - <property name="horizontalScrollBarPolicy"> - <enum>Qt::ScrollBarAlwaysOff</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="iconSize"> - <size> - <width>48</width> - <height>48</height> - </size> + <item row="2" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QListView" name="packView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="iconSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QTextBrowser" name="packDescription"/> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Search and filter...</string> </property> </widget> </item> - <item> - <widget class="MCModInfoFrame" name="frame"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> + <item row="1" column="1"> + <widget class="QPushButton" name="searchButton"> + <property name="text"> + <string>Search</string> </property> </widget> </item> </layout> </widget> - <customwidgets> - <customwidget> - <class>MCModInfoFrame</class> - <extends>QFrame</extends> - <header>ui/widgets/MCModInfoFrame.h</header> - <container>1</container> - </customwidget> - </customwidgets> - <tabstops> - <tabstop>searchEdit</tabstop> - <tabstop>searchButton</tabstop> - <tabstop>packView</tabstop> - </tabstops> <resources/> <connections/> </ui> |