Merge pull request #32 from flowln/modpack_update_page

Closes https://github.com/PrismLauncher/PrismLauncher/issues/180
Closes https://github.com/PrismLauncher/PrismLauncher/issues/170
This commit is contained in:
flow 2022-12-10 06:34:18 -08:00 committed by GitHub
commit 4a13d72997
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 983 additions and 106 deletions

View File

@ -161,6 +161,8 @@ class Config {
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
QString versionString() const;
/**
* \brief Converts the Version to a string.

View File

@ -680,6 +680,8 @@ SET(LAUNCHER_SOURCES
ui/pages/instance/GameOptionsPage.h
ui/pages/instance/VersionPage.cpp
ui/pages/instance/VersionPage.h
ui/pages/instance/ManagedPackPage.cpp
ui/pages/instance/ManagedPackPage.h
ui/pages/instance/TexturePackPage.h
ui/pages/instance/ResourcePackPage.h
ui/pages/instance/ShaderPackPage.h
@ -919,6 +921,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/instance/OtherLogsPage.ui
ui/pages/instance/InstanceSettingsPage.ui
ui/pages/instance/VersionPage.ui
ui/pages/instance/ManagedPackPage.ui
ui/pages/instance/WorldListPage.ui
ui/pages/instance/ScreenshotsPage.ui
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui

View File

@ -55,11 +55,9 @@
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{
m_sourceUrl = sourceUrl;
m_parent = parent;
}
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info)
: m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent)
{}
bool InstanceImportTask::abort()
{
@ -259,14 +257,28 @@ void InstanceImportTask::extractAborted()
void InstanceImportTask::processFlame()
{
auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent);
auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd());
auto pack_id = pack_id_it.value();
auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
Q_ASSERT(pack_version_id_it != m_extra_info.constEnd());
auto pack_version_id = pack_version_id_it.value();
QString original_instance_id;
auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
if (original_instance_id_it != m_extra_info.constEnd())
original_instance_id = original_instance_id_it.value();
auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
emitSucceeded();
});
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
@ -323,14 +335,29 @@ void InstanceImportTask::processMultiMC()
void InstanceImportTask::processModrinth()
{
auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString());
auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd());
auto pack_id = pack_id_it.value();
QString pack_version_id;
auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
if (pack_version_id_it != m_extra_info.constEnd())
pack_version_id = pack_version_id_it.value();
QString original_instance_id;
auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
if (original_instance_id_it != m_extra_info.constEnd())
original_instance_id = original_instance_id_it.value();
auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
emitSucceeded();
});
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);

View File

@ -56,7 +56,7 @@ class InstanceImportTask : public InstanceTask
{
Q_OBJECT
public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr);
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
bool abort() override;
const QVector<Flame::File> &getBlockedFiles() const
@ -101,6 +101,10 @@ private: /* data */
Modrinth,
} m_modpackType = ModpackType::Unknown;
// Extra info we might need, that's available before, but can't be derived from
// the source URL / the resource it points to alone.
QMap<QString, QString> m_extra_info;
//FIXME: nuke
QWidget* m_parent;
};

View File

@ -816,7 +816,7 @@ class InstanceStaging : public Task {
void childSucceded()
{
unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride()))
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get()))
{
emitSucceeded();
return;
@ -880,25 +880,22 @@ QString InstanceList::getStagedInstancePath()
return path;
}
bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override)
bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, InstanceTask const& commiting)
{
QDir dir;
QString instID;
InstancePtr inst;
auto should_override = commiting.shouldOverride();
if (should_override) {
// This is to avoid problems when the instance folder gets manually renamed
if ((inst = getInstanceByManagedName(instanceName.originalName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else {
instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-');
}
instID = commiting.originalInstanceID();
} else {
instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir);
}
Q_ASSERT(!instID.isEmpty());
{
WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID);

View File

@ -133,7 +133,7 @@ public:
* should_override is used when another similar instance already exists, and we want to override it
* - for instance, when updating it.
*/
bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override);
bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, const InstanceTask&);
/**
* Destroy a previously created staging area given by @keyPath - used when creation fails.

View File

@ -5,6 +5,7 @@
#include "ui/pages/BasePageProvider.h"
#include "ui/pages/instance/LogPage.h"
#include "ui/pages/instance/VersionPage.h"
#include "ui/pages/instance/ManagedPackPage.h"
#include "ui/pages/instance/ModFolderPage.h"
#include "ui/pages/instance/ResourcePackPage.h"
#include "ui/pages/instance/TexturePackPage.h"
@ -33,6 +34,7 @@ public:
values.append(new LogPage(inst));
std::shared_ptr<MinecraftInstance> onesix = std::dynamic_pointer_cast<MinecraftInstance>(inst);
values.append(new VersionPage(onesix.get()));
values.append(ManagedPackPage::createPage(onesix.get()));
auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList());
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
values.append(modsPage);

View File

@ -18,6 +18,29 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol
return InstanceNameChange::ShouldKeep;
}
ShouldUpdate askIfShouldUpdate(QWidget *parent, QString original_version_name)
{
auto info = CustomMessageBox::selectable(
parent, QObject::tr("Similar modpack was found!"),
QObject::tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance"));
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance"));
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok))
return ShouldUpdate::Update;
if (info->clickedButton() == info->button(QMessageBox::Abort))
return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel;
}
QString InstanceName::name() const
{
if (!m_modified_name.isEmpty())

View File

@ -6,6 +6,8 @@
/* Helpers */
enum class InstanceNameChange { ShouldChange, ShouldKeep };
[[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name);
enum class ShouldUpdate { Update, SkipUpdating, Cancel };
[[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name);
struct InstanceName {
public:
@ -42,10 +44,20 @@ class InstanceTask : public Task, public InstanceName {
void setGroup(const QString& group) { m_instGroup = group; }
QString group() const { return m_instGroup; }
[[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; }
void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; }
bool shouldOverride() const { return m_override_existing; }
[[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; };
protected:
void setOverride(bool override) { m_override_existing = override; }
void setOverride(bool override, QString instance_id_to_override = {})
{
m_override_existing = override;
if (!instance_id_to_override.isEmpty())
m_original_instance_id = instance_id_to_override;
}
protected: /* data */
SettingsObjectPtr m_globalSettings;
@ -54,4 +66,7 @@ class InstanceTask : public Task, public InstanceName {
QString m_stagingPath;
bool m_override_existing = false;
bool m_confirm_update = true;
QString m_original_instance_id;
};

View File

@ -81,13 +81,19 @@ bool FlameCreationTask::updateInstance()
auto instance_list = APPLICATION->instances();
// FIXME: How to handle situations when there's more than one install already for a given modpack?
auto inst = instance_list->getInstanceByManagedName(originalName());
InstancePtr inst;
if (auto original_id = originalInstanceID(); !original_id.isEmpty()) {
inst = instance_list->getInstanceById(original_id);
Q_ASSERT(inst);
} else {
inst = instance_list->getInstanceByManagedName(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
if (!inst)
return false;
if (!inst)
return false;
}
}
QString index_path(FS::PathCombine(m_stagingPath, "manifest.json"));
@ -102,25 +108,14 @@ bool FlameCreationTask::updateInstance()
auto version_id = inst->getManagedPackVersionName();
auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : "";
auto info = CustomMessageBox::selectable(
m_parent, tr("Similar modpack was found!"),
tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(version_str),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, tr("Update existing instance"));
info->setButtonText(QMessageBox::Abort, tr("Create new instance"));
info->setButtonText(QMessageBox::Reset, tr("Cancel"));
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Abort))
return false;
if (info->clickedButton() == info->button(QMessageBox::Reset)) {
m_abort = true;
return false;
if (shouldConfirmUpdate()) {
auto should_update = askIfShouldUpdate(m_parent, version_str);
if (should_update == ShouldUpdate::SkipUpdating)
return false;
if (should_update == ShouldUpdate::Cancel) {
m_abort = true;
return false;
}
}
QDir old_inst_dir(inst->instanceRoot());
@ -244,7 +239,7 @@ bool FlameCreationTask::updateInstance()
}
}
setOverride(true);
setOverride(true, inst->id());
qDebug() << "Will override instance!";
m_instance = inst;
@ -366,7 +361,7 @@ bool FlameCreationTask::createInstance()
FS::deletePath(jarmodsPath);
}
instance.setManagedPack("flame", {}, m_pack.name, {}, m_pack.version);
instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
instance.setName(name());
m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack);
@ -390,14 +385,6 @@ bool FlameCreationTask::createInstance()
setAbortable(false);
auto inst = m_instance.value();
// Only change the name if it didn't use a custom name, so that the previous custom name
// is preserved, but if we're using the original one, we update the version string.
// NOTE: This needs to come before the copyManagedPack call!
if (inst->name().contains(inst->getManagedPackVersionName())) {
if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange)
inst->setName(instance.name());
}
inst->copyManagedPack(instance);
}

View File

@ -51,11 +51,21 @@ class FlameCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
FlameCreationTask(const QString& staging_path, SettingsObjectPtr global_settings, QWidget* parent)
: InstanceCreationTask(), m_parent(parent)
FlameCreationTask(const QString& staging_path,
SettingsObjectPtr global_settings,
QWidget* parent,
QString id,
QString version_id,
QString original_instance_id = {})
: InstanceCreationTask()
, m_parent(parent)
, m_managed_id(std::move(id))
, m_managed_version_id(std::move(version_id))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
m_original_instance_id = std::move(original_instance_id);
}
bool abort() override;
@ -78,5 +88,7 @@ class FlameCreationTask final : public InstanceCreationTask {
NetJob* m_process_update_file_info_job = nullptr;
NetJob::Ptr m_files_job = nullptr;
QString m_managed_id, m_managed_version_id;
std::optional<InstancePtr> m_instance;
};

View File

@ -33,13 +33,19 @@ bool ModrinthCreationTask::updateInstance()
auto instance_list = APPLICATION->instances();
// FIXME: How to handle situations when there's more than one install already for a given modpack?
auto inst = instance_list->getInstanceByManagedName(originalName());
InstancePtr inst;
if (auto original_id = originalInstanceID(); !original_id.isEmpty()) {
inst = instance_list->getInstanceById(original_id);
Q_ASSERT(inst);
} else {
inst = instance_list->getInstanceByManagedName(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
if (!inst)
return false;
if (!inst)
return false;
}
}
QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json");
@ -49,25 +55,14 @@ bool ModrinthCreationTask::updateInstance()
auto version_name = inst->getManagedPackVersionName();
auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : "";
auto info = CustomMessageBox::selectable(
m_parent, tr("Similar modpack was found!"),
tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(version_str),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, tr("Create new instance"));
info->setButtonText(QMessageBox::Abort, tr("Update existing instance"));
info->setButtonText(QMessageBox::Reset, tr("Cancel"));
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok))
return false;
if (info->clickedButton() == info->button(QMessageBox::Reset)) {
m_abort = true;
return false;
if (shouldConfirmUpdate()) {
auto should_update = askIfShouldUpdate(m_parent, version_str);
if (should_update == ShouldUpdate::SkipUpdating)
return false;
if (should_update == ShouldUpdate::Cancel) {
m_abort = true;
return false;
}
}
// Remove repeated files, we don't need to download them!
@ -149,7 +144,7 @@ bool ModrinthCreationTask::updateInstance()
}
setOverride(true);
setOverride(true, inst->id());
qDebug() << "Will override instance!";
m_instance = inst;
@ -222,7 +217,7 @@ bool ModrinthCreationTask::createInstance()
instance.setIconKey("modrinth");
}
instance.setManagedPack("modrinth", getManagedPackID(), m_managed_name, m_managed_version_id, version());
instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
instance.setName(name());
instance.saveNow();
@ -295,7 +290,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<
}
if (set_managed_info) {
m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID");
if (m_managed_version_id.isEmpty())
m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID");
m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name");
}
@ -395,13 +391,3 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<
return true;
}
QString ModrinthCreationTask::getManagedPackID() const
{
if (!m_source_url.isEmpty()) {
QRegularExpression regex(R"(data\/(.*)\/versions)");
return regex.match(m_source_url).captured(1);
}
return {};
}

View File

@ -14,11 +14,21 @@ class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
ModrinthCreationTask(QString staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString source_url = {})
: InstanceCreationTask(), m_parent(parent), m_source_url(std::move(source_url))
ModrinthCreationTask(QString staging_path,
SettingsObjectPtr global_settings,
QWidget* parent,
QString id,
QString version_id = {},
QString original_instance_id = {})
: InstanceCreationTask()
, m_parent(parent)
, m_managed_id(std::move(id))
, m_managed_version_id(std::move(version_id))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
m_original_instance_id = std::move(original_instance_id);
}
bool abort() override;
@ -28,14 +38,12 @@ class ModrinthCreationTask final : public InstanceCreationTask {
private:
bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_managed_info = true, bool show_optional_dialog = true);
QString getManagedPackID() const;
private:
QWidget* m_parent = nullptr;
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
QString m_managed_id, m_managed_version_id, m_managed_name;
QString m_source_url;
std::vector<Modrinth::File> m_files;
NetJob::Ptr m_files_job;

View File

@ -128,6 +128,7 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
file.name = Json::requireString(obj, "name");
file.version = Json::requireString(obj, "version_number");
file.changelog = Json::ensureString(obj, "changelog");
file.id = Json::requireString(obj, "id");
file.project_id = Json::requireString(obj, "project_id");

View File

@ -80,6 +80,7 @@ struct ModpackExtra {
struct ModpackVersion {
QString name;
QString version;
QString changelog;
QString id;
QString project_id;

View File

@ -132,6 +132,12 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent)
{
connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged);
}
// add ourself as the modpack page's instance window
{
static_cast<ManagedPackPage*>(m_container->getPage("managed_pack"))->setInstanceWindow(this);
}
show();
}

View File

@ -1,10 +1,13 @@
#pragma once
class BasePage;
class BasePageContainer
{
public:
virtual ~BasePageContainer(){};
virtual bool selectPage(QString pageId) = 0;
virtual BasePage* getPage(QString pageId) { return nullptr; };
virtual void refreshContainer() = 0;
virtual bool requestClose() = 0;
};

View File

@ -0,0 +1,432 @@
// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "ManagedPackPage.h"
#include "ui_ManagedPackPage.h"
#include <QListView>
#include <QProxyStyle>
#include <HoeDown.h>
#include "Application.h"
#include "BuildConfig.h"
#include "InstanceImportTask.h"
#include "InstanceList.h"
#include "InstanceTask.h"
#include "Json.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "ui/InstanceWindow.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
/** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen.
* ... thanks Qt.
*/
class NoBigComboBoxStyle : public QProxyStyle {
Q_OBJECT
public:
NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {}
// clang-format off
int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override
{
if (hint == QStyle::SH_ComboBox_Popup)
return false;
return QProxyStyle::styleHint(hint, option, widget, returnData);
}
// clang-format on
};
ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent)
{
if (type == "modrinth")
return new ModrinthManagedPackPage(inst, nullptr, parent);
if (type == "flame" && (APPLICATION->capabilities() & Application::SupportsFlame))
return new FlameManagedPackPage(inst, nullptr, parent);
return new GenericManagedPackPage(inst, nullptr, parent);
}
ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: QWidget(parent), m_instance_window(instance_window), ui(new Ui::ManagedPackPage), m_inst(inst)
{
Q_ASSERT(inst);
ui->setupUi(this);
ui->versionsComboBox->setStyle(new NoBigComboBoxStyle(ui->versionsComboBox->style()));
ui->reloadButton->setVisible(false);
connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){
ui->reloadButton->setVisible(false);
m_loaded = false;
// Pretend we're opening the page again
openedImpl();
});
}
ManagedPackPage::~ManagedPackPage()
{
delete ui;
}
void ManagedPackPage::openedImpl()
{
ui->packName->setText(m_inst->getManagedPackName());
ui->packVersion->setText(m_inst->getManagedPackVersionName());
ui->packOrigin->setText(tr("Website: <a href=%1>%2</a> | Pack ID: %3 | Version ID: %4")
.arg(url(), displayName(), m_inst->getManagedPackID(), m_inst->getManagedPackVersionID()));
parseManagedPack();
}
QString ManagedPackPage::displayName() const
{
auto type = m_inst->getManagedPackType();
if (type.isEmpty())
return {};
if (type == "flame")
type = "CurseForge";
return type.replace(0, 1, type[0].toUpper());
}
QIcon ManagedPackPage::icon() const
{
return APPLICATION->getThemedIcon(m_inst->getManagedPackType());
}
QString ManagedPackPage::helpPage() const
{
return {};
}
void ManagedPackPage::retranslate()
{
ui->retranslateUi(this);
}
bool ManagedPackPage::shouldDisplay() const
{
return m_inst->isManagedPack();
}
bool ManagedPackPage::runUpdateTask(InstanceTask* task)
{
Q_ASSERT(task);
unique_qobject_ptr<Task> wrapped_task(APPLICATION->instances()->wrapInstanceTask(task));
connect(task, &Task::failed,
[this](QString reason) { 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();
});
connect(task, &Task::aborted, [this] {
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)
->show();
});
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task);
return task->wasSuccessful();
}
void ManagedPackPage::suggestVersion()
{
ui->updateButton->setText(tr("Update pack"));
ui->updateButton->setDisabled(false);
}
void ManagedPackPage::setFailState()
{
qDebug() << "Setting fail state!";
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
ui->versionsComboBox->clear();
ui->versionsComboBox->addItem(tr("Failed to search for available versions."), {});
ui->versionsComboBox->blockSignals(false);
ui->changelogTextBrowser->setText(tr("Failed to request changelog data for this modpack."));
ui->updateButton->setText(tr("Cannot update!"));
ui->updateButton->setDisabled(true);
ui->reloadButton->setVisible(true);
}
ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent)
{
Q_ASSERT(inst->isManagedPack());
connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion()));
connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update);
}
// MODRINTH
void ModrinthManagedPackPage::parseManagedPack()
{
qDebug() << "Parsing Modrinth pack";
// No need for the extra work because we already have everything we need.
if (m_loaded)
return;
if (m_fetch_job && m_fetch_job->isRunning())
m_fetch_job->abort();
m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network()));
auto response = std::make_shared<QByteArray>();
QString id = m_inst->getManagedPackID();
m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get()));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
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;
setFailState();
return;
}
try {
Modrinth::loadIndexedVersions(m_pack, doc);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading modrinth modpack version: " << e.cause();
setFailState();
return;
}
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
ui->versionsComboBox->clear();
ui->versionsComboBox->blockSignals(false);
for (auto version : m_pack.versions) {
QString name;
if (!version.name.contains(version.version))
name = QString("%1 — %2").arg(version.name, version.version);
else
name = version.name;
// NOTE: the id from version isn't the same id in the modpack format spec...
// e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index..............
if (version.version == m_inst->getManagedPackVersionName())
name.append(tr(" (Current)"));
ui->versionsComboBox->addItem(name, QVariant(version.id));
}
suggestVersion();
m_loaded = true;
});
QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState);
QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState);
ui->changelogTextBrowser->setText(tr("Fetching changelogs..."));
m_fetch_job->start();
}
QString ModrinthManagedPackPage::url() const
{
return "https://modrinth.com/mod/" + m_inst->getManagedPackID();
}
void ModrinthManagedPackPage::suggestVersion()
{
auto index = ui->versionsComboBox->currentIndex();
auto version = m_pack.versions.at(index);
HoeDown md_parser;
ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8()));
ManagedPackPage::suggestVersion();
}
void ModrinthManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info;
// NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field.
extra_info.insert("pack_id", m_inst->getManagedPackID());
extra_info.insert("pack_version_id", version.id);
extra_info.insert("original_instance_id", m_inst->id());
auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info));
InstanceName inst_name(m_inst->getManagedPackName(), version.version);
inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version));
extracted->setName(inst_name);
extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id()));
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
}
// FLAME
FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent)
{
Q_ASSERT(inst->isManagedPack());
connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion()));
connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update);
}
void FlameManagedPackPage::parseManagedPack()
{
qDebug() << "Parsing Flame pack";
// We need to tell the user to redownload the pack, since we didn't save the required info previously
if (m_inst->getManagedPackID().isEmpty()) {
setFailState();
QString message =
tr("<h1>Hey there!</h1>"
"<h4>"
"It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.<br/>"
"Unfortunately, we can't do the proper API requests without this information.<br/>"
"<br/>"
"So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.<br/>"
"<br/>"
"Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!"
"</h4>");
ui->changelogTextBrowser->setHtml(message);
return;
}
// No need for the extra work because we already have everything we need.
if (m_loaded)
return;
if (m_fetch_job && m_fetch_job->isRunning())
m_fetch_job->abort();
m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network()));
auto response = std::make_shared<QByteArray>();
QString id = m_inst->getManagedPackID();
m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get()));
QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] {
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;
setFailState();
return;
}
try {
auto obj = doc.object();
auto data = Json::ensureArray(obj, "data");
Flame::loadIndexedPackVersions(m_pack, data);
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading flame modpack version: " << e.cause();
setFailState();
return;
}
// We block signals here so that suggestVersion() doesn't get called, causing an assertion fail.
ui->versionsComboBox->blockSignals(true);
ui->versionsComboBox->clear();
ui->versionsComboBox->blockSignals(false);
for (auto version : m_pack.versions) {
QString name;
name = version.version;
if (version.fileId == m_inst->getManagedPackVersionID().toInt())
name.append(tr(" (Current)"));
ui->versionsComboBox->addItem(name, QVariant(version.fileId));
}
suggestVersion();
m_loaded = true;
});
QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState);
QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState);
m_fetch_job->start();
}
QString FlameManagedPackPage::url() const
{
// FIXME: We should display the websiteUrl field, but this requires doing the API request first :(
return {};
}
void FlameManagedPackPage::suggestVersion()
{
auto index = ui->versionsComboBox->currentIndex();
auto version = m_pack.versions.at(index);
ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId));
ManagedPackPage::suggestVersion();
}
void FlameManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
auto version = m_pack.versions.at(index);
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", m_inst->getManagedPackID());
extra_info.insert("pack_version_id", QString::number(version.fileId));
extra_info.insert("original_instance_id", m_inst->id());
auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info));
extracted->setName(m_inst->name());
extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id()));
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
}
#include "ManagedPackPage.moc"

View File

@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "BaseInstance.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlamePackIndex.h"
#include "ui/pages/BasePage.h"
#include <QWidget>
namespace Ui {
class ManagedPackPage;
}
class InstanceTask;
class InstanceWindow;
class ManagedPackPage : public QWidget, public BasePage {
Q_OBJECT
public:
inline static ManagedPackPage* createPage(BaseInstance* inst, QWidget* parent = nullptr)
{
return ManagedPackPage::createPage(inst, inst->getManagedPackType(), parent);
}
static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr);
~ManagedPackPage() override;
[[nodiscard]] QString displayName() const override;
[[nodiscard]] QIcon icon() const override;
[[nodiscard]] QString helpPage() const override;
[[nodiscard]] QString id() const override { return "managed_pack"; }
[[nodiscard]] bool shouldDisplay() const override;
void openedImpl() override;
bool apply() override { return true; }
void retranslate() override;
/** Gets the necessary information about the managed pack, such as
* available versions*/
virtual void parseManagedPack(){};
/** URL of the managed pack.
* Not the version-specific one.
*/
[[nodiscard]] virtual QString url() const { return {}; };
void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; }
public slots:
/** Gets the current version selection and update the UI, including the update button and the changelog.
*/
virtual void suggestVersion();
virtual void update(){};
protected slots:
/** Does the necessary UI changes for when something failed.
*
* This includes:
* - Setting an appropriate text on the version selector to indicate a fail;
* - Setting an appropriate text on the changelog text browser to indicate a fail;
* - Disable the update button.
*/
void setFailState();
protected:
ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr);
/** Run the InstanceTask, with a progress dialog and all.
* Similar to MainWindow::instanceFromInstanceTask
*
* Returns whether the task was successful.
*/
bool runUpdateTask(InstanceTask*);
protected:
InstanceWindow* m_instance_window = nullptr;
Ui::ManagedPackPage* ui;
BaseInstance* m_inst;
bool m_loaded = false;
};
/** Simple page for when we aren't a managed pack. */
class GenericManagedPackPage final : public ManagedPackPage {
Q_OBJECT
public:
GenericManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr)
: ManagedPackPage(inst, instance_window, parent)
{}
~GenericManagedPackPage() override = default;
// TODO: We may want to show this page with some useful info at some point.
[[nodiscard]] bool shouldDisplay() const override { return false; };
};
class ModrinthManagedPackPage final : public ManagedPackPage {
Q_OBJECT
public:
ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr);
~ModrinthManagedPackPage() override = default;
void parseManagedPack() override;
[[nodiscard]] QString url() const override;
public slots:
void suggestVersion() override;
void update() override;
private:
NetJob::Ptr m_fetch_job = nullptr;
Modrinth::Modpack m_pack;
ModrinthAPI m_api;
};
class FlameManagedPackPage final : public ManagedPackPage {
Q_OBJECT
public:
FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr);
~FlameManagedPackPage() override = default;
void parseManagedPack() override;
[[nodiscard]] QString url() const override;
public slots:
void suggestVersion() override;
void update() override;
private:
NetJob::Ptr m_fetch_job = nullptr;
Flame::IndexedPack m_pack;
FlameAPI m_api;
};

View File

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ManagedPackPage</class>
<widget class="QWidget" name="ManagedPackPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>731</width>
<height>538</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="packInformationBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Pack information</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<layout class="QHBoxLayout" name="packNameLayout">
<item>
<widget class="QLabel" name="packNameLabel">
<property name="text">
<string>Pack name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="packName">
<property name="text">
<string notr="true">placeholder</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="packVersionLayout">
<item>
<widget class="QLabel" name="packVersionLabel">
<property name="text">
<string>Current version:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="packVersion">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
<string notr="true">placeholder</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="packOriginLayout">
<item>
<widget class="QLabel" name="packOriginLabel">
<property name="text">
<string>Provider information:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="packOrigin">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
<string notr="true">placeholder</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="updateToVersionLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Update to version:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="versionsComboBox"/>
</item>
<item>
<widget class="QPushButton" name="updateButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Fetching versions...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="changelogBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Changelog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextBrowser" name="changelogTextBrowser">
<property name="placeholderText">
<string>No changelog available for this version!</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="reloadButton">
<property name="text">
<string>Reload page</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -197,12 +197,18 @@ void FlamePage::suggestCurrent()
return;
}
if (selectedVersion.isEmpty() || selectedVersion == "-1") {
if (m_selected_version_index == -1) {
dialog->setSuggestedPack();
return;
}
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
auto version = current.versions.at(m_selected_version_index);
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", QString::number(current.addonId));
extra_info.insert("pack_version_id", QString::number(version.fileId));
dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl,
@ -211,11 +217,18 @@ void FlamePage::suggestCurrent()
void FlamePage::onVersionSelectionChanged(QString data)
{
if (data.isNull() || data.isEmpty()) {
selectedVersion = "";
bool is_blocked = false;
ui->versionSelectionBox->currentData().toInt(&is_blocked);
if (data.isNull() || data.isEmpty() || is_blocked) {
m_selected_version_index = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toString();
m_selected_version_index = ui->versionSelectionBox->currentIndex();
Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString());
suggestCurrent();
}

View File

@ -99,5 +99,5 @@ private:
Flame::ListModel* listModel = nullptr;
Flame::IndexedPack current;
QString selectedVersion;
int m_selected_version_index = -1;
};

View File

@ -300,7 +300,11 @@ void ModrinthPage::suggestCurrent()
for (auto& ver : current.versions) {
if (ver.id == selectedVersion) {
dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this));
QMap<QString, QString> extra_info;
extra_info.insert("pack_id", current.id);
extra_info.insert("pack_version_id", ver.id);
dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this, std::move(extra_info)));
auto iconName = current.iconName;
m_model->getLogo(iconName, current.iconUrl.toString(),
[this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); });

View File

@ -130,6 +130,11 @@ bool PageContainer::selectPage(QString pageId)
return false;
}
BasePage* PageContainer::getPage(QString pageId)
{
return m_model->findPageEntryById(pageId);
}
void PageContainer::refreshContainer()
{
m_proxyModel->invalidate();

View File

@ -79,6 +79,7 @@ public:
}
virtual bool selectPage(QString pageId) override;
BasePage* getPage(QString pageId) override;
void refreshContainer() override;
virtual void setParentContainer(BasePageContainer * container)