Show each mod's size in the mod manager

Show each mod's size in the mod manager

A mod's size was only visible in the package overview and the install
prompt, so there was no telling from either list what a mod costs to
download or how much room the installed ones take.

- MY MODS: each row carries the size of the mod's folder on disk after
  the version ("v1.0.1 / 410 MB / by anton / 1 map"), and so does the
  line under the title in a mod's overview. The folders are measured
  when the Mods folder is scanned, so the figure follows an install, an
  update or Refresh. The game's merged patch hard links a mod's archives
  rather than copying them, so this is also what removing the mod gives
  back.
- GET MODS: each row carries the download size Thunderstore reports for
  the latest version, in the same place ("v1.0.0 / 101 MB / 0 downloads
  / 2026-10-04"), as the package overview already did.
- size_text moves out of gui_mods_browse.cpp's file-local namespace so
  both pages format a size the same way.

The two figures are not the same thing: one is the unpacked folder, the
other the zip, so a mod reads a little larger once installed.
This commit is contained in:
AyeZeeBB committed 2026-10-04 00:37:15 -05:00
1 parent 04e50f7dee
commit 9c03edda34
3 files changed
+38 -11

No files matched your search

+5
View File
@@ -379,6 +379,9 @@ struct ModsPanel {
bool scanned{};
fs::path root;
mods::ModList list;
// What each installed mod's folder holds on disk, by folder name; measured
// when the folder is scanned.
std::map<std::string, std::uint64_t, std::less<>> sizes;
int selected{-1}; // the mod whose overview is open
bool overview{}; // the overview popup is showing
int tab{}; // 0 MY MODS, 1 GET MODS
@@ -414,6 +417,8 @@ void start_install(ModsPanel& panel, const fs::path& source, bool replace);
// ---------------------------------------------------------------- Thunderstore (gui_mods_browse.cpp)
// "1.27 GB", "263 MB", "12 KB": a mod's size, on both pages.
std::string size_text(std::uint64_t bytes);
// Starts a listing fetch when none ran yet, or when `force`.
void refresh_listing(ModsPanel& panel, double time, bool force = false);
// Takes over a fetch the worker finished; call once a frame.
+24 -3
View File
@@ -80,9 +80,28 @@ fs::path pick(HWND owner, bool folder) {
return result;
}
std::string summary(const mods::Mod& mod) {
// Everything in the mod's folder, as Explorer would count it. The game's merged
// patch hard links a mod's archives rather than copying them, so this is also
// what removing the mod gives back.
std::uint64_t folder_size(const fs::path& directory) {
std::uint64_t total{};
std::error_code error;
for (fs::recursive_directory_iterator it(directory, fs::directory_options::skip_permission_denied, error), end;
it != end && !error; it.increment(error)) {
std::error_code ignored;
if (it->is_regular_file(ignored) && !ignored) {
const auto size = it->file_size(ignored);
if (!ignored) total += size;
}
}
return total;
}
std::string summary(const ModsPanel& panel, const mods::Mod& mod) {
std::vector<std::string> parts;
if (!mod.version.empty()) parts.push_back("v" + mod.version);
if (const auto size = panel.sizes.find(mod.name); size != panel.sizes.end() && size->second)
parts.push_back(size_text(size->second));
if (!mod.author.empty()) parts.push_back("by " + mod.author);
if (!mod.levels.empty()) parts.push_back(mod.levels.size() == 1 ? "1 map" : std::to_string(mod.levels.size()) + " maps");
else if (mod.provides_layout) parts.push_back("game data");
@@ -169,7 +188,7 @@ void installed_row(const Fonts& fonts, ModsPanel& panel, const thunderstore::Ins
const auto title = std::to_string(index + 1) + ". " + mod.title;
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(text_x, start.y + S(16)),
entry.enabled ? color::text : color::muted, title.c_str());
if (const auto detail = summary(mod); !detail.empty())
if (const auto detail = summary(panel, mod); !detail.empty())
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, start.y + S(40)), color::muted, detail.c_str());
// The widgets that sit on the row, over its Selectable.
@@ -316,7 +335,7 @@ void mod_overview(const Fonts& fonts, ModsPanel& panel, const thunderstore::Inst
ImGui::TextUnformatted(mod.title.c_str());
ImGui::PopTextWrapPos();
ImGui::PopFont();
ImGui::TextDisabled("%s", summary(mod).c_str());
ImGui::TextDisabled("%s", summary(panel, mod).c_str());
ImGui::Spacing();
if (update) {
ImGui::BeginDisabled(installing);
@@ -463,6 +482,8 @@ void refresh_mods(Launcher& launcher, ModsPanel& panel) {
void scan(ModsPanel& panel, const launcher_app::Session& session) {
panel.root = launcher_mods::mods_root(session.paths.directory);
panel.list = mods::scan_mods(panel.root.parent_path());
panel.sizes.clear();
for (const auto& entry : panel.list.entries) panel.sizes[entry.mod.name] = folder_size(entry.mod.directory);
panel.scanned = true;
if (panel.selected >= static_cast<int>(panel.list.entries.size())) panel.selected = -1;
}
+9 -8
View File
@@ -137,12 +137,6 @@ std::string count_text(std::uint64_t value) {
return std::to_string(value);
}
std::string size_text(std::uint64_t bytes) {
if (bytes >= 1024ull * 1024 * 1024) return std::format("{:.2f} GB", static_cast<double>(bytes) / (1024.0 * 1024 * 1024));
if (bytes >= 1024ull * 1024) return std::format("{:.0f} MB", static_cast<double>(bytes) / (1024.0 * 1024));
return std::format("{} KB", std::max<std::uint64_t>(1, bytes / 1024));
}
std::string date_text(const std::string& iso) { return iso.substr(0, std::min<std::size_t>(iso.size(), 10)); }
void set_activity(ModsPanel& panel, std::string text) {
@@ -252,6 +246,12 @@ void mod_icon(ModsPanel& panel, const thunderstore::Package* package, ImVec2 pos
draw->AddRectFilled(position, end, rgba(255, 255, 255, 0.06f), S(4));
}
std::string size_text(std::uint64_t bytes) {
if (bytes >= 1024ull * 1024 * 1024) return std::format("{:.2f} GB", static_cast<double>(bytes) / (1024.0 * 1024 * 1024));
if (bytes >= 1024ull * 1024) return std::format("{:.0f} MB", static_cast<double>(bytes) / (1024.0 * 1024));
return std::format("{} KB", std::max<std::uint64_t>(1, bytes / 1024));
}
void refresh_listing(ModsPanel& panel, double time, bool force) {
auto& store = panel.store;
// Once per launch, unless asked; a failed fetch retries after a minute.
@@ -569,8 +569,9 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, start.y + S(32)), color::text, line.c_str(),
nullptr, 0, &clip);
draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(text_x, start.y + S(55)), color::muted,
std::format("v{} / {} downloads / {}", version.number, count_text(package.downloads),
date_text(package.date_updated)).c_str());
std::format("v{}{} / {} downloads / {}", version.number,
version.file_size ? " / " + size_text(version.file_size) : std::string(),
count_text(package.downloads), date_text(package.date_updated)).c_str());
ImGui::PopID();
});
ImGui::EndDisabled();