feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across every tab, dialog, and component (~20 UI files) - Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with complete translations (~37k lines added) - Improve i18n loader with exe-relative path fallback and English base fallback for missing keys - Add pool-side hashrate polling via pool stats API in xmrig_manager - Introduce Layout::beginFrame() per-frame caching and refresh balance layout config only on schema generation change - Offload daemon output parsing to worker thread - Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../data/exchange_info.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -147,15 +148,17 @@ void RenderMarketTab(App* app)
|
||||
float mkCardBudget = std::max(200.0f, marketAvail.y - mkOverhead);
|
||||
|
||||
Layout::SectionBudget mb(mkCardBudget);
|
||||
float statsMarketBudH = mb.allocate(0.14f, S.drawElement("tabs.market", "stats-card-min-height").size);
|
||||
float portfolioBudgetH = mb.allocate(0.18f, 50.0f);
|
||||
|
||||
// ================================================================
|
||||
// PRICE HERO — Large glass card with price + change badge
|
||||
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
|
||||
// ================================================================
|
||||
{
|
||||
float dp = Layout::dpiScale();
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
float cardH = std::max(S.drawElement("tabs.market", "hero-card-min-height").size, S.drawElement("tabs.market", "hero-card-height").size * vs);
|
||||
float heroMinH = S.drawElement("tabs.market", "hero-card-min-height").size;
|
||||
float statsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad;
|
||||
float cardH = std::max(heroMinH + statsRowH + pad, (S.drawElement("tabs.market", "hero-card-height").size + statsRowH + pad) * vs);
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
@@ -172,36 +175,118 @@ void RenderMarketTab(App* app)
|
||||
float cy = cardMin.y + Layout::spacingLg();
|
||||
|
||||
if (market.price_usd > 0) {
|
||||
// Large price with text shadow
|
||||
// ---- HERO PRICE (large, prominent) ----
|
||||
ImFont* h3 = Type().h3();
|
||||
std::string priceStr = FormatPrice(market.price_usd);
|
||||
ImU32 priceCol = Success();
|
||||
DrawTextShadow(dl, h4, h4->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
|
||||
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
|
||||
|
||||
// BTC price beside it
|
||||
float priceW = h4->CalcTextSizeA(h4->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
|
||||
snprintf(buf, sizeof(buf), "%.10f BTC", market.price_btc);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + priceW + Layout::spacingXl(), cy + (h4->LegacySize - capFont->LegacySize)),
|
||||
OnSurfaceMedium(), buf);
|
||||
// Ticker label after price
|
||||
float priceW = h3->CalcTextSizeA(h3->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
|
||||
dl->AddText(body2, body2->LegacySize,
|
||||
ImVec2(cx + priceW + Layout::spacingSm(), cy + (h3->LegacySize - body2->LegacySize)),
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
|
||||
// 24h change badge
|
||||
float badgeY = cy + h4->LegacySize + 8;
|
||||
// 24h change badge — to the right of ticker
|
||||
float tickerW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, DRAGONX_TICKER).x;
|
||||
float badgeX = cx + priceW + Layout::spacingSm() + tickerW + Layout::spacingMd();
|
||||
ImU32 chgCol = market.change_24h >= 0 ? Success() : Error();
|
||||
snprintf(buf, sizeof(buf), "%s%.2f%%", market.change_24h >= 0 ? "+" : "", market.change_24h);
|
||||
|
||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float badgePad = Layout::spacingSm() + Layout::spacingXs();
|
||||
ImVec2 bMin(cx, badgeY);
|
||||
ImVec2 bMax(cx + txtSz.x + badgePad * 2, badgeY + txtSz.y + badgePad);
|
||||
snprintf(buf, sizeof(buf), "%s%.2f%% 24h", market.change_24h >= 0 ? "+" : "", market.change_24h);
|
||||
ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float badgePadH = Layout::spacingSm();
|
||||
float badgePadV = Layout::spacingXs();
|
||||
ImVec2 bMin(badgeX, cy + (h3->LegacySize - chgSz.y - badgePadV * 2) * 0.5f);
|
||||
ImVec2 bMax(badgeX + chgSz.x + badgePadH * 2, bMin.y + chgSz.y + badgePadV * 2);
|
||||
ImU32 badgeBg = market.change_24h >= 0 ? WithAlpha(Success(), 30) : WithAlpha(Error(), 30);
|
||||
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + badgePad, badgeY + badgePad * 0.5f), chgCol, buf);
|
||||
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
|
||||
|
||||
// "24h" label after badge
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(bMax.x + 6, badgeY + badgePad * 0.5f), OnSurfaceDisabled(), "24h");
|
||||
// ---- SEPARATOR ----
|
||||
float sepY = cy + h3->LegacySize + Layout::spacingMd();
|
||||
float rnd = glassSpec.rounding;
|
||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, sepY),
|
||||
ImVec2(cardMax.x - rnd * 0.5f, sepY),
|
||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||
|
||||
// ---- STATS ROW (BTC Price | Volume | Market Cap) ----
|
||||
float statsY = sepY + Layout::spacingSm();
|
||||
float colW = (availWidth - Layout::spacingLg() * 2) / 3.0f;
|
||||
|
||||
struct StatItem { const char* label; std::string value; ImU32 valueCol; };
|
||||
StatItem stats[3] = {
|
||||
{TR("market_btc_price"), "", OnSurface()},
|
||||
{TR("market_24h_volume"), FormatCompactUSD(market.volume_24h), OnSurface()},
|
||||
{TR("market_cap"), FormatCompactUSD(market.market_cap), OnSurface()},
|
||||
};
|
||||
snprintf(buf, sizeof(buf), "%.10f", market.price_btc);
|
||||
stats[0].value = buf;
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float sx = cardMin.x + Layout::spacingLg() + i * colW;
|
||||
float centerX = sx + colW * 0.5f;
|
||||
|
||||
// Label (overline, centered)
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[i].label);
|
||||
dl->AddText(ovFont, ovFont->LegacySize,
|
||||
ImVec2(centerX - lblSz.x * 0.5f, statsY), OnSurfaceMedium(), stats[i].label);
|
||||
|
||||
// Value (subtitle1, centered)
|
||||
float valY = statsY + ovFont->LegacySize + Layout::spacingXs();
|
||||
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[i].value.c_str());
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
|
||||
}
|
||||
|
||||
// ---- TRADE BUTTON (top-right of card) ----
|
||||
if (!currentExchange.pairs.empty()) {
|
||||
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, pairName);
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_OPEN_IN_NEW);
|
||||
float iconGap = Layout::spacingSm();
|
||||
float tradePadH = Layout::spacingMd();
|
||||
float tradePadV = Layout::spacingSm();
|
||||
float tradeBtnW = textSz.x + iconGap + iconSz.x + tradePadH * 2;
|
||||
float tradeBtnH = std::max(textSz.y, iconSz.y) + tradePadV * 2;
|
||||
float tradeBtnX = cardMax.x - pad - tradeBtnW;
|
||||
float tradeBtnY = cardMin.y + Layout::spacingSm();
|
||||
|
||||
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
|
||||
bool tradeHov = material::IsRectHovered(tMin, tMax);
|
||||
|
||||
// Glass pill background
|
||||
GlassPanelSpec tradeBtnGlass;
|
||||
tradeBtnGlass.rounding = tradeBtnH * 0.5f;
|
||||
tradeBtnGlass.fillAlpha = tradeHov ? 35 : 20;
|
||||
DrawGlassPanel(dl, tMin, tMax, tradeBtnGlass);
|
||||
if (tradeHov)
|
||||
dl->AddRectFilled(tMin, tMax, WithAlpha(Primary(), 20), tradeBtnH * 0.5f);
|
||||
|
||||
// Text (pair name with body2, icon with icon font)
|
||||
ImU32 tradeCol = tradeHov ? OnSurface() : OnSurfaceMedium();
|
||||
float contentY = tradeBtnY + tradePadV;
|
||||
float curX = tradeBtnX + tradePadH;
|
||||
dl->AddText(body2, body2->LegacySize,
|
||||
ImVec2(curX, contentY), tradeCol, pairName);
|
||||
curX += textSz.x + iconGap;
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(curX, contentY + (textSz.y - iconSz.y) * 0.5f), tradeCol, ICON_MD_OPEN_IN_NEW);
|
||||
|
||||
// Click
|
||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(tMin);
|
||||
ImGui::InvisibleButton("##TradeOnExchange", ImVec2(tradeBtnW, tradeBtnH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(TR("market_trade_on"), currentExchange.name.c_str());
|
||||
}
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
} else {
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), "Price data unavailable");
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), TR("market_price_unavailable"));
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
@@ -209,42 +294,6 @@ void RenderMarketTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// STATS — Three glass cards (Price | Volume | Market Cap)
|
||||
// ================================================================
|
||||
{
|
||||
float cardW = (availWidth - 2 * gap) / 3.0f;
|
||||
float cardH = std::min(StatCardHeight(vs), statsMarketBudH);
|
||||
ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
|
||||
struct StatInfo { const char* label; std::string value; ImU32 col; ImU32 accent; };
|
||||
StatInfo cards[3] = {
|
||||
{"PRICE", market.price_usd > 0 ? FormatPrice(market.price_usd) : "N/A",
|
||||
OnSurface(), WithAlpha(Success(), 200)},
|
||||
{"24H VOLUME", FormatCompactUSD(market.volume_24h),
|
||||
OnSurface(), WithAlpha(Secondary(), 200)},
|
||||
{"MARKET CAP", FormatCompactUSD(market.market_cap),
|
||||
OnSurface(), WithAlpha(Warning(), 200)},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float xOff = i * (cardW + gap);
|
||||
ImVec2 cMin(origin.x + xOff, origin.y);
|
||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||
|
||||
StatCardSpec sc;
|
||||
sc.overline = cards[i].label;
|
||||
sc.value = cards[i].value.c_str();
|
||||
sc.valueCol = cards[i].col;
|
||||
sc.accentCol = cards[i].accent;
|
||||
sc.centered = true;
|
||||
DrawStatCard(dl, cMin, cMax, sc, glassSpec);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(availWidth, cardH));
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PRICE CHART — Custom drawn inside glass panel (matches app design)
|
||||
// ================================================================
|
||||
@@ -340,7 +389,7 @@ void RenderMarketTab(App* app)
|
||||
|
||||
// X-axis labels
|
||||
const int xlabels[] = {0, 6, 12, 18, 23};
|
||||
const char* xlabelText[] = {"24h", "18h", "12h", "6h", "Now"};
|
||||
const char* xlabelText[] = {TR("market_24h"), TR("market_18h"), TR("market_12h"), TR("market_6h"), TR("market_now")};
|
||||
for (int xi = 0; xi < 5; xi++) {
|
||||
int idx = xlabels[xi];
|
||||
float t = (float)idx / (float)(n - 1);
|
||||
@@ -395,7 +444,7 @@ void RenderMarketTab(App* app)
|
||||
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
||||
}
|
||||
} else {
|
||||
const char* msg = "No price history available";
|
||||
const char* msg = TR("market_no_history");
|
||||
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, chartMin.y + chartH * 0.45f),
|
||||
@@ -429,7 +478,7 @@ void RenderMarketTab(App* app)
|
||||
s_history_initialized = false;
|
||||
s_last_refresh_time = ImGui::GetTime();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh price data");
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("market_refresh_price"));
|
||||
|
||||
// Timestamp text to the left of refresh button
|
||||
if (s_last_refresh_time > 0.0) {
|
||||
@@ -452,7 +501,7 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// EXCHANGE SELECTOR — Combo dropdown + pair name + trade link
|
||||
// EXCHANGE SELECTOR — Combo dropdown + attribution
|
||||
// ================================================================
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
|
||||
{
|
||||
@@ -483,28 +532,9 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// Show current pair name beside combo
|
||||
if (!currentExchange.pairs.empty()) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
Type().textColored(TypeStyle::Subtitle1, OnSurface(),
|
||||
currentExchange.pairs[s_pair_idx].displayName.c_str());
|
||||
|
||||
// "Open on exchange" button
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
ImGui::PushFont(material::Typography::instance().iconSmall());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
|
||||
snprintf(buf, sizeof(buf), ICON_MD_OPEN_IN_NEW "##TradeLink");
|
||||
if (ImGui::Button(buf)) {
|
||||
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open on %s", currentExchange.name.c_str());
|
||||
}
|
||||
|
||||
// Attribution
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "Price data from CoinGecko API");
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("market_attribution"));
|
||||
|
||||
if (!market.last_updated.empty()) {
|
||||
ImGui::SameLine(0, 12);
|
||||
@@ -722,7 +752,7 @@ void RenderMarketTab(App* app)
|
||||
// PORTFOLIO — Glass card with balance breakdown
|
||||
// ================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PORTFOLIO");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
double total_balance = state.totalBalance;
|
||||
@@ -760,7 +790,7 @@ void RenderMarketTab(App* app)
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMax.x - valW - pad, cy + 2), OnSurfaceMedium(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), "No price data");
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price"));
|
||||
}
|
||||
|
||||
cy += sub1->LegacySize + 8;
|
||||
@@ -799,7 +829,7 @@ void RenderMarketTab(App* app)
|
||||
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
|
||||
|
||||
int pct = (int)(shieldedRatio * 100.0f + 0.5f);
|
||||
snprintf(buf, sizeof(buf), "%d%% Shielded", pct);
|
||||
snprintf(buf, sizeof(buf), TR("market_pct_shielded"), pct);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user