Files
ObsidianDragon/src/ui/windows/block_info_dialog.cpp
DanS de11850c74 refactor(audit): batch 4 — UI design-system helpers + i18n
Mechanical, behavior-preserving consolidations from the audit. Rendering
extractions were kept byte-equivalent; sites that couldn't be made identical
were left as-is (noted below).

Shared helpers:
- util::truncateMiddle(s,maxLen) / (s,front,back) in text_format.h — replaces
  6 file-local middle-ellipsis truncators + several inline substr sites
  (send/receive/transactions/balance_recent_tx/explorer + app.cpp + 3 dialogs).
  Carries the maxLen<=3 guard, fixing the latent unsigned-underflow copies.
- material::LoadingDots() in draw_helpers.h — one animated-ellipsis source for
  8 copy-pasted spinner sites (identical GetTime()*3 phase preserved).
- Reuse the existing FormatHashrate() in explorer_tab + peers_tab (dropped two
  inline hashrate ladders).
- material::DrawButtonGlassOverlay() — the glass-fill/rim/tactile overlay block
  shared by TactileButton / TactileSmallButton / schema TactileButton.
- material::CollapsibleHeader() — the invisible-button + label + chevron idiom
  (3 of 5 settings_page sites; RPC/Debug headers skipped — non-identical).
- material::GlassCardScope (RAII) — the ChannelsSplit/Indent/DrawGlassPanel/
  ChannelsMerge card scaffold (5 settings_page cards; About/send/receive skipped
  — different padding / logo interleaving).
- material::DialogWarningHeader()/DialogConfirmFooter() — warning header +
  50/50 Cancel/danger footer across the confirm dialogs; button height moved
  from a hardcoded 40px into ui.toml (components.overlay-dialog.confirm-btn-height).
- File-local enterLowSpec()/exitLowSpec() collapse the 3 low-spec snapshot copies.

i18n: wrapped hardcoded English in the shared balance render paths and the
whole Lite lifecycle/security section in TR(), with English defaults added to
loadBuiltinEnglish() (res/lang/*.json left for the translation tooling — TR
falls back to English, so output is unchanged).

Full-node + Lite build clean; ctest 1/1; hygiene clean. These are rendering
changes — the Settings page (cards/headers/confirm dialogs), all tactile
buttons, and the Lite settings section warrant a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:29:12 -05:00

326 lines
11 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "block_info_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <string>
#include <ctime>
namespace dragonx {
namespace ui {
using json = nlohmann::json;
// Static state
static bool s_open = false;
static int s_height = 0;
static bool s_loading = false;
static bool s_has_data = false;
static std::string s_error;
// Block data
static std::string s_block_hash;
static int64_t s_block_time = 0;
static int s_tx_count = 0;
static int s_block_size = 0;
static std::string s_bits;
static double s_difficulty = 0.0;
static std::string s_prev_hash;
static std::string s_next_hash;
static std::string s_merkle_root;
static int s_confirmations = 0;
// Pending RPC app pointer (for async callback)
static App* s_pending_app = nullptr;
void BlockInfoDialog::show(int initialHeight)
{
s_open = true;
s_height = initialHeight > 0 ? initialHeight : 1;
s_loading = false;
s_has_data = false;
s_error.clear();
}
// Callback to handle getblock response
static void handleBlockResponseUnified(const json& result, const std::string& error)
{
s_loading = false;
if (!error.empty()) {
s_error = "Error: " + error;
return;
}
if (!result.is_null()) {
auto block = result;
s_block_hash = block.value("hash", "");
s_block_time = block.value("time", (int64_t)0);
s_confirmations = block.value("confirmations", 0);
s_block_size = block.value("size", 0);
s_bits = block.value("bits", "");
s_difficulty = block.value("difficulty", 0.0);
s_prev_hash = block.value("previousblockhash", "");
s_next_hash = block.value("nextblockhash", "");
s_merkle_root = block.value("merkleroot", "");
if (block.contains("tx") && block["tx"].is_array()) {
s_tx_count = static_cast<int>(block["tx"].size());
} else {
s_tx_count = 0;
}
s_has_data = true;
} else {
s_error = "Invalid response from daemon";
}
}
void BlockInfoDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.block-info");
auto heightInput = S.input("dialogs.block-info", "height-input");
auto lbl = S.label("dialogs.block-info", "label");
auto hashLbl = S.label("dialogs.block-info", "hash-label");
auto hashFrontLbl = S.label("dialogs.block-info", "hash-front-label");
auto hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
auto closeBtn = S.button("dialogs.block-info", "close-button");
if (material::BeginOverlayDialog(TR("block_info_title"), &s_open, win.width, 0.94f)) {
auto* rpc = app->rpc();
const auto& state = app->getWalletState();
// Height input
ImGui::Text("%s", TR("block_height"));
ImGui::SetNextItemWidth(heightInput.width);
ImGui::InputInt("##Height", &s_height);
if (s_height < 1) s_height = 1;
ImGui::SameLine();
// Current block info
if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
}
ImGui::SameLine();
// Fetch button
if (s_loading) {
ImGui::BeginDisabled();
}
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (rpc && rpc->isConnected() && app->worker()) {
s_loading = true;
s_error.clear();
s_has_data = false;
s_pending_app = app;
// Run the two chained RPCs (getblockhash → getblock) on the worker thread;
// doing them inline froze the UI for two round-trips. Guard the hash type.
int height = s_height;
app->worker()->post([rpc, height]() -> rpc::RPCWorker::MainCb {
json block;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) {
error = "unexpected getblockhash result";
} else {
block = rpc->call("getblock", {hashResult.get<std::string>()});
}
} catch (const std::exception& e) {
error = e.what();
}
return [block, error]() { handleBlockResponseUnified(block, error); };
});
}
}
if (s_loading) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("%s", TR("loading"));
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Error display
if (!s_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::TextWrapped("%s", s_error.c_str());
ImGui::PopStyleColor();
}
// Block info display
if (s_has_data) {
// Block hash
ImGui::Text("%s", TR("block_hash"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
ImGui::TextWrapped("%s", s_block_hash.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("click_to_copy"));
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(s_block_hash.c_str());
Notifications::instance().success(TR("block_hash_copied"));
}
ImGui::Spacing();
// Timestamp
ImGui::Text("%s", TR("block_timestamp"));
ImGui::SameLine(lbl.position);
if (s_block_time > 0) {
std::time_t t = static_cast<std::time_t>(s_block_time);
char time_buf[64];
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
ImGui::Text("%s", time_buf);
} else {
ImGui::TextDisabled("%s", TR("unknown"));
}
// Confirmations
ImGui::Text("%s", TR("confirmations"));
ImGui::SameLine(lbl.position);
ImGui::Text("%d", s_confirmations);
// Transaction count
ImGui::Text("%s", TR("block_transactions"));
ImGui::SameLine(lbl.position);
ImGui::Text("%d", s_tx_count);
// Size
ImGui::Text("%s", TR("block_size"));
ImGui::SameLine(lbl.position);
if (s_block_size > 1024 * 1024) {
ImGui::Text("%.2f MB", s_block_size / (1024.0 * 1024.0));
} else if (s_block_size > 1024) {
ImGui::Text("%.2f KB", s_block_size / 1024.0);
} else {
ImGui::Text("%d bytes", s_block_size);
}
// Difficulty
ImGui::Text("%s", TR("difficulty"));
ImGui::SameLine(lbl.position);
ImGui::Text("%.4f", s_difficulty);
// Bits
ImGui::Text("%s", TR("block_bits"));
ImGui::SameLine(lbl.position);
ImGui::Text("%s", s_bits.c_str());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Merkle root
ImGui::Text("%s", TR("block_merkle_root"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
ImGui::TextWrapped("%s", s_merkle_root.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
// Previous block
if (!s_prev_hash.empty()) {
ImGui::Text("%s", TR("block_previous"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
// Truncate for display
std::string prev_short = s_prev_hash;
if (prev_short.length() > static_cast<size_t>(hashLbl.truncate)) {
prev_short = util::truncateMiddle(prev_short, hashFrontLbl.truncate, hashBackLbl.truncate);
}
ImGui::Text("%s", prev_short.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("block_click_prev"));
}
if (ImGui::IsItemClicked() && s_height > 1) {
s_height--;
s_has_data = false;
}
}
// Next block
if (!s_next_hash.empty()) {
ImGui::Text("%s", TR("block_next"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
// Truncate for display
std::string next_short = s_next_hash;
if (next_short.length() > static_cast<size_t>(hashLbl.truncate)) {
next_short = util::truncateMiddle(next_short, hashFrontLbl.truncate, hashBackLbl.truncate);
}
ImGui::Text("%s", next_short.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("block_click_next"));
}
if (ImGui::IsItemClicked()) {
s_height++;
s_has_data = false;
}
}
}
ImGui::Spacing();
ImGui::Spacing();
// Navigation buttons
if (s_has_data) {
if (s_height > 1) {
if (material::StyledButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height--;
s_has_data = false;
s_error.clear();
}
ImGui::SameLine();
}
if (!s_next_hash.empty()) {
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height++;
s_has_data = false;
s_error.clear();
}
}
}
// Close button at bottom
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40);
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();
}
}
} // namespace ui
} // namespace dragonx