#include "mods_window.hpp" #include "dusk/mod_loader.hpp" #include "dusk/mods/svc/ui.hpp" #include "fmt/format.h" #include "logs_window.hpp" #include "mod_texture_provider.hpp" #include "pane.hpp" #include "Z2AudioLib/Z2SeMgr.h" #include "m_Do/m_Do_audio.h" #include #include #include namespace dusk::ui { namespace { struct ModStatus { const char* badgeClass = ""; const char* text = ""; }; bool mod_enabled(const mods::LoadedMod& mod) { return mod.cvarIsEnabled != nullptr && mod.cvarIsEnabled->getValue(); } ModStatus mod_status(const mods::LoadedMod& mod) { if (mod.loadFailed) { return {"failed", "Failed"}; } if (mod.active) { return {"active", "Active"}; } if (mod.suspendedByProvider) { return {"suspended", "Suspended"}; } return {"", "Disabled"}; } // Truncates to at most maxBytes without splitting a UTF-8 sequence. std::string snippet(std::string_view text, size_t maxBytes) { if (text.size() <= maxBytes) { return std::string{text}; } size_t end = maxBytes; while (end > 0 && (static_cast(text[end]) & 0xC0) == 0x80) { --end; } return std::string{text.substr(0, end)} + "..."; } class ModListEntry : public FluentComponent { public: ModListEntry(Rml::Element* parent, const mods::LoadedMod& mod) : FluentComponent{append(parent, "mod-entry")} { Rml::String iconRml; if (!mod.metadata.iconPath.empty()) { iconRml = fmt::format(R"()", mod_image_source(mod, mod.metadata.iconPath)); } else { iconRml = R"()"; } const auto status = mod_status(mod); mRoot->SetInnerRML(fmt::format( R"({})" R"()", iconRml, escape(mod.metadata.name), escape(mod.metadata.version), escape(mod.metadata.author), status.badgeClass, status.text, escape(snippet(mod.metadata.description, 90)))); mRoot->SetClass("inactive", !mod.active); mRoot->SetClass("failed", mod.loadFailed); on_nav_command([this](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm) { mRoot->DispatchEvent(Rml::EventId::Submit, {}); return true; } return false; }); } }; class ModDetailHeader : public FluentComponent { public: ModDetailHeader( Rml::Element* parent, const mods::LoadedMod& mod, std::function onShowLogs) : FluentComponent{append(parent, "mod-header")} { const bool hasBanner = !mod.metadata.bannerPath.empty(); mRoot->SetClass(hasBanner ? "has-banner" : "no-banner", true); if (hasBanner) { mRoot->SetProperty("decorator", fmt::format(R"(image("{}" cover center top))", mod_image_source(mod, mod.metadata.bannerPath))); } auto* actions = append(mRoot, "div"); actions->SetClass("mod-actions", true); const std::string modId = mod.metadata.id; if (mod_enabled(mod)) { if (!mod.inPlace) { make_button(actions, "Reload").on_pressed([modId] { mods::ModLoader::instance().request_reload(modId); }); } make_button(actions, "Disable").on_pressed([modId] { mods::ModLoader::instance().request_disable(modId); }); } else { make_button(actions, "Enable").on_pressed([modId] { mods::ModLoader::instance().request_enable(modId); }); } make_button(actions, "Logs").on_pressed(std::move(onShowLogs)); listen(Rml::EventId::Keydown, [this](Rml::Event& event) { const auto cmd = map_nav_event(event); if (cmd != NavCommand::Left && cmd != NavCommand::Right) { return; } int index = -1; for (int i = 0; i < static_cast(mButtons.size()); ++i) { if (mButtons[i]->contains(event.GetTargetElement())) { index = i; break; } } if (index == -1) { return; } const int next = index + (cmd == NavCommand::Right ? 1 : -1); if (next >= 0 && next < static_cast(mButtons.size()) && mButtons[next]->focus()) { mDoAud_seStartMenu(kSoundItemFocus); event.StopPropagation(); } }); } bool focus() override { for (auto* button : mButtons) { if (button->focus()) { return true; } } return false; } private: Button& make_button(Rml::Element* parent, Rml::String text) { auto button = std::make_unique