add installation confirm popup

This commit is contained in:
HJfod 2024-05-09 15:15:06 +03:00
parent cdc41a5c10
commit 9c7c87fb17
12 changed files with 122 additions and 379 deletions

View file

@ -17,7 +17,7 @@ class ModDownload::Impl final {
public:
std::string m_id;
std::optional<VersionInfo> m_version;
std::optional<std::string> m_dependencyFor;
std::optional<DependencyFor> m_dependencyFor;
DownloadStatus m_status;
EventListener<ServerRequest<ServerModVersion>> m_infoListener;
EventListener<web::WebTask> m_downloadListener;
@ -25,7 +25,7 @@ public:
Impl(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
std::optional<DependencyFor> const& dependencyFor
)
: m_id(id),
m_version(version),
@ -40,11 +40,12 @@ public:
auto data = result->unwrap();
m_version = data.metadata.getVersion();
// Start downloads for any missing dependencies
// Start downloads for any missing required dependencies
for (auto dep : data.metadata.getDependencies()) {
if (!dep.mod) {
if (!dep.mod && dep.importance != ModMetadata::Dependency::Importance::Suggested) {
ModDownloadManager::get()->startDownload(
dep.id, dep.version.getUnderlyingVersion(), m_id
dep.id, dep.version.getUnderlyingVersion(),
std::make_pair(m_id, dep.importance)
);
}
}
@ -145,14 +146,14 @@ public:
ModDownload::ModDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
std::optional<DependencyFor> const& dependencyFor
) : m_impl(std::make_shared<Impl>(id, version, dependencyFor)) {}
void ModDownload::confirm() {
m_impl->confirm();
}
std::optional<std::string> ModDownload::getDependencyFor() const {
std::optional<DependencyFor> ModDownload::getDependencyFor() const {
return m_impl->m_dependencyFor;
}
bool ModDownload::isDone() const {
@ -193,8 +194,8 @@ public:
for (auto& [_, d] : m_downloads) {
if (auto depFor = d.m_impl->m_dependencyFor) {
if (
!m_downloads.contains(*depFor) ||
!(m_downloads.at(*depFor).isActive() || m_downloads.at(*depFor).isDone())
!m_downloads.contains(depFor->first) ||
!(m_downloads.at(depFor->first).isActive() || m_downloads.at(depFor->first).isDone())
) {
// d.cancel() will cause cancelOrphanedDependencies() to be called again
// We want that anyway because cancelling one dependency might cause
@ -222,7 +223,7 @@ void ModDownload::cancel() {
std::optional<ModDownload> ModDownloadManager::startDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
std::optional<DependencyFor> const& dependencyFor
) {
// If this mod has already been succesfully downloaded or is currently
// being downloaded, return as you can't download multiple versions of the
@ -277,13 +278,6 @@ bool ModDownloadManager::checkAutoConfirm() {
for (auto& [_, download] : m_impl->m_downloads) {
auto status = download.getStatus();
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
for (auto dep : confirm->version.metadata.getDependencies()) {
// If some mod has an optional dependency that isn't installed,
// we need to ask for confirmation
if (!dep.mod && dep.importance != ModMetadata::Dependency::Importance::Required) {
return false;
}
}
for (auto inc : confirm->version.metadata.getIncompatibilities()) {
// If some mod has an incompatability that is installed,
// we need to ask for confirmation

View file

@ -54,6 +54,8 @@ namespace server {
ModDownloadFilter(std::string const& id);
};
using DependencyFor = std::pair<std::string, ModMetadata::Dependency::Importance>;
class ModDownload final {
private:
class Impl;
@ -63,7 +65,7 @@ namespace server {
ModDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
std::optional<DependencyFor> const& dependencyFor
);
friend class ModDownloadManager;
@ -75,7 +77,7 @@ namespace server {
bool isDone() const;
bool isActive() const;
bool canRetry() const;
std::optional<std::string> getDependencyFor() const;
std::optional<DependencyFor> getDependencyFor() const;
std::string getID() const;
DownloadStatus getStatus() const;
std::optional<VersionInfo> getVersion() const;
@ -98,7 +100,7 @@ namespace server {
std::optional<ModDownload> startDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor = std::nullopt
std::optional<DependencyFor> const& dependencyFor = std::nullopt
);
void startUpdateAll();
void confirmAll();

View file

@ -3,7 +3,7 @@
#include <Geode/ui/TextInput.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include "popups/ConfirmInstallPopup.hpp"
#include "popups/ConfirmInstall.hpp"
#include "GeodeStyle.hpp"
bool ModsStatusNode::init() {
@ -245,7 +245,7 @@ void ModsStatusNode::onViewErrors(CCObject*) {
);
}
void ModsStatusNode::onConfirm(CCObject*) {
ConfirmInstallPopup::askForCustomize();
askConfirmModInstalls();
}
void ModsStatusNode::onCancel(CCObject*) {
server::ModDownloadManager::get()->cancelAll();

View file

@ -1,97 +0,0 @@
#include "ModProblemItem.hpp"
#include <Geode/utils/ColorProvider.hpp>
#include <Geode/ui/GeodeUI.hpp>
bool ModProblemItem::init() {
if (!CCNode::init())
return false;
this->setContentSize({ 250, 28 });
auto bg = CCScale9Sprite::create("square02b_small.png");
bg->setOpacity(20);
bg->ignoreAnchorPointForPosition(false);
bg->setAnchorPoint({ .5f, .5f });
bg->setScale(.7f);
bg->setContentSize(m_obContentSize / bg->getScale());
this->addChildAtPosition(bg, Anchor::Center);
m_menu = CCMenu::create();
m_menu->setContentWidth(m_obContentSize.width / 2 - 10);
m_menu->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .35f)
->setAxisAlignment(AxisAlignment::End)
);
this->addChildAtPosition(m_menu, Anchor::Right, ccp(-5, 0), ccp(1, .5f));
this->setLoading();
return true;
}
void ModProblemItem::clearState() {
this->removeChildByID("loading-spinner");
this->removeChildByID("error-label");
}
void ModProblemItem::setLoading() {
this->clearState();
auto loadingCircle = createLoadingCircle(20);
this->addChildAtPosition(loadingCircle, Anchor::Center);
}
void ModProblemItem::setError(std::string const& error) {
this->clearState();
auto label = CCLabelBMFont::create(error.c_str(), "bigFont.fnt");
label->setColor("mod-list-errors-found"_cc3b);
label->limitLabelWidth(m_obContentSize.width, .5f, .1f);
label->setID("error-label");
this->addChildAtPosition(label, Anchor::Center);
}
void ModProblemItem::setMod(ModMetadata const& metadata) {
this->clearState();
auto h = m_obContentSize.height;
auto logo = createServerModLogo(metadata.getID());
limitNodeSize(logo, { h / 1.4f, h / 1.4f }, 5.f, .1f);
this->addChildAtPosition(logo, Anchor::Left, ccp(h / 2, 0));
auto title = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt");
title->limitLabelWidth(m_obContentSize.width / 2, .35f, .1f);
this->addChildAtPosition(title, Anchor::Left, ccp(h, h / 5), ccp(0, .5f));
auto versionLabel = CCLabelBMFont::create(
metadata.getVersion().toVString().c_str(),
"bigFont.fnt"
);
versionLabel->setColor("mod-list-version-label"_cc3b);
versionLabel->limitLabelWidth(m_obContentSize.width / 2, .25f, .1f);
this->addChildAtPosition(
versionLabel, Anchor::Left,
ccp(h + title->getScaledContentWidth() + 3, h / 5), ccp(0, .5f)
);
auto developer = CCLabelBMFont::create(
fmt::format("By {}", metadata.getDeveloper()).c_str(),
"goldFont.fnt"
);
developer->limitLabelWidth(m_obContentSize.width / 2, .35f, .1f);
this->addChildAtPosition(developer, Anchor::Left, ccp(h, -h / 5), ccp(0, .5f));
}
ModProblemItem* ModProblemItem::create() {
auto ret = new ModProblemItem();
if (ret && ret->init()) {
ret->setError("Unknown Type");
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
ModProblemItem* ModProblemItem::parse(LoadProblem const& problem) {
return ModProblemItem::create();
}

View file

@ -1,25 +0,0 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include <Geode/loader/Loader.hpp>
#include <server/Server.hpp>
#include "../GeodeStyle.hpp"
using namespace geode::prelude;
using VersionDownload = server::ServerRequest<server::ServerModVersion>;
class ModProblemItem : public CCNode {
protected:
CCMenu* m_menu;
bool init();
void clearState();
void setLoading();
void setError(std::string const& error);
void setMod(ModMetadata const& metadata);
public:
static ModProblemItem* create();
static ModProblemItem* parse(LoadProblem const& problem);
};

View file

@ -1,33 +0,0 @@
#include "ModProblemItemList.hpp"
#include "ModProblemItem.hpp"
bool ModProblemItemList::init(float height) {
if (!CCNode::init())
return false;
this->setContentSize({ 250, height });
m_scrollLayer = ScrollLayer::create({ m_obContentSize.width, height });
for (auto problem : Loader::get()->getProblems()) {
m_scrollLayer->m_contentLayer->addChild(ModProblemItem::parse(problem));
}
m_scrollLayer->m_contentLayer->setLayout(
ColumnLayout::create()
->setAutoGrowAxis(height)
->setAxisReverse(true)
->setAxisAlignment(AxisAlignment::End)
);
this->addChildAtPosition(m_scrollLayer, Anchor::BottomLeft);
return true;
}
ModProblemItemList* ModProblemItemList::create(float height) {
auto ret = new ModProblemItemList();
if (ret && ret->init(height)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}

View file

@ -1,16 +0,0 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include <Geode/ui/ScrollLayer.hpp>
using namespace geode::prelude;
class ModProblemItemList : public CCNode {
protected:
ScrollLayer* m_scrollLayer;
bool init(float height);
public:
static ModProblemItemList* create(float height);
};

View file

@ -0,0 +1,94 @@
#include "ConfirmInstall.hpp"
#include <server/DownloadManager.hpp>
using namespace geode::prelude;
using namespace server;
void askConfirmModInstalls() {
struct ToConfirm final {
size_t modCount = 0;
size_t dependencyCount = 0;
std::unordered_set<Mod*> toDisable;
std::unordered_set<Mod*> toEnable;
};
auto toConfirm = ToConfirm();
// Collect all things we need to ask confirmation for
for (auto download : ModDownloadManager::get()->getDownloads()) {
auto status = download.getStatus();
if (auto conf = std::get_if<DownloadStatusConfirm>(&status)) {
if (auto dep = download.getDependencyFor()) {
toConfirm.dependencyCount += 1;
}
else {
toConfirm.modCount += 1;
// Since the user has already explicitly chosen to download these mods, we
// are going to assume they want these mods enabled over already installed
// ones
// If this mod has incompatabilities that are installed, disable them
for (auto inc : conf->version.metadata.getIncompatibilities()) {
if (inc.mod && inc.mod->isOrWillBeEnabled()) {
toConfirm.toDisable.insert(inc.mod);
}
}
// If some installed mods are incompatible with this one, disable them
for (auto mod : Loader::get()->getAllMods()) {
for (auto inc : mod->getMetadata().getIncompatibilities()) {
if (inc.id == conf->version.metadata.getID() && mod->isOrWillBeEnabled()) {
toConfirm.toDisable.insert(mod);
}
}
}
// If this mod has required dependencies that are disabled, enable them
for (auto dep : conf->version.metadata.getDependencies()) {
if (
dep.importance == ModMetadata::Dependency::Importance::Required &&
dep.mod && !dep.mod->isOrWillBeEnabled()
) {
toConfirm.toEnable.insert(dep.mod);
}
}
}
}
}
auto joinModsToIDs = [](std::unordered_set<Mod*> const& mods) {
return ranges::join(
ranges::map<std::vector<std::string>>(
mods, [](Mod* mod) { return fmt::format("<cp>{}</c>", mod->getID()); }
),
", "
);
};
createQuickPopup(
"Confirm Install",
fmt::format(
"<cj>{}</c> mods will be installed, of which <cy>{}</c> are <cy>dependencies</c>.\n"
"<cr>{} mods will be force-disabled, as they are incompatible</c>: {}\n"
"<cg>{} mods will be force-enabled</c>: {}",
toConfirm.modCount, toConfirm.dependencyCount,
toConfirm.toDisable.size(), joinModsToIDs(toConfirm.toDisable),
toConfirm.toEnable.size(), joinModsToIDs(toConfirm.toEnable)
),
"Cancel", "Continue",
[toConfirm = std::move(toConfirm)](auto*, bool btn2) {
if (btn2) {
for (auto mod : toConfirm.toDisable) {
(void)mod->disable();
}
for (auto mod : toConfirm.toEnable) {
(void)mod->enable();
}
ModDownloadManager::get()->confirmAll();
}
else {
ModDownloadManager::get()->cancelAll();
}
}
);
}

View file

@ -0,0 +1,10 @@
#pragma once
class ConfirmInstall final {
protected:
public:
static void startConfirming();
};
void askConfirmModInstalls();

View file

@ -1,162 +0,0 @@
#include "ConfirmInstallPopup.hpp"
#include <Geode/ui/GeodeUI.hpp>
class SmallModItem : public CCNode {
protected:
bool init(ModMetadata const& metadata) {
if (!CCNode::init())
return false;
this->setContentSize({ 60, 80 });
auto logo = createServerModLogo(metadata.getID());
limitNodeSize(logo, { 45, 45 }, 5.f, .1f);
this->addChildAtPosition(logo, Anchor::Center, ccp(0, 10));
auto title = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt");
title->limitLabelWidth(m_obContentSize.width, .3f, .1f);
this->addChildAtPosition(title, Anchor::Center, ccp(0, -20));
auto developer = CCLabelBMFont::create(fmt::format("by {}", metadata.getDeveloper()).c_str(), "goldFont.fnt");
developer->limitLabelWidth(m_obContentSize.width, .3f, .1f);
this->addChildAtPosition(developer, Anchor::Center, ccp(0, -30));
return true;
}
public:
static SmallModItem* create(ModMetadata const& metadata) {
auto ret = new SmallModItem();
if (ret && ret->init(metadata)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
};
bool ConfirmInstallPopup::setup() {
m_noElasticity = true;
this->setTitle("Customize Installation");
auto installationsTitle = CCLabelBMFont::create("You are Installing:", "bigFont.fnt");
installationsTitle->setScale(.5f);
m_mainLayer->addChildAtPosition(installationsTitle, Anchor::Center, ccp(0, 100));
m_installations = CCNode::create();
m_installations->setAnchorPoint({ .5f, .5f });
m_installations->setContentSize({ m_size.width, 60 });
m_installations->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .65f)
->setGrowCrossAxis(true)
->setCrossAxisOverflow(false)
);
m_mainLayer->addChildAtPosition(m_installations, Anchor::Center, ccp(0, 60));
auto dependenciesTitle = CCLabelBMFont::create("Which Depend(s) on:", "bigFont.fnt");
dependenciesTitle->setScale(.5f);
m_mainLayer->addChildAtPosition(dependenciesTitle, Anchor::Center, ccp(0, 20));
m_dependencies = CCNode::create();
m_dependencies->setAnchorPoint({ .5f, .5f });
m_dependencies->setContentSize({ m_size.width, 60 });
m_dependencies->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .65f)
->setGrowCrossAxis(true)
->setCrossAxisOverflow(false)
);
m_mainLayer->addChildAtPosition(m_dependencies, Anchor::Center, ccp(0, -20));
auto incompatablitiesTitle = CCLabelBMFont::create("But is/are Incompatible with:", "bigFont.fnt");
incompatablitiesTitle->setScale(.5f);
m_mainLayer->addChildAtPosition(incompatablitiesTitle, Anchor::Center, ccp(0, -60));
m_incompatabilities = CCNode::create();
m_incompatabilities->setAnchorPoint({ .5f, .5f });
m_incompatabilities->setContentSize({ m_size.width, 60 });
m_incompatabilities->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .65f)
->setGrowCrossAxis(true)
->setCrossAxisOverflow(false)
);
m_mainLayer->addChildAtPosition(m_incompatabilities, Anchor::Center, ccp(0, -100));
m_downloadListener.bind([this](auto) { this->updateState(); });
this->updateState();
return true;
}
void ConfirmInstallPopup::updateState() {
m_installations->removeAllChildren();
m_dependencies->removeAllChildren();
m_incompatabilities->removeAllChildren();
for (auto& download : server::ModDownloadManager::get()->getDownloads()) {
auto status = download.getStatus();
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
if (download.getDependencyFor()) {
m_dependencies->addChild(SmallModItem::create(confirm->version.metadata));
}
else {
m_installations->addChild(SmallModItem::create(confirm->version.metadata));
}
for (auto& inc : confirm->version.metadata.getIncompatibilities()) {
if (inc.mod) {
m_installations->addChild(SmallModItem::create(inc.mod->getMetadata()));
}
}
}
}
m_installations->updateLayout();
m_dependencies->updateLayout();
m_incompatabilities->updateLayout();
}
ConfirmInstallPopup* ConfirmInstallPopup::create() {
auto ret = new ConfirmInstallPopup();
if (ret && ret->init(350, 280)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void ConfirmInstallPopup::askForCustomize() {
size_t confirmCount = 0;
size_t dependencyCount = 0;
for (auto& download : server::ModDownloadManager::get()->getDownloads()) {
auto status = download.getStatus();
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
confirmCount += 1;
if (download.getDependencyFor()) {
dependencyCount += 1;
}
}
}
createQuickPopup(
"Confirm Install",
fmt::format(
"<cy>{} mods</c> will be installed, out of which <cr>{}</c> are <cr>dependencies</c>. "
"Do you want to <cb>customize</c> the installation, or continue with default options?",
confirmCount, dependencyCount
),
"Customize", "Continue",
[](auto*, bool btn2) {
if (btn2) {
server::ModDownloadManager::get()->confirmAll();
}
else {
ConfirmInstallPopup::create()->show();
}
}
);
}

View file

@ -1,23 +0,0 @@
#pragma once
#include <Geode/ui/Popup.hpp>
#include <server/DownloadManager.hpp>
#include "../GeodeStyle.hpp"
using namespace geode::prelude;
class ConfirmInstallPopup : public GeodePopup<> {
protected:
CCNode* m_installations;
CCNode* m_dependencies;
CCNode* m_incompatabilities;
EventListener<server::ModDownloadFilter> m_downloadListener;
bool setup() override;
void updateState();
public:
static ConfirmInstallPopup* create();
static void askForCustomize();
};

View file

@ -9,7 +9,6 @@ class ConfirmUninstallPopup : public Popup<Mod*> {
protected:
Mod* m_mod;
CCMenuItemToggler* m_deleteDataToggle;
EventListener<UpdateModListStateFilter> m_updateStateListener;
bool setup(Mod* mod) override;