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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user