feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings

Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:37:16 -05:00
co-authored by Claude Opus 4.8
parent ed675f90d8
commit d27f387d6d
35 changed files with 867 additions and 366 deletions
+81 -58
View File
@@ -580,11 +580,11 @@ enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXP
static void renderSettingsTabBar(float availWidth) {
using namespace material;
struct T { int id; const char* label; const char* idstr; };
static const T tabs[] = {
{TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"},
{TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"},
{TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"},
{TAB_ABOUT, "About", "##stabT"},
const T tabs[] = {
{TAB_APPEARANCE, TR("grpa_tab_appearance"), "##stabA"}, {TAB_WALLET, TR("grpa_tab_wallet"), "##stabW"},
{TAB_BACKUP, TR("grpa_tab_backup_data"), "##stabB"}, {TAB_NODE, TR("grpa_tab_node_security"), "##stabN"},
{TAB_EXPLORER, TR("grpa_tab_explorer"), "##stabE"}, {TAB_CHAT, TR("grpa_tab_chat"), "##stabC"},
{TAB_ABOUT, TR("grpa_tab_about"), "##stabT"},
};
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* f = Type().body2();
@@ -811,7 +811,7 @@ void RenderSettingsPage(App* app) {
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string lbl = skin.name + " (invalid)";
std::string lbl = skin.name + TR("grpa_invalid_suffix");
ImGui::Selectable(lbl.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
@@ -1196,14 +1196,25 @@ void RenderSettingsPage(App* app) {
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
if (app && app->daemonAutoShieldProbed()) {
// This status must NOT flow with ImGui's cursor: the checkbox grid is absolutely positioned
// (SetCursorScreenPos at rowY), so a normal-flow Text would land under the auto-shield checkbox
// and the next grid row (Use Tor / Keep daemon) would draw on top of it. Instead, draw it on its
// own full-width row at the grid's current rowY, wrapped to the card, then advance rowY past it.
if (app && app->daemonAutoShieldProbed() &&
(app->daemonAutoShieldActive() || !app->daemonAutoShieldDisabledReason().empty())) {
ImGui::SetCursorScreenPos(ImVec2(cx, rowY));
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
if (app->daemonAutoShieldActive()) {
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
if (!app->daemonAutoShieldAddress().empty())
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
} else if (!app->daemonAutoShieldDisabledReason().empty()) {
} else {
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
}
ImGui::PopTextWrapPos();
last = ImGui::GetCursorScreenPos().y; // cursor is now below the status text
rowY = last + gp; // next grid row starts below it
c = 0;
}
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
@@ -1214,26 +1225,7 @@ void RenderSettingsPage(App* app) {
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
// O2: host a RandomX stratum pool from this node. Only offered on daemons that implement
// it (v1.3.0+, version encoded major*1e6+minor*1e4+rev*100+build), so we never show a
// toggle that does nothing. Takes effect on the next daemon start/restart. Blank allow-IP
// = loopback only (safe); a subnet opens it to that LAN.
if (app->state().daemon_version >= 1030000) {
if (CB(TrId("stratum_host", "strat_host"), &s_settingsState.stratum_host))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
if (s_settingsState.stratum_host) {
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
saveSettingsPageState(app->settings());
if (s_settingsState.stratum_allowip[0] != '\0')
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
TR("stratum_expose_warn"));
}
}
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
}
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
@@ -1697,7 +1689,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_birthday_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
@@ -1709,7 +1701,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_account_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
@@ -1889,7 +1881,7 @@ void RenderSettingsPage(App* app) {
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
if (liteKey.empty()) {
s_settingsState.lite_backup_status = "Enter a private key to import.";
s_settingsState.lite_backup_status = TR("grpa_enter_private_key_to_import");
} else {
const auto r = app->liteWallet()->importKey(liteKey);
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
@@ -2650,6 +2642,10 @@ void RenderSettingsPage(App* app) {
if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
lf.next(material::ActionButtonWidth(TR("faq"), ICON_MD_QUESTION_MARK));
if (material::ActionButton("##aboutfaq", TR("faq"), ICON_MD_QUESTION_MARK, AT::Secondary))
app->showFaqDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("faq_open_tooltip"));
}
const float rightBottom = ImGui::GetCursorScreenPos().y;
@@ -2676,6 +2672,33 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.current_tab == TAB_NODE)
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// MINING POOL HOSTING — run this node's built-in RandomX stratum server so other miners can point
// at this machine. It's a node feature, so it belongs here (not the Wallet tab). Full-node only, and
// only on a daemon new enough to implement -stratum (v1.3.0+, version encoded
// major*1e6+minor*1e4+rev*100+build). Takes effect on the next daemon start/restart; a blank allow-IP
// keeps it loopback-only (safe), a subnet opens it to that LAN.
// ====================================================================
if (app->supportsFullNodeLifecycleActions() && s_settingsState.current_tab == TAB_NODE &&
app->state().daemon_version >= 1030000) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("stratum_host_section"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
if (ImGui::Checkbox(TR("stratum_host"), &s_settingsState.stratum_host))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
if (s_settingsState.stratum_host) {
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
saveSettingsPageState(app->settings());
if (s_settingsState.stratum_allowip[0] != '\0')
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
TR("stratum_expose_warn"));
}
}
// ====================================================================
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
// daemon debug= categories written to DRAGONX.conf; lite has no daemon). Shown on the Node tab.
@@ -2738,7 +2761,7 @@ void RenderSettingsPage(App* app) {
if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine();
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
if (TactileButton(TR("grpa_seed_demo_chat"), ImVec2(0, 0), S.resolveFont("button")))
app->seedChatDemoData();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
}
@@ -2765,29 +2788,29 @@ void RenderSettingsPage(App* app) {
"paymentdisclosure", "pow", "proxy", "prune", "rand",
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
};
static const char* debugTips[] = {
"Peer address tracking and management",
"Alert system messages",
"Benchmark timings for operations",
"Coin database read/write operations",
"Berkeley DB operations",
"Fee estimation algorithm",
"HTTP RPC server activity",
"Libevent networking library",
"Lock contention debugging",
"Transaction memory pool activity",
"Network connections and messages",
"Payment disclosure protocol",
"Proof-of-work mining activity",
"SOCKS5 proxy connections",
"Block pruning operations",
"Random number generation",
"Blockchain reindexing progress",
"RPC command processing",
"Coin selection for transactions",
"Tor integration and circuit info",
"ZeroMQ notification system",
"Shielded (z-addr) RPC operations"
const char* debugTips[] = {
TR("grpa_dbg_addrman"),
TR("grpa_dbg_alert"),
TR("grpa_dbg_bench"),
TR("grpa_dbg_coindb"),
TR("grpa_dbg_db"),
TR("grpa_dbg_estimatefee"),
TR("grpa_dbg_http"),
TR("grpa_dbg_libevent"),
TR("grpa_dbg_lock"),
TR("grpa_dbg_mempool"),
TR("grpa_dbg_net"),
TR("grpa_dbg_paymentdisclosure"),
TR("grpa_dbg_pow"),
TR("grpa_dbg_proxy"),
TR("grpa_dbg_prune"),
TR("grpa_dbg_rand"),
TR("grpa_dbg_reindex"),
TR("grpa_dbg_rpc"),
TR("grpa_dbg_selectcoins"),
TR("grpa_dbg_tor"),
TR("grpa_dbg_zmq"),
TR("grpa_dbg_zrpc")
};
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
@@ -3037,7 +3060,7 @@ void RenderSettingsPage(App* app) {
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
ImGui::Spacing();
ImGui::Text("%s", TR("rescan_from_height"));
ImGui::SetNextItemWidth(160.0f);
ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
} else {
@@ -3051,12 +3074,12 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40 * dp))) {
s_settingsState.confirm_rescan = false;
}
ImGui::SameLine();
ImGui::BeginDisabled(detecting);
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40 * dp))) {
if (bootstrapped) {
app->runtimeRescan(s_settingsState.rescan_start_height);
} else {