Refactor app services and stabilize refresh/UI flows

- Add refresh scheduler and network refresh service boundaries for typed
  refresh results, ordered RPC collectors, applicators, and price parsing.
- Add daemon lifecycle and wallet security workflow helpers while preserving
  App-owned command RPC, decrypt, cancellation, and UI handoff behavior.
- Split balance, console, mining, amount formatting, and async task logic into
  focused modules with expanded Phase 4 test coverage.
- Fix market price loading by triggering price refresh immediately, avoiding
  queue-pressure drops, tracking loading/error state, and adding translations.
- Polish send, explorer, peers, settings, theme/schema, and related tab UI.
- Replace checked-in generated language headers with build-generated resources.
- Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
2026-04-29 12:47:57 -05:00
parent ee8a08e569
commit 9edab31728
95 changed files with 8776 additions and 37563 deletions

View File

@@ -3,6 +3,9 @@
// Released under the GPLv3
#include "balance_tab.h"
#include "balance_address_list.h"
#include "balance_tab_helpers.h"
#include "balance_recent_tx.h"
#include "key_export_dialog.h"
#include "qr_popup_dialog.h"
#include "address_label_dialog.h"
@@ -32,92 +35,12 @@
namespace dragonx {
namespace ui {
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
static std::string TrId(const char* tr_key, const char* id) {
std::string s = TR(tr_key);
s += "##";
s += id;
return s;
}
// Case-insensitive substring search
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
if (search.empty()) return true;
std::string s = str, q = search;
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
return s.find(q) != std::string::npos;
}
// Relative time string ("2m ago", "3h ago", etc.)
static std::string timeAgo(int64_t timestamp) {
if (timestamp <= 0) return "";
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
}
// Draw a small transaction-type icon (send=up, receive=down, mined=construction)
static void DrawTxIcon(ImDrawList* dl, const std::string& type,
float cx, float cy, float /*s*/, ImU32 col)
{
using namespace material;
ImFont* iconFont = Type().iconSmall();
const char* icon;
if (type == "send") {
icon = ICON_MD_CALL_MADE;
} else if (type == "receive") {
icon = ICON_MD_CALL_RECEIVED;
} else {
icon = ICON_MD_CONSTRUCTION;
}
ImVec2 sz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
}
// Animated balance state — lerps smoothly toward target
static double s_dispTotal = 0.0;
static double s_dispShielded = 0.0;
static double s_dispTransparent = 0.0;
static double s_dispUnconfirmed = 0.0;
// Helper to truncate address for display
static std::string truncateAddress(const std::string& addr, int maxLen = 32) {
if (addr.length() <= static_cast<size_t>(maxLen)) return addr;
int half = (maxLen - 3) / 2;
return addr.substr(0, half) + "..." + addr.substr(addr.length() - half);
}
// Helper to draw a sparkline polyline within a bounding box
static void DrawSparkline(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax,
const std::vector<double>& data, ImU32 color,
float thickness = 1.5f)
{
if (data.size() < 2) return;
double lo = *std::min_element(data.begin(), data.end());
double hi = *std::max_element(data.begin(), data.end());
double range = hi - lo;
if (range < 1e-12) range = 1.0;
float w = pMax.x - pMin.x;
float h = pMax.y - pMin.y;
int n = (int)data.size();
std::vector<ImVec2> pts;
pts.reserve(n);
for (int i = 0; i < n; i++) {
float x = pMin.x + (float)i / (float)(n - 1) * w;
float y = pMax.y - (float)((data[i] - lo) / range) * h;
pts.push_back(ImVec2(x, y));
}
dl->AddPolyline(pts.data(), n, color, ImDrawFlags_None, thickness);
}
// Forward declarations for all layout functions
static void RenderBalanceClassic(App* app);
static void RenderBalanceDonut(App* app);
@@ -1207,51 +1130,37 @@ static void RenderBalanceClassic(App* app)
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowY = rowPos.y + rowH * 0.5f;
// Icon
ImU32 iconCol;
if (tx.type == "send")
iconCol = Error();
else if (tx.type == "receive")
iconCol = Success();
else
iconCol = Warning();
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, iconCol);
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
// Type label
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize,
ImVec2(tx_x, rowPos.y + 2 * dp),
OnSurfaceMedium(), tx.getTypeDisplay().c_str());
OnSurfaceMedium(), display.typeText.c_str());
// Address (truncated)
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
std::string trAddr = truncateAddress(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
dl->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2 * dp),
OnSurfaceDisabled(), trAddr.c_str());
OnSurfaceDisabled(), display.addressText.c_str());
// Amount (right-aligned area)
char amtBuf[32];
snprintf(amtBuf, sizeof(amtBuf), "%s%.4f %s",
tx.type == "send" ? "-" : "+",
std::abs(tx.amount), DRAGONX_TICKER);
ImVec2 amtSz = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, amtBuf);
capFont->LegacySize, 10000, 0, display.amountText.c_str());
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, S.drawElement("tabs.balance", "amount-right-margin").size * hs);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2 * dp),
tx.type == "send" ? Error()
: Success(),
amtBuf);
recentTxAmountColor(tx.type),
display.amountText.c_str());
// Time ago
std::string ago = timeAgo(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, ago.c_str());
capFont->LegacySize, 10000, 0, display.timeText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
OnSurfaceDisabled(), ago.c_str());
OnSurfaceDisabled(), display.timeText.c_str());
// Clickable row — hover highlight + navigate to History
float rowW = ImGui::GetContentRegionAvail().x;
@@ -1380,45 +1289,22 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
static float s_copiedTime = 0.0f;
// ---- Build and filter address rows ----
struct AddrRow {
const AddressInfo* info;
bool isZ, hidden, favorite;
std::string label, icon;
int sortOrder;
};
std::vector<AddrRow> rows;
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
std::vector<AddressListInput> rowInputs;
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
auto addRows = [&](const auto& addrs, bool isZ) {
for (const auto& a : addrs) {
std::string filter(addr_search);
std::string addrLabel = app->getAddressLabel(a.address);
if (!containsIgnoreCase(a.address, filter) &&
!containsIgnoreCase(a.label, filter) &&
!containsIgnoreCase(addrLabel, filter)) continue;
bool isHidden = app->isAddressHidden(a.address);
if (isHidden && !s_showHidden) continue;
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
rows.push_back({&a, isZ, isHidden, isFav,
addrLabel, app->getAddressIcon(a.address),
app->getAddressSortOrder(a.address)});
rowInputs.push_back({&a, isZ, isHidden, isFav,
addrLabel, app->getAddressIcon(a.address),
app->getAddressSortOrder(a.address)});
}
};
addRows(state.z_addresses, true);
addRows(state.t_addresses, false);
// Sort: custom order (if any) → favorites → Z first → balance desc
std::sort(rows.begin(), rows.end(),
[](const AddrRow& a, const AddrRow& b) -> bool {
bool aHasOrder = a.sortOrder >= 0;
bool bHasOrder = b.sortOrder >= 0;
if (aHasOrder && bHasOrder) return a.sortOrder < b.sortOrder;
if (aHasOrder != bHasOrder) return aHasOrder > bHasOrder;
if (a.favorite != b.favorite) return a.favorite > b.favorite;
if (a.isZ != b.isZ) return a.isZ > b.isZ;
return a.info->balance > b.info->balance;
});
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
// ---- Toolbar: search + checkboxes + create buttons ----
float avail = ImGui::GetContentRegionAvail().x;
@@ -1677,24 +1563,21 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
float cx = rowPos.x + rowPadLeft;
float cy = rowPos.y + Layout::spacingMd();
auto rowLayout = ComputeAddressRowLayout(
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
float cx = rowLayout.contentStartX;
float cy = rowLayout.contentStartY;
// ---- Button zone (right edge): [eye] [star] ----
float btnH = rowH - Layout::spacingMd() * 2.0f;
float btnW = btnH;
float btnGap = Layout::spacingSm();
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
float rightEdge = rowPos.x + innerW;
float starX = rightEdge - btnW - Layout::spacingXs();
float eyeX = starX - btnGap - btnW;
float btnRound = 6.0f * dp;
bool btnClicked = false;
if (!isDragged) {
// Star button
{
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
const auto& starRect = rowLayout.favoriteButton;
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
@@ -1703,7 +1586,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
ImVec2(starRect.x + (starRect.width - iSz.x) * 0.5f,
starRect.y + (starRect.height - iSz.y) * 0.5f), starCol, starIcon);
if (bHov && mouseClicked) {
if (row.favorite) app->unfavoriteAddress(addr.address);
else app->favoriteAddress(addr.address);
@@ -1715,10 +1599,11 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
// Eye button (zero balance or hidden)
bool showEye = true;
// Always reserve space for both buttons so content doesn't shift
float contentRight = eyeX - Layout::spacingSm();
float contentRight = rowLayout.contentRight;
if (showEye) {
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
const auto& eyeRect = rowLayout.visibilityButton;
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
@@ -1727,7 +1612,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
ImVec2(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
eyeRect.y + (eyeRect.height - iSz.y) * 0.5f), iconCol, hideIcon);
if (bHov && mouseClicked) {
if (row.hidden) app->unhideAddress(addr.address);
else app->hideAddress(addr.address);
@@ -1756,7 +1642,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
}
// ---- Type label (first line) ----
float labelX = cx + rowIconSz * 2.0f + Layout::spacingMd();
float labelX = rowLayout.labelX;
{
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
@@ -1805,18 +1691,13 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
// ---- USD value (second line, right-aligned) ----
{
double priceUsd = state.market.price_usd;
if (priceUsd > 0.0 && addr.balance > 0.0) {
char usdBuf[32];
double usdVal = addr.balance * priceUsd;
if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
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());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(contentRight - usdSz.x,
cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), usdBuf);
OnSurfaceDisabled(), usdText.c_str());
}
}
@@ -2006,39 +1887,30 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
const auto& tx = txs[i];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowY = rowPos.y + rowH * 0.5f;
ImU32 iconCol;
if (tx.type == "send") iconCol = Error();
else if (tx.type == "receive") iconCol = Success();
else iconCol = Warning();
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, iconCol);
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize,
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), tx.getTypeDisplay().c_str());
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
std::string trAddr = truncateAddress(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
dl->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), trAddr.c_str());
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
char amtBuf[32];
snprintf(amtBuf, sizeof(amtBuf), "%s%.4f %s",
tx.type == "send" ? "-" : "+",
std::abs(tx.amount), DRAGONX_TICKER);
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, amtBuf);
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(
S.drawElement("tabs.balance", "amount-right-min-margin").size,
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2 * dp),
tx.type == "send" ? Error() : Success(), amtBuf);
recentTxAmountColor(tx.type), display.amountText.c_str());
std::string ago = timeAgo(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, ago.c_str());
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
OnSurfaceDisabled(), ago.c_str());
OnSurfaceDisabled(), display.timeText.c_str());
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);