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
+156 -110
View File
@@ -37,6 +37,7 @@
#include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h"
#include "ui/windows/faq_dialog.h"
#include "embedded/IconsMaterialDesign.h"
#include "ui/windows/key_export_dialog.h"
#include "ui/windows/transaction_details_dialog.h"
@@ -410,8 +411,8 @@ bool App::init()
settings_->setActiveWalletFile("wallet.dat");
settings_->save();
ui::Notifications::instance().warning(
"Your last-used wallet file (" + active + ") was not found — opened the default wallet "
"instead. If you moved it, restore it and switch back from the wallet list.", 20.0f);
std::string(TR("appx_last_used_wallet_not_found_prefix")) + active +
TR("appx_last_used_wallet_not_found_suffix"), 20.0f);
}
}
}
@@ -805,7 +806,7 @@ void App::update()
const std::string& err = lite_wallet_->lastOpenError();
if (!err.empty() && err != lite_open_error_) {
lite_open_error_ = err;
ui::Notifications::instance().error(std::string("Wallet open failed: ") + err, 8.0f);
ui::Notifications::instance().error(std::string(TR("appx_wallet_open_failed_prefix")) + err, 8.0f);
}
}
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
@@ -989,7 +990,7 @@ void App::update()
// poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked.
if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
resetWitnessRescanProgress();
@@ -1027,8 +1028,8 @@ void App::update()
state_.pool_mining.hashrate_15m = 0.0;
pool_starting_.store(false, std::memory_order_relaxed);
const std::string err = xmrig_manager_->getLastError();
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
: ("Miner stopped: " + err));
ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
: (std::string(TR("appx_miner_stopped_prefix")) + err));
}
// Poll xmrig stats every ~2 seconds (use a simple toggle)
@@ -1053,7 +1054,7 @@ void App::update()
// Pool mining has a connect delay — announce once it's actually connected/hashing.
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
pool_starting_.store(false, std::memory_order_relaxed);
ui::Notifications::instance().success("Pool miner connected and hashing.");
ui::Notifications::instance().success(TR("appx_pool_miner_connected_and_hashing"));
}
// Get memory directly from OS (more reliable than API)
double memMB = xmrig_manager_->getMemoryUsageMB();
@@ -1095,7 +1096,7 @@ void App::update()
const std::string& status = scan.lastStatus;
if (scan.finished) {
if (state_.sync.rescanning && user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
// Witness rebuild finishes with the rescan it's part of.
@@ -1301,7 +1302,7 @@ void App::update()
!runtime_rescan_active_ && !bootstrap_downloading_ &&
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
post_bootstrap_rescan_pending_ = false;
ui::Notifications::instance().info("Bootstrap complete — reconciling your wallet with the new chain data.");
ui::Notifications::instance().info(TR("appx_bootstrap_complete_reconciling"));
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
if (ok && !fullHistory) {
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
@@ -1323,7 +1324,16 @@ void App::update()
if (!state_.warming_up && !runtime_rescan_active_) {
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
refreshTransactionData();
// Throttle the routine new-block full history rescan (z_listreceivedbyaddress —
// O(mapWallet), holds cs_main) by its measured cost, so a large wallet doesn't
// re-scan every block and starve connection near the tip. Bypass when there's an
// explicit need: a dirty set, an in-progress multi-cycle shielded scan (which must
// continue to completion), or an in-flight send — all need fresh data immediately.
const bool txExplicitNeed = transactions_dirty_ || shielded_history_scan_pending_ ||
hasTransactionSendProgress() || !send_txids_.empty();
if (txExplicitNeed || txRefreshDue()) {
refreshTransactionData();
}
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
refreshRecentTransactionData();
}
@@ -1339,7 +1349,13 @@ void App::update()
fastScanChatMemos();
}
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
// Explicit need (a changed address set, or an in-flight send tracking its change output)
// must refresh now; the routine periodic poll on a wallet page is throttled by the last
// address scan's measured cost (addressRefreshDue) — this is the z_listunspent hammering
// that was starving cs_main while synced on a large wallet.
if (addresses_dirty_ || hasTransactionSendProgress()) {
refreshAddressData();
} else if (walletDataPage && addressRefreshDue()) {
refreshAddressData();
}
}
@@ -1396,7 +1412,7 @@ void App::handleGlobalShortcuts()
settings_->setSkinId(skins[cur].id);
settings_->save();
}
ui::Notifications::instance().info("Theme: " + skins[cur].name);
ui::Notifications::instance().info(std::string(TR("appx_theme_prefix")) + skins[cur].name);
}
}
}
@@ -1433,7 +1449,7 @@ void App::handleGlobalShortcuts()
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
}
}
ui::Notifications::instance().info(newLow ? "Low-spec mode enabled" : "Low-spec mode disabled");
ui::Notifications::instance().info(newLow ? TR("appx_low_spec_mode_enabled") : TR("appx_low_spec_mode_disabled"));
}
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
@@ -1445,7 +1461,7 @@ void App::handleGlobalShortcuts()
settings_->setThemeEffectsEnabled(newState);
settings_->save();
}
ui::Notifications::instance().info(newState ? "Theme effects enabled" : "Theme effects disabled");
ui::Notifications::instance().info(newState ? TR("appx_theme_effects_enabled") : TR("appx_theme_effects_disabled"));
}
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
@@ -1454,7 +1470,7 @@ void App::handleGlobalShortcuts()
settings_->setGradientBackground(newGrad);
ui::schema::SkinManager::instance().setGradientMode(newGrad);
settings_->save();
ui::Notifications::instance().info(newGrad ? "Simple background enabled" : "Simple background disabled");
ui::Notifications::instance().info(newGrad ? TR("appx_simple_background_enabled") : TR("appx_simple_background_disabled"));
}
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
@@ -1595,8 +1611,9 @@ void App::render()
{
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
if (deletedN >= 0) {
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
" items). The daemon is restarting to re-sync from the network.");
char deletedMsg[160];
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
ui::Notifications::instance().success(deletedMsg);
}
}
@@ -2138,6 +2155,10 @@ void App::render()
ui::RenderAboutDialog(this, &show_about_);
}
if (show_faq_) {
ui::RenderFaqDialog(this, &show_faq_);
}
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
renderLiteFirstRunPrompt();
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
@@ -2881,6 +2902,31 @@ void App::renderStatusBar()
occupiedX = bellX;
}
// Help (?) — sits just left of the alert bell, on every platform. A plain, subtle question
// mark (no circle) that opens the FAQ.
{
ImFont* helpFont = ui::material::Type().iconSmall();
ImGui::PushFont(helpFont);
const float helpGlyphW = ImGui::CalcTextSize(ICON_MD_QUESTION_MARK).x;
ImGui::PopFont();
const float helpW = helpGlyphW + 10.0f * dp;
const float helpH = helpFont->LegacySize + 4.0f * dp;
const float helpX = occupiedX - helpW - gap;
ImGui::SameLine(helpX);
ui::material::IconButtonStyle hst;
hst.color = ui::material::OnSurfaceMedium();
hst.hoverColor = ui::material::OnSurface();
hst.hoverBg = ui::material::StateHover();
hst.bgRounding = 4.0f * dp;
hst.tooltip = TR("faq_open_tooltip");
if (ui::material::IconButton("##HelpFaq", ICON_MD_QUESTION_MARK, helpFont,
ImVec2(helpW, helpH), hst)) {
show_faq_ = true;
}
occupiedX = helpX;
}
// Version always at far right
ImGui::SameLine(versionX);
ImGui::Text("%s", versionBuf);
@@ -3005,7 +3051,8 @@ void App::renderLiteFirstRunPrompt()
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float btnW = 170.0f;
const float dp = ui::Layout::dpiScale();
const float btnW = 170.0f * dp;
if (step == 0) {
// ── Welcome ──────────────────────────────────────────────────────────
@@ -3013,7 +3060,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::TextUnformatted(TR("lite_welcome_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f);
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f * dp);
ImGui::TextUnformatted(TR("lite_welcome_msg"));
ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
@@ -3021,7 +3068,7 @@ void App::renderLiteFirstRunPrompt()
if (creating) {
// Async create (with server failover) is in flight — driven to completion by
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
ImGui::TextUnformatted("Creating your wallet\xE2\x80\xA6");
ImGui::TextUnformatted(TR("appx_creating_your_wallet"));
if (lite_wallet_->walletOpen()) {
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
if (s.ok && !s.seedPhrase.empty()) {
@@ -3039,7 +3086,7 @@ void App::renderLiteFirstRunPrompt()
} else if (!lite_wallet_->openInProgress() &&
!lite_wallet_->lastOpenError().empty()) {
ui::Notifications::instance().warning(
std::string("Create failed: ") + lite_wallet_->lastOpenError());
std::string(TR("appx_create_failed_prefix")) + lite_wallet_->lastOpenError());
creating = false; // back to the buttons so the user can retry
}
} else {
@@ -3065,14 +3112,12 @@ void App::renderLiteFirstRunPrompt()
} else if (step == 1) {
// ── Reveal the seed + birthday with backup warnings ─────────────────────
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Back up your seed phrase");
ImGui::TextUnformatted(TR("appx_back_up_seed_phrase_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f * dp);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. "
"Write them down in order, store them offline, and never share "
"them. If you lose them, your funds are gone forever.");
ImGui::TextUnformatted(TR("appx_seed_backup_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
ImGui::Spacing();
@@ -3082,17 +3127,17 @@ void App::renderLiteFirstRunPrompt()
char cell[96];
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
ImGui::TextUnformatted(cell);
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f);
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f * dp);
}
ImGui::Spacing();
char bday[80];
snprintf(bday, sizeof(bday), "Birthday (block height): %llu — back this up too.", birthday);
snprintf(bday, sizeof(bday), TR("appx_birthday_block_height"), birthday);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextUnformatted(bday);
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
if (ui::material::TactileButton("I've written it down", ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
chips.clear();
for (const auto& w : words) chips.emplace_back(w, false);
std::mt19937 rng{std::random_device{}()};
@@ -3102,9 +3147,9 @@ void App::renderLiteFirstRunPrompt()
step = 2;
}
ImGui::SameLine();
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
ImGui::SameLine();
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click
} else {
@@ -3116,27 +3161,26 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
"Skip anyway?");
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
}
} else if (step == 2) {
// ── Verify: tap the words in order ──────────────────────────────────────
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Confirm your backup");
ImGui::TextUnformatted(TR("appx_confirm_your_backup"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted("Tap the words in the correct order to confirm you saved them.");
ImGui::TextUnformatted(TR("appx_tap_words_in_order"));
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::TextDisabled("Progress: %d / %d", progress, (int)words.size());
ImGui::TextDisabled(TR("appx_progress_n_of_n"), progress, (int)words.size());
if (ImGui::GetTime() < wrongFlashUntil) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted(" — that's not the next word");
ImGui::TextUnformatted(TR("appx_not_next_word"));
ImGui::PopStyleColor();
}
ImGui::Spacing();
@@ -3145,9 +3189,9 @@ void App::renderLiteFirstRunPrompt()
ImGui::PushID((int)i);
if (chips[i].second) {
ImGui::BeginDisabled();
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0));
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0));
ImGui::EndDisabled();
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0))) {
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0))) {
if (progress < (int)words.size() && chips[i].first == words[progress]) {
chips[i].second = true; // correct next word
++progress;
@@ -3162,15 +3206,15 @@ void App::renderLiteFirstRunPrompt()
const bool verified = progress == (int)words.size();
if (!verified) ImGui::BeginDisabled();
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) {
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
if (ui::material::TactileButton(TR("appx_done"), ImVec2(btnW, 0))) {
ui::Notifications::instance().success(TR("appx_wallet_created_and_backed_up"), 6.0f);
finish();
}
if (!verified) ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Back", ImVec2(80, 0))) { skipConfirm = false; step = 1; }
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) { skipConfirm = false; step = 1; }
ImGui::SameLine();
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click
} else {
@@ -3182,8 +3226,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
"Skip anyway?");
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
}
@@ -3200,7 +3243,7 @@ void App::renderLiteFirstRunPrompt()
if (restoring) {
// Async restore (with server failover) in flight — driven by pumpAsyncOpen().
ImGui::TextUnformatted("Restoring your wallet\xE2\x80\xA6");
ImGui::TextUnformatted(TR("appx_restoring_your_wallet"));
if (lite_wallet_->walletOpen()) {
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
finish(); // wipes restoreSeed; the wallet then syncs from the lite server
@@ -3212,10 +3255,10 @@ void App::renderLiteFirstRunPrompt()
} else {
ImGui::TextUnformatted(TR("lite_restore_seed_label"));
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
ImVec2(380.0f, ImGui::GetTextLineHeight() * 3.2f));
ImVec2(380.0f * dp, ImGui::GetTextLineHeight() * 3.2f));
ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
ImGui::SetNextItemWidth(160.0f);
ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
if (restoreBirthday < 0) restoreBirthday = 0;
@@ -3242,8 +3285,10 @@ void App::renderLiteFirstRunPrompt()
if (!seedTrim.empty() && !seedLenOk) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " +
std::to_string(seedWords) + ".").c_str());
char recoveryWordsMsg[96];
snprintf(recoveryWordsMsg, sizeof(recoveryWordsMsg),
TR("appx_recovery_phrase_word_count"), seedWords);
ImGui::TextUnformatted(recoveryWordsMsg);
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
ImGui::Spacing();
@@ -3260,13 +3305,13 @@ void App::renderLiteFirstRunPrompt()
restoring = true;
} else {
restoreErr = lite_wallet_->lastOpenError().empty()
? std::string("Could not start restore")
? std::string(TR("appx_could_not_start_restore"))
: lite_wallet_->lastOpenError();
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) {
sodium_memzero(restoreSeed, sizeof(restoreSeed));
restoreErr.clear();
step = 0;
@@ -3290,17 +3335,18 @@ void App::renderLiteUnlockPrompt()
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float dp = ui::Layout::dpiScale();
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted(TR("lite_unlock_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_unlock_msg"));
ImGui::Spacing();
ImGui::SetNextItemWidth(280.0f);
ImGui::SetNextItemWidth(280.0f * dp);
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing();
const float btnW = 130.0f;
const float btnW = 130.0f * dp;
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
if (doUnlock) {
const bool ok = lite_wallet_->unlockWallet(pass);
@@ -5072,7 +5118,7 @@ void App::handlePaymentURI(const std::string& uri)
auto payment = util::parsePaymentURI(uri);
if (!payment.valid) {
ui::Notifications::instance().error("Invalid payment URI: " + payment.error);
ui::Notifications::instance().error(std::string(TR("appx_invalid_payment_uri_prefix")) + payment.error);
return;
}
@@ -5087,7 +5133,7 @@ void App::handlePaymentURI(const std::string& uri)
setCurrentPage(ui::NavPage::Send);
// Notify user
std::string msg = "Payment request loaded";
std::string msg = TR("appx_payment_request_loaded");
if (payment.amount > 0) {
char buf[64];
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
@@ -5436,7 +5482,7 @@ bool App::stopDaemonForWalletSwitch()
void App::rescanBlockchain()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5451,17 +5497,17 @@ void App::rescanBlockchain()
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
// running would stomp each other — a second confirm must not launch a duplicate operation.
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
// Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
@@ -5491,7 +5537,7 @@ void App::rescanBlockchain()
void App::repairWallet()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5504,16 +5550,16 @@ void App::repairWallet()
}
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
@@ -5542,11 +5588,11 @@ void App::repairWallet()
void App::reinstallBundledDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
if (!resources::getBundledDaemonInfo().available) {
ui::Notifications::instance().warning("This build has no bundled daemon to install");
ui::Notifications::instance().warning(TR("appx_no_bundled_daemon_to_install"));
return;
}
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
@@ -5554,17 +5600,17 @@ void App::reinstallBundledDaemon()
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
if (!supportsEmbeddedDaemon()) {
ui::Notifications::instance().warning("This build has no embedded daemon to install");
ui::Notifications::instance().warning(TR("appx_no_embedded_daemon_to_install"));
return;
}
if (async_tasks_.isRunning("reinstall-daemon")) {
ui::Notifications::instance().warning("The daemon reinstall is already in progress.");
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
return;
}
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this);
@@ -5596,7 +5642,7 @@ void App::reinstallBundledDaemon()
void App::deleteBlockchainData()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5609,12 +5655,12 @@ void App::deleteBlockchainData()
}
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
@@ -5719,7 +5765,7 @@ void App::beginShutdown()
// Stop xmrig pool miner before stopping the daemon
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
shutdown_status_ = "Stopping pool miner...";
shutdown_status_ = TR("appx_stopping_pool_miner");
xmrig_manager_->stop(3000);
}
@@ -5731,7 +5777,7 @@ void App::beginShutdown()
// Worker join + RPC disconnect happen in shutdown().
if (!daemon_controller_) {
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
shutdown_status_ = "Disconnecting...";
shutdown_status_ = TR("appx_disconnecting");
if (settings_) {
settings_->save();
}
@@ -5762,15 +5808,15 @@ void App::beginShutdown()
// modal "Please wait" dialog).
shutdown_thread_ = std::thread([this]() {
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
shutdown_status_ = "Sending stop command to daemon...";
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
// Send RPC stop command
stopEmbeddedDaemon();
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
shutdown_status_ = "Cleaning up...";
shutdown_status_ = TR("appx_cleaning_up");
DEBUG_LOGF("shutdown thread: complete\n");
shutdown_status_ = "Shutdown complete";
shutdown_status_ = TR("appx_shutdown_complete");
shutdown_complete_ = true;
});
}
@@ -5883,18 +5929,16 @@ void App::renderDaemonStopConfirm()
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted("Node is rebuilding its witness cache");
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
if (Type().subtitle1()) ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
ImGui::TextUnformatted(
"Stopping the node now discards the in-progress rebuild and restarts it (several minutes) "
"the next time you open the wallet. You can keep the node running instead.");
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::Spacing();
if (TactileButton("Keep node running & quit", ImVec2(0, 0))) {
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
shutdown_keep_daemon_override_ = true;
shutdown_confirmed_ = true;
daemon_stop_confirm_open_ = false;
@@ -5905,7 +5949,7 @@ void App::renderDaemonStopConfirm()
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
const bool stopAnyway = TactileButton("Stop anyway & quit", ImVec2(0, 0));
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
ImGui::PopStyleColor(3);
if (stopAnyway) {
shutdown_confirmed_ = true;
@@ -5914,7 +5958,7 @@ void App::renderDaemonStopConfirm()
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton("Cancel", ImVec2(0, 0))) {
if (TactileButton(TR("appx_cancel"), ImVec2(0, 0))) {
daemon_stop_confirm_open_ = false; // abort the quit; stay open
ImGui::CloseCurrentPopup();
}
@@ -5930,6 +5974,7 @@ void App::renderDaemonStopConfirm()
void App::renderShutdownScreen()
{
using namespace ui::material;
const float dp = ui::Layout::dpiScale();
auto shutElem = [](const char* key, float fb) {
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
return v >= 0 ? v : fb;
@@ -5983,7 +6028,7 @@ void App::renderShutdownScreen()
// -------------------------------------------------------------------
float lineH = ImGui::GetTextLineHeightWithSpacing();
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
float spinnerD = shutElem("spinner-radius", 20.0f) * 2.0f + 12.0f;
float spinnerD = shutElem("spinner-radius", 20.0f) * dp * 2.0f + 12.0f * dp;
float statusH = lineH * 2.0f;
float sepH = lineH;
float panelH = shutElem("panel-max-height", 160.0f);
@@ -6014,10 +6059,10 @@ void App::renderShutdownScreen()
// 2. Animated arc spinner
// -------------------------------------------------------------------
{
float r = shutElem("spinner-radius", 20.0f);
float thick = shutElem("spinner-thickness", 3.0f);
float r = shutElem("spinner-radius", 20.0f) * dp;
float thick = shutElem("spinner-thickness", 3.0f) * dp;
// Screen-space centre for draw list
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f);
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f * dp);
// Background ring (dim)
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
@@ -6031,7 +6076,7 @@ void App::renderShutdownScreen()
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
// Advance cursor past the spinner
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f));
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f * dp));
}
ImGui::Spacing();
@@ -6056,9 +6101,9 @@ void App::renderShutdownScreen()
char elapsed[64];
int secs = (int)shutdown_timer_;
if (secs < 60)
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs);
snprintf(elapsed, sizeof(elapsed), TR("appx_n_seconds"), secs);
else
snprintf(elapsed, sizeof(elapsed), "%d min %d sec", secs / 60, secs % 60);
snprintf(elapsed, sizeof(elapsed), TR("appx_n_min_n_sec"), secs / 60, secs % 60);
ImGui::PushFont(Type().caption());
ImVec2 ts = ImGui::CalcTextSize(elapsed);
@@ -6078,7 +6123,8 @@ void App::renderShutdownScreen()
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
// chainstate; say so instead of a bare button.
if (shutdownStalled && !curShut.empty()) {
std::string stalledMsg = "Still \"" + curShut + "\" — force quitting now may corrupt chain data.";
std::string stalledMsg = std::string(TR("appx_still_status_prefix")) + curShut +
TR("appx_still_status_suffix");
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
@@ -6087,7 +6133,7 @@ void App::renderShutdownScreen()
ImGui::Spacing();
}
const char* forceLabel = TR("force_quit");
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f * dp, 0);
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
@@ -6117,7 +6163,7 @@ void App::renderShutdownScreen()
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
float btnW = 120.0f * dp;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
@@ -6149,7 +6195,7 @@ void App::renderShutdownScreen()
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
ImGui::Dummy(ImVec2(0, 4.0f));
ImGui::Dummy(ImVec2(0, 4.0f * dp));
}
ImGui::Spacing();
@@ -6182,7 +6228,7 @@ void App::renderShutdownScreen()
ImGui::GetCursorPosY() + panelPad));
ImGui::PushFont(Type().caption());
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
ImGui::TextUnformatted("dragonxd output");
ImGui::TextUnformatted(TR("appx_dragonxd_output"));
ImGui::PopStyleColor();
ImGui::PopFont();
@@ -6222,7 +6268,7 @@ void App::renderShutdownScreen()
// Advance cursor past the panel
float panelBottom = panelMax.y - wp.y;
ImGui::SetCursorPosY(panelBottom + 4.0f);
ImGui::SetCursorPosY(panelBottom + 4.0f * dp);
ImGui::Dummy(ImVec2(0, 0));
}
}
@@ -6417,14 +6463,14 @@ void App::renderLoadingOverlay(float contentH)
char wbuf[128];
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_blocks_left"),
progress * 100.0f, state_.sync.witness_remaining);
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f);
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_pct"), progress * 100.0f);
else if (progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f);
snprintf(wbuf, sizeof(wbuf), TR("appx_setting_initial_sapling_witnesses"), progress * 100.0f);
else
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
snprintf(wbuf, sizeof(wbuf), "%s", TR("appx_rebuilding_sapling_note_witnesses"));
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
@@ -6462,7 +6508,7 @@ void App::renderLoadingOverlay(float contentH)
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf), "Syncing %.1f%% — Block %d / %d",
snprintf(syncBuf, sizeof(syncBuf), TR("appx_syncing_pct_block_n_of_n"),
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
@@ -6474,7 +6520,7 @@ void App::renderLoadingOverlay(float contentH)
} else if (!state_.connected && state_.sync.blocks > 0) {
// Show last known block height while reconnecting
char blockBuf[64];
snprintf(blockBuf, sizeof(blockBuf), "Last block: %d", state_.sync.blocks);
snprintf(blockBuf, sizeof(blockBuf), TR("appx_last_block_n"), state_.sync.blocks);
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
@@ -6493,8 +6539,8 @@ void App::renderLoadingOverlay(float contentH)
if (!capFont) capFont = ImGui::GetFont();
const char* encLabel = encrypt_in_progress_
? "Encrypting wallet..."
: "Waiting for daemon to encrypt wallet...";
? TR("appx_encrypting_wallet")
: TR("appx_waiting_for_daemon_to_encrypt_wallet");
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
ImU32 encCol = IM_COL32(255, 218, 0, 200);
dl->AddText(capFont, capFont->LegacySize,
@@ -6529,7 +6575,7 @@ void App::renderLoadingOverlay(float contentH)
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
// Error title
const char* errTitle = "Daemon Error";
const char* errTitle = TR("appx_daemon_error");
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
dl->AddText(bodyFont2, bodyFont2->LegacySize,
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
@@ -6549,7 +6595,7 @@ void App::renderLoadingOverlay(float contentH)
}
// Crash count hint
if (daemon_controller_->crashCount() >= 3) {
const char* hint = "Use Settings > Restart Daemon to try again";
const char* hint = TR("appx_use_settings_restart_daemon_hint");
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
@@ -6822,7 +6868,7 @@ void App::copySecretToClipboard(const std::string& secret)
clipboard_secret_hash_ = secret.empty() ? 0 : h;
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
if (!secret.empty())
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
ui::Notifications::instance().info(TR("appx_copied_clipboard_autoclears"), 4.0f);
}
void App::clearSecretClipboardIfArmed()
@@ -6916,7 +6962,7 @@ std::string App::buildDiagnosticsReport()
void App::restartDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -6956,7 +7002,7 @@ void App::restartDaemon()
void App::reindexBlockDatabase()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
if (!daemon_controller_) return;