Compare commits

..

4 commits

Author SHA1 Message Date
TheSillyDoggo
c06c5aff25
Merge bd6bfb661f into 7bcf50da57 2024-11-18 11:36:49 -08:00
HJfod
7bcf50da57 grid view in mod list; some minor stuff left to do
Some checks failed
Build Binaries / Build Windows (push) Has been cancelled
Build Binaries / Build macOS (push) Has been cancelled
Build Binaries / Build Android (64-bit) (push) Has been cancelled
Build Binaries / Build Android (32-bit) (push) Has been cancelled
Build Binaries / Publish (push) Has been cancelled
2024-11-17 23:35:39 +02:00
matcool
38f3385c90 add safe mode tip text to windows crashlog window 2024-11-17 15:14:34 -03:00
dankmeme01
4d5e465ade fix utils::string::replaceIP hanging if filter is empty 2024-11-17 18:13:04 +01:00
11 changed files with 372 additions and 143 deletions

View file

@ -71,6 +71,7 @@ namespace geode::utils {
std::string m_msg; std::string m_msg;
Timer<Clock> m_timer; Timer<Clock> m_timer;
// @geode-ignore(geode-alternative)
LogPerformance(std::string const& msg = "", std::ostream& out = std::cout) : LogPerformance(std::string const& msg = "", std::ostream& out = std::cout) :
m_msg(msg), m_output(out) { m_msg(msg), m_output(out) {
m_timer = Timer<Clock>(); m_timer = Timer<Clock>();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -10,11 +10,13 @@
#include <unistd.h> #include <unistd.h>
void Loader::Impl::platformMessageBox(char const* title, std::string const& info) { void Loader::Impl::platformMessageBox(char const* title, std::string const& info) {
// @geode-ignore(geode-alternative)
std::cout << title << ": " << info << std::endl; std::cout << title << ": " << info << std::endl;
} }
void Loader::Impl::logConsoleMessageWithSeverity(std::string const& msg, Severity severity) { void Loader::Impl::logConsoleMessageWithSeverity(std::string const& msg, Severity severity) {
if (m_platformConsoleOpen) { if (m_platformConsoleOpen) {
// @geode-ignore(geode-alternative)
std::cout << msg << "\n" << std::flush; std::cout << msg << "\n" << std::flush;
} }
} }

View file

@ -18,6 +18,7 @@ enum {
ID_BUTTON_OPEN_FOLDER = 103, ID_BUTTON_OPEN_FOLDER = 103,
ID_BUTTON_COPY_CLIPBOARD = 104, ID_BUTTON_COPY_CLIPBOARD = 104,
ID_BUTTON_RESTART_GAME = 105, ID_BUTTON_RESTART_GAME = 105,
ID_SAFE_MODE_TIP_TEXT = 106,
}; };
#define TO_HMENU(x) reinterpret_cast<HMENU>(static_cast<size_t>(x)) #define TO_HMENU(x) reinterpret_cast<HMENU>(static_cast<size_t>(x))
@ -63,6 +64,25 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
DEFAULT_PITCH | FF_DONTCARE, TEXT("Consolas")); DEFAULT_PITCH | FF_DONTCARE, TEXT("Consolas"));
auto guiFont = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT)); auto guiFont = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
auto calculateTextSize = [&](std::string_view text) -> SIZE {
HDC hdc = GetDC(hwnd);
SelectObject(hdc, monoFont);
SIZE size;
GetTextExtentPoint32A(hdc, text.data(), text.size(), &size);
ReleaseDC(hwnd, hdc);
return size;
};
auto tipTextStr = "Tip: You can hold shift while launching the game to enter safe mode.";
auto tipTextSize = calculateTextSize(tipTextStr);
auto tipText = CreateWindowA(
"STATIC", tipTextStr,
WS_CHILD | WS_VISIBLE | SS_SIMPLE,
0, 0, tipTextSize.cx, tipTextSize.cy,
hwnd, TO_HMENU(ID_SAFE_MODE_TIP_TEXT), NULL, NULL
);
SendMessage(tipText, WM_SETFONT, WPARAM(monoFont), TRUE);
auto handleText = CreateWindowA( auto handleText = CreateWindowA(
"EDIT", "Crashlog text goes here", WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_LEFT | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_BORDER, "EDIT", "Crashlog text goes here", WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_LEFT | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_BORDER,
0, 0, 100, 100, 0, 0, 100, 100,
@ -109,10 +129,20 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
RECT clientRect; RECT clientRect;
GetClientRect(hwnd, &clientRect); GetClientRect(hwnd, &clientRect);
RECT textRect;
GetClientRect(GetDlgItem(hwnd, ID_SAFE_MODE_TIP_TEXT), &textRect);
SetWindowPos(
GetDlgItem(hwnd, ID_SAFE_MODE_TIP_TEXT), NULL,
layout::PADDING, layout::PADDING,
0, 0,
SWP_NOZORDER | SWP_NOSIZE
);
SetWindowPos( SetWindowPos(
GetDlgItem(hwnd, ID_CRASHLOG_TEXT), NULL, GetDlgItem(hwnd, ID_CRASHLOG_TEXT), NULL,
layout::PADDING, layout::PADDING, layout::PADDING, layout::PADDING * 2 + textRect.bottom,
clientRect.right - layout::PADDING * 2, clientRect.bottom - layout::BUTTON_HEIGHT - layout::PADDING * 3, clientRect.right - layout::PADDING * 2, clientRect.bottom - layout::BUTTON_HEIGHT - layout::PADDING * 4 - textRect.bottom,
SWP_NOZORDER SWP_NOZORDER
); );
@ -150,6 +180,9 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
} break; } break;
case WM_CTLCOLORSTATIC: { case WM_CTLCOLORSTATIC: {
auto hdc = (HDC)wParam;
// make every text have transparent background
SetBkMode(hdc, TRANSPARENT);
return (LRESULT)(COLOR_WINDOWFRAME); return (LRESULT)(COLOR_WINDOWFRAME);
} break; } break;

View file

@ -499,28 +499,48 @@ bool ModsLayer::init() {
// Actions // Actions
auto listActionsMenu = CCMenu::create(); auto listDisplayMenu = CCMenu::create();
listActionsMenu->setID("list-actions-menu"); listDisplayMenu->setID("list-actions-menu");
listActionsMenu->setContentHeight(100); listDisplayMenu->setContentHeight(100);
listActionsMenu->setAnchorPoint({ 1, 0 }); listDisplayMenu->setAnchorPoint({ 1, 0 });
listActionsMenu->setScale(.65f); listDisplayMenu->setScale(.65f);
auto smallSizeBtn = CCMenuItemSpriteExtra::create(
GeodeSquareSprite::createWithSpriteFrameName("GJ_smallModeIcon_001.png"),
this, menu_selector(ModsLayer::onDisplay)
);
smallSizeBtn->setTag(static_cast<int>(ModListDisplay::SmallList));
smallSizeBtn->setID("list-normal-size-button");
listDisplayMenu->addChild(smallSizeBtn);
m_displayBtns.push_back(smallSizeBtn);
auto bigSizeBtn = CCMenuItemSpriteExtra::create( auto bigSizeBtn = CCMenuItemSpriteExtra::create(
GeodeSquareSprite::createWithSpriteFrameName("GJ_smallModeIcon_001.png", &m_bigView), GeodeSquareSprite::createWithSpriteFrameName("GJ_extendedIcon_001.png"),
this, menu_selector(ModsLayer::onBigView) this, menu_selector(ModsLayer::onDisplay)
); );
bigSizeBtn->setTag(static_cast<int>(ModListDisplay::BigList));
bigSizeBtn->setID("list-size-button"); bigSizeBtn->setID("list-size-button");
listActionsMenu->addChild(bigSizeBtn); listDisplayMenu->addChild(bigSizeBtn);
m_displayBtns.push_back(bigSizeBtn);
auto gridBtn = CCMenuItemSpriteExtra::create(
GeodeSquareSprite::createWithSpriteFrameName("grid-view.png"_spr),
this, menu_selector(ModsLayer::onDisplay)
);
gridBtn->setTag(static_cast<int>(ModListDisplay::Grid));
gridBtn->setID("list-size-button");
listDisplayMenu->addChild(gridBtn);
m_displayBtns.push_back(gridBtn);
// auto searchBtn = CCMenuItemSpriteExtra::create( // auto searchBtn = CCMenuItemSpriteExtra::create(
// GeodeSquareSprite::createWithSpriteFrameName("search.png"_spr, &m_showSearch), // GeodeSquareSprite::createWithSpriteFrameName("search.png"_spr, &m_showSearch),
// this, menu_selector(ModsLayer::onSearch) // this, menu_selector(ModsLayer::onSearch)
// ); // );
// searchBtn->setID("search-button"); // searchBtn->setID("search-button");
// listActionsMenu->addChild(searchBtn); // listDisplayMenu->addChild(searchBtn);
listActionsMenu->setLayout(ColumnLayout::create()); listDisplayMenu->setLayout(ColumnLayout::create()->setAxisReverse(true));
m_frame->addChildAtPosition(listActionsMenu, Anchor::Left, ccp(-5, 25)); m_frame->addChildAtPosition(listDisplayMenu, Anchor::Left, ccp(-5, 25));
m_statusNode = ModsStatusNode::create(); m_statusNode = ModsStatusNode::create();
m_statusNode->setZOrder(4); m_statusNode->setZOrder(4);
@ -629,7 +649,7 @@ void ModsLayer::gotoTab(ModListSource* src) {
m_currentSource = src; m_currentSource = src;
// Update the state of the current list // Update the state of the current list
m_lists.at(m_currentSource)->updateSize(m_bigView); m_lists.at(m_currentSource)->updateDisplay(m_display);
m_lists.at(m_currentSource)->activateSearch(m_showSearch); m_lists.at(m_currentSource)->activateSearch(m_showSearch);
m_lists.at(m_currentSource)->updateState(); m_lists.at(m_currentSource)->updateState();
} }
@ -687,6 +707,13 @@ void ModsLayer::updateState() {
else { else {
m_pageMenu->setVisible(false); m_pageMenu->setVisible(false);
} }
// Update display button
for (auto btn : m_displayBtns) {
static_cast<GeodeSquareSprite*>(btn->getNormalImage())->setState(
static_cast<ModListDisplay>(btn->getTag()) == m_display
);
}
} }
void ModsLayer::onTab(CCObject* sender) { void ModsLayer::onTab(CCObject* sender) {
@ -716,12 +743,13 @@ void ModsLayer::onGoToPage(CCObject*) {
popup->setID("go-to-page"_spr); popup->setID("go-to-page"_spr);
popup->show(); popup->show();
} }
void ModsLayer::onBigView(CCObject*) { void ModsLayer::onDisplay(CCObject* sender) {
m_bigView = !m_bigView; m_display = static_cast<ModListDisplay>(sender->getTag());
// Make sure to avoid a crash // Make sure to avoid a crash
if (m_currentSource) { if (m_currentSource) {
m_lists.at(m_currentSource)->updateSize(m_bigView); m_lists.at(m_currentSource)->updateDisplay(m_display);
} }
this->updateState();
} }
void ModsLayer::onSearch(CCObject*) { void ModsLayer::onSearch(CCObject*) {
m_showSearch = !m_showSearch; m_showSearch = !m_showSearch;

View file

@ -66,7 +66,8 @@ protected:
ModsStatusNode* m_statusNode; ModsStatusNode* m_statusNode;
EventListener<UpdateModListStateFilter> m_updateStateListener; EventListener<UpdateModListStateFilter> m_updateStateListener;
bool m_showSearch = true; bool m_showSearch = true;
bool m_bigView = false; ModListDisplay m_display = ModListDisplay::SmallList;
std::vector<CCMenuItemSpriteExtra*> m_displayBtns;
bool init(); bool init();
@ -77,7 +78,7 @@ protected:
void onTab(CCObject* sender); void onTab(CCObject* sender);
void onOpenModsFolder(CCObject*); void onOpenModsFolder(CCObject*);
void onAddModFromFile(CCObject*); void onAddModFromFile(CCObject*);
void onBigView(CCObject*); void onDisplay(CCObject*);
void onSearch(CCObject*); void onSearch(CCObject*);
void onGoToPage(CCObject*); void onGoToPage(CCObject*);
void onRefreshList(CCObject*); void onRefreshList(CCObject*);

View file

@ -29,7 +29,7 @@ bool ModItem::init(ModSource&& source) {
m_bg->ignoreAnchorPointForPosition(false); m_bg->ignoreAnchorPointForPosition(false);
m_bg->setAnchorPoint({ .5f, .5f }); m_bg->setAnchorPoint({ .5f, .5f });
m_bg->setScale(.7f); m_bg->setScale(.7f);
this->addChild(m_bg); this->addChildAtPosition(m_bg, Anchor::Center);
m_logo = m_source.createModLogo(); m_logo = m_source.createModLogo();
m_logo->setID("logo-sprite"); m_logo->setID("logo-sprite");
@ -39,23 +39,10 @@ bool ModItem::init(ModSource&& source) {
m_infoContainer->setID("info-container"); m_infoContainer->setID("info-container");
m_infoContainer->setScale(.4f); m_infoContainer->setScale(.4f);
m_infoContainer->setAnchorPoint({ .0f, .5f }); m_infoContainer->setAnchorPoint({ .0f, .5f });
m_infoContainer->setLayout(
ColumnLayout::create()
->setAxisReverse(true)
->setAxisAlignment(AxisAlignment::Even)
->setCrossAxisLineAlignment(AxisAlignment::Start)
->setGap(0)
);
m_infoContainer->getLayout()->ignoreInvisibleChildren(true);
m_titleContainer = CCNode::create(); m_titleContainer = CCNode::create();
m_titleContainer->setID("title-container"); m_titleContainer->setID("title-container");
m_titleContainer->setAnchorPoint({ .0f, .5f }); m_titleContainer->setAnchorPoint({ .0f, .5f });
m_titleContainer->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, 1.f)
->setAxisAlignment(AxisAlignment::Start)
);
m_titleLabel = CCLabelBMFont::create(m_source.getMetadata().getName().c_str(), "bigFont.fnt"); m_titleLabel = CCLabelBMFont::create(m_source.getMetadata().getName().c_str(), "bigFont.fnt");
m_titleLabel->setID("title-label"); m_titleLabel->setID("title-label");
@ -67,14 +54,20 @@ bool ModItem::init(ModSource&& source) {
m_versionLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .7f)); m_versionLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .7f));
m_titleContainer->addChild(m_versionLabel); m_titleContainer->addChild(m_versionLabel);
m_infoContainer->addChild(m_titleContainer); m_titleContainer->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, 1.f)
->setAxisAlignment(AxisAlignment::Start)
);
m_titleContainer->getLayout()->ignoreInvisibleChildren(true);
m_infoContainer->addChildAtPosition(m_titleContainer, Anchor::Left);
m_developers = CCMenu::create(); m_developers = CCMenu::create();
m_developers->setID("developers-menu"); m_developers->setID("developers-menu");
m_developers->ignoreAnchorPointForPosition(false); m_developers->ignoreAnchorPointForPosition(false);
m_developers->setAnchorPoint({ .0f, .5f }); m_developers->setAnchorPoint({ .0f, .5f });
auto by = "By " + m_source.formatDevelopers(); auto by = m_source.formatDevelopers();
m_developerLabel = CCLabelBMFont::create(by.c_str(), "goldFont.fnt"); m_developerLabel = CCLabelBMFont::create(by.c_str(), "goldFont.fnt");
m_developerLabel->setID("developers-label"); m_developerLabel->setID("developers-label");
auto developersBtn = CCMenuItemSpriteExtra::create( auto developersBtn = CCMenuItemSpriteExtra::create(
@ -87,7 +80,24 @@ bool ModItem::init(ModSource&& source) {
RowLayout::create() RowLayout::create()
->setAxisAlignment(AxisAlignment::Start) ->setAxisAlignment(AxisAlignment::Start)
); );
m_infoContainer->addChild(m_developers); m_infoContainer->addChildAtPosition(m_developers, Anchor::Left);
m_description = CCScale9Sprite::create("square02b_001.png");
m_description->setScale(.5f);
m_description->setContentSize(ccp(450, 30) / m_description->getScale());
m_description->setColor(ccBLACK);
m_description->setOpacity(90);
auto desc = m_source.getMetadata().getDescription();
auto descLabel = CCLabelBMFont::create(
desc.value_or("[No Description Provided]").c_str(),
"chatFont.fnt"
);
descLabel->setColor(desc ? ccWHITE : ccGRAY);
limitNodeWidth(descLabel, m_description->getContentWidth() - 20, 2.f, .1f);
m_description->addChildAtPosition(descLabel, Anchor::Left, ccp(10, 0), ccp(0, .5f));
m_infoContainer->addChildAtPosition(m_description, Anchor::Left);
m_restartRequiredLabel = createTagLabel( m_restartRequiredLabel = createTagLabel(
"Restart Required", "Restart Required",
@ -97,19 +107,19 @@ bool ModItem::init(ModSource&& source) {
} }
); );
m_restartRequiredLabel->setID("restart-required-label"); m_restartRequiredLabel->setID("restart-required-label");
m_restartRequiredLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .75f)); m_restartRequiredLabel->setScale(.75f);
m_infoContainer->addChild(m_restartRequiredLabel); m_infoContainer->addChildAtPosition(m_restartRequiredLabel, Anchor::Left);
m_outdatedLabel = createTagLabel( m_outdatedLabel = createTagLabel(
fmt::format("Outdated (GD {})", m_source.getMetadata().getGameVersion().value_or("*")), "Outdated",
{ {
to3B(ColorProvider::get()->color("mod-list-outdated-label"_spr)), to3B(ColorProvider::get()->color("mod-list-outdated-label"_spr)),
to3B(ColorProvider::get()->color("mod-list-outdated-label-bg"_spr)) to3B(ColorProvider::get()->color("mod-list-outdated-label-bg"_spr))
} }
); );
m_outdatedLabel->setID("outdated-label"); m_outdatedLabel->setID("outdated-label");
m_outdatedLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .75f)); m_outdatedLabel->setScale(.75f);
m_infoContainer->addChild(m_outdatedLabel); m_infoContainer->addChildAtPosition(m_outdatedLabel, Anchor::Left);
m_downloadBarContainer = CCNode::create(); m_downloadBarContainer = CCNode::create();
m_downloadBarContainer->setID("download-bar-container"); m_downloadBarContainer->setID("download-bar-container");
@ -121,7 +131,7 @@ bool ModItem::init(ModSource&& source) {
m_downloadBar->setScale(1.5f); m_downloadBar->setScale(1.5f);
m_downloadBarContainer->addChildAtPosition(m_downloadBar, Anchor::Center, ccp(0, 0), ccp(0, 0)); m_downloadBarContainer->addChildAtPosition(m_downloadBar, Anchor::Center, ccp(0, 0), ccp(0, 0));
m_infoContainer->addChild(m_downloadBarContainer); m_infoContainer->addChildAtPosition(m_downloadBarContainer, Anchor::Left);
m_downloadWaiting = CCNode::create(); m_downloadWaiting = CCNode::create();
m_downloadWaiting->setID("download-waiting-container"); m_downloadWaiting->setID("download-waiting-container");
@ -141,13 +151,12 @@ bool ModItem::init(ModSource&& source) {
ccp(m_downloadWaiting->getContentHeight() / 2, 0) ccp(m_downloadWaiting->getContentHeight() / 2, 0)
); );
m_infoContainer->addChild(m_downloadWaiting); m_infoContainer->addChildAtPosition(m_downloadWaiting, Anchor::Left);
this->addChild(m_infoContainer); this->addChildAtPosition(m_infoContainer, Anchor::Left);
m_viewMenu = CCMenu::create(); m_viewMenu = CCMenu::create();
m_viewMenu->setID("view-menu"); m_viewMenu->setID("view-menu");
m_viewMenu->setAnchorPoint({ 1.f, .5f });
m_viewMenu->setScale(.55f); m_viewMenu->setScale(.55f);
ButtonSprite* spr = nullptr; ButtonSprite* spr = nullptr;
@ -214,43 +223,38 @@ bool ModItem::init(ModSource&& source) {
} }
}, },
[this](server::ServerModMetadata const& metadata) { [this](server::ServerModMetadata const& metadata) {
m_badgeContainer = CCNode::create();
m_badgeContainer->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .8f));
if (metadata.featured) { if (metadata.featured) {
auto star = CCSprite::createWithSpriteFrameName("tag-featured.png"_spr); m_badgeContainer->addChild(CCSprite::createWithSpriteFrameName("tag-featured.png"_spr));
star->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .8f));
m_titleContainer->addChild(star);
} }
if (metadata.tags.contains("paid")) { if (metadata.tags.contains("paid")) {
auto paidModLabel = CCSprite::createWithSpriteFrameName("tag-paid.png"_spr); m_badgeContainer->addChild(CCSprite::createWithSpriteFrameName("tag-paid.png"_spr));
paidModLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .8f));
m_titleContainer->addChild(paidModLabel);
} }
if (metadata.tags.contains("modtober24")) { if (metadata.tags.contains("modtober24")) {
auto modtoberLabel = CCSprite::createWithSpriteFrameName("tag-modtober.png"_spr); m_badgeContainer->addChild(CCSprite::createWithSpriteFrameName("tag-modtober.png"_spr));
modtoberLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .8f));
m_titleContainer->addChild(modtoberLabel);
} }
// Show mod download count here already so people can make informed decisions // Show mod download count here already so people can make informed decisions
// on which mods to install // on which mods to install
auto downloadsContainer = CCNode::create(); m_downloadCountContainer = CCNode::create();
auto downloads = CCLabelBMFont::create(numToAbbreviatedString(metadata.downloadCount).c_str(), "bigFont.fnt"); auto downloads = CCLabelBMFont::create(numToAbbreviatedString(metadata.downloadCount).c_str(), "bigFont.fnt");
downloads->setID("downloads-label"); downloads->setID("downloads-label");
downloads->setColor("mod-list-version-label"_cc3b); downloads->setColor("mod-list-version-label"_cc3b);
downloads->limitLabelWidth(80, .5f, .1f); downloads->limitLabelWidth(80, .5f, .1f);
downloadsContainer->addChildAtPosition(downloads, Anchor::Right, ccp(-0, 0), ccp(1, .5f)); m_downloadCountContainer->addChildAtPosition(downloads, Anchor::Right, ccp(-0, 0), ccp(1, .5f));
auto downloadsIcon = CCSprite::createWithSpriteFrameName("GJ_downloadsIcon_001.png"); auto downloadsIcon = CCSprite::createWithSpriteFrameName("GJ_downloadsIcon_001.png");
downloadsIcon->setID("downloads-icon-sprite"); downloadsIcon->setID("downloads-icon-sprite");
downloadsIcon->setScale(.75f); downloadsIcon->setScale(.75f);
downloadsContainer->addChildAtPosition(downloadsIcon, Anchor::Right, ccp(-downloads->getScaledContentWidth() - 10, 0)); m_downloadCountContainer->addChildAtPosition(downloadsIcon, Anchor::Left, ccp(5, 0));
downloadsContainer->setContentSize({ m_downloadCountContainer->setContentSize({
downloads->getScaledContentWidth() + 10 + downloadsIcon->getScaledContentWidth() + 10, downloads->getScaledContentWidth() + downloadsIcon->getScaledContentWidth(),
25 25
}); });
downloadsContainer->updateLayout(); m_downloadCountContainer->updateLayout();
m_viewMenu->addChild(downloadsContainer);
// Check if mod is recommended by any others, only if not installed // Check if mod is recommended by any others, only if not installed
if (!Loader::get()->isModInstalled(metadata.id)) { if (!Loader::get()->isModInstalled(metadata.id)) {
@ -289,7 +293,7 @@ bool ModItem::init(ModSource&& source) {
->setDefaultScaleLimits(.1f, 1.f) ->setDefaultScaleLimits(.1f, 1.f)
->setAxisAlignment(AxisAlignment::Start) ->setAxisAlignment(AxisAlignment::Start)
); );
m_infoContainer->addChild(m_recommendedBy); m_infoContainer->addChildAtPosition(m_recommendedBy, Anchor::Left);
} }
} }
} }
@ -329,15 +333,89 @@ bool ModItem::init(ModSource&& source) {
void ModItem::updateState() { void ModItem::updateState() {
auto wantsRestart = m_source.wantsRestart(); auto wantsRestart = m_source.wantsRestart();
auto download = server::ModDownloadManager::get()->getDownload(m_source.getID()); auto download = server::ModDownloadManager::get()->getDownload(m_source.getID());
bool isDownloading = download && download->isActive(); bool isDownloading = download && download->isActive();
// Update the size of the mod cell itself
if (m_display == ModListDisplay::Grid) {
auto widthWithoutGaps = m_targetWidth - 10;
this->setContentSize(ccp(widthWithoutGaps / roundf(widthWithoutGaps / 80), 100));
m_bg->setContentSize(m_obContentSize / m_bg->getScale());
}
else {
this->setContentSize(ccp(m_targetWidth, m_display == ModListDisplay::BigList ? 40 : 30));
m_bg->setContentSize((m_obContentSize - ccp(6, 0)) / m_bg->getScale());
}
// On Grid layout the title is a direct child of info so it can be positioned
// more cleanly, while m_titleContainer is just used to position the version
// and downloads next to each other
m_titleLabel->removeFromParent();
if (m_display == ModListDisplay::Grid) {
m_infoContainer->addChildAtPosition(m_titleLabel, Anchor::Top);
}
else {
m_titleContainer->insertBefore(m_titleLabel, nullptr);
}
// Download counts go next to the version like on the website on grid view
if (m_downloadCountContainer) {
m_downloadCountContainer->removeFromParent();
if (m_display == ModListDisplay::Grid) {
m_titleContainer->insertAfter(m_downloadCountContainer, m_versionLabel);
}
else {
m_viewMenu->addChild(m_downloadCountContainer);
}
}
// Move badges to either be next to the title or in the top left corner in grid view
if (m_badgeContainer) {
m_badgeContainer->removeFromParent();
if (m_display == ModListDisplay::Grid) {
m_badgeContainer->setLayout(ColumnLayout::create()->setAutoGrowAxis(true));
m_badgeContainer->setScale(.25f);
this->addChildAtPosition(m_badgeContainer, Anchor::TopLeft, ccp(5, -5), ccp(0, 1));
}
else {
m_badgeContainer->setLayout(RowLayout::create()->setAutoGrowAxis(true));
m_titleContainer->addChild(m_badgeContainer);
}
}
// On Grid View logo has constant size
if (m_display == ModListDisplay::Grid) {
limitNodeSize(m_logo, ccp(30, 30), 999, .1f);
m_logo->setPosition(m_obContentSize.width / 2, m_obContentSize.height - 20);
}
else {
auto logoSize = m_obContentSize.height - 10;
limitNodeSize(m_logo, ccp(logoSize, logoSize), 999, .1f);
m_logo->setPosition(m_obContentSize.height / 2 + 5, m_obContentSize.height / 2);
}
// There's space to show the description only on the big list
// When we do, elements like the download progress bar should replace it
// over the developer name since it's less important
// Couldn't figure out a more concise name
m_description->setVisible(m_display == ModListDisplay::BigList);
m_developers->setVisible(true);
auto elementToReplaceWithOtherAbnormalElement =
m_display == ModListDisplay::BigList ? m_description : m_developers;
auto titleSpace = m_display == ModListDisplay::Grid ?
CCSize(m_obContentSize.width - 10, 35) :
CCSize(m_obContentSize.width / 2 - m_obContentSize.height, m_obContentSize.height - 5);
// Divide by scale of info container since that actually determines the size
// (Since the scale of m_titleContainer and m_developers is managed by its layout)
// If there is an active download ongoing, show that in place of developer name // If there is an active download ongoing, show that in place of developer name
// (or description on big view)
if (isDownloading) { if (isDownloading) {
m_updateBtn->setVisible(false); m_updateBtn->setVisible(false);
m_restartRequiredLabel->setVisible(false); m_restartRequiredLabel->setVisible(false);
m_developers->setVisible(false); elementToReplaceWithOtherAbnormalElement->setVisible(false);
auto status = download->getStatus(); auto status = download->getStatus();
if (auto prog = std::get_if<server::DownloadStatusDownloading>(&status)) { if (auto prog = std::get_if<server::DownloadStatusDownloading>(&status)) {
@ -355,7 +433,7 @@ void ModItem::updateState() {
// Otherwise show "Restart Required" button if needed in place of dev name // Otherwise show "Restart Required" button if needed in place of dev name
else { else {
m_restartRequiredLabel->setVisible(wantsRestart); m_restartRequiredLabel->setVisible(wantsRestart);
m_developers->setVisible(!wantsRestart); elementToReplaceWithOtherAbnormalElement->setVisible(!wantsRestart);
m_downloadBarContainer->setVisible(false); m_downloadBarContainer->setVisible(false);
m_downloadWaiting->setVisible(false); m_downloadWaiting->setVisible(false);
} }
@ -418,11 +496,10 @@ void ModItem::updateState() {
m_versionLabel->setColor(to3B(ColorProvider::get()->color("mod-list-version-label"_spr))); m_versionLabel->setColor(to3B(ColorProvider::get()->color("mod-list-version-label"_spr)));
} }
m_viewMenu->updateLayout(); // Hide by default
m_titleContainer->updateLayout(); m_outdatedLabel->setVisible(false);
// If there were problems, tint the BG red // If there were problems, tint the BG red
m_outdatedLabel->setVisible(false);
if (m_source.asMod()) { if (m_source.asMod()) {
std::optional<LoadProblem> targetsOutdated = m_source.asMod()->targetsOutdatedVersion(); std::optional<LoadProblem> targetsOutdated = m_source.asMod()->targetsOutdatedVersion();
if (m_source.asMod()->hasLoadProblems()) { if (m_source.asMod()->hasLoadProblems()) {
@ -430,32 +507,129 @@ void ModItem::updateState() {
m_bg->setOpacity(isGeodeTheme() ? 25 : 90); m_bg->setOpacity(isGeodeTheme() ? 25 : 90);
} }
if (!wantsRestart && targetsOutdated && !isDownloading) { if (!wantsRestart && targetsOutdated && !isDownloading) {
LoadProblem problem = targetsOutdated.value();
m_bg->setColor("mod-list-outdated-label"_cc3b); m_bg->setColor("mod-list-outdated-label"_cc3b);
m_bg->setOpacity(isGeodeTheme() ? 25 : 90); m_bg->setOpacity(isGeodeTheme() ? 25 : 90);
std::string content;
if (
problem.type == LoadProblem::Type::UnsupportedGeodeVersion ||
problem.type == LoadProblem::Type::NeedsNewerGeodeVersion
) {
content = fmt::format(
"Outdated (Geode {})",
m_source.getMetadata().getGeodeVersion().toNonVString()
);
} else {
content = fmt::format(
"Outdated (GD {})",
m_source.getMetadata().getGameVersion().value_or("*")
);
}
m_outdatedLabel->setString(content.c_str());
m_outdatedLabel->setVisible(true); m_outdatedLabel->setVisible(true);
m_developers->setVisible(false); elementToReplaceWithOtherAbnormalElement->setVisible(false);
if (m_display == ModListDisplay::Grid) {
m_outdatedLabel->setString("Outdated");
}
else {
m_outdatedLabel->setString((
(targetsOutdated->type == LoadProblem::Type::UnsupportedGeodeVersion ||
targetsOutdated->type == LoadProblem::Type::NeedsNewerGeodeVersion) ?
fmt::format(
"Outdated (Geode {})",
m_source.getMetadata().getGeodeVersion().toNonVString()
) :
fmt::format(
"Outdated (GD {})",
m_source.getMetadata().getGameVersion().value_or("*")
)
).c_str());
}
} }
} }
// Update size and direction of title
// On grid view, m_titleContainer contains the version and download count
// but not the actual title lol
m_titleContainer->setContentWidth(titleSpace.width / m_infoContainer->getScale());
if (m_display == ModListDisplay::Grid) {
static_cast<RowLayout*>(m_titleContainer->getLayout())
->setGap(10)
->setAxisAlignment(AxisAlignment::Center);
static_cast<RowLayout*>(m_developers->getLayout())
->setAxisAlignment(AxisAlignment::Center);
}
else {
static_cast<RowLayout*>(m_titleContainer->getLayout())
->setGap(5)
->setAxisAlignment(AxisAlignment::Start);
static_cast<RowLayout*>(m_developers->getLayout())
->setAxisAlignment(AxisAlignment::Start);
}
m_titleContainer->updateLayout();
m_developers->setContentWidth(titleSpace.width / m_infoContainer->getScale());
m_developers->updateLayout();
if (m_recommendedBy) {
m_recommendedBy->setContentWidth(titleSpace.width / m_infoContainer->getScale());
m_recommendedBy->updateLayout();
}
// Update positioning (jesus)
switch (m_display) {
case ModListDisplay::Grid: {
m_infoContainer->updateAnchoredPosition(Anchor::Center, ccp(0, -5), ccp(.5f, .5f));
// m_description is hidden
m_titleLabel->updateAnchoredPosition(Anchor::Top, ccp(0, -10), ccp(.5f, .5f));
limitNodeWidth(m_titleLabel, m_titleContainer->getContentWidth(), .8f, .1f);
m_titleContainer->updateAnchoredPosition(Anchor::Center, ccp(0, 0), ccp(.5f, .5f));
m_developers->updateAnchoredPosition(Anchor::Bottom, ccp(0, 10), ccp(.5f, .5f));
m_restartRequiredLabel->updateAnchoredPosition(Anchor::Bottom, ccp(0, 10), ccp(.5f, .5f));
m_outdatedLabel->updateAnchoredPosition(Anchor::Bottom, ccp(0, 10), ccp(.5f, .5f));
m_downloadBarContainer->updateAnchoredPosition(Anchor::Bottom, ccp(0, 10), ccp(.5f, .5f));
m_downloadWaiting->updateAnchoredPosition(Anchor::Bottom, ccp(0, 10), ccp(.5f, .5f));
if (m_recommendedBy) {
m_recommendedBy->updateAnchoredPosition(Anchor::Bottom, ccp(0, 10), ccp(.5f, .5f));
}
} break;
default:
case ModListDisplay::SmallList: {
m_infoContainer->updateAnchoredPosition(Anchor::Left, ccp(m_obContentSize.height + 10, 0), ccp(0, .5f));
m_titleContainer->updateAnchoredPosition(Anchor::TopLeft, ccp(0, 0), ccp(0, 1));
// m_description is hidden
m_developers->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 3), ccp(0, 0));
m_restartRequiredLabel->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 3), ccp(0, 0));
m_outdatedLabel->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 3), ccp(0, 0));
m_downloadBarContainer->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 3), ccp(0, 0));
m_downloadWaiting->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 3), ccp(0, 0));
if (m_recommendedBy) {
m_recommendedBy->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 3), ccp(0, 0));
}
} break;
case ModListDisplay::BigList: {
m_infoContainer->updateAnchoredPosition(Anchor::Left, ccp(m_obContentSize.height + 10, 0), ccp(0, .5f));
m_titleContainer->updateAnchoredPosition(Anchor::TopLeft, ccp(0, 0), ccp(0, 1));
m_developers->updateAnchoredPosition(Anchor::Left, ccp(0, 0), ccp(0, .5f));
m_description->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
m_restartRequiredLabel->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
m_outdatedLabel->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
m_downloadBarContainer->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
m_downloadWaiting->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
if (m_recommendedBy) {
m_recommendedBy->updateAnchoredPosition(Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
}
} break;
}
m_infoContainer->setContentSize(ccp(titleSpace.width, titleSpace.height) / m_infoContainer->getScale());
m_infoContainer->updateLayout(); m_infoContainer->updateLayout();
// Update button menu state
if (m_display == ModListDisplay::Grid) {
m_viewMenu->setContentWidth(m_obContentSize.width / m_viewMenu->getScaleX());
m_viewMenu->updateAnchoredPosition(Anchor::Bottom, ccp(0, 5), ccp(.5f, 0));
m_viewMenu->setScale(.45f);
static_cast<RowLayout*>(m_viewMenu->getLayout())->setAxisAlignment(AxisAlignment::Center);
}
else {
m_viewMenu->setContentWidth(m_obContentSize.width / m_viewMenu->getScaleX() / 2 - 20);
m_viewMenu->updateAnchoredPosition(Anchor::Right, ccp(-10, 0), ccp(1, .5f));
m_viewMenu->setScale(.55f);
static_cast<RowLayout*>(m_viewMenu->getLayout())->setAxisAlignment(AxisAlignment::End);
}
m_viewMenu->updateLayout();
// Highlight item via BG if it wants to restart for extra UI attention // Highlight item via BG if it wants to restart for extra UI attention
if (wantsRestart) { if (wantsRestart) {
m_bg->setColor("mod-list-restart-required-label"_cc3b); m_bg->setColor("mod-list-restart-required-label"_cc3b);
@ -482,45 +656,15 @@ void ModItem::updateState() {
} }
} }
this->updateLayout();
ModItemUIEvent(std::make_unique<ModItemUIEvent::Impl>(this)).post(); ModItemUIEvent(std::make_unique<ModItemUIEvent::Impl>(this)).post();
} }
void ModItem::updateSize(float width, bool big) { void ModItem::updateDisplay(float width, ModListDisplay display) {
this->setContentSize({ width, big ? 40.f : 30.f }); m_display = display;
m_targetWidth = width;
m_bg->setContentSize((m_obContentSize - ccp(6, 0)) / m_bg->getScale()); this->updateState();
m_bg->setPosition(m_obContentSize / 2);
auto logoSize = m_obContentSize.height - 10;
limitNodeSize(m_logo, { logoSize, logoSize }, 999, .1f);
m_logo->setPosition(m_obContentSize.height / 2 + 5, m_obContentSize.height / 2);
CCSize titleSpace {
m_obContentSize.width / 2 - m_obContentSize.height,
logoSize + 5
};
// Divide by scale of info container since that actually determines the size
// (Since the scale of m_titleContainer and m_developers is managed by its layout)
m_titleContainer->setContentWidth(titleSpace.width / m_infoContainer->getScale());
m_titleContainer->updateLayout();
m_developers->setContentWidth(titleSpace.width / m_infoContainer->getScale());
m_developers->updateLayout();
if (m_recommendedBy) {
m_recommendedBy->setContentWidth(titleSpace.width / m_infoContainer->getScale());
m_recommendedBy->updateLayout();
}
m_infoContainer->setPosition(m_obContentSize.height + 10, m_obContentSize.height / 2);
m_infoContainer->setContentSize(ccp(titleSpace.width, titleSpace.height) / m_infoContainer->getScale());
m_infoContainer->updateLayout();
m_viewMenu->setContentWidth(m_obContentSize.width / m_viewMenu->getScaleX() / 2 - 20);
m_viewMenu->updateLayout();
this->updateLayout();
} }
void ModItem::onCheckUpdates(typename server::ServerRequest<std::optional<server::ServerModUpdate>>::Event* event) { void ModItem::onCheckUpdates(typename server::ServerRequest<std::optional<server::ServerModUpdate>>::Event* event) {

View file

@ -12,6 +12,12 @@
using namespace geode::prelude; using namespace geode::prelude;
enum class ModListDisplay {
SmallList,
BigList,
Grid,
};
class ModItem : public CCNode { class ModItem : public CCNode {
protected: protected:
ModSource m_source; ModSource m_source;
@ -19,10 +25,11 @@ protected:
CCNode* m_logo; CCNode* m_logo;
CCNode* m_infoContainer; CCNode* m_infoContainer;
CCNode* m_titleContainer; CCNode* m_titleContainer;
CCLabelBMFont* m_titleLabel; Ref<CCLabelBMFont> m_titleLabel;
CCLabelBMFont* m_versionLabel; CCLabelBMFont* m_versionLabel;
CCNode* m_developers; CCNode* m_developers;
CCNode* m_recommendedBy; CCNode* m_recommendedBy;
CCScale9Sprite* m_description;
CCLabelBMFont* m_developerLabel; CCLabelBMFont* m_developerLabel;
ButtonSprite* m_restartRequiredLabel; ButtonSprite* m_restartRequiredLabel;
ButtonSprite* m_outdatedLabel; ButtonSprite* m_outdatedLabel;
@ -37,6 +44,10 @@ protected:
EventListener<server::ModDownloadFilter> m_downloadListener; EventListener<server::ModDownloadFilter> m_downloadListener;
std::optional<server::ServerModUpdate> m_availableUpdate; std::optional<server::ServerModUpdate> m_availableUpdate;
EventListener<EventFilter<SettingNodeValueChangeEvent>> m_settingNodeListener; EventListener<EventFilter<SettingNodeValueChangeEvent>> m_settingNodeListener;
Ref<CCNode> m_badgeContainer = nullptr;
Ref<CCNode> m_downloadCountContainer;
ModListDisplay m_display = ModListDisplay::SmallList;
float m_targetWidth = 300;
/** /**
* @warning Make sure `getMetadata` and `createModLogo` are callable * @warning Make sure `getMetadata` and `createModLogo` are callable
@ -57,7 +68,7 @@ protected:
public: public:
static ModItem* create(ModSource&& source); static ModItem* create(ModSource&& source);
void updateSize(float width, bool big); void updateDisplay(float width, ModListDisplay display);
ModSource& getSource() &; ModSource& getSource() &;
}; };

View file

@ -18,13 +18,6 @@ bool ModList::init(ModListSource* src, CCSize const& size) {
m_source->reset(); m_source->reset();
m_list = ScrollLayer::create(size); m_list = ScrollLayer::create(size);
m_list->m_contentLayer->setLayout(
ColumnLayout::create()
->setAxisReverse(true)
->setAxisAlignment(AxisAlignment::End)
->setAutoGrowAxis(size.height)
->setGap(2.5f)
);
this->addChildAtPosition(m_list, Anchor::Bottom, ccp(-m_list->getScaledContentWidth() / 2, 0)); this->addChildAtPosition(m_list, Anchor::Bottom, ccp(-m_list->getScaledContentWidth() / 2, 0));
m_topContainer = CCNode::create(); m_topContainer = CCNode::create();
@ -410,7 +403,7 @@ void ModList::onPromise(ModListSource::PageLoadTask::Event* event) {
first = false; first = false;
m_list->m_contentLayer->addChild(item); m_list->m_contentLayer->addChild(item);
} }
this->updateSize(m_bigSize); this->updateDisplay(m_display);
// Scroll list to top // Scroll list to top
auto listTopScrollPos = -m_list->m_contentLayer->getContentHeight() + m_list->getContentHeight(); auto listTopScrollPos = -m_list->m_contentLayer->getContentHeight() + m_list->getContentHeight();
@ -526,12 +519,10 @@ void ModList::updateTopContainer() {
// (giving a little bit of extra padding for it, the same size as gap) // (giving a little bit of extra padding for it, the same size as gap)
m_list->setContentHeight( m_list->setContentHeight(
m_topContainer->getContentHeight() > 0.f ? m_topContainer->getContentHeight() > 0.f ?
this->getContentHeight() - m_topContainer->getContentHeight() - this->getContentHeight() - m_topContainer->getContentHeight() - 2.5f :
static_cast<AxisLayout*>(m_list->m_contentLayer->getLayout())->getGap() :
this->getContentHeight() this->getContentHeight()
); );
static_cast<ColumnLayout*>(m_list->m_contentLayer->getLayout())->setAutoGrowAxis(m_list->getContentHeight()); this->updateDisplay(m_display);
m_list->m_contentLayer->updateLayout();
// Preserve relative scroll position // Preserve relative scroll position
m_list->m_contentLayer->setPositionY(( m_list->m_contentLayer->setPositionY((
@ -557,14 +548,14 @@ void ModList::updateTopContainer() {
this->updateLayout(); this->updateLayout();
} }
void ModList::updateSize(bool big) { void ModList::updateDisplay(ModListDisplay display) {
m_bigSize = big; m_display = display;
// Update all BaseModItems that are children of the list // Update all BaseModItems that are children of the list
// There may be non-BaseModItems there (like separators) so gotta be type-safe // There may be non-BaseModItems there (like separators) so gotta be type-safe
for (auto& node : CCArrayExt<CCNode*>(m_list->m_contentLayer->getChildren())) { for (auto& node : CCArrayExt<CCNode*>(m_list->m_contentLayer->getChildren())) {
if (auto item = typeinfo_cast<ModItem*>(node)) { if (auto item = typeinfo_cast<ModItem*>(node)) {
item->updateSize(m_list->getContentWidth(), big); item->updateDisplay(m_list->getContentWidth(), display);
} }
} }
@ -574,8 +565,24 @@ void ModList::updateSize(bool big) {
m_list->m_contentLayer->getPositionY() / oldPositionArea : m_list->m_contentLayer->getPositionY() / oldPositionArea :
-1.f; -1.f;
// Auto-grow the size of the list content // Update the list layout based on the display model
m_list->m_contentLayer->updateLayout(); if (display == ModListDisplay::Grid) {
m_list->m_contentLayer->setLayout(
RowLayout::create()
->setGrowCrossAxis(true)
->setAxisAlignment(AxisAlignment::Center)
->setGap(2.5f)
);
}
else {
m_list->m_contentLayer->setLayout(
ColumnLayout::create()
->setAxisReverse(true)
->setAxisAlignment(AxisAlignment::End)
->setAutoGrowAxis(m_obContentSize.height)
->setGap(2.5f)
);
}
// Preserve relative scroll position // Preserve relative scroll position
m_list->m_contentLayer->setPositionY(( m_list->m_contentLayer->setPositionY((

View file

@ -52,7 +52,7 @@ protected:
EventListener<InvalidateCacheFilter> m_invalidateCacheListener; EventListener<InvalidateCacheFilter> m_invalidateCacheListener;
EventListener<server::ServerRequest<std::vector<std::string>>> m_checkUpdatesListener; EventListener<server::ServerRequest<std::vector<std::string>>> m_checkUpdatesListener;
EventListener<server::ModDownloadFilter> m_downloadListener; EventListener<server::ModDownloadFilter> m_downloadListener;
bool m_bigSize = false; ModListDisplay m_display = ModListDisplay::SmallList;
bool m_exiting = false; bool m_exiting = false;
std::atomic<size_t> m_searchInputThreads = 0; std::atomic<size_t> m_searchInputThreads = 0;
@ -83,7 +83,7 @@ public:
void showStatus(ModListStatus status, std::string const& message, std::optional<std::string> const& details = std::nullopt); void showStatus(ModListStatus status, std::string const& message, std::optional<std::string> const& details = std::nullopt);
void updateState(); void updateState();
void updateSize(bool big); void updateDisplay(ModListDisplay display);
void activateSearch(bool activate); void activateSearch(bool activate);
void setIsExiting(bool exiting); void setIsExiting(bool exiting);
}; };

View file

@ -59,6 +59,8 @@ std::string utils::string::toUpper(std::string const& str) {
} }
std::string& utils::string::replaceIP(std::string& str, std::string const& orig, std::string const& repl) { std::string& utils::string::replaceIP(std::string& str, std::string const& orig, std::string const& repl) {
if (orig.empty()) return str;
std::string::size_type n = 0; std::string::size_type n = 0;
while ((n = str.find(orig, n)) != std::string::npos) { while ((n = str.find(orig, n)) != std::string::npos) {
str.replace(n, orig.size(), repl); str.replace(n, orig.size(), repl);