diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 18e0acab1..c73d89ec3 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -916,6 +916,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h + ui/pages/modplatform/OptionalModDialog.cpp + ui/pages/modplatform/OptionalModDialog.h + ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp ui/pages/modplatform/modrinth/ModrinthResourceModels.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -1080,6 +1083,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/import_ftb/ImportFTBPage.ui ui/pages/modplatform/ImportPage.ui + ui/pages/modplatform/OptionalModDialog.ui ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/InstanceCardWidget.ui diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 45b4e2125..ead5b464e 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -62,6 +62,7 @@ #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "net/ApiDownload.h" +#include "ui/pages/modplatform/OptionalModDialog.h" static const FlameAPI api; @@ -509,13 +510,33 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); - for (const auto& result : m_mod_id_resolver->getResults().files) { - QString filename = result.fileName; + auto results = m_mod_id_resolver->getResults().files; + + QStringList optionalFiles; + for (auto& result : results) { if (!result.required) { - filename += ".disabled"; + optionalFiles << FS::PathCombine(result.targetFolder, result.fileName); + } + } + + QStringList selectedOptionalMods; + if (!optionalFiles.empty()) { + OptionalModDialog optionalModDialog(m_parent, optionalFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + loop.quit(); + return; } - auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + selectedOptionalMods = optionalModDialog.getResult(); + } + for (const auto& result : results) { + auto relpath = FS::PathCombine(result.targetFolder, result.fileName); + if (!result.required && !selectedOptionalMods.contains(relpath)) { + relpath += ".disabled"; + } + + relpath = FS::PathCombine("minecraft", relpath); auto path = FS::PathCombine(m_stagingPath, relpath); switch (result.type) { diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 854cdbc41..4417c2430 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -48,7 +48,7 @@ struct File { int projectId = 0; int fileId = 0; - // NOTE: the opposite to 'optional'. This is at the time of writing unused. + // NOTE: the opposite to 'optional' bool required = true; QString hash; // NOTE: only set on blocked files ! Empty otherwise. diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 47a0de5fe..1be58c2b8 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -9,6 +9,7 @@ #include "modplatform/helpers/OverrideUtils.h" +#include "modplatform/modrinth/ModrinthPackManifest.h" #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" @@ -16,8 +17,10 @@ #include "settings/INISettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/pages/modplatform/OptionalModDialog.h" #include +#include bool ModrinthCreationTask::abort() { @@ -319,7 +322,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - bool had_optional = false; + std::vector optionalFiles; for (const auto& modInfo : jsonFiles) { Modrinth::File file; file.path = Json::requireString(modInfo, "path").replace("\\", "/"); @@ -331,18 +334,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, if (support == "unsupported") { continue; } else if (support == "optional") { - // TODO: Make a review dialog for choosing which ones the user wants! - if (!had_optional && show_optional_dialog) { - had_optional = true; - auto info = CustomMessageBox::selectable( - m_parent, tr("Optional mod detected!"), - tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), - QMessageBox::Information); - info->exec(); - } - - if (file.path.endsWith(".jar")) - file.path += ".disabled"; + file.required = false; } } @@ -385,9 +377,29 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } } - files.push_back(file); + (file.required ? files : optionalFiles).push_back(file); } + if (!optionalFiles.empty()) { + QStringList oFiles; + for (auto file : optionalFiles) + oFiles.push_back(file.path); + OptionalModDialog optionalModDialog(m_parent, oFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + return false; + } + + auto selectedMods = optionalModDialog.getResult(); + for (auto file : optionalFiles) { + if (selectedMods.contains(file.path)) { + file.required = true; + } else if (file.path.endsWith(".jar")) { + file.path += ".disabled"; + } + files.push_back(file); + } + } if (set_internal_data) { auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index effa1a84a..93ae69697 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -55,6 +55,7 @@ struct File { QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; QQueue downloads; + bool required = true; }; struct DonationData { diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.cpp b/launcher/ui/pages/modplatform/OptionalModDialog.cpp new file mode 100644 index 000000000..620de4f74 --- /dev/null +++ b/launcher/ui/pages/modplatform/OptionalModDialog.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "OptionalModDialog.h" +#include "ui_OptionalModDialog.h" + +OptionalModListModel::OptionalModListModel(QWidget* parent, QStringList mods) : QAbstractListModel(parent), m_mods(mods) {} + +QStringList OptionalModListModel::getResult() +{ + QStringList result; + for (const auto& mod : m_mods) { + if (m_selected.value(mod, false)) { + result << mod; + } + } + return result; +} + +int OptionalModListModel::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_mods.size(); +} + +int OptionalModListModel::columnCount(const QModelIndex& parent) const +{ + // Enabled, Name + return parent.isValid() ? 0 : 2; +} + +QVariant OptionalModListModel::data(const QModelIndex& index, int role) const +{ + auto row = index.row(); + auto mod = m_mods.at(row); + + if (role == Qt::DisplayRole && index.column() == NameColumn) { + return mod; + } else if (role == Qt::CheckStateRole && index.column() == EnabledColumn) { + return m_selected.value(mod, false) ? Qt::Checked : Qt::Unchecked; + } + + return {}; +} + +bool OptionalModListModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) +{ + if (role == Qt::CheckStateRole) { + auto row = index.row(); + auto mod = m_mods.at(row); + + toggleMod(mod, row); + return true; + } + + return false; +} + +QVariant OptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case EnabledColumn: + return QString(); + case NameColumn: + return QString("Name"); + } + } + + return {}; +} + +Qt::ItemFlags OptionalModListModel::flags(const QModelIndex& index) const +{ + auto flags = QAbstractListModel::flags(index); + if (index.isValid() && index.column() == EnabledColumn) { + flags |= Qt::ItemIsUserCheckable; + } + return flags; +} + +void OptionalModListModel::toggleAll(bool enabled) +{ + for (const auto& mod : m_mods) { + m_selected[mod] = enabled; + } + + emit dataChanged(OptionalModListModel::index(0, EnabledColumn), OptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void OptionalModListModel::toggleMod(QString mod, int index) +{ + auto enable = !m_selected.value(mod, false); + + setMod(mod, index, enable); +} + +void OptionalModListModel::setMod(QString mod, int index, bool enable, bool shouldEmit) +{ + if (m_selected.value(mod, false) == enable) + return; + + m_selected[mod] = enable; + + if (shouldEmit) { + emit dataChanged(OptionalModListModel::index(index, EnabledColumn), OptionalModListModel::index(index, EnabledColumn)); + } +} + +OptionalModDialog::OptionalModDialog(QWidget* parent, QStringList mods) : QDialog(parent), ui(new Ui::OptionalModDialog) +{ + ui->setupUi(this); + + listModel = new OptionalModListModel(this, mods); + ui->treeView->setModel(listModel); + + ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->treeView->header()->setSectionResizeMode(OptionalModListModel::NameColumn, QHeaderView::Stretch); + + connect(ui->selectAllButton, &QPushButton::clicked, listModel, &OptionalModListModel::selectAll); + connect(ui->clearAllButton, &QPushButton::clicked, listModel, &OptionalModListModel::clearAll); + connect(ui->installButton, &QPushButton::clicked, this, &QDialog::accept); + connect(ui->cancelButton, &QPushButton::clicked, this, &QDialog::reject); +} + +OptionalModDialog::~OptionalModDialog() +{ + delete ui; +} diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.h b/launcher/ui/pages/modplatform/OptionalModDialog.h new file mode 100644 index 000000000..4e22d0e46 --- /dev/null +++ b/launcher/ui/pages/modplatform/OptionalModDialog.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include + +namespace Ui { +class OptionalModDialog; +} + +class OptionalModListModel : public QAbstractListModel { + Q_OBJECT + public: + enum Columns { + EnabledColumn = 0, + NameColumn, + }; + + OptionalModListModel(QWidget* parent, QStringList mods); + + QStringList getResult(); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + Qt::ItemFlags flags(const QModelIndex& index) const override; + + public slots: + void selectAll() { toggleAll(true); } + void clearAll() { toggleAll(false); }; + void toggleAll(bool enabled); + + private: + void toggleMod(QString mod, int index); + void setMod(QString mod, int index, bool enable, bool shouldEmit = true); + + private: + QStringList m_mods; + QHash m_selected; +}; + +class OptionalModDialog : public QDialog { + Q_OBJECT + + public: + OptionalModDialog(QWidget* parent, QStringList mods); + ~OptionalModDialog() override; + + QStringList getResult() { return listModel->getResult(); } + + private: + Ui::OptionalModDialog* ui; + + OptionalModListModel* listModel; +}; diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.ui b/launcher/ui/pages/modplatform/OptionalModDialog.ui new file mode 100644 index 000000000..76d1da89d --- /dev/null +++ b/launcher/ui/pages/modplatform/OptionalModDialog.ui @@ -0,0 +1,105 @@ + + + OptionalModDialog + + + + 0 + 0 + 550 + 310 + + + + Select Mods To Install + + + + + + + + + 11 + 75 + true + + + + Select optional mods to install. + + + Qt::AlignCenter + + + + + + + + true + + + + Note: All files will be downloaded but the unselected mods will be disabled. + + + Qt::AlignCenter + + + + + + + + + + + + + + true + + + Cancel + + + + + + + Clear All + + + + + + + Select All + + + + + + + Install + + + true + + + + + + + + + + ModListView + QTreeView +
ui/widgets/ModListView.h
+
+
+ + +
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 55903003b..767d277d9 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -38,7 +38,7 @@ #include #include -#include "modplatform/atlauncher/ATLPackIndex.h" +#include "modplatform/atlauncher/ATLPackManifest.h" #include "net/NetJob.h" namespace Ui {