mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
Launcher: the mod manager's pages redesigned, with a banner and cards
- page_banner: each page opens with its heading, what it is for, and how things stand at the right (a status pill and a caption). card: the panel the pages draw on, lighter edged on hover, blue edged when marked. - MY MODS and GET MODS rows are cards with a gap between them (list_row), in lists without a border. MY MODS' banner counts problems and waiting updates and holds the load-order explanation; GET MODS' says whether the listing loaded and what is installed or waiting. The ticked bar on GET MODS is a card. - OFFICIAL: a card per mod with its status and version pills, its Thunderstore facts, and THUNDERSTORE and OPEN FOLDER. - PROFILES: new-profile and import cards side by side; a card per profile with a picture of its first mods' icons, pills for what it holds, LOAD and UPDATE, and the rest in a menu.
This commit is contained in:
1 parent
72e6ba0301
commit
b63e2d62bd
5 files changed
+437
-145
No files matched your search
+52
-6
@@ -117,6 +117,50 @@ void badge(ImDrawList* draw, const Fonts& fonts, ImVec2 position, const std::str
|
||||
draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(position.x + S(9), position.y + S(4)), ink, text.c_str());
|
||||
}
|
||||
|
||||
float text_height(ImFont* font, float wrap, const std::string& text) {
|
||||
return text.empty() ? 0 : font->CalcTextSizeA(font->FontSize, FLT_MAX, wrap, text.c_str()).y;
|
||||
}
|
||||
|
||||
void page_banner(const Fonts& fonts, const char* heading, const char* pill, const std::string& about,
|
||||
const std::string& status, ImU32 status_fill, const std::string& caption) {
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
const ImVec2 top = ImGui::GetCursorScreenPos();
|
||||
const float width = ImGui::GetContentRegionAvail().x;
|
||||
const float pad = S(20), side = S(230);
|
||||
const float wrap = width - pad * 2 - side - S(20);
|
||||
const float tall = std::max(S(100), pad + fonts.heading->FontSize + S(10) + text_height(fonts.body, wrap, about) + pad);
|
||||
draw->AddRectFilled(top, ImVec2(top.x + width, top.y + tall), color::tile, S(6));
|
||||
draw->AddRectFilled(top, ImVec2(top.x + S(5), top.y + tall), color::blue, S(6), ImDrawFlags_RoundCornersLeft);
|
||||
const float text_x = top.x + pad + S(4);
|
||||
draw->AddText(fonts.heading, fonts.heading->FontSize, ImVec2(text_x, top.y + pad), color::text, heading);
|
||||
if (pill && *pill) {
|
||||
const float heading_width = fonts.heading->CalcTextSizeA(fonts.heading->FontSize, FLT_MAX, 0, heading).x;
|
||||
badge(draw, fonts, ImVec2(text_x + heading_width + S(12), top.y + pad + (fonts.heading->FontSize - fonts.caption->FontSize - S(8)) * 0.5f),
|
||||
pill, color::blue, color::ink);
|
||||
}
|
||||
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, top.y + pad + fonts.heading->FontSize + S(10)), color::muted,
|
||||
about.c_str(), nullptr, wrap);
|
||||
const float right = top.x + width - pad;
|
||||
const float block = fonts.caption->FontSize + S(8) + (caption.empty() ? 0 : S(8) + fonts.caption->FontSize);
|
||||
const float block_y = top.y + (tall - block) * 0.5f;
|
||||
if (!status.empty()) badge(draw, fonts, ImVec2(right - badge_width(fonts, status), block_y), status, status_fill, color::ink);
|
||||
if (!caption.empty()) {
|
||||
const float caption_width = fonts.caption->CalcTextSizeA(fonts.caption->FontSize, FLT_MAX, 0, caption.c_str()).x;
|
||||
draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(right - caption_width, block_y + fonts.caption->FontSize + S(16)),
|
||||
color::muted, caption.c_str());
|
||||
}
|
||||
ImGui::Dummy(ImVec2(width, tall));
|
||||
ImGui::Dummy(ImVec2(0, S(6)));
|
||||
}
|
||||
|
||||
void card(ImDrawList* draw, ImVec2 start, ImVec2 size, bool marked) {
|
||||
const ImVec2 end(start.x + size.x, start.y + size.y);
|
||||
const bool hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseHoveringRect(start, end);
|
||||
draw->AddRectFilled(start, end, color::tile, S(6));
|
||||
draw->AddRect(start, end, hovered ? rgba(255, 255, 255, 0.22f) : color::outline, S(6));
|
||||
if (marked) draw->AddRectFilled(start, ImVec2(start.x + S(5), end.y), color::blue, S(6), ImDrawFlags_RoundCornersLeft);
|
||||
}
|
||||
|
||||
void field(const Fonts& fonts, const char* name, const std::string& value) {
|
||||
if (value.empty()) return;
|
||||
ImGui::PushFont(fonts.caption);
|
||||
@@ -131,16 +175,18 @@ bool list_row(const char* id, float width, float height, bool ticked) {
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 end(start.x + width, start.y + height);
|
||||
draw->AddRectFilled(start, end, ticked ? rgba(28, 38, 52) : rgba(31, 31, 34));
|
||||
// The hover tint is the Selectable's; the row's own colour is under it.
|
||||
// A card, as the OFFICIAL and PROFILES pages draw theirs: its own hover, so the corners stay round.
|
||||
const bool hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseHoveringRect(start, end);
|
||||
draw->AddRectFilled(start, end, ticked ? rgba(28, 38, 52) : color::tile, S(6));
|
||||
if (hovered) draw->AddRectFilled(start, end, rgba(255, 255, 255, 0.035f), S(6));
|
||||
draw->AddRect(start, end, hovered ? rgba(255, 255, 255, 0.22f) : color::outline, S(6));
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::ColorConvertU32ToFloat4(rgba(255, 255, 255, 0.055f)));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImGui::ColorConvertU32ToFloat4(rgba(255, 255, 255, 0.09f)));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImGui::ColorConvertU32ToFloat4(rgba(255, 255, 255, 0.05f)));
|
||||
const bool pressed = ImGui::Selectable(id, false, ImGuiSelectableFlags_AllowOverlap, ImVec2(width, height));
|
||||
ImGui::PopStyleColor(3);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ticked) draw->AddRectFilled(start, ImVec2(start.x + S(3), end.y), color::blue);
|
||||
draw->AddLine(ImVec2(start.x, end.y - S(1)), ImVec2(end.x, end.y - S(1)), rgba(255, 255, 255, 0.07f), S(1));
|
||||
if (ticked) draw->AddRectFilled(start, ImVec2(start.x + S(5), end.y), color::blue, S(6), ImDrawFlags_RoundCornersLeft);
|
||||
return pressed;
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -120,12 +120,22 @@ bool nav_tile(const Fonts& fonts, float width, const char* label, bool selected,
|
||||
float badge_width(const Fonts& fonts, const std::string& text);
|
||||
void badge(ImDrawList* draw, const Fonts& fonts, ImVec2 position, const std::string& text,
|
||||
ImU32 fill = color::blue, ImU32 ink = color::ink);
|
||||
// How tall `text` is in `font`, wrapped at `wrap` (0: not wrapped); 0 for no text.
|
||||
float text_height(ImFont* font, float wrap, const std::string& text);
|
||||
// A page's banner, as wide as the page: its heading with a blue pill beside it (`pill` may be
|
||||
// empty), a line on what the page is for, and at the right how things stand: a pill in
|
||||
// `status_fill`, with `caption` under it (either may be empty). Leaves the cursor under it.
|
||||
void page_banner(const Fonts& fonts, const char* heading, const char* pill, const std::string& about,
|
||||
const std::string& status = {}, ImU32 status_fill = color::good, const std::string& caption = {});
|
||||
// A card behind what a page draws on it: lighter edged while hovered, with a blue edge when `marked`.
|
||||
void card(ImDrawList* draw, ImVec2 start, ImVec2 size, bool marked = false);
|
||||
// A caption over its value: the detail lines of an open list row.
|
||||
void field(const Fonts& fonts, const char* name, const std::string& value);
|
||||
// One row of a mod list: its background, a hover tint, a rule under it and a
|
||||
// blue edge when it is ticked. Returns true when the row itself was clicked,
|
||||
// One row of a mod list, `height` tall: a card, lighter edged while hovered, with a blue edge
|
||||
// when it is ticked. Leave a gap under it (row_gap). Returns true when the row itself was clicked,
|
||||
// which opens the mod's overview; the widgets drawn over it keep their clicks.
|
||||
bool list_row(const char* id, float width, float height, bool ticked);
|
||||
inline float row_gap() { return S(8); }
|
||||
// An on/off switch, blue while on. Returns true when it was flipped.
|
||||
bool toggle(const char* id, bool* on);
|
||||
// A square button with three dots, for a row's menu.
|
||||
|
||||
+27
-13
@@ -496,6 +496,25 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co
|
||||
float height, bool installing) {
|
||||
auto& entries = panel.list.entries;
|
||||
const float top = ImGui::GetCursorPosY();
|
||||
{
|
||||
const auto enabled = std::count_if(entries.begin(), entries.end(), [](const auto& entry) { return entry.enabled; });
|
||||
const auto problems = std::count_if(entries.begin(), entries.end(), [&](const auto& entry) {
|
||||
return !entry.mod.outdated.empty() || (entry.enabled && panel.list.excluded.contains(entry.mod.name));
|
||||
});
|
||||
const auto waiting = std::count_if(entries.begin(), entries.end(),
|
||||
[&](const auto& entry) { return update_waiting(panel, installed, entry.mod.name); });
|
||||
std::uint64_t bytes{};
|
||||
for (const auto& [name, size] : panel.sizes) bytes += size;
|
||||
auto caption = std::format("{} {} / {} on", entries.size(), entries.size() == 1 ? "mod" : "mods", enabled);
|
||||
if (bytes) caption += " / " + size_text(bytes);
|
||||
page_banner(fonts, "MY MODS", nullptr,
|
||||
"Your installed mods. They load top to bottom: where two change the same thing, the higher one wins. "
|
||||
"Changes apply the next time Skate starts.",
|
||||
problems ? std::format("{} {}", problems, problems == 1 ? "PROBLEM" : "PROBLEMS")
|
||||
: waiting ? std::format("{} {} WAITING", waiting, waiting == 1 ? "UPDATE" : "UPDATES")
|
||||
: entries.empty() ? std::string("NONE YET") : std::string("ALL GOOD"),
|
||||
problems ? color::danger : waiting ? color::blue : entries.empty() ? color::tile_grey : color::good, caption);
|
||||
}
|
||||
|
||||
// ------------------------------------------------ search, filter, order, refresh
|
||||
const float combo = S(170), refresh = S(110);
|
||||
@@ -555,11 +574,9 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co
|
||||
return bytes;
|
||||
};
|
||||
if (panel.marked.empty()) {
|
||||
const auto enabled = std::count_if(entries.begin(), entries.end(), [](const auto& entry) { return entry.enabled; });
|
||||
auto line = std::format("{} {} / {} enabled", entries.size(), entries.size() == 1 ? "mod" : "mods", enabled);
|
||||
if (const auto bytes = total([](const std::string&) { return true; })) line += " / " + size_text(bytes);
|
||||
if (view.size() != entries.size()) line += std::format(" / {} shown", view.size());
|
||||
ImGui::TextDisabled("%s", line.c_str());
|
||||
ImGui::TextDisabled("%s", view.size() != entries.size()
|
||||
? std::format("{} of {} shown", view.size(), entries.size()).c_str()
|
||||
: "Tick mods to change several at once. Shift-click ticks a run of them, Ctrl+A every one.");
|
||||
} else {
|
||||
const std::vector<std::string> names(panel.marked.begin(), panel.marked.end());
|
||||
auto line = std::format("{} selected", names.size());
|
||||
@@ -626,9 +643,8 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co
|
||||
ImGui::Spacing();
|
||||
|
||||
// ------------------------------------------------ the list, and under it how its order works
|
||||
const float hint = ImGui::GetTextLineHeightWithSpacing();
|
||||
const float body = std::max(S(120), height - (ImGui::GetCursorPosY() - top) - hint);
|
||||
ImGui::BeginChild("##mod_list", ImVec2(0, body), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened);
|
||||
const float body = std::max(S(120), height - (ImGui::GetCursorPosY() - top));
|
||||
ImGui::BeginChild("##mod_list", ImVec2(0, body), ImGuiChildFlags_NavFlattened);
|
||||
const auto note = [](const char* first, const char* second = nullptr) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Indent(S(14));
|
||||
@@ -640,16 +656,14 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co
|
||||
note(panel.list.present ? "No mods installed yet." : "No Mods folder yet.",
|
||||
"Use GET MODS to browse Thunderstore, or drop a mod .zip on the window.");
|
||||
else if (view.empty()) note("No mods match.");
|
||||
const float row = S(68);
|
||||
const float row = S(72) + row_gap();
|
||||
ImGui::BeginDisabled(installing);
|
||||
virtual_rows(static_cast<int>(view.size()), [&](int) { return row; }, [&](int position, float tall) {
|
||||
installed_row(fonts, panel, installed, view, position, tall, reorder, request);
|
||||
virtual_rows(static_cast<int>(view.size()), [&](int) { return row; }, [&](int position, float slot) {
|
||||
installed_row(fonts, panel, installed, view, position, slot - row_gap(), reorder, request);
|
||||
});
|
||||
ImGui::EndDisabled();
|
||||
keep_focus_in_list();
|
||||
ImGui::EndChild();
|
||||
ImGui::TextDisabled("Mods load top to bottom: where two change the same thing, the higher one wins. "
|
||||
"Changes apply the next time Skate starts.");
|
||||
|
||||
// Ctrl+A and Delete, as in a file list, while nothing is being typed.
|
||||
if (!installing && !ImGui::GetIO().WantTextInput && !ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId)) {
|
||||
|
||||
+144
-38
@@ -2,6 +2,8 @@
|
||||
#include "gui_renderer.h"
|
||||
#include "official_mods.h"
|
||||
|
||||
#include "Engine/Vfs/builtin_packs.h"
|
||||
|
||||
#include "mod_manager.h"
|
||||
#include "updater.h"
|
||||
|
||||
@@ -457,47 +459,129 @@ void pick(Store& store, const std::string& full_name, bool on) {
|
||||
else if (!on && found != store.picked.end()) store.picked.erase(found);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// How one official mod is doing, as a pill: its word and colour.
|
||||
struct Standing {
|
||||
const char* word;
|
||||
ImU32 fill;
|
||||
bool trouble;
|
||||
};
|
||||
Standing standing_of(const official_mods::Mod& mod) {
|
||||
if (mod.working) return {mod.status.empty() ? "CHECKING" : mod.installed.empty() ? "DOWNLOADING" : "UPDATING", color::blue, false};
|
||||
if (mod.status == "Up to date") return {"UP TO DATE", color::good, false};
|
||||
if (mod.status.empty()) return {"WAITING", color::tile_grey, false};
|
||||
return {mod.installed.empty() ? "NOT DOWNLOADED" : "UPDATE FAILED", mod.installed.empty() ? color::danger : color::warning, true};
|
||||
}
|
||||
std::string source_text(const official_mods::State& state) {
|
||||
if (state.source == "ReSkate") return "List from ReSkate's server";
|
||||
if (state.source == "the last list read") return "Offline: using the last list";
|
||||
if (state.source.empty()) return state.syncing ? "Reading the list..." : "";
|
||||
return "Using the list ReSkate came with";
|
||||
}
|
||||
}
|
||||
|
||||
void official_page(const Fonts& fonts, ModsPanel& panel, float height) {
|
||||
// (The listing gives their icons.)
|
||||
// (The listing gives their icons, sizes and download counts.)
|
||||
refresh_listing(panel, ImGui::GetTime());
|
||||
const auto state = official_mods::snapshot();
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextDisabled("Every player has these, always on: ReSkate keeps them at their latest version from Thunderstore.");
|
||||
const char* source = state.syncing ? "Getting them now..."
|
||||
: state.source == "ReSkate" ? "The list comes from ReSkate."
|
||||
: state.source == "the last list read" ? "ReSkate could not be reached: this is the last list it gave."
|
||||
: state.source.empty() ? "" : "No list has been read yet: these are the ones ReSkate starts with.";
|
||||
ImGui::TextDisabled("%s", source);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginChild("##official", ImVec2(0, std::max(S(80), height - ImGui::GetCursorPosY())), ImGuiChildFlags_NavFlattened);
|
||||
const float icon = S(64);
|
||||
const float top_y = ImGui::GetCursorPosY();
|
||||
|
||||
const bool trouble = std::any_of(state.mods.begin(), state.mods.end(), [](const auto& mod) { return standing_of(mod).trouble; });
|
||||
page_banner(fonts, "OFFICIAL MODS", "ALWAYS ON",
|
||||
"Mods made for ReSkate's own features. Every player has them, kept at their latest version from Thunderstore: "
|
||||
"they load after your mods and are always on.",
|
||||
state.syncing ? "CHECKING FOR UPDATES" : trouble ? "NEEDS ATTENTION" : state.mods.empty() ? "NONE RIGHT NOW" : "ALL UP TO DATE",
|
||||
state.syncing ? color::blue : trouble ? color::warning : state.mods.empty() ? color::tile_grey : color::good,
|
||||
source_text(state));
|
||||
|
||||
// ------------------------------------------------ the mods, a card each
|
||||
ImGui::BeginChild("##official", ImVec2(0, std::max(S(80), height - (ImGui::GetCursorPosY() - top_y))), ImGuiChildFlags_NavFlattened);
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
if (state.mods.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("%s", state.syncing ? "Reading the list of official mods..." : "There are no official mods right now.");
|
||||
}
|
||||
const float pad = S(18);
|
||||
const float icon = S(96);
|
||||
const float button = S(150);
|
||||
for (const auto& mod : state.mods) {
|
||||
ImGui::PushID(mod.full_name.c_str());
|
||||
const auto* package = ts::find_package(panel.store.packages, mod.full_name);
|
||||
const auto top = ImGui::GetCursorScreenPos();
|
||||
mod_icon(panel, package, top, icon);
|
||||
ImGui::SetCursorScreenPos(ImVec2(top.x + icon + S(14), top.y));
|
||||
ImGui::BeginGroup();
|
||||
ImGui::PushFont(fonts.bold);
|
||||
ImGui::TextUnformatted(mod.title.c_str());
|
||||
ImGui::PopFont();
|
||||
const auto author = mod.full_name.substr(0, mod.full_name.find('-'));
|
||||
const auto version = !mod.installed.empty() ? "v" + mod.installed : !mod.version.empty() ? "v" + mod.version : std::string("-");
|
||||
ImGui::TextDisabled("%s | by %s | always on", version.c_str(), author.c_str());
|
||||
const auto status = mod.working ? (mod.status.empty() ? std::string("Checking for an update...") : mod.status + "...")
|
||||
: mod.status.empty() ? std::string("Waiting") : mod.status;
|
||||
const bool fine = !mod.working && mod.status == "Up to date";
|
||||
const bool trouble = !mod.working && !fine && !mod.status.empty();
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(fine ? color::good : trouble ? color::warning : color::muted), "%s", status.c_str());
|
||||
if (!mod.description.empty()) ImGui::TextWrapped("%s", mod.description.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndGroup();
|
||||
ImGui::SetCursorScreenPos(ImVec2(top.x, std::max(ImGui::GetCursorScreenPos().y, top.y + icon) + S(14)));
|
||||
const auto standing = standing_of(mod);
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const float across = ImGui::GetContentRegionAvail().x;
|
||||
const float text_x = start.x + pad + icon + S(18);
|
||||
const float buttons_x = start.x + across - pad - button;
|
||||
const float wrap = buttons_x - S(24) - text_x;
|
||||
|
||||
auto author = mod.full_name.substr(0, mod.full_name.find('-'));
|
||||
std::replace(author.begin(), author.end(), '_', ' ');
|
||||
const auto& description = !mod.description.empty() ? mod.description
|
||||
: package ? package->latest().description : std::string();
|
||||
const auto version = !mod.installed.empty() ? mod.installed : mod.version;
|
||||
std::string facts = version.empty() ? std::string() : "v" + version;
|
||||
const auto add = [&](const std::string& fact) { facts += (facts.empty() ? "" : " / ") + fact; };
|
||||
if (package) {
|
||||
if (const auto size = package->latest().file_size) add(size_text(size));
|
||||
add(count_text(package->downloads) + " downloads");
|
||||
add("updated " + date_text(package->date_updated));
|
||||
}
|
||||
if (!mod.installed.empty() && !mod.version.empty() && mod.installed != mod.version) add("v" + mod.version + " is out");
|
||||
const std::string problem = standing.trouble ? mod.status : std::string();
|
||||
|
||||
// Measured first, so the card is drawn behind what is on it.
|
||||
float y = start.y + pad;
|
||||
const float title_y = y;
|
||||
y += fonts.bold->FontSize + S(10);
|
||||
const float pills_y = y;
|
||||
y += fonts.caption->FontSize + S(8) + S(12);
|
||||
const float description_y = y;
|
||||
if (const float tall = text_height(fonts.body, wrap, description)) y += tall + S(10);
|
||||
const float facts_y = y;
|
||||
if (!facts.empty()) y += fonts.caption->FontSize + S(6);
|
||||
const float problem_y = y;
|
||||
if (const float tall = text_height(fonts.caption, wrap, problem)) y += tall + S(6);
|
||||
const float tall = std::max(y - start.y + pad - S(6), icon + pad * 2);
|
||||
|
||||
card(draw, start, ImVec2(across, tall));
|
||||
mod_icon(panel, package, ImVec2(start.x + pad, start.y + pad), icon);
|
||||
|
||||
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(text_x, title_y), color::text, mod.title.c_str());
|
||||
const float title_width = fonts.bold->CalcTextSizeA(fonts.bold->FontSize, FLT_MAX, 0, mod.title.c_str()).x;
|
||||
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x + title_width + S(8), title_y + S(2)), color::muted,
|
||||
("by " + author).c_str());
|
||||
float pill_x = text_x;
|
||||
badge(draw, fonts, ImVec2(pill_x, pills_y), standing.word, standing.fill, color::ink);
|
||||
pill_x += badge_width(fonts, standing.word) + S(8);
|
||||
if (!version.empty()) badge(draw, fonts, ImVec2(pill_x, pills_y), "v" + version, color::tile_grey, color::text);
|
||||
if (!description.empty())
|
||||
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, description_y), color::text, description.c_str(), nullptr, wrap);
|
||||
if (!facts.empty())
|
||||
draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(text_x, facts_y), color::muted, facts.c_str());
|
||||
if (!problem.empty())
|
||||
draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(text_x, problem_y), standing.fill, problem.c_str(), nullptr, wrap);
|
||||
|
||||
// ------------------------------------------- at the right
|
||||
ImGui::SetCursorScreenPos(ImVec2(buttons_x, start.y + pad));
|
||||
if (ImGui::Button("THUNDERSTORE", ImVec2(button, S(32)))) {
|
||||
const auto dash = mod.full_name.find('-');
|
||||
open_url(package ? package->package_url
|
||||
: std::format("https://thunderstore.io/c/{}/p/{}/{}/", ts::community(), mod.full_name.substr(0, dash),
|
||||
mod.full_name.substr(dash + 1)));
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Its page on Thunderstore");
|
||||
const auto folder = panel.root / std::string(mods::official_folder) / wide(mod.full_name);
|
||||
ImGui::SetCursorScreenPos(ImVec2(buttons_x, start.y + pad + S(40)));
|
||||
ImGui::BeginDisabled(mod.installed.empty());
|
||||
if (ImGui::Button("OPEN FOLDER", ImVec2(button, S(32)))) open_path(folder);
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip("%s", mod.installed.empty() ? "Not downloaded yet" : "Where ReSkate keeps it");
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(start.x, start.y + tall + S(12)));
|
||||
ImGui::Dummy(ImVec2(across, 0));
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (state.mods.empty()) ImGui::TextDisabled("%s", state.syncing ? "Reading the list..." : "There are no official mods right now.");
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -505,8 +589,23 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float
|
||||
auto& store = panel.store;
|
||||
const auto installed = installed_versions(panel.list);
|
||||
|
||||
// ------------------------------------------------ search, category, sort, refresh
|
||||
const float top = ImGui::GetCursorPosY();
|
||||
{
|
||||
const auto waiting = store.loaded ? updates(store, installed).size() : 0;
|
||||
const auto have = std::count_if(store.packages.begin(), store.packages.end(),
|
||||
[&](const ts::Package& package) { return installed.contains(ts::folder_for(package.full_name)); });
|
||||
page_banner(fonts, "GET MODS", nullptr,
|
||||
"Mods for ReSkate from Thunderstore. Open one to read about it, or tick several and install them together. "
|
||||
"Official mods are kept for you, so they are not listed here.",
|
||||
!store.loaded ? (store.loading || store.error.empty() ? "LOADING" : "OFFLINE")
|
||||
: waiting ? std::format("{} {} WAITING", waiting, waiting == 1 ? "UPDATE" : "UPDATES")
|
||||
: std::format("{} INSTALLED", have),
|
||||
!store.loaded ? (store.loading || store.error.empty() ? color::blue : color::danger) : waiting ? color::blue : color::good,
|
||||
!store.loaded ? (store.loading || store.error.empty() ? "Reading Thunderstore..." : "Thunderstore could not be reached")
|
||||
: std::format("{} mods on Thunderstore", count_text(store.packages.size())));
|
||||
}
|
||||
|
||||
// ------------------------------------------------ search, category, sort, refresh
|
||||
const float combo = S(190), refresh = S(110);
|
||||
ImGui::SetNextItemWidth(std::max(S(140),
|
||||
ImGui::GetContentRegionAvail().x - (combo + S(10)) * 2 - refresh - S(10)));
|
||||
@@ -541,6 +640,10 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float
|
||||
// ------------------------------------------------ what is ticked
|
||||
if (!store.picked.empty()) {
|
||||
ImGui::Spacing();
|
||||
const ImVec2 bar = ImGui::GetCursorScreenPos();
|
||||
const float pad = S(10), bar_tall = ImGui::GetFrameHeight() + pad * 2;
|
||||
card(ImGui::GetWindowDrawList(), bar, ImVec2(ImGui::GetContentRegionAvail().x, bar_tall), true);
|
||||
ImGui::SetCursorScreenPos(ImVec2(bar.x + S(20), bar.y + pad));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
// What installing them all downloads: each one's newest version.
|
||||
std::uint64_t bytes{};
|
||||
@@ -553,7 +656,7 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float
|
||||
ImGui::TextUnformatted(chosen_text.c_str());
|
||||
ImGui::SameLine();
|
||||
const float action = S(190);
|
||||
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - action - S(90) - S(8));
|
||||
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - action - S(90) - S(8) - pad);
|
||||
if (ImGui::Button("Clear", ImVec2(S(90), 0))) store.picked.clear();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(installing);
|
||||
@@ -569,13 +672,15 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float
|
||||
}
|
||||
pop_primary_button();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SetCursorScreenPos(ImVec2(bar.x, bar.y + bar_tall));
|
||||
ImGui::Dummy(ImVec2(0, 0));
|
||||
}
|
||||
ImGui::Spacing();
|
||||
const float body = std::max(S(120), height - (ImGui::GetCursorPosY() - top));
|
||||
const auto packages = visible_packages(store, installed);
|
||||
|
||||
// ------------------------------------------------ the list
|
||||
ImGui::BeginChild("##store_list", ImVec2(0, body), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened);
|
||||
ImGui::BeginChild("##store_list", ImVec2(0, body), ImGuiChildFlags_NavFlattened);
|
||||
const auto note = [](const char* text) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Indent(S(14));
|
||||
@@ -603,9 +708,10 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float
|
||||
note("No mods match your search.");
|
||||
}
|
||||
|
||||
const float row = S(78);
|
||||
const float row = S(80) + row_gap();
|
||||
ImGui::BeginDisabled(installing);
|
||||
virtual_rows(static_cast<int>(packages.size()), [&](int) { return row; }, [&](int index, float tall) {
|
||||
virtual_rows(static_cast<int>(packages.size()), [&](int) { return row; }, [&](int index, float slot) {
|
||||
const float tall = slot - row_gap();
|
||||
const auto& package = *packages[static_cast<std::size_t>(index)];
|
||||
const auto& version = package.latest();
|
||||
const auto found = installed.find(ts::folder_for(package.full_name));
|
||||
|
||||
+202
-86
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
|
||||
@@ -265,75 +267,143 @@ void settle_profile(ModsPanel& panel) {
|
||||
else say(panel, "Loaded \"" + name + "\" and downloaded what it needed. Changes apply the next time Skate starts.");
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A mod as a profile card names it: its Thunderstore title, else its folder without the namespace.
|
||||
std::string mod_title(const ModsPanel& panel, const std::string& folder) {
|
||||
if (const auto* package = package_for(panel.store, folder)) return package->title();
|
||||
auto name = folder.substr(folder.find('-') + 1);
|
||||
std::replace(name.begin(), name.end(), '_', ' ');
|
||||
return name;
|
||||
}
|
||||
|
||||
// The picture on a profile's card: the icons of its first mods that are on, four at most.
|
||||
void profile_picture(const Fonts& fonts, ModsPanel& panel, const profiles::Profile& profile, ImVec2 at, float size) {
|
||||
std::vector<const ts::Package*> shown;
|
||||
for (const auto& mod : profile.mods) {
|
||||
if (shown.size() == 4) break;
|
||||
if (const auto* package = mod.enabled ? package_for(panel.store, mod.name) : nullptr) shown.push_back(package);
|
||||
}
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
if (shown.size() == 1) {
|
||||
mod_icon(panel, shown[0], at, size);
|
||||
return;
|
||||
}
|
||||
if (shown.empty()) {
|
||||
draw->AddRectFilled(at, ImVec2(at.x + size, at.y + size), rgba(255, 255, 255, 0.06f), S(4));
|
||||
const std::string letter(1, profile.name.empty() ? '?' : static_cast<char>(std::toupper(static_cast<unsigned char>(profile.name[0]))));
|
||||
const auto extent = fonts.tile->CalcTextSizeA(fonts.tile->FontSize, FLT_MAX, 0, letter.c_str());
|
||||
draw->AddText(fonts.tile, fonts.tile->FontSize, ImVec2(at.x + (size - extent.x) * 0.5f, at.y + (size - extent.y) * 0.5f),
|
||||
color::muted, letter.c_str());
|
||||
return;
|
||||
}
|
||||
const float gap = S(4), cell = (size - gap) * 0.5f;
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
const ImVec2 corner(at.x + static_cast<float>(i % 2) * (cell + gap), at.y + static_cast<float>(i / 2) * (cell + gap));
|
||||
if (i < shown.size()) mod_icon(panel, shown[i], corner, cell);
|
||||
else draw->AddRectFilled(corner, ImVec2(corner.x + cell, corner.y + cell), rgba(255, 255, 255, 0.04f), S(4));
|
||||
}
|
||||
}
|
||||
|
||||
// A small heading over a line saying what the card is for.
|
||||
void card_heading(const Fonts& fonts, ImVec2 at, const char* heading, const char* line) {
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
draw->AddText(fonts.bold, fonts.bold->FontSize, at, color::text, heading);
|
||||
draw->AddText(fonts.caption, fonts.caption->FontSize, ImVec2(at.x, at.y + fonts.bold->FontSize + S(4)), color::muted, line);
|
||||
}
|
||||
}
|
||||
|
||||
void profiles_page(const Fonts& fonts, ModsPanel& panel, HWND window, float height, bool installing) {
|
||||
collect_profile(panel);
|
||||
const bool busy = installing || panel.profile_busy;
|
||||
const float top = ImGui::GetCursorPosY();
|
||||
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextDisabled("A profile remembers which of your mods are on and in what order. Every mod stays installed "
|
||||
"once; loading a profile switches the rest off. Share one as a code or a file, with ReSkate "
|
||||
"or r2modman.");
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
const profiles::Profile* using_now = nullptr;
|
||||
for (const auto& profile : panel.profiles.list)
|
||||
if (!using_now && profiles::in_use(profile, panel.list)) using_now = &profile;
|
||||
page_banner(fonts, "PROFILES", nullptr,
|
||||
"A profile remembers which of your mods are on and in what order. Every mod stays installed once; loading a "
|
||||
"profile switches the rest off. Share one as a code or a file, with ReSkate or r2modman.",
|
||||
using_now ? "IN USE: " + using_now->name : "NO PROFILE IN USE", using_now ? color::good : color::tile_grey,
|
||||
std::format("{} of {} saved", panel.profiles.list.size(), profiles::max_profiles));
|
||||
|
||||
ImGui::BeginDisabled(busy);
|
||||
// ------------------------------------------------ new, and the two ways in
|
||||
const float button = S(280), gap = S(10);
|
||||
ImGui::SetNextItemWidth(std::max(S(160), ImGui::GetContentRegionAvail().x - button - gap));
|
||||
const bool entered_name = ImGui::InputTextWithHint("##profile_name", "Name for a new profile", panel.profile_name.data(),
|
||||
panel.profile_name.size(), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
ImGui::SameLine(0, gap);
|
||||
push_primary_button();
|
||||
const bool create = ImGui::Button("SAVE CURRENT MODS", ImVec2(button, 0)) || entered_name;
|
||||
pop_primary_button();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Save what MY MODS shows now as a new profile");
|
||||
if (create) {
|
||||
const auto name = trimmed(panel.profile_name.data());
|
||||
if (!profiles::valid_profile_name(name))
|
||||
say(panel, "Give the profile a name first (up to 48 characters, none of < > : \" / \\ | ? *).", true);
|
||||
else if (panel.profiles.find(name)) say(panel, "A profile named \"" + name + "\" already exists.", true);
|
||||
else {
|
||||
const auto before = panel.profiles.list.size();
|
||||
add_profile(panel, profiles::capture(name, panel.list), "Saved");
|
||||
if (panel.profiles.list.size() > before) {
|
||||
panel.profiles.active = panel.profiles.list.back().name;
|
||||
store(panel);
|
||||
panel.profile_name.fill(0);
|
||||
// ------------------------------------------------ new, and the two ways in, a card each
|
||||
{
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const float gap = S(12), pad = S(16);
|
||||
const float across = (ImGui::GetContentRegionAvail().x - gap) * 0.5f;
|
||||
const float frame = ImGui::GetFrameHeight();
|
||||
const float heading = fonts.bold->FontSize + S(4) + fonts.caption->FontSize + S(12);
|
||||
const float tall = pad + heading + frame + S(8) + frame + pad;
|
||||
const ImVec2 left = start, right(start.x + across + gap, start.y);
|
||||
card(draw, left, ImVec2(across, tall));
|
||||
card(draw, right, ImVec2(across, tall));
|
||||
card_heading(fonts, ImVec2(left.x + pad, left.y + pad), "NEW PROFILE", "Saves what MY MODS shows now");
|
||||
card_heading(fonts, ImVec2(right.x + pad, right.y + pad), "IMPORT",
|
||||
panel.profile_busy ? "Talking to Thunderstore..." : "A code or a .r2z file, from ReSkate or r2modman");
|
||||
const float inner = across - pad * 2;
|
||||
const float row = start.y + pad + heading;
|
||||
|
||||
ImGui::BeginDisabled(busy);
|
||||
ImGui::SetCursorScreenPos(ImVec2(left.x + pad, row));
|
||||
ImGui::SetNextItemWidth(inner);
|
||||
const bool entered_name = ImGui::InputTextWithHint("##profile_name", "Name for a new profile", panel.profile_name.data(),
|
||||
panel.profile_name.size(), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
ImGui::SetCursorScreenPos(ImVec2(left.x + pad, row + frame + S(8)));
|
||||
push_primary_button();
|
||||
const bool create = ImGui::Button("SAVE CURRENT MODS", ImVec2(inner, 0)) || entered_name;
|
||||
pop_primary_button();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Save what MY MODS shows now as a new profile");
|
||||
if (create) {
|
||||
const auto name = trimmed(panel.profile_name.data());
|
||||
if (!profiles::valid_profile_name(name))
|
||||
say(panel, "Give the profile a name first (up to 48 characters, none of < > : \" / \\ | ? *).", true);
|
||||
else if (panel.profiles.find(name)) say(panel, "A profile named \"" + name + "\" already exists.", true);
|
||||
else {
|
||||
const auto before = panel.profiles.list.size();
|
||||
add_profile(panel, profiles::capture(name, panel.list), "Saved");
|
||||
if (panel.profiles.list.size() > before) {
|
||||
panel.profiles.active = panel.profiles.list.back().name;
|
||||
store(panel);
|
||||
panel.profile_name.fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const float half = (button - gap) * 0.5f;
|
||||
ImGui::SetNextItemWidth(std::max(S(160), ImGui::GetContentRegionAvail().x - button - gap));
|
||||
const bool entered_code = ImGui::InputTextWithHint("##profile_code", "Paste a profile code", panel.profile_code.data(),
|
||||
panel.profile_code.size(), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
ImGui::SameLine(0, gap);
|
||||
if (ImGui::Button("IMPORT CODE", ImVec2(half, 0)) || entered_code) {
|
||||
const auto code = trimmed(panel.profile_code.data());
|
||||
if (!profiles::valid_code(code)) say(panel, "That is not a profile code. Paste the whole code you were given.", true);
|
||||
else {
|
||||
import_code(panel, code);
|
||||
panel.profile_code.fill(0);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(right.x + pad, row));
|
||||
ImGui::SetNextItemWidth(inner);
|
||||
const bool entered_code = ImGui::InputTextWithHint("##profile_code", "Paste a profile code", panel.profile_code.data(),
|
||||
panel.profile_code.size(), ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
const float half = (inner - S(8)) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(right.x + pad, row + frame + S(8)));
|
||||
if (ImGui::Button("IMPORT CODE", ImVec2(half, 0)) || entered_code) {
|
||||
const auto code = trimmed(panel.profile_code.data());
|
||||
if (!profiles::valid_code(code)) say(panel, "That is not a profile code. Paste the whole code you were given.", true);
|
||||
else {
|
||||
import_code(panel, code);
|
||||
panel.profile_code.fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine(0, gap);
|
||||
if (ImGui::Button("IMPORT FILE", ImVec2(half, 0))) import_file(panel, window);
|
||||
ImGui::EndDisabled();
|
||||
if (panel.profile_busy) {
|
||||
ImGui::SameLine(0, gap);
|
||||
ImGui::TextDisabled("Talking to Thunderstore...");
|
||||
ImGui::SameLine(0, S(8));
|
||||
if (ImGui::Button("IMPORT FILE", ImVec2(half, 0))) import_file(panel, window);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Pick a .r2z or .r2x file");
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(start.x, start.y + tall + S(12)));
|
||||
ImGui::Dummy(ImVec2(0, 0));
|
||||
}
|
||||
|
||||
// ------------------------------------------------ the profiles
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginChild("##profiles", ImVec2(0, height - (ImGui::GetCursorPosY() - top)), ImGuiChildFlags_NavFlattened);
|
||||
// ------------------------------------------------ the profiles, a card each
|
||||
ImGui::BeginChild("##profiles", ImVec2(0, std::max(S(80), height - (ImGui::GetCursorPosY() - top))), ImGuiChildFlags_NavFlattened);
|
||||
auto* draw = ImGui::GetWindowDrawList();
|
||||
if (panel.profiles.list.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, S(24)));
|
||||
ImGui::Dummy(ImVec2(0, S(8)));
|
||||
ImGui::TextDisabled("No profiles yet. Set up MY MODS the way you like, name it above and save it.");
|
||||
}
|
||||
// What a row asked for, carried out after the list is drawn: most of it changes the list.
|
||||
// What a card asked for, carried out after the list is drawn: most of it changes the list.
|
||||
enum class Ask { none, load, update, code, file, rename, remove } ask{Ask::none};
|
||||
std::size_t asked{};
|
||||
const float pad = S(18), picture = S(84), button = S(110), more = S(32);
|
||||
for (std::size_t index = 0; index < panel.profiles.list.size(); ++index) {
|
||||
const auto& profile = panel.profiles.list[index];
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
@@ -341,42 +411,88 @@ void profiles_page(const Fonts& fonts, ModsPanel& panel, HWND window, float heig
|
||||
const auto missing = profiles::apply(profile, panel.list).missing.size();
|
||||
const bool current = profiles::in_use(profile, panel.list);
|
||||
const bool last = !current && profile.name == panel.profiles.active;
|
||||
std::vector<std::string> names;
|
||||
for (const auto& mod : profile.mods)
|
||||
if (mod.enabled) names.push_back(mod_title(panel, mod.name));
|
||||
|
||||
ImGui::Dummy(ImVec2(0, S(6)));
|
||||
ImGui::PushFont(fonts.bold);
|
||||
ImGui::TextUnformatted(profile.name.c_str());
|
||||
ImGui::PopFont();
|
||||
if (current || last) {
|
||||
ImGui::SameLine(0, S(12));
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(current ? color::good : color::warning), "%s",
|
||||
current ? "IN USE" : "CHANGED SINCE LOADED");
|
||||
}
|
||||
ImGui::TextDisabled("%s", (std::format("{} mods, {} on", profile.mods.size(), on) +
|
||||
(missing ? std::format(" | {} to download", missing) : std::string())).c_str());
|
||||
ImGui::BeginDisabled(busy);
|
||||
const auto act = [&](const char* label, Ask what, const char* tip, bool enabled = true, bool primary = false) {
|
||||
ImGui::BeginDisabled(!enabled);
|
||||
if (primary) push_primary_button();
|
||||
if (ImGui::Button(label, ImVec2(S(122), 0))) {
|
||||
ask = what;
|
||||
asked = index;
|
||||
}
|
||||
if (primary) pop_primary_button();
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", tip);
|
||||
ImGui::SameLine(0, S(8));
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
const float across = ImGui::GetContentRegionAvail().x;
|
||||
const float tall = std::max(picture + pad * 2,
|
||||
pad + fonts.bold->FontSize + S(10) + fonts.caption->FontSize + S(8) + S(10) + fonts.body->FontSize + pad);
|
||||
const float text_x = start.x + pad + picture + S(18);
|
||||
const float more_x = start.x + across - pad - more;
|
||||
const float update_x = more_x - S(8) - button;
|
||||
const float load_x = update_x - S(8) - button;
|
||||
card(draw, start, ImVec2(across, tall), current);
|
||||
profile_picture(fonts, panel, profile, ImVec2(start.x + pad, start.y + (tall - picture) * 0.5f), picture);
|
||||
|
||||
const ImVec4 clip(text_x, start.y, load_x - S(18), start.y + tall);
|
||||
float y = start.y + pad;
|
||||
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(text_x, y), color::text, profile.name.c_str(), nullptr, 0, &clip);
|
||||
y += fonts.bold->FontSize + S(10);
|
||||
float pill_x = text_x;
|
||||
const auto pill = [&](const std::string& word, ImU32 fill, ImU32 ink) {
|
||||
badge(draw, fonts, ImVec2(pill_x, y), word, fill, ink);
|
||||
pill_x += badge_width(fonts, word) + S(8);
|
||||
};
|
||||
act("LOAD", Ask::load, current ? "MY MODS already matches this profile"
|
||||
: "Switch MY MODS to this profile, downloading what is missing", !current, true);
|
||||
act("UPDATE", Ask::update, "Overwrite this profile with what MY MODS shows now", !current);
|
||||
act("COPY CODE", Ask::code, "Upload this profile to Thunderstore and copy its code");
|
||||
act("SAVE FILE", Ask::file, "Save this profile as a .r2z file to send to someone");
|
||||
act("RENAME", Ask::rename, "Give this profile another name");
|
||||
act("DELETE", Ask::remove, "Delete this profile. No mod is uninstalled.");
|
||||
ImGui::NewLine();
|
||||
if (current) pill("IN USE", color::good, color::ink);
|
||||
else if (last) pill("CHANGED SINCE LOADED", color::warning, color::ink);
|
||||
pill(std::format("{} MODS", profile.mods.size()), color::tile_grey, color::text);
|
||||
pill(std::format("{} ON", on), color::tile_grey, color::text);
|
||||
if (missing) pill(std::format("{} TO DOWNLOAD", missing), color::blue, color::ink);
|
||||
y += fonts.caption->FontSize + S(8) + S(10);
|
||||
const auto list = names.empty() ? std::string("Every mod off") : named(names);
|
||||
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, y), color::muted, list.c_str(), nullptr, 0, &clip);
|
||||
|
||||
// ------------------------------------------- at the right
|
||||
const float button_y = start.y + (tall - S(34)) * 0.5f;
|
||||
ImGui::BeginDisabled(busy);
|
||||
ImGui::SetCursorScreenPos(ImVec2(load_x, button_y));
|
||||
ImGui::BeginDisabled(current);
|
||||
push_primary_button();
|
||||
if (ImGui::Button(current ? "LOADED" : "LOAD", ImVec2(button, S(34)))) {
|
||||
ask = Ask::load;
|
||||
asked = index;
|
||||
}
|
||||
pop_primary_button();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, S(6)));
|
||||
ImGui::Separator();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip("%s", current ? "MY MODS already matches this profile"
|
||||
: missing ? "Switch MY MODS to this profile, downloading what is missing"
|
||||
: "Switch MY MODS to this profile");
|
||||
ImGui::SetCursorScreenPos(ImVec2(update_x, button_y));
|
||||
ImGui::BeginDisabled(current);
|
||||
if (ImGui::Button("UPDATE", ImVec2(button, S(34)))) {
|
||||
ask = Ask::update;
|
||||
asked = index;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip("%s", current ? "It already matches MY MODS" : "Overwrite this profile with what MY MODS shows now");
|
||||
ImGui::SetCursorScreenPos(ImVec2(more_x, start.y + (tall - more) * 0.5f));
|
||||
if (more_button("##more", more)) ImGui::OpenPopup("##profile_menu");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Share, rename or delete");
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::BeginPopup("##profile_menu")) {
|
||||
const auto item = [&](const char* label, Ask what) {
|
||||
if (ImGui::MenuItem(label)) {
|
||||
ask = what;
|
||||
asked = index;
|
||||
}
|
||||
};
|
||||
item("Copy code", Ask::code);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Upload this profile to Thunderstore and copy its code");
|
||||
item("Save as file", Ask::file);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Save this profile as a .r2z file to send to someone");
|
||||
ImGui::Separator();
|
||||
item("Rename", Ask::rename);
|
||||
item("Delete", Ask::remove);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Delete this profile. No mod is uninstalled.");
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(start.x, start.y + tall + S(12)));
|
||||
ImGui::Dummy(ImVec2(across, 0));
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
Reference in new issue
Block a user