feat(wallet): persist history and surface pending sends
Add an encrypted SQLite transaction history cache with cached tip metadata and per-address shielded scan progress so startup and full refreshes avoid re-scanning every z-address while still invalidating on wallet/address/rescan changes. Improve wallet history loading by paging transparent transactions, preserving cached shielded and sent rows, keeping recent/unconfirmed activity visible, and classifying mining-address receives. Show z_sendmany opid sends immediately in History and Overview, pin pending rows through refreshes, and apply optimistic address/balance debits until opids resolve. Add timestamped RPC console tracing by source/method without logging params or results, reduce redundant refresh/RPC calls, and cache Explorer recent block summaries in SQLite. Expand focused tests for transaction cache encryption, scan-progress persistence/invalidation, history preservation, operation-status parsing, pending send visibility, and Explorer/RPC refresh behavior.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../notifications.h"
|
||||
#include "../theme.h"
|
||||
#include "imgui.h"
|
||||
|
||||
@@ -171,11 +172,6 @@ public:
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons
|
||||
const char* cancelLabel = TR("cancel");
|
||||
const char* confirmLabel = TR("confirm_transfer");
|
||||
@@ -192,6 +188,20 @@ public:
|
||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
|
||||
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
|
||||
float totalW = cancelW + confirmW + Layout::spacingMd();
|
||||
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f + 1.0f;
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
|
||||
float footerTopY = cardBottomY - 24.0f * dp - footerH;
|
||||
float currentY = ImGui::GetCursorScreenPos().y;
|
||||
if (currentY < footerTopY) {
|
||||
ImGui::Dummy(ImVec2(0, footerTopY - currentY));
|
||||
} else {
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float rowStartX = ImGui::GetCursorPosX();
|
||||
float contentW = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
|
||||
@@ -209,11 +219,14 @@ public:
|
||||
[](bool ok, const std::string& result) {
|
||||
s_sending = false;
|
||||
s_success = ok;
|
||||
if (ok)
|
||||
s_resultMsg = result; // opid
|
||||
else
|
||||
s_resultMsg = result; // error message
|
||||
s_resultMsg = result;
|
||||
if (ok) {
|
||||
Notifications::instance().success(TR("transfer_sent_desc"));
|
||||
} else {
|
||||
Notifications::instance().error(result.empty() ? TR("transfer_failed") : result);
|
||||
}
|
||||
});
|
||||
s_open = false;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ void BackupWalletDialog::render(App* app)
|
||||
bool success = false;
|
||||
std::string statusMsg;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Backup wallet");
|
||||
rpc->call("backupwallet", json::array({dest}));
|
||||
// Check if file was created
|
||||
if (fs::exists(dest)) {
|
||||
|
||||
@@ -42,7 +42,7 @@ std::vector<AddressListRow> BuildAddressListRows(const std::vector<AddressListIn
|
||||
if (!AddressListMatchesFilter(input, filter)) continue;
|
||||
if (input.hidden && !showHidden) continue;
|
||||
if (hideZeroBalances && input.info->balance < 1e-9 && !input.hidden && !input.favorite) continue;
|
||||
rows.push_back({input.info, input.isZ, input.hidden, input.favorite,
|
||||
rows.push_back({input.info, input.isZ, input.hidden, input.favorite, input.mining,
|
||||
input.label, input.icon, input.sortOrder});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ struct AddressListInput {
|
||||
bool isZ = false;
|
||||
bool hidden = false;
|
||||
bool favorite = false;
|
||||
bool mining = false;
|
||||
std::string label;
|
||||
std::string icon;
|
||||
int sortOrder = -1;
|
||||
@@ -23,6 +24,7 @@ struct AddressListRow {
|
||||
bool isZ = false;
|
||||
bool hidden = false;
|
||||
bool favorite = false;
|
||||
bool mining = false;
|
||||
std::string label;
|
||||
std::string icon;
|
||||
int sortOrder = -1;
|
||||
|
||||
@@ -652,6 +652,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool isZ;
|
||||
bool hidden;
|
||||
bool favorite;
|
||||
bool mining;
|
||||
};
|
||||
std::vector<AddrRow> rows;
|
||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
@@ -665,7 +666,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
||||
continue;
|
||||
rows.push_back({&a, true, isHidden, isFav});
|
||||
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||
}
|
||||
for (const auto& a : state.t_addresses) {
|
||||
std::string filter(addr_search);
|
||||
@@ -677,7 +678,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
||||
continue;
|
||||
rows.push_back({&a, false, isHidden, isFav});
|
||||
rows.push_back({&a, false, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||
}
|
||||
|
||||
// Sort: favorites first, then Z addresses, then by balance descending
|
||||
@@ -951,8 +952,9 @@ static void RenderBalanceClassic(App* app)
|
||||
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
||||
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
||||
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, viewOnlyTag);
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// Label (if present, next to type)
|
||||
@@ -1033,6 +1035,9 @@ static void RenderBalanceClassic(App* app)
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key"))) {
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
}
|
||||
@@ -1111,18 +1116,19 @@ static void RenderBalanceClassic(App* app)
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int maxTx = kRecentTxCount;
|
||||
int count = (int)txs.size();
|
||||
if (count > maxTx) count = maxTx;
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
"No transactions yet");
|
||||
} else {
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
@@ -1176,6 +1182,7 @@ static void RenderBalanceClassic(App* app)
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1297,7 +1304,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
std::string addrLabel = app->getAddressLabel(a.address);
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav,
|
||||
bool isMining = app->isMiningAddress(a.address);
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
}
|
||||
@@ -1646,8 +1654,9 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// User label next to type
|
||||
@@ -1761,6 +1770,9 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
if (ImGui::MenuItem(TR("set_label"))) {
|
||||
AddressLabelDialog::show(app, addr.address, row.isZ);
|
||||
}
|
||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
if (row.isZ) {
|
||||
@@ -1869,10 +1881,13 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
|
||||
ImGui::Spacing();
|
||||
|
||||
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
int maxTx = std::clamp((int)(recentH / scaledRowH), 2, 5);
|
||||
float availableListH = ImGui::GetContentRegionAvail().y;
|
||||
float listH = std::max({scaledRowH, recentH, availableListH});
|
||||
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
if (count > maxTx) count = maxTx;
|
||||
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
||||
@@ -1923,6 +1938,7 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// Render sync progress bar (used by multiple layouts)
|
||||
|
||||
@@ -20,6 +20,7 @@ bool consoleLinePassesFilter(const std::string& lineText,
|
||||
const ConsoleOutputFilter& filter)
|
||||
{
|
||||
if (!filter.daemonMessagesEnabled && lineColor == filter.daemonColor) return false;
|
||||
if (!filter.rpcTraceEnabled && lineColor == filter.rpcTraceColor) return false;
|
||||
if (filter.errorsOnly && lineColor != filter.errorColor) return false;
|
||||
if (!filter.text.empty()) {
|
||||
std::string needle = lowerCopy(filter.text);
|
||||
|
||||
@@ -11,8 +11,10 @@ struct ConsoleOutputFilter {
|
||||
std::string text;
|
||||
bool daemonMessagesEnabled = true;
|
||||
bool errorsOnly = false;
|
||||
bool rpcTraceEnabled = false;
|
||||
ImU32 daemonColor = 0;
|
||||
ImU32 errorColor = 0;
|
||||
ImU32 rpcTraceColor = 0;
|
||||
};
|
||||
|
||||
bool consoleLinePassesFilter(const std::string& lineText,
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <ctime>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dragonx {
|
||||
@@ -38,10 +40,36 @@ ImU32 ConsoleTab::COLOR_RESULT = IM_COL32(200, 200, 200, 255);
|
||||
ImU32 ConsoleTab::COLOR_ERROR = IM_COL32(246, 71, 64, 255);
|
||||
ImU32 ConsoleTab::COLOR_DAEMON = IM_COL32(160, 160, 160, 180);
|
||||
ImU32 ConsoleTab::COLOR_INFO = IM_COL32(191, 209, 229, 255);
|
||||
ImU32 ConsoleTab::COLOR_RPC = IM_COL32(120, 180, 255, 210);
|
||||
bool ConsoleTab::s_scanline_enabled = true;
|
||||
float ConsoleTab::s_console_zoom = 1.0f;
|
||||
bool ConsoleTab::s_daemon_messages_enabled = true;
|
||||
bool ConsoleTab::s_errors_only_enabled = false;
|
||||
bool ConsoleTab::s_rpc_trace_enabled = false;
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex s_rpc_trace_console_mutex;
|
||||
ConsoleTab* s_rpc_trace_console = nullptr;
|
||||
|
||||
std::string rpcTraceTimestamp()
|
||||
{
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm localTime{};
|
||||
static std::mutex timeMutex;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(timeMutex);
|
||||
if (const std::tm* current = std::localtime(&now)) {
|
||||
localTime = *current;
|
||||
}
|
||||
}
|
||||
|
||||
char buffer[16];
|
||||
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ConsoleTab::refreshColors()
|
||||
{
|
||||
@@ -55,18 +83,21 @@ void ConsoleTab::refreshColors()
|
||||
auto err = S.drawElement("console", "color-error");
|
||||
auto dmn = S.drawElement("console", "color-daemon");
|
||||
auto inf = S.drawElement("console", "color-info");
|
||||
auto rpc = S.drawElement("console", "color-rpc");
|
||||
|
||||
ImU32 defCmd = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
||||
ImU32 defRes = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255);
|
||||
ImU32 defErr = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255);
|
||||
ImU32 defDmn = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
|
||||
ImU32 defInf = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
||||
ImU32 defRpc = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
|
||||
|
||||
COLOR_COMMAND = !cmd.color.empty() ? S.resolveColor(cmd.color, defCmd) : defCmd;
|
||||
COLOR_RESULT = !res.color.empty() ? S.resolveColor(res.color, defRes) : defRes;
|
||||
COLOR_ERROR = !err.color.empty() ? S.resolveColor(err.color, defErr) : defErr;
|
||||
COLOR_DAEMON = !dmn.color.empty() ? S.resolveColor(dmn.color, defDmn) : defDmn;
|
||||
COLOR_INFO = !inf.color.empty() ? S.resolveColor(inf.color, defInf) : defInf;
|
||||
COLOR_RPC = !rpc.color.empty() ? S.resolveColor(rpc.color, defRpc) : defRpc;
|
||||
} else {
|
||||
// No schema — use hardcoded defaults per theme
|
||||
COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
||||
@@ -74,11 +105,26 @@ void ConsoleTab::refreshColors()
|
||||
COLOR_ERROR = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255);
|
||||
COLOR_DAEMON = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
|
||||
COLOR_INFO = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
|
||||
COLOR_RPC = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
|
||||
}
|
||||
}
|
||||
|
||||
ConsoleTab::ConsoleTab()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
||||
s_rpc_trace_console = this;
|
||||
}
|
||||
rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
|
||||
ConsoleTab* console = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
||||
console = s_rpc_trace_console;
|
||||
}
|
||||
if (console) console->addRpcTraceLine(source, method);
|
||||
});
|
||||
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
|
||||
|
||||
// Load console colors from ui.toml schema (uses current theme)
|
||||
refreshColors();
|
||||
|
||||
@@ -88,6 +134,14 @@ ConsoleTab::ConsoleTab()
|
||||
addLine("", COLOR_RESULT);
|
||||
}
|
||||
|
||||
ConsoleTab::~ConsoleTab()
|
||||
{
|
||||
rpc::RPCClient::setTraceEnabled(false);
|
||||
rpc::RPCClient::setTraceCallback(nullptr);
|
||||
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
|
||||
if (s_rpc_trace_console == this) s_rpc_trace_console = nullptr;
|
||||
}
|
||||
|
||||
void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc::RPCWorker* worker, daemon::XmrigManager* xmrig)
|
||||
{
|
||||
using namespace material;
|
||||
@@ -100,7 +154,7 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
|
||||
// Save old colors to remap existing lines
|
||||
ImU32 oldCmd = COLOR_COMMAND, oldRes = COLOR_RESULT;
|
||||
ImU32 oldErr = COLOR_ERROR, oldDmn = COLOR_DAEMON;
|
||||
ImU32 oldInf = COLOR_INFO;
|
||||
ImU32 oldInf = COLOR_INFO, oldRpc = COLOR_RPC;
|
||||
refreshColors();
|
||||
// Remap stored line colors from old to new
|
||||
{
|
||||
@@ -111,6 +165,7 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
|
||||
else if (line.color == oldErr) line.color = COLOR_ERROR;
|
||||
else if (line.color == oldDmn) line.color = COLOR_DAEMON;
|
||||
else if (line.color == oldInf) line.color = COLOR_INFO;
|
||||
else if (line.color == oldRpc) line.color = COLOR_RPC;
|
||||
}
|
||||
}
|
||||
s_lastDark = nowDark;
|
||||
@@ -282,81 +337,46 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
|
||||
|
||||
// CRT scanline effect over output area — aligned to text lines
|
||||
if (s_scanline_enabled) {
|
||||
float panelH = outPanelMax.y - outPanelMin.y;
|
||||
|
||||
// --- Text-aligned horizontal scanlines ---
|
||||
// Stride matches the actual text line height so each band sits between lines.
|
||||
float textLineH = output_line_height_;
|
||||
if (textLineH <= 1.0f) textLineH = Type().caption()->LegacySize * s_console_zoom + 2.0f; // fallback
|
||||
float bandH = schema::UI().drawElement("tabs.console", "scanline-gap").sizeOr(2.0f);
|
||||
int lineAlpha = (int)schema::UI().drawElement("tabs.console", "scanline-line-alpha").sizeOr(18.0f);
|
||||
if (textLineH <= 1.0f) textLineH = Type().caption()->LegacySize * s_console_zoom + 2.0f;
|
||||
|
||||
// Glow fringe parameters (soft gradient above/below each band)
|
||||
float glowSpread = schema::UI().drawElement("tabs.console", "scanline-glow-spread").sizeOr(0.0f);
|
||||
float glowIntensity = schema::UI().drawElement("tabs.console", "scanline-glow-intensity").sizeOr(0.6f);
|
||||
int glowRGB = (int)schema::UI().drawElement("tabs.console", "scanline-glow-color").sizeOr(255.0f);
|
||||
bool drawGlow = glowSpread > 0.0f && glowIntensity > 0.0f && lineAlpha > 0;
|
||||
int glowAlpha = drawGlow ? std::min(255, (int)(lineAlpha * glowIntensity)) : 0;
|
||||
ImU32 glowPeak = IM_COL32(glowRGB, glowRGB, glowRGB, glowAlpha);
|
||||
ImU32 glowClear = IM_COL32(glowRGB, glowRGB, glowRGB, 0);
|
||||
|
||||
if (textLineH >= 1.0f && lineAlpha > 0) {
|
||||
ImU32 lineCol = IM_COL32(255, 255, 255, lineAlpha);
|
||||
float stride = textLineH; // one text line per scanline period
|
||||
// Align with text: account for inner padding and scroll position
|
||||
float padY = Layout::spacingSm();
|
||||
float scrollFrac = std::fmod(consoleScrollY, stride);
|
||||
float startY = outPanelMin.y + padY - scrollFrac;
|
||||
// Ensure first band starts above the visible area
|
||||
while (startY > outPanelMin.y) startY -= stride;
|
||||
for (float y = startY; y < outPanelMax.y; y += stride) {
|
||||
// Place the dark band at the bottom edge of each text line period
|
||||
float bandTop = y + stride - bandH;
|
||||
float bandBot = y + stride;
|
||||
float yTop = std::max(bandTop, outPanelMin.y);
|
||||
float yBot = std::min(bandBot, outPanelMax.y);
|
||||
int lightAlpha = std::clamp((int)schema::UI().drawElement("tabs.console", "scanline-line-alpha").sizeOr(10.0f), 0, 255);
|
||||
int darkAlpha = std::clamp(lightAlpha + 10, 0, 255);
|
||||
if (textLineH >= 1.0f && (lightAlpha > 0 || darkAlpha > 0)) {
|
||||
ImU32 lightCol = IM_COL32(255, 255, 255, lightAlpha);
|
||||
ImU32 darkCol = IM_COL32(0, 0, 0, darkAlpha);
|
||||
for (const auto& row : scanline_rows_) {
|
||||
float yTop = std::max(row.yTop, outPanelMin.y);
|
||||
float yBot = std::min(row.yBot, outPanelMax.y);
|
||||
if (yTop < yBot) {
|
||||
// Glow fringes (gradient tapers away from band)
|
||||
if (drawGlow) {
|
||||
// Above fringe: transparent at top, glowPeak at bottom
|
||||
float gTop = std::max(yTop - glowSpread, outPanelMin.y);
|
||||
if (gTop < yTop) {
|
||||
dlOut->AddRectFilledMultiColor(
|
||||
ImVec2(outPanelMin.x, gTop), ImVec2(outPanelMax.x, yTop),
|
||||
glowClear, glowClear, glowPeak, glowPeak);
|
||||
}
|
||||
// Below fringe: glowPeak at top, transparent at bottom
|
||||
float gBot = std::min(yBot + glowSpread, outPanelMax.y);
|
||||
if (yBot < gBot) {
|
||||
dlOut->AddRectFilledMultiColor(
|
||||
ImVec2(outPanelMin.x, yBot), ImVec2(outPanelMax.x, gBot),
|
||||
glowPeak, glowPeak, glowClear, glowClear);
|
||||
}
|
||||
}
|
||||
// Opaque scanline band (drawn on top of glow)
|
||||
dlOut->AddRectFilled(ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, yBot), lineCol);
|
||||
dlOut->AddRectFilled(ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, yBot),
|
||||
(row.rowIndex % 2 == 0) ? lightCol : darkCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Animated sweep band (brighter moving highlight) ---
|
||||
float panelH = outPanelMax.y - outPanelMin.y;
|
||||
float scanSpeed = schema::UI().drawElement("tabs.console", "scanline-speed").sizeOr(40.0f);
|
||||
float scanH = schema::UI().drawElement("tabs.console", "scanline-height").sizeOr(30.0f);
|
||||
int scanAlpha = (int)schema::UI().drawElement("tabs.console", "scanline-alpha").sizeOr(12.0f);
|
||||
float t = (float)std::fmod(ImGui::GetTime() * scanSpeed, (double)(panelH + scanH));
|
||||
float scanY = outPanelMin.y + t - scanH;
|
||||
float yTop = std::max(scanY, outPanelMin.y);
|
||||
float yBot = std::min(scanY + scanH, outPanelMax.y);
|
||||
if (yTop < yBot) {
|
||||
float mid = (yTop + yBot) * 0.5f;
|
||||
ImU32 clear = IM_COL32(255, 255, 255, 0);
|
||||
ImU32 peak = IM_COL32(255, 255, 255, scanAlpha);
|
||||
dlOut->AddRectFilledMultiColor(
|
||||
ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, mid),
|
||||
clear, clear, peak, peak);
|
||||
dlOut->AddRectFilledMultiColor(
|
||||
ImVec2(outPanelMin.x, mid), ImVec2(outPanelMax.x, yBot),
|
||||
peak, peak, clear, clear);
|
||||
float rawScanH = schema::UI().drawElement("tabs.console", "scanline-height").sizeOr(textLineH * 2.0f);
|
||||
int scanAlpha = std::clamp((int)schema::UI().drawElement("tabs.console", "scanline-alpha").sizeOr(8.0f), 0, 255);
|
||||
if (panelH > 1.0f && textLineH >= 1.0f && scanSpeed > 0.0f && scanAlpha > 0) {
|
||||
float scanLines = std::max(1.0f, std::round(rawScanH / textLineH));
|
||||
float scanH = scanLines * textLineH;
|
||||
float t = (float)std::fmod(ImGui::GetTime() * scanSpeed, (double)(panelH + scanH));
|
||||
float scanY = outPanelMin.y + t - scanH;
|
||||
float yTop = std::max(scanY, outPanelMin.y);
|
||||
float yBot = std::min(scanY + scanH, outPanelMax.y);
|
||||
if (yTop < yBot) {
|
||||
float mid = (yTop + yBot) * 0.5f;
|
||||
ImU32 clear = IM_COL32(255, 255, 255, 0);
|
||||
ImU32 peak = IM_COL32(255, 255, 255, scanAlpha);
|
||||
dlOut->AddRectFilledMultiColor(
|
||||
ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, mid),
|
||||
clear, clear, peak, peak);
|
||||
dlOut->AddRectFilledMultiColor(
|
||||
ImVec2(outPanelMin.x, mid), ImVec2(outPanelMax.x, yBot),
|
||||
peak, peak, clear, clear);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +512,25 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", TR("console_show_errors_only"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Spacing();
|
||||
ImGui::SameLine();
|
||||
|
||||
// App RPC trace toggle — captures method/source only, never results or params
|
||||
{
|
||||
static bool s_prev_rpc_trace_enabled = false;
|
||||
if (ImGui::Checkbox(TR("console_rpc_trace"), &s_rpc_trace_enabled)) {
|
||||
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
|
||||
}
|
||||
if (s_prev_rpc_trace_enabled != s_rpc_trace_enabled && auto_scroll_) {
|
||||
scroll_to_bottom_ = true;
|
||||
}
|
||||
s_prev_rpc_trace_enabled = s_rpc_trace_enabled;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", TR("console_show_rpc_trace"));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Spacing();
|
||||
@@ -607,12 +646,15 @@ void ConsoleTab::renderOutput()
|
||||
output_line_height_ = line_height; // store for scanline alignment
|
||||
output_origin_ = ImGui::GetCursorScreenPos();
|
||||
output_scroll_y_ = ImGui::GetScrollY();
|
||||
scanline_rows_.clear();
|
||||
|
||||
// Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
|
||||
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
||||
s_errors_only_enabled, COLOR_DAEMON, COLOR_ERROR};
|
||||
s_errors_only_enabled, s_rpc_trace_enabled,
|
||||
COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
|
||||
bool has_text_filter = !outputFilter.text.empty();
|
||||
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled || outputFilter.errorsOnly;
|
||||
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
|
||||
!outputFilter.rpcTraceEnabled || outputFilter.errorsOnly;
|
||||
visible_indices_.clear();
|
||||
if (has_filter) {
|
||||
for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
|
||||
@@ -826,6 +868,11 @@ void ConsoleTab::renderOutput()
|
||||
float rowY = lineOrigin.y + seg.yOffset;
|
||||
const char* segStart = line.text.c_str() + seg.byteStart;
|
||||
const char* segEnd = line.text.c_str() + seg.byteEnd;
|
||||
|
||||
if (s_scanline_enabled && line_height > 0.0f) {
|
||||
int rowIndex = static_cast<int>(std::floor((cumulative_y_offsets_[vi] + seg.yOffset) / line_height + 0.5f));
|
||||
scanline_rows_.push_back({rowY, rowY + seg.height, rowIndex});
|
||||
}
|
||||
|
||||
// Selection highlight for this sub-row
|
||||
if (lineSelected && selByteStart < seg.byteEnd && selByteEnd > seg.byteStart) {
|
||||
@@ -1469,6 +1516,7 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
std::string result_str;
|
||||
bool is_error = false;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Console tab / User command");
|
||||
result_str = rpc->callRaw(method, params);
|
||||
} catch (const std::exception& e) {
|
||||
result_str = e.what();
|
||||
@@ -1499,6 +1547,7 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
} else {
|
||||
// Fallback: synchronous execution if no worker available
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Console tab / User command");
|
||||
std::string result_str = rpc->callRaw(method, params);
|
||||
for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, false)) {
|
||||
addLine(resultLine.text, COLOR_RESULT);
|
||||
@@ -1545,6 +1594,11 @@ void ConsoleTab::addLine(const std::string& line, ImU32 color)
|
||||
scroll_to_bottom_ = auto_scroll_;
|
||||
}
|
||||
|
||||
void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)
|
||||
{
|
||||
addLine("[rpc] [" + rpcTraceTimestamp() + "] [" + source + "] " + method, COLOR_RPC);
|
||||
}
|
||||
|
||||
void ConsoleTab::addCommandResult(const std::string& cmd, const std::string& result, bool is_error)
|
||||
{
|
||||
addLine("> " + cmd, COLOR_COMMAND);
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace ui {
|
||||
class ConsoleTab {
|
||||
public:
|
||||
ConsoleTab();
|
||||
~ConsoleTab() = default;
|
||||
~ConsoleTab();
|
||||
|
||||
/**
|
||||
* @brief Render the console tab
|
||||
@@ -46,6 +46,7 @@ public:
|
||||
* @brief Add a line to the console output
|
||||
*/
|
||||
void addLine(const std::string& line, ImU32 color = IM_COL32(200, 200, 200, 255));
|
||||
void addRpcTraceLine(const std::string& source, const std::string& method);
|
||||
|
||||
/**
|
||||
* @brief Clear console output
|
||||
@@ -69,6 +70,9 @@ public:
|
||||
// Show only error messages (filter toggle)
|
||||
static bool s_errors_only_enabled;
|
||||
|
||||
// Show app RPC calls made through RPCClient (method/source only)
|
||||
static bool s_rpc_trace_enabled;
|
||||
|
||||
/// Refresh console text colors for current theme (call after theme switch)
|
||||
static void refreshColors();
|
||||
|
||||
@@ -78,6 +82,7 @@ public:
|
||||
static ImU32 COLOR_ERROR;
|
||||
static ImU32 COLOR_DAEMON;
|
||||
static ImU32 COLOR_INFO;
|
||||
static ImU32 COLOR_RPC;
|
||||
|
||||
private:
|
||||
struct ConsoleLine {
|
||||
@@ -129,6 +134,13 @@ private:
|
||||
float output_scroll_y_ = 0.0f; // Track scroll position for selection
|
||||
ImVec2 output_origin_ = {0, 0}; // Top-left of output area
|
||||
float output_line_height_ = 0.0f; // Text line height (for scanline alignment)
|
||||
|
||||
struct ScanlineRow {
|
||||
float yTop = 0.0f;
|
||||
float yBot = 0.0f;
|
||||
int rowIndex = 0;
|
||||
};
|
||||
std::vector<ScanlineRow> scanline_rows_;
|
||||
|
||||
std::mutex lines_mutex_;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../../rpc/rpc_worker.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../explorer/explorer_block_cache.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -25,6 +26,8 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -39,18 +42,13 @@ static char s_search_buf[128] = {};
|
||||
static bool s_search_loading = false;
|
||||
static std::string s_search_error;
|
||||
|
||||
// Recent blocks cache
|
||||
struct BlockSummary {
|
||||
int height = 0;
|
||||
std::string hash;
|
||||
int tx_count = 0;
|
||||
int size = 0;
|
||||
int64_t time = 0;
|
||||
double difficulty = 0.0;
|
||||
};
|
||||
static std::vector<BlockSummary> s_recent_blocks;
|
||||
static int s_last_known_height = 0;
|
||||
using BlockSummary = ExplorerBlockSummary;
|
||||
static ExplorerBlockCache s_recent_block_cache;
|
||||
static std::set<int> s_pending_block_heights;
|
||||
static int s_recent_page = 0;
|
||||
static int s_recent_max_page = 0;
|
||||
static std::atomic<int> s_pending_block_fetches{0};
|
||||
static bool s_recent_disk_cache_validation_pending = false;
|
||||
|
||||
// Block detail modal
|
||||
static bool s_show_detail_modal = false;
|
||||
@@ -116,6 +114,34 @@ static std::string truncateHashToFit(const std::string& hash, ImFont* font, floa
|
||||
return truncateHash(hash, front, back);
|
||||
}
|
||||
|
||||
static void validateRecentBlockCache(App* app, const WalletState& state) {
|
||||
if (s_recent_disk_cache_validation_pending) return;
|
||||
|
||||
auto validation = s_recent_block_cache.prepareValidation(state.sync.blocks, state.sync.best_blockhash);
|
||||
if (!validation.needed) return;
|
||||
|
||||
auto* worker = app ? app->worker() : nullptr;
|
||||
auto* rpc = app ? app->rpc() : nullptr;
|
||||
if (!worker || !rpc || !rpc->isConnected()) return;
|
||||
|
||||
int currentHeight = state.sync.blocks;
|
||||
std::string currentHash = state.sync.best_blockhash;
|
||||
s_recent_disk_cache_validation_pending = true;
|
||||
worker->post([rpc, validation, currentHeight, currentHash = std::move(currentHash)]() mutable -> rpc::RPCWorker::MainCb {
|
||||
std::string actualHash;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Cache validation");
|
||||
actualHash = rpc->call("getblockhash", {validation.height}).get<std::string>();
|
||||
} catch (...) {}
|
||||
|
||||
return [validation, currentHeight, currentHash = std::move(currentHash),
|
||||
actualHash = std::move(actualHash)]() mutable {
|
||||
s_recent_disk_cache_validation_pending = false;
|
||||
s_recent_block_cache.applySavedTipValidation(validation, actualHash, currentHeight, currentHash);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static std::string formatSize(int bytes) {
|
||||
char b[32];
|
||||
if (bytes >= 1048576)
|
||||
@@ -127,6 +153,21 @@ static std::string formatSize(int bytes) {
|
||||
return b;
|
||||
}
|
||||
|
||||
static std::string formatHashrate(double hashrate) {
|
||||
char b[32];
|
||||
if (hashrate >= 1e12)
|
||||
snprintf(b, sizeof(b), "%.2f TH/s", hashrate / 1e12);
|
||||
else if (hashrate >= 1e9)
|
||||
snprintf(b, sizeof(b), "%.2f GH/s", hashrate / 1e9);
|
||||
else if (hashrate >= 1e6)
|
||||
snprintf(b, sizeof(b), "%.2f MH/s", hashrate / 1e6);
|
||||
else if (hashrate >= 1e3)
|
||||
snprintf(b, sizeof(b), "%.2f KH/s", hashrate / 1e3);
|
||||
else
|
||||
snprintf(b, sizeof(b), "%.0f H/s", hashrate);
|
||||
return b;
|
||||
}
|
||||
|
||||
// Copy icon button — draws a small icon that copies text to clipboard on click
|
||||
static void copyButton(const char* id, const std::string& text, float x, float y) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
@@ -211,6 +252,7 @@ static void fetchBlockDetail(App* app, int height) {
|
||||
json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Block detail");
|
||||
auto hashResult = rpc->call("getblockhash", {height});
|
||||
std::string hash = hashResult.get<std::string>();
|
||||
result = rpc->call("getblock", {hash});
|
||||
@@ -236,12 +278,14 @@ static void fetchBlockDetailByHash(App* app, const std::string& hash) {
|
||||
bool gotTx = false;
|
||||
// Try as block hash first
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||
blockResult = rpc->call("getblock", {hash});
|
||||
gotBlock = true;
|
||||
} catch (...) {}
|
||||
// If not a block hash, try as txid
|
||||
if (!gotBlock) {
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||
txResult = rpc->call("getrawtransaction", {hash, 1});
|
||||
gotTx = true;
|
||||
} catch (...) {}
|
||||
@@ -267,43 +311,39 @@ static void fetchBlockDetailByHash(App* app, const std::string& hash) {
|
||||
});
|
||||
}
|
||||
|
||||
static bool fetchRecentBlocks(App* app, int currentHeight, int count = 10) {
|
||||
static bool fetchRecentBlock(App* app, int height) {
|
||||
auto* worker = app->worker();
|
||||
auto* rpc = app->rpc();
|
||||
if (!worker || !rpc || s_pending_block_fetches > 0) return false;
|
||||
if (height < 1 || !worker || !rpc) return false;
|
||||
if (s_pending_block_heights.find(height) != s_pending_block_heights.end()) return false;
|
||||
|
||||
if (s_recent_blocks.empty()) s_recent_blocks.resize(count);
|
||||
s_pending_block_fetches = 1; // single batched fetch
|
||||
s_pending_block_heights.insert(height);
|
||||
s_pending_block_fetches.fetch_add(1);
|
||||
|
||||
worker->post([rpc, currentHeight, count]() -> rpc::RPCWorker::MainCb {
|
||||
std::vector<BlockSummary> results(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int h = currentHeight - i;
|
||||
if (h < 1) continue;
|
||||
try {
|
||||
auto hashResult = rpc->call("getblockhash", {h});
|
||||
auto hash = hashResult.get<std::string>();
|
||||
auto result = rpc->call("getblock", {hash});
|
||||
auto& bs = results[i];
|
||||
bs.height = result.value("height", 0);
|
||||
bs.hash = result.value("hash", "");
|
||||
bs.time = result.value("time", (int64_t)0);
|
||||
bs.size = result.value("size", 0);
|
||||
bs.difficulty = result.value("difficulty", 0.0);
|
||||
if (result.contains("tx") && result["tx"].is_array())
|
||||
bs.tx_count = static_cast<int>(result["tx"].size());
|
||||
} catch (...) {}
|
||||
}
|
||||
return [results = std::move(results)]() mutable {
|
||||
bool gotAny = false;
|
||||
for (const auto& block : results) {
|
||||
if (block.height > 0) {
|
||||
gotAny = true;
|
||||
break;
|
||||
}
|
||||
worker->post([rpc, height]() -> rpc::RPCWorker::MainCb {
|
||||
BlockSummary block;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Recent blocks");
|
||||
auto hashResult = rpc->call("getblockhash", {height});
|
||||
auto hash = hashResult.get<std::string>();
|
||||
auto result = rpc->call("getblock", {hash});
|
||||
block.height = result.value("height", 0);
|
||||
block.hash = result.value("hash", "");
|
||||
block.time = result.value("time", (int64_t)0);
|
||||
block.size = result.value("size", 0);
|
||||
block.difficulty = result.value("difficulty", 0.0);
|
||||
if (result.contains("tx") && result["tx"].is_array())
|
||||
block.tx_count = static_cast<int>(result["tx"].size());
|
||||
} catch (...) {}
|
||||
|
||||
return [height, block = std::move(block)]() mutable {
|
||||
if (block.height > 0) {
|
||||
s_recent_block_cache.storeBlock(block);
|
||||
}
|
||||
s_pending_block_heights.erase(height);
|
||||
if (s_pending_block_fetches.load() > 0) {
|
||||
s_pending_block_fetches.fetch_sub(1);
|
||||
}
|
||||
if (gotAny) s_recent_blocks = std::move(results);
|
||||
s_pending_block_fetches = 0;
|
||||
};
|
||||
});
|
||||
return true;
|
||||
@@ -318,6 +358,7 @@ static void fetchMempoolInfo(App* app) {
|
||||
int txCount = 0;
|
||||
int64_t bytes = 0;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Mempool summary");
|
||||
auto result = rpc->call("getmempoolinfo", json::array());
|
||||
txCount = result.value("size", 0);
|
||||
bytes = result.value("bytes", (int64_t)0);
|
||||
@@ -338,7 +379,10 @@ static void fetchTxDetail(App* app, const std::string& txid) {
|
||||
s_tx_detail = json();
|
||||
worker->post([rpc, txid]() -> rpc::RPCWorker::MainCb {
|
||||
json result;
|
||||
try { result = rpc->call("getrawtransaction", {txid, 1}); }
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Transaction detail");
|
||||
result = rpc->call("getrawtransaction", {txid, 1});
|
||||
}
|
||||
catch (...) {}
|
||||
return [result]() {
|
||||
s_tx_loading = false;
|
||||
@@ -377,6 +421,16 @@ static void renderSearchBar(App* app, float availWidth) {
|
||||
auto& S = schema::UI();
|
||||
float pad = Layout::cardInnerPadding();
|
||||
|
||||
ImFont* capFont = Type().caption();
|
||||
float navBtnSz = std::max(24.0f * Layout::dpiScale(), capFont->LegacySize + 8.0f * Layout::dpiScale());
|
||||
if (s_recent_page > s_recent_max_page) s_recent_page = s_recent_max_page;
|
||||
if (s_recent_page < 0) s_recent_page = 0;
|
||||
char pageBuf[32];
|
||||
snprintf(pageBuf, sizeof(pageBuf), "%d / %d", s_recent_page + 1, s_recent_max_page + 1);
|
||||
float pageW = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, pageBuf).x;
|
||||
float navGap = Layout::spacingXs();
|
||||
float navW = navBtnSz * 2.0f + pageW + navGap * 2.0f;
|
||||
|
||||
float inputW = std::min(
|
||||
S.drawElement("tabs.explorer", "search-input-width").size,
|
||||
availWidth * 0.65f);
|
||||
@@ -384,11 +438,12 @@ static void renderSearchBar(App* app, float availWidth) {
|
||||
float barH = S.drawElement("tabs.explorer", "search-bar-height").size;
|
||||
|
||||
// Clamp so search bar never overflows
|
||||
float maxInputW = availWidth - btnW - pad * 3 - Type().iconMed()->LegacySize;
|
||||
float maxInputW = availWidth - btnW - navW - pad * 4 - Type().iconMed()->LegacySize;
|
||||
if (inputW > maxInputW) inputW = maxInputW;
|
||||
if (inputW < 80.0f) inputW = 80.0f;
|
||||
|
||||
ImGui::Spacing();
|
||||
float rowStartX = ImGui::GetCursorScreenPos().x;
|
||||
|
||||
// Icon
|
||||
ImGui::PushFont(Type().iconMed());
|
||||
@@ -429,6 +484,32 @@ static void renderSearchBar(App* app, float availWidth) {
|
||||
ImGui::TextDisabled("%s", TR("loading"));
|
||||
}
|
||||
|
||||
float navX = rowStartX + availWidth - navW;
|
||||
float navY = cursorY + std::max(0.0f, (barH - navBtnSz) * 0.5f);
|
||||
ImGui::SetCursorScreenPos(ImVec2(navX, navY));
|
||||
ImGui::BeginDisabled(s_recent_page <= 0);
|
||||
ImGui::PushID("RecentBlocksSearchPrev");
|
||||
if (TactileButton(ICON_MD_CHEVRON_LEFT, ImVec2(navBtnSz, navBtnSz), Type().iconSmall())) {
|
||||
--s_recent_page;
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine(0, navGap);
|
||||
ImVec2 pagePos = ImGui::GetCursorScreenPos();
|
||||
ImGui::GetWindowDrawList()->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(pagePos.x, pagePos.y + (navBtnSz - capFont->LegacySize) * 0.5f), OnSurfaceMedium(), pageBuf);
|
||||
ImGui::Dummy(ImVec2(pageW, navBtnSz));
|
||||
ImGui::SameLine(0, navGap);
|
||||
ImGui::BeginDisabled(s_recent_page >= s_recent_max_page);
|
||||
ImGui::PushID("RecentBlocksSearchNext");
|
||||
if (TactileButton(ICON_MD_CHEVRON_RIGHT, ImVec2(navBtnSz, navBtnSz), Type().iconSmall())) {
|
||||
++s_recent_page;
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowStartX, cursorY + barH));
|
||||
|
||||
// Error
|
||||
if (!s_search_error.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
@@ -443,6 +524,7 @@ static void renderChainStats(App* app, float availWidth) {
|
||||
const auto& state = app->getWalletState();
|
||||
float pad = Layout::cardInnerPadding();
|
||||
float gap = Layout::cardGap();
|
||||
float dp = Layout::dpiScale();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
GlassPanelSpec glassSpec;
|
||||
@@ -451,109 +533,185 @@ static void renderChainStats(App* app, float availWidth) {
|
||||
ImFont* ovFont = Type().overline();
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
ImFont* heroFont = Type().h2();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
|
||||
float cardW = (availWidth - gap) * 0.5f;
|
||||
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
|
||||
float headerH = ovFont->LegacySize + Layout::spacingSm();
|
||||
float cardH = pad * 0.5f + headerH + rowH * 3 + Layout::spacingSm() * 2 + pad * 0.5f;
|
||||
float heroLineH = capFont->LegacySize + Layout::spacingXs() + heroFont->LegacySize;
|
||||
float hashLineH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
|
||||
float cardH = std::max(148.0f * dp,
|
||||
pad * 1.5f + headerH + heroLineH + hashLineH + Layout::spacingLg() * 2.0f);
|
||||
|
||||
bool stacked = availWidth < 760.0f * dp;
|
||||
float chainW = stacked ? availWidth : (availWidth - gap) * 0.60f;
|
||||
float metricsW = stacked ? availWidth : availWidth - chainW - gap;
|
||||
|
||||
ImVec2 basePos = ImGui::GetCursorScreenPos();
|
||||
char buf[128];
|
||||
|
||||
// ── Chain Card (left) ──
|
||||
{
|
||||
ImVec2 cardMin = basePos;
|
||||
auto drawStatusPill = [&](const ImVec2& cardMin, float cardW) {
|
||||
bool connected = app->rpc() && app->rpc()->isConnected();
|
||||
ImU32 pillCol = connected ? Success() : Error();
|
||||
std::string statusText;
|
||||
|
||||
if (!connected) {
|
||||
statusText = TR("not_connected");
|
||||
} else if (state.sync.syncing && state.sync.headers > 0) {
|
||||
snprintf(buf, sizeof(buf), "%s %.1f%%", TR("syncing"), state.sync.verification_progress * 100.0);
|
||||
statusText = buf;
|
||||
pillCol = Warning();
|
||||
} else {
|
||||
statusText = TR("connected");
|
||||
}
|
||||
|
||||
float maxTextW = cardW * 0.34f;
|
||||
statusText = truncateHashToFit(statusText, capFont, maxTextW);
|
||||
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, statusText.c_str());
|
||||
float pillPadX = Layout::spacingSm();
|
||||
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
|
||||
float pillW = textSz.x + pillPadX * 2.0f;
|
||||
ImVec2 pillMin(cardMin.x + cardW - pad - pillW, cardMin.y + pad * 0.5f);
|
||||
ImVec2 pillMax(pillMin.x + pillW, pillMin.y + pillH);
|
||||
|
||||
dl->AddRectFilled(pillMin, pillMax, WithAlpha(pillCol, 32), pillH * 0.5f);
|
||||
dl->AddRect(pillMin, pillMax, WithAlpha(pillCol, 110), pillH * 0.5f, 0, 1.0f * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(pillMin.x + pillPadX, pillMin.y + Layout::spacingXs()), pillCol, statusText.c_str());
|
||||
};
|
||||
|
||||
auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {
|
||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
dl->AddText(ovFont, ovFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
|
||||
drawStatusPill(cardMin, cardW);
|
||||
|
||||
float colW = (cardW - pad * 2) / 2.0f;
|
||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||
float labelY = cardMin.y + pad * 0.5f + headerH + Layout::spacingLg();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, labelY), OnSurfaceMedium(), TR("explorer_block_height"));
|
||||
|
||||
// Row 1: Height | Difficulty
|
||||
{
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_block_height"));
|
||||
snprintf(buf, sizeof(buf), "%d", state.sync.blocks);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
snprintf(buf, sizeof(buf), "%d", state.sync.blocks);
|
||||
float valueY = labelY + capFont->LegacySize + Layout::spacingXs();
|
||||
DrawTextShadow(dl, heroFont, heroFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, valueY), OnSurface(), buf);
|
||||
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
|
||||
snprintf(buf, sizeof(buf), "%.4f", state.mining.difficulty);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float progress = std::clamp(static_cast<float>(state.sync.verification_progress), 0.0f, 1.0f);
|
||||
float barY = valueY + heroFont->LegacySize + Layout::spacingMd();
|
||||
float barH = std::max(3.0f * dp, 3.0f);
|
||||
float barW = cardW - pad * 2.0f;
|
||||
dl->AddRectFilled(ImVec2(cardMin.x + pad, barY),
|
||||
ImVec2(cardMin.x + pad + barW, barY + barH), WithAlpha(OnSurface(), 18), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(cardMin.x + pad, barY),
|
||||
ImVec2(cardMin.x + pad + barW * progress, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f);
|
||||
}
|
||||
ry += rowH + Layout::spacingSm();
|
||||
|
||||
// Row 2: Hashrate | Notarized
|
||||
{
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
|
||||
double hr = state.mining.networkHashrate;
|
||||
if (hr >= 1e9)
|
||||
snprintf(buf, sizeof(buf), "%.2f GH/s", hr / 1e9);
|
||||
else if (hr >= 1e6)
|
||||
snprintf(buf, sizeof(buf), "%.2f MH/s", hr / 1e6);
|
||||
else if (hr >= 1e3)
|
||||
snprintf(buf, sizeof(buf), "%.2f KH/s", hr / 1e3);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%.0f H/s", hr);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
float hashLabelY = cardMax.y - pad - hashLineH;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));
|
||||
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
|
||||
snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
float hashY = hashLabelY + capFont->LegacySize + Layout::spacingXs();
|
||||
float copyW = iconFont->LegacySize + Layout::spacingSm() * 2.0f;
|
||||
float hashMaxW = cardW - pad * 2.0f - (state.sync.best_blockhash.empty() ? 0.0f : copyW + Layout::spacingSm());
|
||||
std::string hashDisp = state.sync.best_blockhash.empty()
|
||||
? std::string("--")
|
||||
: truncateHashToFit(state.sync.best_blockhash, sub1, hashMaxW);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(cardMin.x + pad, hashY), OnSurface(), hashDisp.c_str());
|
||||
|
||||
if (!state.sync.best_blockhash.empty()) {
|
||||
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
|
||||
copyButton("ChainTipBestHash", state.sync.best_blockhash,
|
||||
cardMax.x - pad - copyW, hashY);
|
||||
ImGui::SetCursorScreenPos(savedCursor);
|
||||
}
|
||||
ry += rowH + Layout::spacingSm();
|
||||
};
|
||||
|
||||
// Row 3: Best Block Hash — adaptive truncation to fit card
|
||||
{
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
||||
float maxHashW = cardW - pad * 2;
|
||||
std::string hashDisp = truncateHashToFit(state.sync.best_blockhash, sub1, maxHashW);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), hashDisp.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mempool Card (right) ──
|
||||
{
|
||||
ImVec2 cardMin(basePos.x + cardW + gap, basePos.y);
|
||||
auto drawMetricGrid = [&](const ImVec2& cardMin, float cardW) {
|
||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
dl->AddText(ovFont, ovFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_mempool"));
|
||||
struct MetricSpec {
|
||||
const char* icon;
|
||||
const char* label;
|
||||
std::string value;
|
||||
std::string detail;
|
||||
ImU32 accent;
|
||||
};
|
||||
|
||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||
snprintf(buf, sizeof(buf), "%.4f", state.mining.difficulty);
|
||||
std::string difficulty = buf;
|
||||
snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||
std::string notarized = buf;
|
||||
snprintf(buf, sizeof(buf), "%d", s_mempool_tx_count);
|
||||
std::string mempoolTxs = s_mempool_loading ? std::string("...") : std::string(buf);
|
||||
|
||||
// Row 1: Transactions
|
||||
{
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_mempool_txs"));
|
||||
snprintf(buf, sizeof(buf), "%d", s_mempool_tx_count);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
MetricSpec metrics[4] = {
|
||||
{ ICON_MD_SPEED, TR("difficulty"), difficulty, "", Warning() },
|
||||
{ ICON_MD_SHOW_CHART, TR("peers_hashrate"), formatHashrate(state.mining.networkHashrate), "", Success() },
|
||||
{ ICON_MD_CONFIRMATION_NUMBER, TR("peers_notarized"), notarized, "", Primary() },
|
||||
{ ICON_MD_STORAGE, TR("explorer_mempool"), mempoolTxs, s_mempool_loading ? std::string("") : formatSize(static_cast<int>(s_mempool_size)), Secondary() },
|
||||
};
|
||||
|
||||
float gridX = cardMin.x + pad;
|
||||
float gridY = cardMin.y + pad;
|
||||
float gridW = cardW - pad * 2.0f;
|
||||
float gridH = cardH - pad * 2.0f;
|
||||
float cellW = gridW * 0.5f;
|
||||
float cellH = gridH * 0.5f;
|
||||
ImU32 dividerCol = WithAlpha(OnSurface(), 22);
|
||||
|
||||
dl->AddLine(ImVec2(gridX + cellW, gridY), ImVec2(gridX + cellW, gridY + gridH), dividerCol, 1.0f * dp);
|
||||
dl->AddLine(ImVec2(gridX, gridY + cellH), ImVec2(gridX + gridW, gridY + cellH), dividerCol, 1.0f * dp);
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int col = i % 2;
|
||||
int row = i / 2;
|
||||
ImVec2 cellMin(gridX + col * cellW, gridY + row * cellH);
|
||||
ImVec2 cellMax(cellMin.x + cellW, cellMin.y + cellH);
|
||||
float inset = Layout::spacingSm();
|
||||
float textX = cellMin.x + inset;
|
||||
float textY = cellMin.y + inset;
|
||||
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(cellMax.x - inset - iconFont->LegacySize, textY),
|
||||
WithAlpha(metrics[i].accent, 180), metrics[i].icon);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(textX, textY), OnSurfaceMedium(), metrics[i].label);
|
||||
|
||||
float valueY = textY + capFont->LegacySize + Layout::spacingXs();
|
||||
float valueMaxW = cellW - inset * 2.0f;
|
||||
std::string value = truncateHashToFit(metrics[i].value, sub1, valueMaxW);
|
||||
dl->AddText(sub1, sub1->LegacySize,
|
||||
ImVec2(textX, valueY), OnSurface(), value.c_str());
|
||||
|
||||
if (!metrics[i].detail.empty()) {
|
||||
std::string detail = truncateHashToFit(metrics[i].detail, capFont, valueMaxW);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(textX, valueY + sub1->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), detail.c_str());
|
||||
}
|
||||
}
|
||||
ry += rowH + Layout::spacingSm();
|
||||
};
|
||||
|
||||
// Row 2: Size
|
||||
{
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_mempool_size"));
|
||||
std::string sizeStr = formatSize(static_cast<int>(s_mempool_size));
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sizeStr.c_str());
|
||||
}
|
||||
drawChainTip(basePos, chainW);
|
||||
|
||||
if (stacked) {
|
||||
drawMetricGrid(ImVec2(basePos.x, basePos.y + cardH + gap), metricsW);
|
||||
ImGui::Dummy(ImVec2(availWidth, cardH * 2.0f + gap + Layout::spacingMd()));
|
||||
} else {
|
||||
drawMetricGrid(ImVec2(basePos.x + chainW + gap, basePos.y), metricsW);
|
||||
ImGui::Dummy(ImVec2(availWidth, cardH + Layout::spacingMd()));
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(availWidth, cardH + Layout::spacingMd()));
|
||||
}
|
||||
|
||||
static void renderRecentBlocks(App* app, float availWidth) {
|
||||
auto& S = schema::UI();
|
||||
float pad = Layout::cardInnerPadding();
|
||||
float dp = Layout::dpiScale();
|
||||
const auto& state = app->getWalletState();
|
||||
validateRecentBlockCache(app, state);
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
GlassPanelSpec glassSpec;
|
||||
@@ -564,19 +722,13 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
ImFont* body2 = Type().body2();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
|
||||
float rowH = S.drawElement("tabs.explorer", "row-height").size;
|
||||
float baseRowH = S.drawElement("tabs.explorer", "row-height").size;
|
||||
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size;
|
||||
float headerH = ovFont->LegacySize + Layout::spacingSm() + pad * 0.5f;
|
||||
|
||||
// Filter out empty entries
|
||||
std::vector<const BlockSummary*> blocks;
|
||||
for (const auto& bs : s_recent_blocks) {
|
||||
if (bs.height > 0) blocks.push_back(&bs);
|
||||
}
|
||||
|
||||
// Stretch card to fill the remaining tab height; rows scroll inside.
|
||||
float maxRows = 10.0f;
|
||||
float contentH = capFont->LegacySize + Layout::spacingXs() + rowH * maxRows;
|
||||
float contentH = capFont->LegacySize + Layout::spacingXs() + baseRowH * maxRows;
|
||||
float minTableH = headerH + contentH + pad;
|
||||
float remainingH = ImGui::GetContentRegionAvail().y;
|
||||
float tableH = std::max(minTableH, remainingH - Layout::spacingSm());
|
||||
@@ -585,10 +737,84 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + tableH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// Header
|
||||
dl->AddText(ovFont, ovFont->LegacySize,
|
||||
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_recent_blocks"));
|
||||
|
||||
float hdrY = cardMin.y + pad * 0.5f + ovFont->LegacySize + Layout::spacingSm();
|
||||
float rowAreaTop = hdrY + capFont->LegacySize + Layout::spacingXs();
|
||||
float rowAreaH = cardMax.y - rowAreaTop - pad * 0.5f;
|
||||
int rowsPerPage = std::max(1, (int)std::ceil(rowAreaH / std::max(1.0f, baseRowH)));
|
||||
float rowH = baseRowH;
|
||||
|
||||
int maxPage = (state.sync.blocks > 0) ? (state.sync.blocks - 1) / rowsPerPage : 0;
|
||||
s_recent_max_page = maxPage;
|
||||
if (s_recent_page > maxPage) s_recent_page = maxPage;
|
||||
if (s_recent_page < 0) s_recent_page = 0;
|
||||
|
||||
int startHeight = state.sync.blocks - s_recent_page * rowsPerPage;
|
||||
std::vector<int> pageHeights;
|
||||
pageHeights.reserve(rowsPerPage);
|
||||
for (int i = 0; i < rowsPerPage; ++i) {
|
||||
int height = startHeight - i;
|
||||
if (height < 1) break;
|
||||
pageHeights.push_back(height);
|
||||
}
|
||||
|
||||
std::map<int, BlockSummary> cachedBlocks;
|
||||
if (!pageHeights.empty()) {
|
||||
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
|
||||
}
|
||||
|
||||
bool canFetchBlocks = state.sync.blocks > 0 &&
|
||||
!s_show_detail_modal && !s_detail_loading && !s_tx_loading;
|
||||
if (canFetchBlocks) {
|
||||
bool currentPageComplete = true;
|
||||
bool scheduledFetch = false;
|
||||
auto* rpc = app->rpc();
|
||||
if (rpc && rpc->isConnected()) {
|
||||
for (int height : pageHeights) {
|
||||
if (cachedBlocks.find(height) == cachedBlocks.end()) {
|
||||
currentPageComplete = false;
|
||||
scheduledFetch = fetchRecentBlock(app, height) || scheduledFetch;
|
||||
}
|
||||
}
|
||||
if (currentPageComplete && s_recent_page < maxPage) {
|
||||
std::vector<int> nextPageHeights;
|
||||
nextPageHeights.reserve(rowsPerPage);
|
||||
int nextStartHeight = state.sync.blocks - (s_recent_page + 1) * rowsPerPage;
|
||||
for (int i = 0; i < rowsPerPage; ++i) {
|
||||
int height = nextStartHeight - i;
|
||||
if (height < 1) break;
|
||||
nextPageHeights.push_back(height);
|
||||
}
|
||||
std::map<int, BlockSummary> nextCachedBlocks;
|
||||
if (!nextPageHeights.empty()) {
|
||||
nextCachedBlocks = s_recent_block_cache.loadRange(nextPageHeights.back(), nextPageHeights.front());
|
||||
}
|
||||
for (int height : nextPageHeights) {
|
||||
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
|
||||
scheduledFetch = fetchRecentBlock(app, height) || scheduledFetch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scheduledFetch) {
|
||||
fetchMempoolInfo(app);
|
||||
}
|
||||
}
|
||||
|
||||
struct RecentBlockRow {
|
||||
int height = 0;
|
||||
const BlockSummary* block = nullptr;
|
||||
};
|
||||
|
||||
std::vector<RecentBlockRow> blocks;
|
||||
blocks.reserve(pageHeights.size());
|
||||
for (int height : pageHeights) {
|
||||
auto it = cachedBlocks.find(height);
|
||||
blocks.push_back({height,
|
||||
it != cachedBlocks.end() ? &it->second : nullptr});
|
||||
}
|
||||
// Responsive column layout — give height more room, use remaining for data
|
||||
float innerW = availWidth - pad * 2;
|
||||
float colHeight = pad;
|
||||
@@ -598,7 +824,6 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
float colHash = colHeight + innerW * 0.56f;
|
||||
float colTime = colHeight + innerW * 0.82f;
|
||||
|
||||
float hdrY = cardMin.y + pad * 0.5f + ovFont->LegacySize + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colHeight, hdrY), OnSurfaceMedium(), TR("explorer_block_height"));
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTxs, hdrY), OnSurfaceMedium(), TR("explorer_block_txs"));
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colSize, hdrY), OnSurfaceMedium(), TR("explorer_block_size"));
|
||||
@@ -607,13 +832,11 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTime, hdrY), OnSurfaceMedium(), TR("explorer_block_time"));
|
||||
|
||||
// Scrollable child region for rows
|
||||
float rowAreaTop = hdrY + capFont->LegacySize + Layout::spacingXs();
|
||||
float rowAreaH = cardMax.y - rowAreaTop - pad * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, rowAreaTop));
|
||||
|
||||
int parentVtx = dl->VtxBuffer.Size;
|
||||
ImGui::BeginChild("##BlockRows", ImVec2(availWidth, rowAreaH), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ApplySmoothScroll();
|
||||
|
||||
ImDrawList* childDL = ImGui::GetWindowDrawList();
|
||||
@@ -624,12 +847,14 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
char buf[128];
|
||||
float rowInset = 2 * dp;
|
||||
|
||||
if (blocks.empty() && s_pending_block_fetches > 0) {
|
||||
if (blocks.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
ImGui::TextDisabled("%s", TR("loading"));
|
||||
} else {
|
||||
for (size_t i = 0; i < blocks.size(); i++) {
|
||||
const auto* bs = blocks[i];
|
||||
const auto& row = blocks[i];
|
||||
const auto* bs = row.block;
|
||||
int blockHeight = bs ? bs->height : row.height;
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float rowW = ImGui::GetContentRegionAvail().x - rowInset * 2;
|
||||
|
||||
@@ -651,19 +876,19 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
}
|
||||
|
||||
// Hover highlight — clear clickable feedback
|
||||
if (hovered) {
|
||||
if (hovered && bs) {
|
||||
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 20), rowRound);
|
||||
childDL->AddRect(rowMin, rowMax, WithAlpha(Primary(), 40), rowRound);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
|
||||
// Selected highlight
|
||||
if (s_show_detail_modal && s_detail_height == bs->height) {
|
||||
if (bs && s_show_detail_modal && s_detail_height == bs->height) {
|
||||
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 25), rowRound);
|
||||
}
|
||||
|
||||
// Click opens block detail modal
|
||||
if (clicked && app->rpc() && app->rpc()->isConnected()) {
|
||||
if (bs && clicked && app->rpc() && app->rpc()->isConnected()) {
|
||||
fetchBlockDetail(app, bs->height);
|
||||
}
|
||||
|
||||
@@ -671,30 +896,32 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
float textY2 = rowPos.y + (rowH - body2->LegacySize) * 0.5f;
|
||||
|
||||
// Height — emphasized with larger font and primary color
|
||||
snprintf(buf, sizeof(buf), "#%d", bs->height);
|
||||
snprintf(buf, sizeof(buf), "#%d", blockHeight);
|
||||
childDL->AddText(sub1, sub1->LegacySize, ImVec2(cardMin.x + colHeight, textY),
|
||||
hovered ? Primary() : schema::UI().resolveColor("var(--primary-light)"), buf);
|
||||
(hovered && bs) ? Primary() : schema::UI().resolveColor("var(--primary-light)"), buf);
|
||||
|
||||
// Tx count
|
||||
snprintf(buf, sizeof(buf), "%d tx", bs->tx_count);
|
||||
if (bs) snprintf(buf, sizeof(buf), "%d tx", bs->tx_count);
|
||||
else snprintf(buf, sizeof(buf), "...");
|
||||
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTxs, textY2), OnSurface(), buf);
|
||||
|
||||
// Size
|
||||
std::string sizeStr = formatSize(bs->size);
|
||||
std::string sizeStr = bs ? formatSize(bs->size) : "...";
|
||||
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colSize, textY2), OnSurface(), sizeStr.c_str());
|
||||
|
||||
// Difficulty
|
||||
snprintf(buf, sizeof(buf), "%.2f", bs->difficulty);
|
||||
if (bs) snprintf(buf, sizeof(buf), "%.2f", bs->difficulty);
|
||||
else snprintf(buf, sizeof(buf), "...");
|
||||
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colDiff, textY2), OnSurface(), buf);
|
||||
|
||||
// Hash — adaptive to available column width
|
||||
float hashMaxW = (colTime - colHash) - Layout::spacingSm();
|
||||
std::string hashDisp = truncateHashToFit(bs->hash, body2, hashMaxW);
|
||||
std::string hashDisp = bs ? truncateHashToFit(bs->hash, body2, hashMaxW) : "...";
|
||||
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colHash, textY2),
|
||||
OnSurfaceMedium(), hashDisp.c_str());
|
||||
|
||||
// Time
|
||||
const char* timeStr = relativeTime(bs->time);
|
||||
const char* timeStr = bs ? relativeTime(bs->time) : "...";
|
||||
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTime, textY2), OnSurfaceMedium(), timeStr);
|
||||
}
|
||||
}
|
||||
@@ -1023,30 +1250,14 @@ static void renderBlockDetailModal(App* app) {
|
||||
|
||||
void RenderExplorerTab(App* app)
|
||||
{
|
||||
const auto& state = app->getWalletState();
|
||||
auto* rpc = app->rpc();
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##ExplorerScroll", avail, false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
ApplySmoothScroll();
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// Auto-refresh recent blocks when chain height changes, but avoid
|
||||
// starting expensive block fetches while the user is viewing details.
|
||||
if (state.sync.blocks > 0 && state.sync.blocks != s_last_known_height &&
|
||||
!s_show_detail_modal && !s_detail_loading && !s_tx_loading) {
|
||||
if (rpc && rpc->isConnected()) {
|
||||
if (fetchRecentBlocks(app, state.sync.blocks)) {
|
||||
s_last_known_height = state.sync.blocks;
|
||||
fetchMempoolInfo(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderSearchBar(app, availWidth);
|
||||
renderChainStats(app, availWidth);
|
||||
renderSearchBar(app, availWidth);
|
||||
renderRecentBlocks(app, availWidth);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -170,6 +170,7 @@ void ExportAllKeysDialog::render(App* app)
|
||||
keys += "# === Z-Addresses (Shielded) ===\n\n";
|
||||
for (const auto& addr : z_addrs) {
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export all keys");
|
||||
auto result = rpc->call("z_exportkey", {addr});
|
||||
if (result.is_string()) {
|
||||
keys += "# Address: " + addr + "\n";
|
||||
@@ -185,6 +186,7 @@ void ExportAllKeysDialog::render(App* app)
|
||||
keys += "# === T-Addresses (Transparent) ===\n\n";
|
||||
for (const auto& addr : t_addrs) {
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export all keys");
|
||||
auto result = rpc->call("dumpprivkey", {addr});
|
||||
if (result.is_string()) {
|
||||
keys += "# Address: " + addr + "\n";
|
||||
|
||||
@@ -309,7 +309,7 @@ void ImportKeyDialog::render(App* app)
|
||||
bool rescan = s_rescan;
|
||||
int rescanHeight = s_rescan_height;
|
||||
if (app->worker()) {
|
||||
app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
|
||||
app->worker()->post([app, rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
|
||||
int imported = 0;
|
||||
int failed = 0;
|
||||
|
||||
@@ -317,6 +317,7 @@ void ImportKeyDialog::render(App* app)
|
||||
std::string keyType = detectKeyType(key);
|
||||
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import private keys");
|
||||
if (keyType == "z-spending") {
|
||||
// z_importkey "key" "yes"|"no" startheight
|
||||
if (rescan && rescanHeight > 0) {
|
||||
@@ -348,13 +349,14 @@ void ImportKeyDialog::render(App* app)
|
||||
// single rescanblockchain from that height now.
|
||||
if (rescan && rescanHeight > 0 && imported > 0) {
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import private keys");
|
||||
rpc->call("rescanblockchain", {rescanHeight});
|
||||
} catch (...) {
|
||||
// rescan failure is non-fatal; user can retry
|
||||
}
|
||||
}
|
||||
|
||||
return [imported, failed]() {
|
||||
return [app, imported, failed]() {
|
||||
s_imported_keys = imported;
|
||||
s_failed_keys = failed;
|
||||
s_importing = false;
|
||||
@@ -363,6 +365,8 @@ void ImportKeyDialog::render(App* app)
|
||||
imported, failed);
|
||||
s_status = buf;
|
||||
if (imported > 0) {
|
||||
app->invalidateAddressValidationCache();
|
||||
app->refreshNow();
|
||||
Notifications::instance().success(TR("import_key_success"), 5.0f);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,6 +127,7 @@ void KeyExportDialog::render(App* app)
|
||||
std::string key;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export key");
|
||||
auto result = rpc->call(method, {addr});
|
||||
key = result.get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
@@ -152,6 +153,7 @@ void KeyExportDialog::render(App* app)
|
||||
std::string key;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export viewing key");
|
||||
auto result = rpc->call("z_exportviewingkey", {addr});
|
||||
key = result.get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
|
||||
@@ -162,15 +162,6 @@ void RenderMarketTab(App* app)
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// Accent stripe — clipped to card rounded corners
|
||||
{
|
||||
float sw = S.drawElement("tabs.market", "accent-stripe-width").size;
|
||||
dl->PushClipRect(cardMin, ImVec2(cardMin.x + sw, cardMax.y), true);
|
||||
dl->AddRectFilled(cardMin, cardMax, WithAlpha(S.resolveColor("var(--accent-market)", Success()), 200),
|
||||
glassSpec.rounding, ImDrawFlags_RoundCornersLeft);
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
float cx = cardMin.x + Layout::spacingLg();
|
||||
float cy = cardMin.y + Layout::spacingLg();
|
||||
|
||||
@@ -797,15 +788,6 @@ void RenderMarketTab(App* app)
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// Accent stripe
|
||||
{
|
||||
float sw = S.drawElement("tabs.market", "accent-stripe-width").size;
|
||||
dl->PushClipRect(cardMin, ImVec2(cardMin.x + sw, cardMax.y), true);
|
||||
dl->AddRectFilled(cardMin, cardMax, WithAlpha(S.resolveColor("var(--accent-portfolio)", Secondary()), 200),
|
||||
glassSpec.rounding, ImDrawFlags_RoundCornersLeft);
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
float cx = cardMin.x + Layout::spacingLg();
|
||||
float cy = cardMin.y + Layout::spacingLg();
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ void RenderPeersTab(App* app)
|
||||
|
||||
// Click to copy full hash
|
||||
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
|
||||
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, valY));
|
||||
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -360,6 +361,7 @@ void RenderPeersTab(App* app)
|
||||
ImGui::SetClipboardText(hash.c_str());
|
||||
ui::Notifications::instance().info(TR("peers_hash_copied"));
|
||||
}
|
||||
ImGui::SetCursorScreenPos(savedCursor);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
@@ -482,6 +484,7 @@ void RenderPeersTab(App* app)
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), bannedCol, buf); }
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(basePos);
|
||||
ImGui::Dummy(ImVec2(availWidth, infoCardsH));
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
// - Recent received at bottom
|
||||
|
||||
#include "receive_tab.h"
|
||||
#include "send_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
@@ -360,145 +359,99 @@ static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col)
|
||||
// ============================================================================
|
||||
// Recent received transactions — styled to match transactions list
|
||||
// ============================================================================
|
||||
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
|
||||
static void RenderRecentReceived(const AddressInfo& /* addr */,
|
||||
const WalletState& state, float width,
|
||||
ImFont* capFont, App* app) {
|
||||
auto& S = schema::UI();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float hs = Layout::hScale(width);
|
||||
float glassRound = Layout::glassRounding();
|
||||
|
||||
ImFont* body2 = Type().body2();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
||||
float iconSz = std::max(schema::UI().drawElement("tabs.receive", "recent-icon-min-size").size, schema::UI().drawElement("tabs.receive", "recent-icon-size").size * hs);
|
||||
float vs = Layout::vScale(std::max(1.0f, ImGui::GetContentRegionAvail().y));
|
||||
float dp = Layout::dpiScale();
|
||||
float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
ImU32 recvCol = Success();
|
||||
ImU32 greenCol = WithAlpha(Success(), (int)schema::UI().drawElement("tabs.receive", "recent-green-alpha").size);
|
||||
float rowPadLeft = Layout::spacingLg();
|
||||
|
||||
// Collect matching transactions
|
||||
std::vector<const TransactionInfo*> recvs;
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type != "receive") continue;
|
||||
if (tx.type != "receive" && tx.type != "mined") continue;
|
||||
recvs.push_back(&tx);
|
||||
if (recvs.size() >= (size_t)schema::UI().drawElement("tabs.receive", "max-recent-receives").size) break;
|
||||
}
|
||||
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
|
||||
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
ImDrawList* rowDL = ImGui::GetWindowDrawList();
|
||||
|
||||
char buf[64];
|
||||
if (recvs.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
// Outer glass panel wrapping all rows
|
||||
float itemSpacingY = ImGui::GetStyle().ItemSpacing.y;
|
||||
float listH = rowH * (float)recvs.size() + itemSpacingY * (float)(recvs.size() - 1);
|
||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 listPanelMax(listPanelMin.x + width, listPanelMin.y + listH);
|
||||
GlassPanelSpec glassSpec;
|
||||
glassSpec.rounding = glassRound;
|
||||
DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);
|
||||
|
||||
// Clip draw commands to panel bounds to prevent overflow
|
||||
dl->PushClipRect(listPanelMin, listPanelMax, true);
|
||||
|
||||
char buf[64];
|
||||
for (size_t ri = 0; ri < recvs.size(); ri++) {
|
||||
const auto& tx = *recvs[ri];
|
||||
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowEnd(rowPos.x + width, rowPos.y + rowH);
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
|
||||
// Hover glow
|
||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
||||
if (hovered) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.receive", "row-hover-alpha").size), schema::UI().drawElement("tabs.receive", "row-hover-rounding").size);
|
||||
// Icon
|
||||
DrawRecvIcon(rowDL, rowPos.x + Layout::spacingMd(), rowY, iconSz * 0.5f, recvCol);
|
||||
|
||||
// Type label (first line)
|
||||
float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
const char* typeText = tx.type == "mined" ? TR("mined_type") : TR("received_label");
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), typeText);
|
||||
|
||||
// Address (second line)
|
||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
std::string addrDisplay = TruncateAddress(tx.address,
|
||||
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
|
||||
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);
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf);
|
||||
|
||||
// Time ago
|
||||
std::string ago = recvTimeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
|
||||
rowPos.y + 2.0f * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Clickable row — hover highlight + navigate to History
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
rowDL->AddRectFilled(rowPos, rowEnd,
|
||||
IM_COL32(255, 255, 255, 15),
|
||||
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) {
|
||||
app->setCurrentPage(ui::NavPage::History);
|
||||
}
|
||||
}
|
||||
|
||||
float cx = rowPos.x + rowPadLeft;
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// Icon
|
||||
DrawRecvIcon(dl, cx + iconSz, cy + body2->LegacySize * 0.5f, iconSz, recvCol);
|
||||
|
||||
// Type label (first line)
|
||||
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, TR("received_label"));
|
||||
|
||||
// Time (next to type)
|
||||
std::string ago = recvTimeAgo(tx.timestamp);
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("received_label")).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Address (second line)
|
||||
std::string addr_display = TruncateAddress(tx.address, (int)schema::UI().drawElement("tabs.receive", "recent-addr-trunc-len").size);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), addr_display.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
snprintf(buf, sizeof(buf), "+%.8f", tx.amount);
|
||||
ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf);
|
||||
float amtX = rowPos.x + width - amtSz.x - Layout::spacingLg();
|
||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), recvCol, buf,
|
||||
schema::UI().drawElement("tabs.receive", "text-shadow-offset-x").size, schema::UI().drawElement("tabs.receive", "text-shadow-offset-y").size, IM_COL32(0, 0, 0, (int)schema::UI().drawElement("tabs.receive", "text-shadow-alpha").size));
|
||||
|
||||
// USD equivalent (right-aligned, second line)
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0) {
|
||||
double usdVal = tx.amount * priceUsd;
|
||||
if (usdVal >= 1.0)
|
||||
snprintf(buf, sizeof(buf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "$%.4f", usdVal);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rowPos.x + width - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// Status badge
|
||||
{
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < (int)schema::UI().drawElement("tabs.receive", "confirmed-threshold").size) {
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else {
|
||||
statusStr = TR("confirmed"); statusCol = greenCol;
|
||||
}
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
float minStatusX = cx + width * schema::UI().drawElement("tabs.receive", "status-min-x-ratio").size;
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>((int)schema::UI().drawElement("tabs.receive", "status-pill-bg-alpha").size) << 24);
|
||||
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + (int)schema::UI().drawElement("tabs.receive", "status-pill-y-offset").size);
|
||||
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.receive", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
|
||||
// Subtle divider between rows
|
||||
if (ri < recvs.size() - 1) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(ImVec2(divStart.x + rowPadLeft + iconSz * 2.0f, divStart.y),
|
||||
ImVec2(divStart.x + width - Layout::spacingLg(), divStart.y),
|
||||
IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.receive", "row-divider-alpha").size));
|
||||
}
|
||||
}
|
||||
|
||||
dl->PopClipRect();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -843,23 +796,6 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
|
||||
}
|
||||
|
||||
// Clear button
|
||||
{
|
||||
bool hasData = (s_request_amount > 0 || s_request_memo[0]);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "clear-btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
|
||||
ImGui::BeginDisabled(!hasData);
|
||||
if (TactileSmallButton(TrId("clear_request", "recv").c_str(), S.resolveFont("button"))) {
|
||||
s_request_amount = 0.0;
|
||||
s_request_usd_amount = 0.0;
|
||||
s_request_memo[0] = '\0';
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
}
|
||||
float leftBottom = ImGui::GetCursorScreenPos().y;
|
||||
|
||||
@@ -869,9 +805,14 @@ void RenderReceiveTab(App* app)
|
||||
float rx = qrColX;
|
||||
float ry = sectionTop.y;
|
||||
|
||||
float maxQrSize = std::min(qrAvailW - Layout::spacingMd() * 2, S.drawElement("tabs.receive", "qr-max-size").size);
|
||||
float qrSize = std::max(S.drawElement("tabs.receive", "qr-min-size").size, maxQrSize);
|
||||
float qrPadding = Layout::spacingMd();
|
||||
float qrWidthBound = std::max(0.0f, qrAvailW - qrPadding * 2.0f);
|
||||
float qrHeightBound = std::max(0.0f, leftBottom - sectionTop.y - qrPadding * 2.0f);
|
||||
float qrMaxSize = qrHeightBound > 0.0f
|
||||
? std::min(qrWidthBound, qrHeightBound)
|
||||
: qrWidthBound;
|
||||
float qrMinSize = std::min(S.drawElement("tabs.receive", "qr-min-size").size, qrWidthBound);
|
||||
float qrSize = std::max(qrMinSize, qrMaxSize);
|
||||
float totalQrSize = qrSize + qrPadding * 2;
|
||||
float qrOffsetX = std::max(0.0f, (qrAvailW - totalQrSize) * 0.5f);
|
||||
ImVec2 qrPanelMin(rx + qrOffsetX, ry);
|
||||
@@ -917,6 +858,18 @@ void RenderReceiveTab(App* app)
|
||||
}
|
||||
|
||||
// Divider before action buttons
|
||||
{
|
||||
float actionBtnH = std::max(S.drawElement("tabs.receive", "action-btn-min-height").size,
|
||||
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
|
||||
float footerH = innerGap + actionBtnH + pad;
|
||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||
float targetCardH = Layout::mainCardTargetH(formW, vScale);
|
||||
float footerTopH = targetCardH - footerH;
|
||||
if (currentCardH < footerTopH) {
|
||||
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||
{
|
||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
||||
@@ -964,6 +917,25 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
{
|
||||
bool hasData = (s_request_amount > 0 || s_request_memo[0]);
|
||||
if (!firstBtn) ImGui::SameLine(0, btnGap);
|
||||
firstBtn = false;
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "clear-btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
|
||||
ImGui::BeginDisabled(!hasData);
|
||||
if (TactileButton(TrId("clear_request", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
s_request_amount = 0.0;
|
||||
s_request_usd_amount = 0.0;
|
||||
s_request_memo[0] = '\0';
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
if (!firstBtn) ImGui::SameLine(0, btnGap);
|
||||
firstBtn = false;
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
@@ -978,20 +950,6 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PopStyleVar(); // FrameBorderSize
|
||||
ImGui::PopStyleColor(4);
|
||||
|
||||
if (selected.balance > 0) {
|
||||
if (!firstBtn) ImGui::SameLine(0, btnGap);
|
||||
firstBtn = false;
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
|
||||
if (TactileButton(TrId("send", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
SetSendFromAddress(selected.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Bottom padding
|
||||
@@ -1020,7 +978,7 @@ void RenderReceiveTab(App* app)
|
||||
// ================================================================
|
||||
// RECENT RECEIVED
|
||||
// ================================================================
|
||||
RenderRecentReceived(dl, selected, state, formW, capFont, app);
|
||||
RenderRecentReceived(selected, state, formW, capFont, app);
|
||||
|
||||
ImGui::EndGroup();
|
||||
float measuredH = ImGui::GetCursorPosY() - contentStartY;
|
||||
|
||||
@@ -971,8 +971,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
// ============================================================================
|
||||
// Recent Sends section — styled to match transactions list
|
||||
// ============================================================================
|
||||
static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
float width, ImFont* capFont, App* app) {
|
||||
static void RenderRecentSends(const WalletState& state, float width, ImFont* capFont, App* app) {
|
||||
auto& S = schema::UI();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recent_sends"));
|
||||
@@ -980,139 +979,89 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float hs = Layout::hScale(avail.x);
|
||||
float glassRound = Layout::glassRounding();
|
||||
|
||||
ImFont* body2 = Type().body2();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
||||
float iconSz = std::max(schema::UI().drawElement("tabs.send", "recent-icon-min-size").size, schema::UI().drawElement("tabs.send", "recent-icon-size").size * hs);
|
||||
float vs = Layout::vScale(std::max(1.0f, avail.y));
|
||||
float dp = Layout::dpiScale();
|
||||
float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
ImU32 sendCol = Error();
|
||||
ImU32 greenCol = WithAlpha(Success(), (int)S.drawElement("tabs.send", "recent-green-alpha").size);
|
||||
float rowPadLeft = Layout::spacingLg();
|
||||
|
||||
// Collect matching transactions
|
||||
std::vector<const TransactionInfo*> sends;
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type != "send") continue;
|
||||
if (tx.type != "send" && tx.type != "shield") continue;
|
||||
sends.push_back(&tx);
|
||||
if (sends.size() >= (size_t)S.drawElement("tabs.send", "max-recent-sends").size) break;
|
||||
}
|
||||
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
|
||||
ImGui::BeginChild("##RecentSendRows", ImVec2(width, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
ImDrawList* rowDL = ImGui::GetWindowDrawList();
|
||||
|
||||
char buf[64];
|
||||
if (sends.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
// Outer glass panel wrapping all rows
|
||||
float itemSpacingY = ImGui::GetStyle().ItemSpacing.y;
|
||||
float listH = rowH * (float)sends.size() + itemSpacingY * (float)(sends.size() - 1);
|
||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||
float listW = width;
|
||||
ImVec2 listPanelMax(listPanelMin.x + listW, listPanelMin.y + listH);
|
||||
GlassPanelSpec glassSpec;
|
||||
glassSpec.rounding = glassRound;
|
||||
DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);
|
||||
|
||||
// Clip draw commands to panel bounds to prevent overflow
|
||||
dl->PushClipRect(listPanelMin, listPanelMax, true);
|
||||
|
||||
char buf[64];
|
||||
for (size_t si = 0; si < sends.size(); si++) {
|
||||
const auto& tx = *sends[si];
|
||||
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float innerW = listW;
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
|
||||
// Hover glow
|
||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
||||
if (hovered) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "row-hover-alpha").size), schema::UI().drawElement("tabs.send", "row-hover-rounding").size);
|
||||
// Icon
|
||||
DrawTxIcon(rowDL, "send", rowPos.x + Layout::spacingMd(), rowY, iconSz, sendCol);
|
||||
|
||||
// Type label (first line)
|
||||
float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), TR("sent_type"));
|
||||
|
||||
// Address (second line)
|
||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
std::string addrDisplay = TruncateAddress(tx.address,
|
||||
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
snprintf(buf, sizeof(buf), "-%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
|
||||
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);
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2.0f * dp), sendCol, buf);
|
||||
|
||||
// Time ago
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
|
||||
rowPos.y + 2.0f * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Clickable row — hover highlight + navigate to History
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
rowDL->AddRectFilled(rowPos, rowEnd,
|
||||
IM_COL32(255, 255, 255, 15),
|
||||
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) {
|
||||
app->setCurrentPage(ui::NavPage::History);
|
||||
}
|
||||
}
|
||||
|
||||
float cx = rowPos.x + rowPadLeft;
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// Icon
|
||||
DrawTxIcon(dl, tx.type, cx + iconSz, cy + body2->LegacySize * 0.5f, iconSz, sendCol);
|
||||
|
||||
// Type label (first line)
|
||||
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, TR("sent_upper"));
|
||||
|
||||
// Time (next to type)
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("sent_upper")).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Address (second line)
|
||||
std::string addr_display = TruncateAddress(tx.address, (int)S.drawElement("tabs.send", "recent-addr-trunc-len").size);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), addr_display.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
snprintf(buf, sizeof(buf), "-%.8f", std::abs(tx.amount));
|
||||
ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf);
|
||||
float amtX = rowPos.x + innerW - amtSz.x - Layout::spacingLg();
|
||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), sendCol, buf,
|
||||
S.drawElement("tabs.send", "text-shadow-offset-x").size, S.drawElement("tabs.send", "text-shadow-offset-y").size, IM_COL32(0, 0, 0, (int)S.drawElement("tabs.send", "text-shadow-alpha").size));
|
||||
|
||||
// USD equivalent (right-aligned, second line)
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0) {
|
||||
double usdVal = std::abs(tx.amount) * priceUsd;
|
||||
if (usdVal >= 1.0)
|
||||
snprintf(buf, sizeof(buf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "$%.4f", usdVal);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rowPos.x + innerW - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// Status badge
|
||||
{
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < (int)S.drawElement("tabs.send", "confirmed-threshold").size) {
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else {
|
||||
statusStr = TR("confirmed"); statusCol = greenCol;
|
||||
}
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
float minStatusX = cx + innerW * S.drawElement("tabs.send", "status-min-x-ratio").size;
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>((int)S.drawElement("tabs.send", "status-pill-bg-alpha").size) << 24);
|
||||
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + (int)S.drawElement("tabs.send", "status-pill-y-offset").size);
|
||||
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.send", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
|
||||
// Subtle divider between rows
|
||||
if (si < sends.size() - 1) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(ImVec2(divStart.x + rowPadLeft + iconSz * 2.0f, divStart.y),
|
||||
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "row-divider-alpha").size));
|
||||
}
|
||||
}
|
||||
|
||||
dl->PopClipRect();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1456,6 +1405,18 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
|
||||
// Divider before action buttons
|
||||
{
|
||||
float actionBtnH = std::max(S.drawElement("tabs.send", "action-btn-min-height").size,
|
||||
S.drawElement("tabs.send", "action-btn-height").size * vScale);
|
||||
float footerH = innerGap + actionBtnH + pad * vScale;
|
||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||
float targetCardH = Layout::mainCardTargetH(formW, vScale);
|
||||
float footerTopH = targetCardH - footerH;
|
||||
if (currentCardH < footerTopH) {
|
||||
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||
{
|
||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
||||
@@ -1507,7 +1468,7 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
|
||||
// ---- RECENT SENDS ----
|
||||
RenderRecentSends(dl, state, formW, capFont, app);
|
||||
RenderRecentSends(state, formW, capFont, app);
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndDisabled(); // sendSyncing guard
|
||||
|
||||
@@ -429,6 +429,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
|
||||
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
|
||||
if (app->rpc()) {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Test connection");
|
||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
||||
if (error.empty()) {
|
||||
std::string version = result.value("version", "unknown");
|
||||
|
||||
@@ -185,6 +185,7 @@ void ShieldDialog::render(App* app)
|
||||
nlohmann::json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Send tab / Shield coinbase");
|
||||
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
@@ -215,6 +216,7 @@ void ShieldDialog::render(App* app)
|
||||
nlohmann::json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Send tab / Merge funds");
|
||||
result = rpc->call("z_mergetoaddress", {addrs, to, fee, 0, limit});
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
@@ -258,6 +260,7 @@ void ShieldDialog::render(App* app)
|
||||
nlohmann::json result;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Send tab / Shield operation status");
|
||||
nlohmann::json ids = nlohmann::json::array();
|
||||
ids.push_back(opid);
|
||||
result = rpc->call("z_getoperationstatus", {ids});
|
||||
|
||||
@@ -104,7 +104,7 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
|
||||
} else if (type == "receive") {
|
||||
icon = ICON_MD_CALL_RECEIVED;
|
||||
} else if (type == "shield") {
|
||||
icon = ICON_MD_SHIELD;
|
||||
icon = ICON_MD_CALL_MADE;
|
||||
} else {
|
||||
icon = ICON_MD_CONSTRUCTION;
|
||||
}
|
||||
@@ -147,6 +147,8 @@ void RenderTransactionsTab(App* app)
|
||||
ImU32 greenCol = Success();
|
||||
ImU32 redCol = Error();
|
||||
ImU32 goldCol = Warning();
|
||||
std::string txLoadingText = app->transactionRefreshProgressText();
|
||||
bool txLoading = !txLoadingText.empty();
|
||||
|
||||
// Expanded row index for inline detail
|
||||
static int s_expanded_row = -1;
|
||||
@@ -319,6 +321,18 @@ void RenderTransactionsTab(App* app)
|
||||
ExportTransactionsDialog::show();
|
||||
}
|
||||
|
||||
if (txLoading) {
|
||||
ImGui::SameLine(0, filterGap);
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
float pulse = 0.55f + 0.45f * std::sin((float)ImGui::GetTime() * 3.0f);
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
|
||||
|
||||
// ================================================================
|
||||
@@ -415,8 +429,8 @@ void RenderTransactionsTab(App* app)
|
||||
for (size_t i = 0; i < display_txns.size(); i++) {
|
||||
const auto& dtx = display_txns[i];
|
||||
if (type_filter != 0) {
|
||||
if (type_filter == 1 && dtx.display_type != "send") continue;
|
||||
if (type_filter == 2 && dtx.display_type != "receive" && dtx.display_type != "shield") continue;
|
||||
if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
|
||||
if (type_filter == 2 && dtx.display_type != "receive") continue;
|
||||
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
|
||||
}
|
||||
if (!search_str.empty()) {
|
||||
@@ -567,7 +581,14 @@ void RenderTransactionsTab(App* app)
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
} else if (state.transactions.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
||||
if (txLoading) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
} else {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
||||
}
|
||||
} else if (filtered_indices.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
|
||||
@@ -596,10 +617,11 @@ void RenderTransactionsTab(App* app)
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
|
||||
// Determine type info
|
||||
bool shieldedDisplay = tx.display_type == "shield";
|
||||
ImU32 iconCol;
|
||||
const char* typeStr;
|
||||
if (tx.display_type == "shield") {
|
||||
iconCol = Primary(); typeStr = TR("shielded_type");
|
||||
if (shieldedDisplay) {
|
||||
iconCol = redCol; typeStr = TR("sent_type");
|
||||
} else if (tx.display_type == "receive") {
|
||||
iconCol = greenCol; typeStr = TR("recv_type");
|
||||
} else if (tx.display_type == "send") {
|
||||
@@ -627,7 +649,8 @@ void RenderTransactionsTab(App* app)
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// Icon
|
||||
DrawTxIcon(dl, tx.display_type, cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
|
||||
DrawTxIcon(dl, shieldedDisplay ? "send" : tx.display_type,
|
||||
cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
|
||||
|
||||
// Type label
|
||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
||||
@@ -687,16 +710,35 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
// Position status badge in the middle-right area
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
const char* shieldedStr = TR("shielded_type");
|
||||
ImVec2 shieldSz = shieldedDisplay
|
||||
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr)
|
||||
: ImVec2(0, 0);
|
||||
float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f;
|
||||
float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x;
|
||||
float statusX = amtX - stackW - Layout::spacingXxl();
|
||||
float minStatusX = cx + innerW * 0.25f; // don't overlap address
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
float statusTextX = statusX + (stackW - sSz.x) * 0.5f;
|
||||
if (shieldedDisplay) {
|
||||
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
|
||||
ImU32 shieldCol = Primary();
|
||||
ImU32 shieldBg = (shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
|
||||
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
|
||||
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
|
||||
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(shieldPillMin, shieldPillMax, shieldBg,
|
||||
schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(shieldX, cy), shieldCol, shieldedStr);
|
||||
}
|
||||
// Background pill
|
||||
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
|
||||
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||
ImVec2 pillMax(statusTextX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
}
|
||||
|
||||
// Click to expand/collapse + invisible button for interaction
|
||||
|
||||
@@ -91,6 +91,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
bool valid = false, mine = false;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
|
||||
auto result = rpc->call("validateaddress", {address});
|
||||
valid = result.value("isvalid", false);
|
||||
mine = result.value("ismine", false);
|
||||
@@ -117,6 +118,7 @@ void ValidateAddressDialog::render(App* app)
|
||||
bool valid = false, mine = false;
|
||||
std::string error;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
|
||||
auto result = rpc->call("validateaddress", {address});
|
||||
valid = result.value("isvalid", false);
|
||||
mine = result.value("ismine", false);
|
||||
|
||||
Reference in New Issue
Block a user