From 064c9d1b61b4022bb569788637f0a55f745ecf7c Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 15 Oct 2026 11:22:12 -0500 Subject: [PATCH] feat(balance): inline period-earnings in the address list and totals Surfaces the period-earnings aggregator in the Balance tab: - Toolbar (shared address list): interval pills (1H/1D/1M/1Y), a received- vs-net mode toggle, and a global show-all toggle, placed right of "Hide 0 Balances". The New Z-Address / New mining address buttons move to a footer row across from "Showing X of Y" to make room. - Per address: the row draws its earnings on line 2, left of the $USD value (green positive, red negative in net mode), shown when the global toggle is on or the address is opted in via a new "Show/Hide earnings" right-click item. Works in every layout via the shared list. - Total: FormatTotalEarnings (gated on the global toggle, so the default appearance is unchanged) is wired into the compact hero (dashboard/minimal) and the classic layout, left of / beside their USD figure. The remaining layouts' totals are not wired yet. - Settings > Appearance: an "Earnings Display Format" selector (Coin / Coin + fiat / Fiat). New i18n keys are translated into all eight locales; the CJK subset font is rebuilt for the new glyphs. Builds + test suites pass on both variants. Co-Authored-By: Claude Opus 4.8 --- res/fonts/NotoSansCJK-Subset.ttf | Bin 770636 -> 771772 bytes res/lang/de.json | 12 ++ res/lang/es.json | 12 ++ res/lang/fr.json | 12 ++ res/lang/ja.json | 12 ++ res/lang/ko.json | 12 ++ res/lang/pt.json | 12 ++ res/lang/ru.json | 12 ++ res/lang/zh.json | 12 ++ src/ui/pages/settings_page.cpp | 22 +++ src/ui/windows/balance_components.cpp | 230 +++++++++++++++++++++----- src/ui/windows/balance_components.h | 7 + src/ui/windows/balance_tab.cpp | 8 +- src/util/i18n.cpp | 12 ++ 14 files changed, 335 insertions(+), 40 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index e4e1e0110b58363a6d25e0a79556b2bb1a746390..a46551a11f87d712ba952aa22cf251e68634029e 100644 GIT binary patch delta 23590 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 Ccd~K- delta 22687 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 s_settingsState.window_opacity = settings->getWindowOpacity(); s_settingsState.favorite_opacity = settings->getFavoriteOpacity(); + s_settingsState.earnings_format = settings->getEarningsFormat(); s_settingsState.noise_opacity = settings->getNoiseOpacity(); s_settingsState.gradient_background = settings->getGradientBackground(); @@ -535,6 +537,7 @@ static void saveSettingsPageState(config::Settings* settings) { settings->setUIOpacity(lowSpecOverrides ? realPrefs.ui_opacity : s_settingsState.ui_opacity); settings->setWindowOpacity(s_settingsState.window_opacity); // 1.0 under low-spec, as the hotkey saves it settings->setFavoriteOpacity(s_settingsState.favorite_opacity); // not low-spec gated — cosmetic + settings->setEarningsFormat(s_settingsState.earnings_format); settings->setNoiseOpacity(s_settingsState.noise_opacity); settings->setGradientBackground(s_settingsState.gradient_background); settings->setScanlineEnabled(lowSpecOverrides ? realPrefs.scanline_enabled : s_settingsState.scanline_enabled); @@ -1052,6 +1055,25 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + // --- Earnings display format (Balance tab per-address + total earnings figures) --- + { + ImGui::PushFont(body2); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); + ImGui::TextUnformatted(TR("earnings_format")); + ImGui::PopStyleColor(); + float efW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW), 360.0f * dp); + ImGui::SetNextItemWidth(efW); + const char* efItems[] = { TR("earnings_format_coin"), TR("earnings_format_coin_fiat"), TR("earnings_format_fiat") }; + if (ImGui::Combo("##EarningsFormat", &s_settingsState.earnings_format, efItems, IM_ARRAYSIZE(efItems))) { + s_settingsState.earnings_format = std::max(0, std::min(2, s_settingsState.earnings_format)); + if (app->settings()) { app->settings()->setEarningsFormat(s_settingsState.earnings_format); app->settings()->save(); } + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_earnings_format")); + ImGui::PopFont(); + } + + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + // --- Effect toggles (always visible, horizontal wrapping flow) --- { ImGui::PushFont(body2); diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 8963ff5..e71d152 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include "../../data/earnings_series.h" #include #include #include @@ -56,6 +58,44 @@ void UpdateBalanceLerp(App* app) { StepDisplayLerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, kBalanceLerpSpeed, instant); } +// Total period-earnings for the whole wallet, formatted per the earnings settings. Only shown when the +// global earnings toggle is on, so the default hero/layout appearance is unchanged. Own revision-keyed +// cache (independent of the per-row one in RenderSharedAddressList). +bool FormatTotalEarnings(App* app, std::string& outText, ImU32& outCol) { + using namespace material; + if (!app->settings() || !app->settings()->getEarningsShowAll()) return false; + auto& S = schema::UI(); + const auto& state = app->state(); + auto* est = app->settings(); + static dragonx::data::EarningsCache s_totalEarnCache; + auto iv = (dragonx::data::EarningsInterval)est->getEarningsInterval(); + auto md = (dragonx::data::EarningsMode)est->getEarningsMode(); + const int fmt = est->getEarningsFormat(); + const std::int64_t nowSec = (std::int64_t)std::time(nullptr); + const std::int64_t nowMs = (std::int64_t)std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + dragonx::data::EarningsKey ek; + ek.txRevision = state.tx_revision; + ek.txCount = state.transactions.size(); + ek.interval = iv; + ek.mode = md; + ek.periodStart = dragonx::data::earningsPeriodStart(nowSec, iv); + const auto& r = s_totalEarnCache.get(ek, state.transactions, nowSec, nowMs); + const double ev = r.total; + const double fiat = ev * state.market.price_usd; + const bool haveFiat = state.market.price_usd > 0.0; + const int f = haveFiat ? fmt : 0; + char buf[64]; + if (f == 0) snprintf(buf, sizeof(buf), "%+.4f", ev); + else if (f == 2) snprintf(buf, sizeof(buf), "%s$%.2f", ev < 0 ? "-" : "+", std::fabs(fiat)); + else snprintf(buf, sizeof(buf), "%+.4f ($%.2f)", ev, fiat); + outText = buf; + outCol = ev > 1e-12 ? S.resolveColor("var(--accent-shielded)", Success()) + : (ev < -1e-12 ? S.resolveColor("var(--accent-error)", ReadableError()) + : OnSurfaceDisabled()); + return true; +} + // Render compact hero line: logo + balance + USD + mining on one line void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) { using namespace material; @@ -96,6 +136,18 @@ void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float v ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH)); ImGui::SameLine(); + // Total period-earnings (left of the USD) when the global earnings toggle is on. + { + std::string earnText; ImU32 earnCol; + if (FormatTotalEarnings(app, earnText, earnCol)) { + dl->AddText(capFont, tickerFontSize, + ImVec2(ImGui::GetCursorScreenPos().x, tickerY), earnCol, earnText.c_str()); + float ew = capFont->CalcTextSizeA(tickerFontSize, 10000, 0, earnText.c_str()).x; + ImGui::Dummy(ImVec2(ew + Layout::spacingLg(), lineH)); + ImGui::SameLine(); + } + } + // USD value const auto& state = app->state(); double usd_value = state.getBalanceUSD(); @@ -164,6 +216,29 @@ void RenderSharedAddressList(App* app, float listH, float availW, addRows(state.t_addresses, false); auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden); + // ---- Inline period-earnings: compute once (cached on tx_revision + interval/mode/period) ---- + static dragonx::data::EarningsCache s_earnCache; + const dragonx::data::EarningsResult* earnResult = nullptr; + bool earnShowAll = false; + int earnFormat = 1; // 0=coin 1=coin+fiat 2=fiat + if (app->settings()) { + auto* est = app->settings(); + earnShowAll = est->getEarningsShowAll(); + earnFormat = est->getEarningsFormat(); + auto iv = (dragonx::data::EarningsInterval)est->getEarningsInterval(); + auto md = (dragonx::data::EarningsMode)est->getEarningsMode(); + const std::int64_t nowSec = (std::int64_t)std::time(nullptr); + const std::int64_t nowMs = (std::int64_t)std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + dragonx::data::EarningsKey ek; + ek.txRevision = state.tx_revision; + ek.txCount = state.transactions.size(); + ek.interval = iv; + ek.mode = md; + ek.periodStart = dragonx::data::earningsPeriodStart(nowSec, iv); + earnResult = &s_earnCache.get(ek, state.transactions, nowSec, nowMs); + } + // ---- Toolbar: search + checkboxes + create buttons ---- float avail = ImGui::GetContentRegionAvail().x; float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f; @@ -185,39 +260,46 @@ void RenderSharedAddressList(App* app, float listH, float availW, } } - float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp; - float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp; - float totalButtonsWidth = buttonWidth * 2 + spacing; - float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size, - S.drawElement("tabs.balance", "buttons-position").size * hs); - ImGui::SameLine(std::max(kMinButtonsPosition, - ImGui::GetWindowWidth() - totalButtonsWidth - - S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f))); + // ---- Inline period-earnings controls: interval pills (1H/1D/1M/1Y), mode toggle (received / + // net), and a global show-all toggle. The create-address buttons moved below the list (footer). ---- + if (app->settings()) { + auto* est = app->settings(); + ImFont* icoF = Type().iconSmall(); + const float segH = ImGui::GetFrameHeight(); // fixed height so pills + icon buttons align + // A compact selected/unselected toggle. `font` nullptr = default font (text pills). + auto segBtn = [&](const char* idlabel, bool selected, const char* tip, ImFont* font) -> bool { + if (font) ImGui::PushFont(font); + ImGui::PushStyleColor(ImGuiCol_Button, selected ? WithAlpha(Primary(), 70) : IM_COL32(255, 255, 255, 12)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, WithAlpha(Primary(), 110)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, WithAlpha(Primary(), 140)); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(selected ? OnSurface() : OnSurfaceMedium())); + bool clk = ImGui::Button(idlabel, ImVec2(0, segH)); + ImGui::PopStyleColor(4); + if (font) ImGui::PopFont(); + if (tip && ImGui::IsItemHovered()) material::Tooltip("%s", tip); + return clk; + }; - bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced(); - ImGui::BeginDisabled(sharedAddrSyncing); - if (s_generating_z_address) { - char genLabel[64]; - snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots()); - TactileButton(genLabel, ImVec2(buttonWidth, 0), - S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font)); - } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), - S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { - s_generating_z_address = true; - app->createNewZAddress([](const std::string& addr) { - s_generating_z_address = false; - DEBUG_LOGF("Created new z-address: %s\n", addr.c_str()); - }); + ImGui::SameLine(0, Layout::spacingLg()); + // Interval pills + const char* ivText[4] = { "1H", "1D", "1M", "1Y" }; + int iv = est->getEarningsInterval(); + for (int k = 0; k < 4; ++k) { + char lbl[16]; snprintf(lbl, sizeof(lbl), "%s##earniv%d", ivText[k], k); + if (segBtn(lbl, iv == k, TR("tt_earn_interval"), nullptr)) { est->setEarningsInterval(k); est->save(); } + if (k < 3) ImGui::SameLine(0, 2.0f * dp); + } + // Mode toggle (received-only vs net) + ImGui::SameLine(0, Layout::spacingMd()); + int emode = est->getEarningsMode(); + if (segBtn(ICON_MD_CALL_RECEIVED "##earnmodein", emode == 0, TR("tt_earn_mode_inflows"), icoF)) { est->setEarningsMode(0); est->save(); } + ImGui::SameLine(0, 2.0f * dp); + if (segBtn(ICON_MD_SWAP_VERT "##earnmodenet", emode == 1, TR("tt_earn_mode_net"), icoF)) { est->setEarningsMode(1); est->save(); } + // Global earnings-column toggle + ImGui::SameLine(0, Layout::spacingMd()); + bool showAll = est->getEarningsShowAll(); + if (segBtn(ICON_MD_TRENDING_UP "##earnshowall", showAll, TR("tt_earn_show_all"), icoF)) { est->setEarningsShowAll(!showAll); est->save(); } } - ImGui::SameLine(); - if (TactileButton(TR("new_mining_address"), ImVec2(buttonWidth, 0), - S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { - app->createNewTAddress([](const std::string& addr) { - DEBUG_LOGF("Created new t-address: %s\n", addr.c_str()); - }); - } - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_new_mining_address")); - ImGui::EndDisabled(); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); @@ -279,6 +361,7 @@ void RenderSharedAddressList(App* app, float listH, float availW, addrScrollMaxY = ImGui::GetScrollMaxY(); ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success()); ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning()); + ImU32 earnNegCol = S.resolveColor("var(--accent-error)", material::ReadableError()); // negative net earnings float rowPadLeft = Layout::cardInnerPadding(); float rowPadRight = Layout::cardInnerPadding(); float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, @@ -594,9 +677,33 @@ void RenderSharedAddressList(App* app, float listH, float availW, } } + // ---- Line-2 right content widths: USD + optional period-earnings to its left ---- + std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd); + float usdW = usdText.empty() ? 0.0f + : capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str()).x; + const bool haveFiat = state.market.price_usd > 0.0; + bool earnShown = earnResult && app->settings() && + (earnShowAll || app->settings()->isAddressEarningsShown(addr.address)); + char earnBuf[48]; earnBuf[0] = '\0'; + float earnW = 0.0f; ImU32 earnCol = OnSurfaceDisabled(); + if (earnShown) { + double ev = dragonx::data::earningsForAddress(*earnResult, addr.address); + double fiat = ev * state.market.price_usd; + int fmt = (!haveFiat) ? 0 : earnFormat; // no price -> coin only + if (fmt == 0) snprintf(earnBuf, sizeof(earnBuf), "%+.4f", ev); + else if (fmt == 2) snprintf(earnBuf, sizeof(earnBuf), "%s$%.2f", ev < 0 ? "-" : "+", std::fabs(fiat)); + else snprintf(earnBuf, sizeof(earnBuf), "%+.4f ($%.2f)", ev, fiat); + earnW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, earnBuf).x; + earnCol = ev > 1e-12 ? greenCol : (ev < -1e-12 ? earnNegCol : OnSurfaceDisabled()); + if (row.hidden) earnCol = OnSurfaceDisabled(); + } + // Left edge of the line-2 right content, so the address truncates before it. + const float line2RightStart = contentRight - usdW + - (earnShown && earnBuf[0] ? (earnW + Layout::spacingMd()) : 0.0f); + // ---- Address (second line) ---- { - float addrAvailW = contentRight - labelX; + float addrAvailW = line2RightStart - Layout::spacingMd() - labelX; ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str()); std::string display_addr; if (fullAddrSz.x <= addrAvailW) { @@ -624,14 +731,17 @@ void RenderSharedAddressList(App* app, float listH, float availW, DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf); } - // ---- USD value (second line, right-aligned) ---- + // ---- Line 2 right: period-earnings (left of) + USD value, right-aligned ---- { - std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd); - if (!usdText.empty()) { - ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str()); + const float line2Y = cy + body2->LegacySize + Layout::spacingXs(); + if (earnShown && earnBuf[0]) { dl->AddText(capFont, capFont->LegacySize, - ImVec2(contentRight - usdSz.x, - cy + body2->LegacySize + Layout::spacingXs()), + ImVec2(contentRight - usdW - Layout::spacingMd() - earnW, line2Y), + earnCol, earnBuf); + } + if (!usdText.empty()) { + dl->AddText(capFont, capFont->LegacySize, + ImVec2(contentRight - usdW, line2Y), OnSurfaceDisabled(), usdText.c_str()); } } @@ -728,6 +838,15 @@ void RenderSharedAddressList(App* app, float listH, float availW, if (ImGui::MenuItem(TR("favorite_address"))) app->favoriteAddress(addr.address); } + // Per-address earnings column toggle (when the global show-all toggle is off). + if (app->settings()) { + const bool shown = app->settings()->isAddressEarningsShown(addr.address); + if (ImGui::MenuItem(shown ? TR("hide_earnings") : TR("show_earnings"))) { + if (shown) app->settings()->hideAddressEarnings(addr.address); + else app->settings()->showAddressEarnings(addr.address); + app->settings()->save(); + } + } // Portfolio membership — toggle this address in each Market-tab portfolio entry. ImGui::Separator(); if (ImGui::BeginMenu(TR("portfolio_add_to"))) { @@ -818,7 +937,8 @@ void RenderSharedAddressList(App* app, float listH, float availW, listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY); } - // Address count + // Address count + create-address buttons (relocated below the list to free the toolbar row for + // the earnings controls). Count on the left, New Z / New mining right-aligned on the same row. { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); char countBuf[128]; @@ -826,6 +946,38 @@ void RenderSharedAddressList(App* app, float listH, float availW, snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"), (int)rows.size(), totalAddrs); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf); + + float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp; + float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp; + float totalButtonsWidth = buttonWidth * 2 + spacing; + float rightMargin = S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f); + ImGui::SameLine(std::max(ImGui::GetCursorPosX(), + ImGui::GetWindowWidth() - totalButtonsWidth - rightMargin)); + + bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced(); + ImGui::BeginDisabled(sharedAddrSyncing); + if (s_generating_z_address) { + char genLabel[64]; + snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots()); + TactileButton(genLabel, ImVec2(buttonWidth, 0), + S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font)); + } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), + S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { + s_generating_z_address = true; + app->createNewZAddress([](const std::string& addr) { + s_generating_z_address = false; + DEBUG_LOGF("Created new z-address: %s\n", addr.c_str()); + }); + } + ImGui::SameLine(); + if (TactileButton(TR("new_mining_address"), ImVec2(buttonWidth, 0), + S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { + app->createNewTAddress([](const std::string& addr) { + DEBUG_LOGF("Created new t-address: %s\n", addr.c_str()); + }); + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_new_mining_address")); + ImGui::EndDisabled(); } } diff --git a/src/ui/windows/balance_components.h b/src/ui/windows/balance_components.h index fb028ac..c25fa70 100644 --- a/src/ui/windows/balance_components.h +++ b/src/ui/windows/balance_components.h @@ -8,6 +8,8 @@ #pragma once +#include + #include "imgui.h" namespace dragonx { @@ -31,5 +33,10 @@ void RenderSharedAddressList(App* app, float listH, float availW, float glassRou void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs); void RenderSyncBar(App* app, ImDrawList* dl, float vs); +// Total period-earnings for the whole wallet, formatted per the earnings settings. Returns false +// (and leaves out* untouched) when the global earnings toggle is off, so layouts show it only when +// the user has opted into the earnings view. Shared by RenderCompactHero and the layout totals. +bool FormatTotalEarnings(App* app, std::string& outText, ImU32& outCol); + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index 86bd58b..23a7f2e 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -364,7 +364,7 @@ static void RenderBalanceClassic(App* app) OnSurfaceMedium(), DRAGONX_TICKER); cy += sub1->LegacySize + valGap; - // USD value + // USD value (+ total period-earnings to its right when the earnings toggle is on) { double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) @@ -373,6 +373,12 @@ static void RenderBalanceClassic(App* app) snprintf(buf, sizeof(buf), "$-.-- USD"); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf); + std::string earnText; ImU32 earnCol; + if (FormatTotalEarnings(app, earnText, earnCol)) { + float usdW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(cx + usdW + tickGap * 3.0f, cy), earnCol, earnText.c_str()); + } } cy += capFont->LegacySize + 2 * dp; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1434aa0..f132766 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1065,6 +1065,18 @@ void I18n::loadBuiltinEnglish() strings_["filter"] = "Filter..."; strings_["no_addresses_yet"] = "No addresses yet"; strings_["showing_x_of_y"] = "Showing %d of %d addresses"; + strings_["tt_earn_interval"] = "Earnings period: 1H = this hour, 1D = today, 1M = this month, 1Y = this year"; + strings_["tt_earn_mode_inflows"] = "Earnings mode: amounts received (including mined)"; + strings_["tt_earn_mode_net"] = "Earnings mode: net (received minus sent)"; + strings_["tt_earn_show_all"] = "Show the earnings figure for every address"; + strings_["show_earnings"] = "Show earnings"; + strings_["hide_earnings"] = "Hide earnings"; + strings_["earnings_label"] = "Earnings"; + strings_["earnings_format"] = "Earnings Display Format"; + strings_["tt_earnings_format"] = "How the per-address and total earnings figures are shown"; + strings_["earnings_format_coin"] = "Coin only"; + strings_["earnings_format_coin_fiat"] = "Coin + fiat"; + strings_["earnings_format_fiat"] = "Fiat only"; strings_["set_label"] = "Set Label"; strings_["copied"] = "Copied!"; strings_["hidden_tag"] = " (hidden)";