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
parent ed675f90d8
commit d27f387d6d
35 changed files with 867 additions and 366 deletions

View File

@@ -521,6 +521,8 @@ set(APP_SOURCES
src/ui/windows/settings_window.cpp src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp src/ui/windows/about_dialog.cpp
src/ui/windows/faq_dialog.cpp
src/ui/windows/faq_content.cpp
src/ui/windows/key_export_dialog.cpp src/ui/windows/key_export_dialog.cpp
src/ui/windows/transaction_details_dialog.cpp src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp src/ui/windows/qr_popup_dialog.cpp
@@ -657,6 +659,8 @@ set(APP_HEADERS
src/ui/windows/console_tab_helpers.h src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h src/ui/windows/about_dialog.h
src/ui/windows/faq_dialog.h
src/ui/windows/faq_content.h
src/ui/windows/key_export_dialog.h src/ui/windows/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h src/ui/windows/qr_popup_dialog.h

View File

@@ -965,6 +965,11 @@ edition-label = { position = 120 }
link-button = { width = 100, font = "button-sm" } link-button = { width = 100, font = "button-sm" }
close-button = { width = 120, font = "button", align = "center" } close-button = { width = 120, font = "button", align = "center" }
[dialogs.faq]
width = 860.0
height = 660.0
window = { width = 860, height = 660 }
[dialogs.settings] [dialogs.settings]
width = 600.0 width = 600.0
height = 550.0 height = 550.0

View File

@@ -37,6 +37,7 @@
#include "ui/windows/market_tab.h" #include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h" #include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h" #include "ui/windows/about_dialog.h"
#include "ui/windows/faq_dialog.h"
#include "embedded/IconsMaterialDesign.h" #include "embedded/IconsMaterialDesign.h"
#include "ui/windows/key_export_dialog.h" #include "ui/windows/key_export_dialog.h"
#include "ui/windows/transaction_details_dialog.h" #include "ui/windows/transaction_details_dialog.h"
@@ -410,8 +411,8 @@ bool App::init()
settings_->setActiveWalletFile("wallet.dat"); settings_->setActiveWalletFile("wallet.dat");
settings_->save(); settings_->save();
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Your last-used wallet file (" + active + ") was not found — opened the default wallet " std::string(TR("appx_last_used_wallet_not_found_prefix")) + active +
"instead. If you moved it, restore it and switch back from the wallet list.", 20.0f); TR("appx_last_used_wallet_not_found_suffix"), 20.0f);
} }
} }
} }
@@ -805,7 +806,7 @@ void App::update()
const std::string& err = lite_wallet_->lastOpenError(); const std::string& err = lite_wallet_->lastOpenError();
if (!err.empty() && err != lite_open_error_) { if (!err.empty() && err != lite_open_error_) {
lite_open_error_ = err; 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" // 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) // poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked. // would fire a false "complete" the instant rescan was clicked.
if (user_initiated_rescan_) { 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 user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
} }
resetWitnessRescanProgress(); resetWitnessRescanProgress();
@@ -1027,8 +1028,8 @@ void App::update()
state_.pool_mining.hashrate_15m = 0.0; state_.pool_mining.hashrate_15m = 0.0;
pool_starting_.store(false, std::memory_order_relaxed); pool_starting_.store(false, std::memory_order_relaxed);
const std::string err = xmrig_manager_->getLastError(); const std::string err = xmrig_manager_->getLastError();
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly." ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
: ("Miner stopped: " + err)); : (std::string(TR("appx_miner_stopped_prefix")) + err));
} }
// Poll xmrig stats every ~2 seconds (use a simple toggle) // 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. // 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)) { if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
pool_starting_.store(false, std::memory_order_relaxed); 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) // Get memory directly from OS (more reliable than API)
double memMB = xmrig_manager_->getMemoryUsageMB(); double memMB = xmrig_manager_->getMemoryUsageMB();
@@ -1095,7 +1096,7 @@ void App::update()
const std::string& status = scan.lastStatus; const std::string& status = scan.lastStatus;
if (scan.finished) { if (scan.finished) {
if (state_.sync.rescanning && user_initiated_rescan_) { 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 user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
} }
// Witness rebuild finishes with the rescan it's part of. // Witness rebuild finishes with the rescan it's part of.
@@ -1301,7 +1302,7 @@ void App::update()
!runtime_rescan_active_ && !bootstrap_downloading_ && !runtime_rescan_active_ && !bootstrap_downloading_ &&
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
post_bootstrap_rescan_pending_ = false; post_bootstrap_rescan_pending_ = false;
ui::Notifications::instance().info("Bootstrap completereconciling your wallet with the new chain data."); ui::Notifications::instance().info(TR("appx_bootstrap_complete_reconciling"));
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) { detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
if (ok && !fullHistory) { if (ok && !fullHistory) {
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
@@ -1323,7 +1324,16 @@ void App::update()
if (!state_.warming_up && !runtime_rescan_active_) { if (!state_.warming_up && !runtime_rescan_active_) {
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) { if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) { if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
// 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(); refreshTransactionData();
}
} else if (walletDataPage && shouldRefreshRecentTransactions()) { } else if (walletDataPage && shouldRefreshRecentTransactions()) {
refreshRecentTransactionData(); refreshRecentTransactionData();
} }
@@ -1339,7 +1349,13 @@ void App::update()
fastScanChatMemos(); fastScanChatMemos();
} }
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) { 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(); refreshAddressData();
} }
} }
@@ -1396,7 +1412,7 @@ void App::handleGlobalShortcuts()
settings_->setSkinId(skins[cur].id); settings_->setSkinId(skins[cur].id);
settings_->save(); 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::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) // Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
@@ -1445,7 +1461,7 @@ void App::handleGlobalShortcuts()
settings_->setThemeEffectsEnabled(newState); settings_->setThemeEffectsEnabled(newState);
settings_->save(); 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 // Keyboard shortcut: Ctrl+Up to toggle simple gradient background
@@ -1454,7 +1470,7 @@ void App::handleGlobalShortcuts()
settings_->setGradientBackground(newGrad); settings_->setGradientBackground(newGrad);
ui::schema::SkinManager::instance().setGradientMode(newGrad); ui::schema::SkinManager::instance().setGradientMode(newGrad);
settings_->save(); 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) // 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); int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
if (deletedN >= 0) { if (deletedN >= 0) {
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) + char deletedMsg[160];
" items). The daemon is restarting to re-sync from the network."); 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_); 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. // Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
renderLiteFirstRunPrompt(); renderLiteFirstRunPrompt();
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet). // Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
@@ -2881,6 +2902,31 @@ void App::renderStatusBar()
occupiedX = bellX; 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 // Version always at far right
ImGui::SameLine(versionX); ImGui::SameLine(versionX);
ImGui::Text("%s", versionBuf); ImGui::Text("%s", versionBuf);
@@ -3005,7 +3051,8 @@ void App::renderLiteFirstRunPrompt()
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr, if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float btnW = 170.0f; const float dp = ui::Layout::dpiScale();
const float btnW = 170.0f * dp;
if (step == 0) { if (step == 0) {
// ── Welcome ────────────────────────────────────────────────────────── // ── Welcome ──────────────────────────────────────────────────────────
@@ -3013,7 +3060,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::TextUnformatted(TR("lite_welcome_title")); ImGui::TextUnformatted(TR("lite_welcome_title"));
ImGui::PopFont(); ImGui::PopFont();
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f * dp);
ImGui::TextUnformatted(TR("lite_welcome_msg")); ImGui::TextUnformatted(TR("lite_welcome_msg"));
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing(); ImGui::Spacing(); ImGui::Spacing();
@@ -3021,7 +3068,7 @@ void App::renderLiteFirstRunPrompt()
if (creating) { if (creating) {
// Async create (with server failover) is in flight — driven to completion by // Async create (with server failover) is in flight — driven to completion by
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome. // 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()) { if (lite_wallet_->walletOpen()) {
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast) auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
if (s.ok && !s.seedPhrase.empty()) { if (s.ok && !s.seedPhrase.empty()) {
@@ -3039,7 +3086,7 @@ void App::renderLiteFirstRunPrompt()
} else if (!lite_wallet_->openInProgress() && } else if (!lite_wallet_->openInProgress() &&
!lite_wallet_->lastOpenError().empty()) { !lite_wallet_->lastOpenError().empty()) {
ui::Notifications::instance().warning( 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 creating = false; // back to the buttons so the user can retry
} }
} else { } else {
@@ -3065,14 +3112,12 @@ void App::renderLiteFirstRunPrompt()
} else if (step == 1) { } else if (step == 1) {
// ── Reveal the seed + birthday with backup warnings ───────────────────── // ── Reveal the seed + birthday with backup warnings ─────────────────────
ImGui::PushFont(ui::material::Type().subtitle1()); 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::PopFont();
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f * dp);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError()); ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. " ImGui::TextUnformatted(TR("appx_seed_backup_warning"));
"Write them down in order, store them offline, and never share "
"them. If you lose them, your funds are gone forever.");
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
@@ -3082,17 +3127,17 @@ void App::renderLiteFirstRunPrompt()
char cell[96]; char cell[96];
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str()); snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
ImGui::TextUnformatted(cell); 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(); ImGui::Spacing();
char bday[80]; 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::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextUnformatted(bday); ImGui::TextUnformatted(bday);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing(); 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(); chips.clear();
for (const auto& w : words) chips.emplace_back(w, false); for (const auto& w : words) chips.emplace_back(w, false);
std::mt19937 rng{std::random_device{}()}; std::mt19937 rng{std::random_device{}()};
@@ -3102,9 +3147,9 @@ void App::renderLiteFirstRunPrompt()
step = 2; step = 2;
} }
ImGui::SameLine(); 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(); 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) { if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click skipConfirm = true; // require a second, deliberate click
} else { } else {
@@ -3116,27 +3161,26 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError()); ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. " ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
"Skip anyway?");
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
} }
} else if (step == 2) { } else if (step == 2) {
// ── Verify: tap the words in order ────────────────────────────────────── // ── Verify: tap the words in order ──────────────────────────────────────
ImGui::PushFont(ui::material::Type().subtitle1()); ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Confirm your backup"); ImGui::TextUnformatted(TR("appx_confirm_your_backup"));
ImGui::PopFont(); ImGui::PopFont();
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); 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::PopTextWrapPos();
ImGui::Spacing(); 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) { if (ImGui::GetTime() < wrongFlashUntil) {
ImGui::SameLine(); ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError()); 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::PopStyleColor();
} }
ImGui::Spacing(); ImGui::Spacing();
@@ -3145,9 +3189,9 @@ void App::renderLiteFirstRunPrompt()
ImGui::PushID((int)i); ImGui::PushID((int)i);
if (chips[i].second) { if (chips[i].second) {
ImGui::BeginDisabled(); 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(); 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]) { if (progress < (int)words.size() && chips[i].first == words[progress]) {
chips[i].second = true; // correct next word chips[i].second = true; // correct next word
++progress; ++progress;
@@ -3162,15 +3206,15 @@ void App::renderLiteFirstRunPrompt()
const bool verified = progress == (int)words.size(); const bool verified = progress == (int)words.size();
if (!verified) ImGui::BeginDisabled(); if (!verified) ImGui::BeginDisabled();
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) { if (ui::material::TactileButton(TR("appx_done"), ImVec2(btnW, 0))) {
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f); ui::Notifications::instance().success(TR("appx_wallet_created_and_backed_up"), 6.0f);
finish(); finish();
} }
if (!verified) ImGui::EndDisabled(); if (!verified) ImGui::EndDisabled();
ImGui::SameLine(); 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(); 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) { if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click skipConfirm = true; // require a second, deliberate click
} else { } else {
@@ -3182,8 +3226,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError()); ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. " ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
"Skip anyway?");
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
} }
@@ -3200,7 +3243,7 @@ void App::renderLiteFirstRunPrompt()
if (restoring) { if (restoring) {
// Async restore (with server failover) in flight — driven by pumpAsyncOpen(). // 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()) { if (lite_wallet_->walletOpen()) {
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f); ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
finish(); // wipes restoreSeed; the wallet then syncs from the lite server finish(); // wipes restoreSeed; the wallet then syncs from the lite server
@@ -3212,10 +3255,10 @@ void App::renderLiteFirstRunPrompt()
} else { } else {
ImGui::TextUnformatted(TR("lite_restore_seed_label")); ImGui::TextUnformatted(TR("lite_restore_seed_label"));
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed), ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
ImVec2(380.0f, ImGui::GetTextLineHeight() * 3.2f)); ImVec2(380.0f * dp, ImGui::GetTextLineHeight() * 3.2f));
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_restore_birthday_label")); ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
ImGui::SetNextItemWidth(160.0f); ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday); ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
if (restoreBirthday < 0) restoreBirthday = 0; if (restoreBirthday < 0) restoreBirthday = 0;
@@ -3242,8 +3285,10 @@ void App::renderLiteFirstRunPrompt()
if (!seedTrim.empty() && !seedLenOk) { if (!seedTrim.empty() && !seedLenOk) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " + char recoveryWordsMsg[96];
std::to_string(seedWords) + ".").c_str()); snprintf(recoveryWordsMsg, sizeof(recoveryWordsMsg),
TR("appx_recovery_phrase_word_count"), seedWords);
ImGui::TextUnformatted(recoveryWordsMsg);
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
@@ -3260,13 +3305,13 @@ void App::renderLiteFirstRunPrompt()
restoring = true; restoring = true;
} else { } else {
restoreErr = lite_wallet_->lastOpenError().empty() restoreErr = lite_wallet_->lastOpenError().empty()
? std::string("Could not start restore") ? std::string(TR("appx_could_not_start_restore"))
: lite_wallet_->lastOpenError(); : lite_wallet_->lastOpenError();
} }
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); 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)); sodium_memzero(restoreSeed, sizeof(restoreSeed));
restoreErr.clear(); restoreErr.clear();
step = 0; step = 0;
@@ -3290,17 +3335,18 @@ void App::renderLiteUnlockPrompt()
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr, if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float dp = ui::Layout::dpiScale();
ImGui::PushFont(ui::material::Type().subtitle1()); ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted(TR("lite_unlock_title")); ImGui::TextUnformatted(TR("lite_unlock_title"));
ImGui::PopFont(); ImGui::PopFont();
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_unlock_msg")); ImGui::TextUnformatted(TR("lite_unlock_msg"));
ImGui::Spacing(); ImGui::Spacing();
ImGui::SetNextItemWidth(280.0f); ImGui::SetNextItemWidth(280.0f * dp);
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass), const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue); ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing(); 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; bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
if (doUnlock) { if (doUnlock) {
const bool ok = lite_wallet_->unlockWallet(pass); const bool ok = lite_wallet_->unlockWallet(pass);
@@ -5072,7 +5118,7 @@ void App::handlePaymentURI(const std::string& uri)
auto payment = util::parsePaymentURI(uri); auto payment = util::parsePaymentURI(uri);
if (!payment.valid) { 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; return;
} }
@@ -5087,7 +5133,7 @@ void App::handlePaymentURI(const std::string& uri)
setCurrentPage(ui::NavPage::Send); setCurrentPage(ui::NavPage::Send);
// Notify user // Notify user
std::string msg = "Payment request loaded"; std::string msg = TR("appx_payment_request_loaded");
if (payment.amount > 0) { if (payment.amount > 0) {
char buf[64]; char buf[64];
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount); snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
@@ -5436,7 +5482,7 @@ bool App::stopDaemonForWalletSwitch()
void App::rescanBlockchain() void App::rescanBlockchain()
{ {
if (!supportsFullNodeLifecycleActions()) { 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; return;
} }
@@ -5451,17 +5497,17 @@ void App::rescanBlockchain()
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already // 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. // running would stomp each other — a second confirm must not launch a duplicate operation.
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) { 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; return;
} }
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start. // Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
if (daemon_restarting_) { 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; return;
} }
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n"); 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 // 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 // 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() void App::repairWallet()
{ {
if (!supportsFullNodeLifecycleActions()) { 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; return;
} }
@@ -5504,16 +5550,16 @@ void App::repairWallet()
} }
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) { 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; return;
} }
if (daemon_restarting_) { 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; return;
} }
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n"); 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 // -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 // chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
@@ -5542,11 +5588,11 @@ void App::repairWallet()
void App::reinstallBundledDaemon() void App::reinstallBundledDaemon()
{ {
if (!supportsFullNodeLifecycleActions()) { 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; return;
} }
if (!resources::getBundledDaemonInfo().available) { 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; return;
} }
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is // 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 — // 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. // which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
if (!supportsEmbeddedDaemon()) { 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; return;
} }
if (async_tasks_.isRunning("reinstall-daemon")) { 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; return;
} }
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n"); 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) { async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this); AppDaemonLifecycleRuntime runtime(*this);
@@ -5596,7 +5642,7 @@ void App::reinstallBundledDaemon()
void App::deleteBlockchainData() void App::deleteBlockchainData()
{ {
if (!supportsFullNodeLifecycleActions()) { 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; return;
} }
@@ -5609,12 +5655,12 @@ void App::deleteBlockchainData()
} }
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) { 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; return;
} }
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n"); 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()); daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) { 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 // Stop xmrig pool miner before stopping the daemon
if (xmrig_manager_ && xmrig_manager_->isRunning()) { if (xmrig_manager_ && xmrig_manager_->isRunning()) {
shutdown_status_ = "Stopping pool miner..."; shutdown_status_ = TR("appx_stopping_pool_miner");
xmrig_manager_->stop(3000); xmrig_manager_->stop(3000);
} }
@@ -5731,7 +5777,7 @@ void App::beginShutdown()
// Worker join + RPC disconnect happen in shutdown(). // Worker join + RPC disconnect happen in shutdown().
if (!daemon_controller_) { if (!daemon_controller_) {
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n"); DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
shutdown_status_ = "Disconnecting..."; shutdown_status_ = TR("appx_disconnecting");
if (settings_) { if (settings_) {
settings_->save(); settings_->save();
} }
@@ -5762,15 +5808,15 @@ void App::beginShutdown()
// modal "Please wait" dialog). // modal "Please wait" dialog).
shutdown_thread_ = std::thread([this]() { shutdown_thread_ = std::thread([this]() {
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n"); 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 // Send RPC stop command
stopEmbeddedDaemon(); stopEmbeddedDaemon();
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n"); 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"); DEBUG_LOGF("shutdown thread: complete\n");
shutdown_status_ = "Shutdown complete"; shutdown_status_ = TR("appx_shutdown_complete");
shutdown_complete_ = true; shutdown_complete_ = true;
}); });
} }
@@ -5883,18 +5929,16 @@ void App::renderDaemonStopConfirm()
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr, if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1()); 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(); if (Type().subtitle1()) ImGui::PopFont();
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
ImGui::TextUnformatted( ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
"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::PopTextWrapPos(); ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
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_keep_daemon_override_ = true;
shutdown_confirmed_ = true; shutdown_confirmed_ = true;
daemon_stop_confirm_open_ = false; daemon_stop_confirm_open_ = false;
@@ -5905,7 +5949,7 @@ void App::renderDaemonStopConfirm()
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210))); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160))); 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); ImGui::PopStyleColor(3);
if (stopAnyway) { if (stopAnyway) {
shutdown_confirmed_ = true; shutdown_confirmed_ = true;
@@ -5914,7 +5958,7 @@ void App::renderDaemonStopConfirm()
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
} }
ImGui::SameLine(); 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 daemon_stop_confirm_open_ = false; // abort the quit; stay open
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
} }
@@ -5930,6 +5974,7 @@ void App::renderDaemonStopConfirm()
void App::renderShutdownScreen() void App::renderShutdownScreen()
{ {
using namespace ui::material; using namespace ui::material;
const float dp = ui::Layout::dpiScale();
auto shutElem = [](const char* key, float fb) { auto shutElem = [](const char* key, float fb) {
float v = ui::schema::UI().drawElement("components.shutdown", key).size; float v = ui::schema::UI().drawElement("components.shutdown", key).size;
return v >= 0 ? v : fb; return v >= 0 ? v : fb;
@@ -5983,7 +6028,7 @@ void App::renderShutdownScreen()
// ------------------------------------------------------------------- // -------------------------------------------------------------------
float lineH = ImGui::GetTextLineHeightWithSpacing(); float lineH = ImGui::GetTextLineHeightWithSpacing();
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f; 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 statusH = lineH * 2.0f;
float sepH = lineH; float sepH = lineH;
float panelH = shutElem("panel-max-height", 160.0f); float panelH = shutElem("panel-max-height", 160.0f);
@@ -6014,10 +6059,10 @@ void App::renderShutdownScreen()
// 2. Animated arc spinner // 2. Animated arc spinner
// ------------------------------------------------------------------- // -------------------------------------------------------------------
{ {
float r = shutElem("spinner-radius", 20.0f); float r = shutElem("spinner-radius", 20.0f) * dp;
float thick = shutElem("spinner-thickness", 3.0f); float thick = shutElem("spinner-thickness", 3.0f) * dp;
// Screen-space centre for draw list // 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) // Background ring (dim)
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48); 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); dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
// Advance cursor past the spinner // 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(); ImGui::Spacing();
@@ -6056,9 +6101,9 @@ void App::renderShutdownScreen()
char elapsed[64]; char elapsed[64];
int secs = (int)shutdown_timer_; int secs = (int)shutdown_timer_;
if (secs < 60) if (secs < 60)
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs); snprintf(elapsed, sizeof(elapsed), TR("appx_n_seconds"), secs);
else 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()); ImGui::PushFont(Type().caption());
ImVec2 ts = ImGui::CalcTextSize(elapsed); 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 // State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
// chainstate; say so instead of a bare button. // chainstate; say so instead of a bare button.
if (shutdownStalled && !curShut.empty()) { 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()); ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
ImGui::SetCursorPosX(cx - ms.x * 0.5f); ImGui::SetCursorPosX(cx - ms.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
@@ -6087,7 +6133,7 @@ void App::renderShutdownScreen()
ImGui::Spacing(); ImGui::Spacing();
} }
const char* forceLabel = TR("force_quit"); 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::SetCursorPosX(cx - btnSize.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f)); 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)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
@@ -6117,7 +6163,7 @@ void App::renderShutdownScreen()
ImGui::Spacing(); ImGui::Spacing();
ImGui::Spacing(); ImGui::Spacing();
float btnW = 120.0f; float btnW = 120.0f * dp;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x; float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f); ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
@@ -6149,7 +6195,7 @@ void App::renderShutdownScreen()
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY()); ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
ImVec2 p1(wp.x + vp_size.x - pad, p0.y); 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); 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(); ImGui::Spacing();
@@ -6182,7 +6228,7 @@ void App::renderShutdownScreen()
ImGui::GetCursorPosY() + panelPad)); ImGui::GetCursorPosY() + panelPad));
ImGui::PushFont(Type().caption()); ImGui::PushFont(Type().caption());
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f)); 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::PopStyleColor();
ImGui::PopFont(); ImGui::PopFont();
@@ -6222,7 +6268,7 @@ void App::renderShutdownScreen()
// Advance cursor past the panel // Advance cursor past the panel
float panelBottom = panelMax.y - wp.y; float panelBottom = panelMax.y - wp.y;
ImGui::SetCursorPosY(panelBottom + 4.0f); ImGui::SetCursorPosY(panelBottom + 4.0f * dp);
ImGui::Dummy(ImVec2(0, 0)); ImGui::Dummy(ImVec2(0, 0));
} }
} }
@@ -6417,14 +6463,14 @@ void App::renderLoadingOverlay(float contentH)
char wbuf[128]; char wbuf[128];
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0) 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); progress * 100.0f, state_.sync.witness_remaining);
else if (state_.sync.witness_phase == 2 && progress > 0.01f) 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) 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 else
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses"); snprintf(wbuf, sizeof(wbuf), "%s", TR("appx_rebuilding_sapling_note_witnesses"));
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont(); if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf); 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" // Progress text — "Syncing 45.2% — Block 123456 / 234567"
char syncBuf[128]; 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); progress * 100.0f, state_.sync.blocks, state_.sync.headers);
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont(); if (!capFont) capFont = ImGui::GetFont();
@@ -6474,7 +6520,7 @@ void App::renderLoadingOverlay(float contentH)
} else if (!state_.connected && state_.sync.blocks > 0) { } else if (!state_.connected && state_.sync.blocks > 0) {
// Show last known block height while reconnecting // Show last known block height while reconnecting
char blockBuf[64]; 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(); ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont(); if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf); 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(); if (!capFont) capFont = ImGui::GetFont();
const char* encLabel = encrypt_in_progress_ const char* encLabel = encrypt_in_progress_
? "Encrypting wallet..." ? TR("appx_encrypting_wallet")
: "Waiting for daemon to encrypt wallet..."; : TR("appx_waiting_for_daemon_to_encrypt_wallet");
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel); ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
ImU32 encCol = IM_COL32(255, 218, 0, 200); ImU32 encCol = IM_COL32(255, 218, 0, 200);
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
@@ -6529,7 +6575,7 @@ void App::renderLoadingOverlay(float contentH)
if (!bodyFont2) bodyFont2 = ImGui::GetFont(); if (!bodyFont2) bodyFont2 = ImGui::GetFont();
// Error title // 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); ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
dl->AddText(bodyFont2, bodyFont2->LegacySize, dl->AddText(bodyFont2, bodyFont2->LegacySize,
ImVec2(wp.x + cx - ts.x * 0.5f, curY), ImVec2(wp.x + cx - ts.x * 0.5f, curY),
@@ -6549,7 +6595,7 @@ void App::renderLoadingOverlay(float contentH)
} }
// Crash count hint // Crash count hint
if (daemon_controller_->crashCount() >= 3) { 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); ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(wp.x + cx - hs2.x * 0.5f, curY), 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_secret_hash_ = secret.empty() ? 0 : h;
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0); clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
if (!secret.empty()) if (!secret.empty())
ui::Notifications::instance().info("Copiedclipboard auto-clears in 45s", 4.0f); ui::Notifications::instance().info(TR("appx_copied_clipboard_autoclears"), 4.0f);
} }
void App::clearSecretClipboardIfArmed() void App::clearSecretClipboardIfArmed()
@@ -6916,7 +6962,7 @@ std::string App::buildDiagnosticsReport()
void App::restartDaemon() void App::restartDaemon()
{ {
if (!supportsFullNodeLifecycleActions()) { 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; return;
} }
@@ -6956,7 +7002,7 @@ void App::restartDaemon()
void App::reindexBlockDatabase() void App::reindexBlockDatabase()
{ {
if (!supportsFullNodeLifecycleActions()) { 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; return;
} }
if (!daemon_controller_) return; if (!daemon_controller_) return;

View File

@@ -462,6 +462,7 @@ public:
return 0; return 0;
} }
void showAboutDialog() { show_about_ = true; } void showAboutDialog() { show_about_ = true; }
void showFaqDialog() { show_faq_ = true; }
// Legacy tab compat — maps int to NavPage // Legacy tab compat — maps int to NavPage
void setCurrentTab(int tab); void setCurrentTab(int tab);
@@ -800,6 +801,7 @@ private:
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_. // balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
bool chat_note_scan_in_flight_ = false; bool chat_note_scan_in_flight_ = false;
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit) double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
double chat_note_scan_ms_ = 0.0; // measured cost of the last note scan (adaptive back-off)
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs // Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads // may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
// this cache rather than requiring the current model to have both — else the budget flickers to 0. // this cache rather than requiring the current model to have both — else the budget flickers to 0.
@@ -809,6 +811,9 @@ private:
// Coordinator helpers (both variants unless noted; see app_network.cpp). // Coordinator helpers (both variants unless noted; see app_network.cpp).
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
@@ -877,6 +882,8 @@ private:
void fastScanChatMemos(); void fastScanChatMemos();
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll) float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
double chat_fast_scan_last_ = 0.0; // ImGui time of the last 0-conf fast scan (adaptive back-off)
double chat_fast_scan_ms_ = 0.0; // measured cost of the last fast scan (z_listreceivedbyaddress)
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame() bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
// Lite first-run welcome prompt: dismissed for the session once the user picks an action. // Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false; bool lite_firstrun_dismissed_ = false;
@@ -1028,6 +1035,7 @@ private:
bool show_demo_window_ = false; bool show_demo_window_ = false;
bool show_settings_ = false; bool show_settings_ = false;
bool show_about_ = false; bool show_about_ = false;
bool show_faq_ = false;
bool show_import_key_ = false; bool show_import_key_ = false;
bool show_export_key_ = false; bool show_export_key_ = false;
bool show_backup_ = false; bool show_backup_ = false;
@@ -1143,6 +1151,14 @@ private:
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling) std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
// starving block connection near the tip (where effectivelySyncing() reads false). See
// addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
// refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
double last_address_scan_ms_ = 0.0; // measured cost of the last address scan
double last_tx_scan_ms_ = 0.0; // measured cost of the last history scan
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept. // Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0; std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0; double clipboard_clear_deadline_ = 0.0;
@@ -1498,6 +1514,11 @@ private:
void applyRefreshPolicy(ui::NavPage page); void applyRefreshPolicy(ui::NavPage page);
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis) bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost? bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
bool addressRefreshDue() const; // same adaptive back-off for the address scan (z_listunspent)
bool txRefreshDue() const; // same adaptive back-off for the history scan (z_listreceivedbyaddress)
// Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
// lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
bool currentPageNeedsWalletDataRefresh() const; bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const; bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const; bool shouldRefreshTransactions() const;

View File

@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
}); });
} else { } else {
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect."); TR("sec_restart_daemon_for_encryption"));
} }
} }
void App::encryptWalletWithPassphrase(const std::string& passphrase) { void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return; if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true; encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet..."; encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) { if (worker_) {
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb { worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (result.encrypted) { if (result.encrypted) {
return [this]() { return [this]() {
encrypt_in_progress_ = false; encrypt_in_progress_ = false;
encrypt_status_ = "Wallet encrypted. Restarting daemon..."; encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n"); DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
// Immediately update local encryption state so the // Immediately update local encryption state so the
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
} }
ui::Notifications::instance().info( ui::Notifications::instance().info(
"Wallet encrypted successfully", 5.0f); TR("sec_wallet_encrypted_successfully"), 5.0f);
// The daemon shuts itself down after encryptwallet. // The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay // Update connection_status_ so the loading overlay
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
std::string err = result.error; std::string err = result.error;
return [this, err]() { return [this, err]() {
encrypt_in_progress_ = false; encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err; encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str()); DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error( ui::Notifications::instance().error(
"Encryption failed: " + err); std::string(TR("sec_encryption_failed_prefix")) + err);
// Return to passphrase entry on failure // Return to passphrase entry on failure
if (show_encrypt_dialog_ && if (show_encrypt_dialog_ &&
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
std::string pin = std::move(deferredEncryption.pin); std::string pin = std::move(deferredEncryption.pin);
encrypt_in_progress_ = true; encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet..."; encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) { if (worker_) {
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb { worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
if (result.pinStored) { if (result.pinStored) {
settings_->setPinEnabled(true); settings_->setPinEnabled(true);
settings_->save(); settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f); ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
} else { } else {
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed"); TR("sec_wallet_encrypted_but_pin_vault_failed"));
} }
} else { } else {
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f); ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
} }
wallet_security_.clearDeferredEncryption(); wallet_security_.clearDeferredEncryption();
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
std::string err = result.error; std::string err = result.error;
return [this, err]() { return [this, err]() {
encrypt_in_progress_ = false; encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err; encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str()); DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error("Encryption failed: " + err); ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
wallet_security_.clearDeferredEncryption(); wallet_security_.clearDeferredEncryption();
}; };
} }
@@ -531,7 +531,7 @@ void App::lockWallet() {
if (!lock_failure_warned_) { if (!lock_failure_warned_) {
lock_failure_warned_ = true; lock_failure_warned_ = true;
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f); TR("sec_couldnt_lock_wallet"), 12.0f);
} }
} }
}; };
@@ -541,7 +541,7 @@ void App::lockWallet() {
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) { void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return; if (!rpc_ || !rpc_->isConnected() || !worker_) return;
encrypt_in_progress_ = true; encrypt_in_progress_ = true;
encrypt_status_ = "Changing passphrase..."; encrypt_status_ = TR("sec_changing_passphrase");
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_)); memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
unlockTransactionHistoryCacheWithPassphrase(newPass); unlockTransactionHistoryCacheWithPassphrase(newPass);
storeTransactionHistoryCacheIfAvailable(); storeTransactionHistoryCacheIfAvailable();
ui::Notifications::instance().info("Passphrase changed successfully"); ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
} else { } else {
encrypt_status_ = "Failed: " + err_msg; encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
} }
util::SecureVault::secureZero(newPass.data(), newPass.size()); util::SecureVault::secureZero(newPass.data(), newPass.size());
}; };
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
!encryption_incomplete_warned_) { !encryption_incomplete_warned_) {
encryption_incomplete_warned_ = true; encryption_incomplete_warned_ = true;
ui::Notifications::instance().warning( ui::Notifications::instance().warning(
"Wallet encryption did not complete — your wallet is NOT encrypted. " TR("sec_encryption_did_not_complete"), 30.0f);
"Open Settings to finish encrypting it.", 30.0f);
} }
if (state_.transactions.empty()) { if (state_.transactions.empty()) {
loadTransactionHistoryCacheIfAvailable(); loadTransactionHistoryCacheIfAvailable();
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
ImU32 textCol = ui::material::OnSurface(); ImU32 textCol = ui::material::OnSurface();
{ {
const char* title = "Wallet Locked"; const char* title = TR("sec_wallet_locked_title");
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title); ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize, dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title); ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0; if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
char msg[128]; char msg[128];
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_); snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg); ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists // Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) { if (hasPinVault) {
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD; const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase"; const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
const char* switchLabel = lock_use_pin_ const char* switchLabel = lock_use_pin_
? "Use passphrase instead" ? TR("sec_use_passphrase_instead")
: "Use PIN instead"; : TR("sec_use_pin_instead");
// Current mode indicator — icon with icon font, text with caption font // Current mode indicator — icon with icon font, text with caption font
ImFont* iconFont = ui::material::Type().iconSmall(); ImFont* iconFont = ui::material::Type().iconSmall();
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
if (lock_unlock_in_progress_) { if (lock_unlock_in_progress_) {
// Animated spinner dots // Animated spinner dots
char msg[64]; char msg[64];
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots()); snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canSubmit); ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH)); bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
r->call("walletpassphrase", {passphrase, timeout}); r->call("walletpassphrase", {passphrase, timeout});
rpcOk = true; rpcOk = true;
} else { } else {
rpcErr = "Not connected to daemon"; rpcErr = TR("sec_not_connected_to_daemon");
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
rpcErr = e.what(); rpcErr = e.what();
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
// so route through applyUnlockFailure so the lockout curve applies // so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math). // (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable { return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
applyUnlockFailure("Unlock failed: " + rpcErr); applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
util::SecureVault::secureZero(passphrase.data(), passphrase.size()); util::SecureVault::secureZero(passphrase.data(), passphrase.size());
}; };
} }
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size()); if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
if (!unlock.ok) { if (!unlock.ok) {
return [this]() { return [this]() {
wallet_security_workflow_.failEntry("Incorrect passphrase"); wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
}; };
} }
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
}); });
ui::Notifications::instance().info( ui::Notifications::instance().info(
"Importing keys & rescanning blockchain — wallet is usable while this runs", TR("sec_importing_keys_rescanning"),
8.0f); 8.0f);
}; };
}); });
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
wallet_security_workflow_.finishImport(); wallet_security_workflow_.finishImport();
ui::Notifications::instance().error( ui::Notifications::instance().error(
err + err +
"\nEncrypted backup: wallet.dat.encrypted.bak", TR("sec_encrypted_backup_suffix"),
12.0f); 12.0f);
}; };
}); });
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
refreshPeerInfo(); refreshPeerInfo();
ui::Notifications::instance().success( ui::Notifications::instance().success(
"Wallet decrypted successfully! All keys imported.", TR("sec_wallet_decrypted_all_keys_imported"),
8.0f); 8.0f);
DEBUG_LOGF("[App] Wallet decrypted successfully\n"); DEBUG_LOGF("[App] Wallet decrypted successfully\n");
}; };
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
int tMins = (int)(totalElapsed / 60); int tMins = (int)(totalElapsed / 60);
int tSecs = (int)(totalElapsed % 60); int tSecs = (int)(totalElapsed % 60);
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs); ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
} }
// ---- Phase 2: Success ---- // ---- Phase 2: Success ----
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) { if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Verifying passphrase..."; pin_status_ = TR("sec_verifying_passphrase");
// Verify passphrase + store vault on worker thread to avoid // Verify passphrase + store vault on worker thread to avoid
// blocking the UI with Argon2id key derivation. // blocking the UI with Argon2id key derivation.
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size()); if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size()); if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
return [this]() { return [this]() {
pin_status_ = "Incorrect passphrase"; pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
pin_in_progress_ = false; pin_in_progress_ = false;
}; };
} }
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
pin_status_.clear(); pin_status_.clear();
pin_in_progress_ = false; pin_in_progress_ = false;
show_pin_setup_ = false; show_pin_setup_ = false;
ui::Notifications::instance().info("PIN set successfully"); ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
} else { } else {
pin_status_ = "Failed to create vault"; pin_status_ = TR("sec_failed_to_create_vault");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
pin_status_ = "Not connected to daemon"; pin_status_ = TR("sec_not_connected_to_daemon_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Changing PIN..."; pin_status_ = TR("sec_changing_pin");
std::string oldPin(pin_old_buf_); std::string oldPin(pin_old_buf_);
std::string newPinCopy = newPin; std::string newPinCopy = newPin;
memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
pin_status_.clear(); pin_status_.clear();
pin_in_progress_ = false; pin_in_progress_ = false;
show_pin_change_ = false; show_pin_change_ = false;
ui::Notifications::instance().info("PIN changed successfully"); ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
} else { } else {
pin_status_ = "Incorrect current PIN"; pin_status_ = TR("sec_incorrect_current_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
pin_status_ = "Internal error"; pin_status_ = TR("sec_internal_error_change_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_); ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) { if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true; pin_in_progress_ = true;
pin_status_ = "Verifying PIN..."; pin_status_ = TR("sec_verifying_pin");
std::string oldPin(pin_old_buf_); std::string oldPin(pin_old_buf_);
memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
pin_status_.clear(); pin_status_.clear();
pin_in_progress_ = false; pin_in_progress_ = false;
show_pin_remove_ = false; show_pin_remove_ = false;
ui::Notifications::instance().info("PIN removed"); ui::Notifications::instance().info(TR("sec_pin_removed"));
} else { } else {
pin_status_ = "Incorrect PIN"; pin_status_ = TR("sec_incorrect_pin_remove");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
}; };
}); });
} else { } else {
pin_status_ = "Internal error"; pin_status_ = TR("sec_internal_error_remove_pin");
pin_in_progress_ = false; pin_in_progress_ = false;
} }
} }

View File

@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); }); [](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview, add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); }); [](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
add("modal-faq", ui::NavPage::Overview,
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt). // Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
add("modal-encrypt", ui::NavPage::Settings, add("modal-encrypt", ui::NavPage::Settings,
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; }, [](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
@@ -708,9 +710,22 @@ void App::startSweepImpl(bool full)
if (sweep_current_theme_only_) if (sweep_current_theme_only_)
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId()); sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
// under review instead of all surfaces x every skin.
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
sweep_full_ = full; sweep_full_ = full;
if (full) { capture_mode_ = true; installDemoWalletData(); } if (full) { capture_mode_ = true; installDemoWalletData(); }
buildSweepCatalog(); buildSweepCatalog();
if (!sweepOnlyStr.empty()) {
std::vector<SweepTarget> keep;
for (const auto& t : sweep_targets_)
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
sweep_targets_.swap(keep);
}
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; } if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
sweep_dir_ = full ? screenshotFullDir() : screenshotDir(); sweep_dir_ = full ? screenshotFullDir() : screenshotDir();

View File

@@ -5,6 +5,7 @@
#include "theme_effects.h" #include "theme_effects.h"
#include "low_spec.h" #include "low_spec.h"
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../layout.h"
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
void ThemeEffects::loadFromTheme() { void ThemeEffects::loadFromTheme() {
auto& S = schema::UI(); auto& S = schema::UI();
const float dp = Layout::dpiScale();
auto eff = [&](const char* name) { auto eff = [&](const char* name) {
return S.drawElement("effects", name); return S.drawElement("effects", name);
}; };
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
// ---- Shimmer ---- // ---- Shimmer ----
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f; shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f); shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f); shimmer_.width = eff("shimmer-width").sizeOr(80.0f) * dp;
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f); shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f); shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
// Shimmer color: read from the schema's color resolver // Shimmer color: read from the schema's color resolver
@@ -124,7 +126,7 @@ void ThemeEffects::loadFromTheme() {
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f); glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f); glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f); glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f); glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
auto glowColorElem = eff("glow-pulse-color"); auto glowColorElem = eff("glow-pulse-color");
if (!glowColorElem.color.empty()) { if (!glowColorElem.color.empty()) {
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255)); glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
@@ -136,7 +138,7 @@ void ThemeEffects::loadFromTheme() {
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f; edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f); edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f); edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f); edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f); edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
auto edgeColorElem = eff("edge-trace-color"); auto edgeColorElem = eff("edge-trace-color");
if (!edgeColorElem.color.empty()) { if (!edgeColorElem.color.empty()) {
@@ -149,7 +151,7 @@ void ThemeEffects::loadFromTheme() {
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f; ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f); ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f); ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f); ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f); ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
auto emberColorElem = eff("ember-rise-color"); auto emberColorElem = eff("ember-rise-color");
if (!emberColorElem.color.empty()) { if (!emberColorElem.color.empty()) {
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero. // accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f; gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f); gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f); gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f); gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
auto gbColorA = eff("gradient-border-color-a"); auto gbColorA = eff("gradient-border-color-a");
if (!gbColorA.color.empty()) { if (!gbColorA.color.empty()) {
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f); sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f); sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f); sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f); sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f); sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f); sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f); sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
@@ -694,6 +696,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const { void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
if (!enabled_ || !ember_rise_.enabled) return; if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
float w = pMax.x - pMin.x; float w = pMax.x - pMin.x;
float h = pMax.y - pMin.y; float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return; if (w <= 0 || h <= 0) return;
@@ -707,7 +710,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
// Deterministic pseudo-random x position per particle // Deterministic pseudo-random x position per particle
// Simple hash: sin of large prime multiples // Simple hash: sin of large prime multiples
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f; float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway
float x = pMin.x + w * xHash + xDrift; float x = pMin.x + w * xHash + xDrift;
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
@@ -745,6 +748,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const { void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (!enabled_ || !ember_rise_.enabled) return; if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
ImGuiViewport* vp = ImGui::GetMainViewport(); ImGuiViewport* vp = ImGui::GetMainViewport();
float vpW = vp->WorkSize.x; float vpW = vp->WorkSize.x;
float vpH = vp->WorkSize.y; float vpH = vp->WorkSize.y;
@@ -765,7 +769,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f; float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
xHash = xHash - (int)xHash; // fractional part xHash = xHash - (int)xHash; // fractional part
if (xHash < 0) xHash += 1.0f; if (xHash < 0) xHash += 1.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f; float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;
float x = vpX + vpW * xHash + xDrift; float x = vpX + vpW * xHash + xDrift;
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top float y = vpY + vpH * (1.0f - phase); // rise from bottom to top

View File

@@ -925,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
// Draw a full-height rounded rect with card rounding (left corners) // Draw a full-height rounded rect with card rounding (left corners)
// and clip to stripe width so the shape follows the corner radius. // and clip to stripe width so the shape follows the corner radius.
if ((card.accentCol & IM_COL32_A_MASK) != 0) { if ((card.accentCol & IM_COL32_A_MASK) != 0) {
float stripeW = 4.0f; float stripeW = 4.0f * Layout::dpiScale();
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true); dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd, dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
ImDrawFlags_RoundCornersLeft); ImDrawFlags_RoundCornersLeft);
@@ -1282,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos(); ImVec2 winPos = ImGui::GetWindowPos();
float winWidth = ImGui::GetWindowWidth(); float winWidth = ImGui::GetWindowWidth();
float barHeight = 36.0f; const float dp = Layout::dpiScale();
float barHeight = 36.0f * dp;
// Get accent color from theme if not provided // Get accent color from theme if not provided
if (!accent_col) { if (!accent_col) {
@@ -1306,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImFont* titleFont = Type().subtitle1(); ImFont* titleFont = Type().subtitle1();
ImGui::PushFont(titleFont); ImGui::PushFont(titleFont);
ImVec2 titleSize = ImGui::CalcTextSize(title); ImVec2 titleSize = ImGui::CalcTextSize(title);
float titleX = barMin.x + 16.0f; float titleX = barMin.x + 16.0f * dp;
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f; float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title); DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
ImGui::PopFont(); ImGui::PopFont();
// Close button (X) on right side // Close button (X) on right side
if (p_open) { if (p_open) {
float btnSize = 24.0f; float btnSize = 24.0f * dp;
float btnX = barMax.x - btnSize - 12.0f; float btnX = barMax.x - btnSize - 12.0f * dp;
float btnY = barMin.y + (barHeight - btnSize) * 0.5f; float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
ImVec2 btnMin(btnX, btnY); ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize); ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
@@ -1327,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Button background on hover // Button background on hover
if (hovered) { if (hovered) {
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f); dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f * dp);
} }
// Draw X icon // Draw X icon
@@ -1348,7 +1349,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
} }
// Reserve space for title bar so content starts below it // Reserve space for title bar so content starts below it
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
return closeClicked; return closeClicked;
} }
@@ -1648,7 +1649,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default // Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining // glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
// tiles, chat) while staying translucent rather than opaque. // tiles, chat) while staying translucent rather than opaque.
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f; cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass); DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
} }
@@ -1662,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Content child. // Content child.
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY)); ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28 * dp, 20 * dp) : ImVec2(28 * dp, 24 * dp));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind) ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose // A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
// content overflowed the viewport); otherwise the child auto-resizes to its content. // content overflowed the viewport); otherwise the child auto-resizes to its content.

View File

@@ -580,11 +580,11 @@ enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXP
static void renderSettingsTabBar(float availWidth) { static void renderSettingsTabBar(float availWidth) {
using namespace material; using namespace material;
struct T { int id; const char* label; const char* idstr; }; struct T { int id; const char* label; const char* idstr; };
static const T tabs[] = { const T tabs[] = {
{TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"}, {TAB_APPEARANCE, TR("grpa_tab_appearance"), "##stabA"}, {TAB_WALLET, TR("grpa_tab_wallet"), "##stabW"},
{TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"}, {TAB_BACKUP, TR("grpa_tab_backup_data"), "##stabB"}, {TAB_NODE, TR("grpa_tab_node_security"), "##stabN"},
{TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"}, {TAB_EXPLORER, TR("grpa_tab_explorer"), "##stabE"}, {TAB_CHAT, TR("grpa_tab_chat"), "##stabC"},
{TAB_ABOUT, "About", "##stabT"}, {TAB_ABOUT, TR("grpa_tab_about"), "##stabT"},
}; };
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* f = Type().body2(); ImFont* f = Type().body2();
@@ -811,7 +811,7 @@ void RenderSettingsPage(App* app) {
if (!skin.valid) { if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true); 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::Selectable(lbl.c_str(), false);
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::PopStyleColor(); 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 // 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 // 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. // 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()) { if (app->daemonAutoShieldActive()) {
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node")); ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
if (!app->daemonAutoShieldAddress().empty()) if (!app->daemonAutoShieldAddress().empty())
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str()); ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
} else if (!app->daemonAutoShieldDisabledReason().empty()) { } else {
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str()); 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); CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_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)) if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
saveSettingsPageState(app->settings()); saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
// 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"));
}
}
} }
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) { if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging); dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
@@ -1697,7 +1689,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_birthday_label")); ImGui::TextUnformatted(TR("lite_birthday_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); 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); ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_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; if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
@@ -1709,7 +1701,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_account_label")); ImGui::TextUnformatted(TR("lite_account_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); 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); ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_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; 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.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(); while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
if (liteKey.empty()) { 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 { } else {
const auto r = app->liteWallet()->importKey(liteKey); const auto r = app->liteWallet()->importKey(liteKey);
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); 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)) if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues"); util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug")); 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; const float rightBottom = ImGui::GetCursorScreenPos().y;
@@ -2676,6 +2672,33 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.current_tab == TAB_NODE) if (s_settingsState.current_tab == TAB_NODE)
ImGui::Dummy(ImVec2(0, gap)); 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 // 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. // 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()) { if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI. // Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine(); 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(); app->seedChatDemoData();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
} }
@@ -2765,29 +2788,29 @@ void RenderSettingsPage(App* app) {
"paymentdisclosure", "pow", "proxy", "prune", "rand", "paymentdisclosure", "pow", "proxy", "prune", "rand",
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc" "reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
}; };
static const char* debugTips[] = { const char* debugTips[] = {
"Peer address tracking and management", TR("grpa_dbg_addrman"),
"Alert system messages", TR("grpa_dbg_alert"),
"Benchmark timings for operations", TR("grpa_dbg_bench"),
"Coin database read/write operations", TR("grpa_dbg_coindb"),
"Berkeley DB operations", TR("grpa_dbg_db"),
"Fee estimation algorithm", TR("grpa_dbg_estimatefee"),
"HTTP RPC server activity", TR("grpa_dbg_http"),
"Libevent networking library", TR("grpa_dbg_libevent"),
"Lock contention debugging", TR("grpa_dbg_lock"),
"Transaction memory pool activity", TR("grpa_dbg_mempool"),
"Network connections and messages", TR("grpa_dbg_net"),
"Payment disclosure protocol", TR("grpa_dbg_paymentdisclosure"),
"Proof-of-work mining activity", TR("grpa_dbg_pow"),
"SOCKS5 proxy connections", TR("grpa_dbg_proxy"),
"Block pruning operations", TR("grpa_dbg_prune"),
"Random number generation", TR("grpa_dbg_rand"),
"Blockchain reindexing progress", TR("grpa_dbg_reindex"),
"RPC command processing", TR("grpa_dbg_rpc"),
"Coin selection for transactions", TR("grpa_dbg_selectcoins"),
"Tor integration and circuit info", TR("grpa_dbg_tor"),
"ZeroMQ notification system", TR("grpa_dbg_zmq"),
"Shielded (z-addr) RPC operations" TR("grpa_dbg_zrpc")
}; };
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]); constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
@@ -3037,7 +3060,7 @@ void RenderSettingsPage(App* app) {
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg")); ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("%s", TR("rescan_from_height")); ImGui::Text("%s", TR("rescan_from_height"));
ImGui::SetNextItemWidth(160.0f); ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height); ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0; if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
} else { } else {
@@ -3051,12 +3074,12 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing(); ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; 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; s_settingsState.confirm_rescan = false;
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(detecting); 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) { if (bootstrapped) {
app->runtimeRescan(s_settingsState.rescan_start_height); app->runtimeRescan(s_settingsState.rescan_start_height);
} else { } else {

View File

@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
float lineW = s_cc.lineW; float lineW = s_cc.lineW;
// --- Outer glow pass: wider, softer dark edge on top-left --- // --- Outer glow pass: wider, softer dark edge on top-left ---
float glowExpand = s_cc.glowExpand; float glowExpand = s_cc.glowExpand * Layout::dpiScale();
int glowA = (int)s_cc.glowAlpha; int glowA = (int)s_cc.glowAlpha;
float glowLineW = s_cc.glowLineW; float glowLineW = s_cc.glowLineW;
{ {
@@ -289,6 +289,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
// inner pass gives crisp bevel edge. All use AddRect with rounding so // inner pass gives crisp bevel edge. All use AddRect with rounding so
// every layer follows the rounded corners perfectly — no clip rects needed. // every layer follows the rounded corners perfectly — no clip rects needed.
{ {
const float dp = Layout::dpiScale();
float cx = (mn.x + mx.x) * 0.5f; float cx = (mn.x + mx.x) * 0.5f;
float cy = (mn.y + mx.y) * 0.5f; float cy = (mn.y + mx.y) * 0.5f;
@@ -304,7 +305,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; }; struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
BevelPass passes[] = { BevelPass passes[] = {
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin) { 0.5f * dp, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel { 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
}; };
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
} }
} }
if (depth > s_ic.threshold) { if (depth > s_ic.threshold) {
float baseInset = s_ic.inset; float baseInset = s_ic.inset * Layout::dpiScale();
int shadowMax = (int)(s_ic.maxAlpha * depth); int shadowMax = (int)(s_ic.maxAlpha * depth);
float fadeRatio = s_ic.fadeRatio; float fadeRatio = s_ic.fadeRatio;
float bW = mx.x - mn.x - baseInset * 2.0f; float bW = mx.x - mn.x - baseInset * 2.0f;
@@ -487,7 +488,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float fixedH = stripH; // collapse strip float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i) for (int i = 0; i < (int)NavPage::Count_; ++i)
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels) if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap; float baseFlexH = baseNavGap;
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (showLabels) { if (showLabels) {
curY += sectionGap; curY += sectionGap;
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY; if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
curY += olFsz + 2.0f + sectionLabelPadBot; curY += olFsz + 2.0f * dp + sectionLabelPadBot;
} else { } else {
curY += sectionGap * 0.4f; curY += sectionGap * 0.4f;
if (nSeparators < 4) separatorY[nSeparators++] = curY; if (nSeparators < 4) separatorY[nSeparators++] = curY;
@@ -653,7 +654,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
fx.drawShimmer(dl, indMin, indMax, btnRnd); fx.drawShimmer(dl, indMin, indMax, btnRnd);
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd); fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
} }
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f); DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f * dp);
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18); DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
buttonRects.push_back({indMin, indMax, btnRnd}); buttonRects.push_back({indMin, indMax, btnRnd});
} }
@@ -684,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
bool itemHasBadge = bool itemHasBadge =
(item.page == NavPage::History && status.unconfirmedTxCount > 0) || (item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
(item.page == NavPage::Mining && status.miningActive) || (item.page == NavPage::Mining && status.miningActive) ||
(item.page == NavPage::Peers && status.peerCount > 0) ||
(item.page == NavPage::Chat && status.chatUnreadCount > 0); (item.page == NavPage::Chat && status.chatUnreadCount > 0);
float badgeReserve = 0.0f; float badgeReserve = 0.0f;
if (itemHasBadge) { if (itemHasBadge) {
@@ -732,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
badgeCol = Warning(); badgeTextCol = OnWarning(); badgeCol = Warning(); badgeTextCol = OnWarning();
} else if (item.page == NavPage::Mining && status.miningActive) { } else if (item.page == NavPage::Mining && status.miningActive) {
dotOnly = true; badgeCol = Success(); dotOnly = true; badgeCol = Success();
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
badgeCount = status.peerCount;
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) { } else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
badgeCount = status.chatUnreadCount; badgeCount = status.chatUnreadCount;
} }

View File

@@ -93,7 +93,7 @@ public:
// Arrow // Arrow
{ {
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f; float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f); ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
ImFont* iconFont = Type().iconMed(); ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont); float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 pos = ImGui::GetCursorScreenPos();
@@ -312,7 +312,8 @@ private:
ImGui::Spacing(); ImGui::Spacing();
ImGui::Spacing(); ImGui::Spacing();
float btnW = 120.0f; const float dp = Layout::dpiScale();
float btnW = 120.0f * dp;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f); ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) { if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false; s_open = false;

View File

@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
} else if (rows.empty()) { } else if (rows.empty()) {
float cw = ImGui::GetContentRegionAvail().x; float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y; float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60; if (ch < 60.0f * dp) ch = 60.0f * dp;
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet"); const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg); ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f); ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);

View File

@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg(); float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
// Card height: must fit the Market card's content (overline + price + 24h) // Card height: must fit the Market card's content (overline + price + 24h)
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f); const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f); const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f); const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
float marketContentH = cardPadLg float marketContentH = cardPadLg
+ ovFont->LegacySize + ovGap + ovFont->LegacySize + ovGap
+ sub1->LegacySize + 2.0f * dp + sub1->LegacySize + 2.0f * dp
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
// Helper: draw accent stripe on left edge, clipped to card rounded corners. // Helper: draw accent stripe on left edge, clipped to card rounded corners.
// We draw a full-size rounded rect (left corners only) and clip it to the // We draw a full-size rounded rect (left corners only) and clip it to the
// stripe width so the shape itself follows the card rounding. // stripe width so the shape itself follows the card rounding.
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f); const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) { auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true); dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding, dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
ImU32 bd = WithAlpha(fg, 90); ImU32 bd = WithAlpha(fg, 90);
snprintf(buf, sizeof(buf), "%s %s", snprintf(buf, sizeof(buf), "%s %s",
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str()); TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd); ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
// Hover → explain what stale means and how old the data actually is. // Hover → explain what stale means and how old the data actually is.
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y))) if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
Tooltip("%s", TR("data_stale_tooltip")); Tooltip("%s", TR("data_stale_tooltip"));
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow // Hover glow
if (material::IsRectHovered(cMin, cMax)) { if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
} }
} }
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
{ {
float privPct = (s_dispTotal > 1e-9) float privPct = (s_dispTotal > 1e-9)
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f; ? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr", snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
privPct, (int)state.z_addresses.size()); privPct, (int)state.z_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
WithAlpha(Success(), 160), buf); WithAlpha(Success(), 160), buf);
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance); snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
ImVec2 ts = capFont->CalcTextSizeA( ImVec2 ts = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, buf); capFont->LegacySize, 10000, 0, buf);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f); float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f); float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
ImVec2 bMin(cMax.x - ts.x - bp * 3, ImVec2 bMin(cMax.x - ts.x - bp * 3,
cMin.y + cardPadLg); cMin.y + cardPadLg);
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp); ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive // Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) { if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive); app->setCurrentPage(NavPage::Receive);
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
OnSurfaceMedium(), DRAGONX_TICKER); OnSurfaceMedium(), DRAGONX_TICKER);
cy += sub1->LegacySize + valGap; cy += sub1->LegacySize + valGap;
snprintf(buf, sizeof(buf), "%d T-addresses", snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
(int)state.t_addresses.size()); (int)state.t_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
OnSurfaceDisabled(), buf); OnSurfaceDisabled(), buf);
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive // Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) { if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive); app->setCurrentPage(NavPage::Receive);
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
[](unsigned char c){ return (char)std::toupper(c); }); [](unsigned char c){ return (char)std::toupper(c); });
float textW = std::max(pSz.x + tickGap + usdSz.x, float textW = std::max(pSz.x + tickGap + usdSz.x,
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x); ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f); float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkLeft = cx + textW + sparkGap; float sparkLeft = cx + textW + sparkGap;
float sparkRight = cMax.x - cardPadLg; float sparkRight = cMax.x - cardPadLg;
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
bool pos = market.change_24h >= 0; bool pos = market.change_24h >= 0;
ImU32 chgCol = pos ? Success() ImU32 chgCol = pos ? Success()
: Error(); : Error();
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
pos ? "+" : "", market.change_24h); pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy), chgCol, buf); ImVec2(cx, cy), chgCol, buf);
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Market // Hover glow + click to Market
if (material::IsRectHovered(cMin, cMax)) { if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Market); app->setCurrentPage(NavPage::Market);
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2(); ImFont* body2 = Type().body2();
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f); float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f); float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f); float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f); float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
// Shielded legend // Shielded legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success()); dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded); snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
legendY += capFont->LegacySize + legendLineGap; legendY += capFont->LegacySize + legendLineGap;
// Transparent legend // Transparent legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning()); dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent); snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
legendY += capFont->LegacySize + legendSectionGap; legendY += capFont->LegacySize + legendSectionGap;
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
const auto& market = state.market; const auto& market = state.market;
if (market.price_usd > 0) { if (market.price_usd > 0) {
if (market.price_usd >= 0.01) if (market.price_usd >= 0.01)
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd); snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
else else
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd); snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
OnSurfaceMedium(), buf); OnSurfaceMedium(), buf);
legendY += capFont->LegacySize + 4 * dp; legendY += capFont->LegacySize + 4 * dp;
bool pos = market.change_24h >= 0; bool pos = market.change_24h >= 0;
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h); snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
pos ? Success() : Error(), buf); pos ? Success() : Error(), buf);
} }
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f); float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY), dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)), IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f)); S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
// Bottom half: proportion bars // Bottom half: proportion bars
float barY = divY + Layout::spacingSm(); float barY = divY + Layout::spacingSm();
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
// Accent stripe — clipped to tile rounded corners // Accent stripe — clipped to tile rounded corners
{ {
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f); float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true); dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding, dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
ImDrawFlags_RoundCornersLeft); ImDrawFlags_RoundCornersLeft);
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
// Click // Click
if (material::IsRectHovered(tMin, tMax)) { if (material::IsRectHovered(tMin, tMax)) {
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) if (ImGui::IsMouseClicked(0))
app->setCurrentPage(tiles[i].nav); app->setCurrentPage(tiles[i].nav);
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Font-content floor per row: icon + label + value must fit // Font-content floor per row: icon + label + value must fit
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize) float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
+ Layout::spacingSm() * 2; + Layout::spacingSm() * 2;
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f); float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3; float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size; float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
float stackH; float stackH;
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
}; };
RowInfo rowInfos[4] = { RowInfo rowInfos[4] = {
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f}, {TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio}, {TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio}, {TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f}, {TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
}; };
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Proportion bar (for shielded/transparent rows — fills gap between label and amount) // Proportion bar (for shielded/transparent rows — fills gap between label and amount)
if (i == 1 || i == 2) { if (i == 1 || i == 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label); ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f); float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f); float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float barH = std::max( float barH = std::max(
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f), S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
// Sparkline in the gap between label and 24h change // Sparkline in the gap between label and 24h change
if (state.market.price_history.size() >= 2) { if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label); ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f); float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f); float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap; float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap; float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) { if (sparkLeft < sparkRight) {
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
ImFont* iconFont = Type().iconSmall(); ImFont* iconFont = Type().iconSmall();
// Font-content floor per row: caption text + vertical padding // Font-content floor per row: caption text + vertical padding
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2; float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f); float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f); float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap; float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
float cardHOverride = S.drawElement(cfgSec, "card-height").size; float cardHOverride = S.drawElement(cfgSec, "card-height").size;
float stackH; float stackH;
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
CellInfo cells[2][2] = { CellInfo cells[2][2] = {
// Row 0: Total Balance (left), Shielded (right) // Row 0: Total Balance (left), Shielded (right)
{ {
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false}, {TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true}, {TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
}, },
// Row 1: Market (left), Transparent (right) // Row 1: Market (left), Transparent (right)
{ {
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false}, {TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true}, {TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
}, },
}; };
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
// Sparkline between label and 24h change // Sparkline between label and 24h change
if (state.market.price_history.size() >= 2) { if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label); ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f); float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f); float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap; float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap; float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) { if (sparkLeft < sparkRight) {
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen, ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
gaugeCy + sinf(needleAngle) * needleLen); gaugeCy + sinf(needleAngle) * needleLen);
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol, dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f)); S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
// Center text: percentage // Center text: percentage
ImFont* sub1 = Type().subtitle1(); ImFont* sub1 = Type().subtitle1();
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
} }
RenderSyncBar(app, dl, vs); RenderSyncBar(app, dl, vs);
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f))); ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
// Row 2: 3 mini-cards inline // Row 2: 3 mini-cards inline
{ {
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f), S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f)); glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f); float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f); int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size; float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm(); float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
// Sparkline between price and percentage // Sparkline between price and percentage
if (market.price_history.size() >= 2) { if (market.price_history.size() >= 2) {
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f); float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f); float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = cx + priceSz.x + sparkGap; float sparkLeft = cx + priceSz.x + sparkGap;
float sparkRightEdge = sparkRight - sparkGap; float sparkRightEdge = sparkRight - sparkGap;
if (sparkLeft < sparkRightEdge) { if (sparkLeft < sparkRightEdge) {
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
{ {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImVec2 sepPos = ImGui::GetCursorScreenPos(); ImVec2 sepPos = ImGui::GetCursorScreenPos();
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f); float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f); float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f); float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f); float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
float x = sepPos.x; float x = sepPos.x;
float endX = sepPos.x + availW; float endX = sepPos.x + availW;
while (x < endX) { while (x < endX) {

View File

@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_loading = false; s_loading = false;
if (!error.empty()) { if (!error.empty()) {
s_error = "Error: " + error; s_error = std::string(TR("grpa_error_prefix")) + error;
return; return;
} }
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_has_data = true; s_has_data = true;
} else { } else {
s_error = "Invalid response from daemon"; s_error = TR("grpa_invalid_response_from_daemon");
} }
} }
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
// Current block info // Current block info
if (state.sync.blocks > 0) { if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks); ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
rpc::RPCClient::TraceScope trace("Explorer / Block info"); rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height}); auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) { if (!hashResult.is_string()) {
error = "unexpected getblockhash result"; error = TR("grpa_unexpected_getblockhash_result");
} else { } else {
block = rpc->call("getblock", {hashResult.get<std::string>()}); block = rpc->call("getblock", {hashResult.get<std::string>()});
} }

View File

@@ -144,7 +144,7 @@ private:
if (!s_bootstrap) { if (!s_bootstrap) {
s_state = State::Failed; s_state = State::Failed;
s_errorMsg = "Bootstrap not initialized"; s_errorMsg = TR("grpc_bootstrap_not_initialized");
return; return;
} }
@@ -227,7 +227,7 @@ private:
s_state = State::Done; s_state = State::Done;
} else { } else {
s_errorMsg = finalProg.error; s_errorMsg = finalProg.error;
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed"; if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
s_state = State::Failed; s_state = State::Failed;
} }
s_bootstrap.reset(); s_bootstrap.reset();

View File

@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; }; struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
const std::string peer = "Ava"; const std::string peer = "Ava";
const PMsg msgs[] = { const PMsg msgs[] = {
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 }, { TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
{ u8"Yep — just confirmed", true, true, false, std::string(TR("chat_you")) + " " + t2 }, { TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() }, { TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
}; };
const int N = 3; const int N = 3;

View File

@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
float outputH = ComputeConsoleOutputHeight( float outputH = ComputeConsoleOutputHeight(
availHeight, availHeight,
input_height, input_height,
schema::UI().drawElement("tabs.console", "output-min-height").size, schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size); schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
ImDrawList* dlOut = ImGui::GetWindowDrawList(); ImDrawList* dlOut = ImGui::GetWindowDrawList();

View File

@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
int64_t diff = now - timestamp; int64_t diff = now - timestamp;
if (diff < 0) diff = 0; if (diff < 0) diff = 0;
if (diff < 60) if (diff < 60)
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff); snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
else if (diff < 3600) else if (diff < 3600)
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60)); snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
else if (diff < 86400) else if (diff < 86400)
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600)); snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
else else
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400)); snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
return buf; return buf;
} }
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
txDL->AddRectFilled(rowStart, txDL->AddRectFilled(rowStart,
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH), ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
WithAlpha(OnSurface(), 10), WithAlpha(OnSurface(), 10),
S.drawElement("tabs.explorer", "row-rounding").size); S.drawElement("tabs.explorer", "row-rounding").size * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", txid.c_str()); material::Tooltip("%s", txid.c_str());
} }
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
} }
if (s_detail_txids.size() > 100) { if (s_detail_txids.size() > 100) {
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size()); snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
ImGui::TextDisabled("%s", buf); ImGui::TextDisabled("%s", buf);
} }
} }

View File

@@ -0,0 +1,115 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_content.h"
namespace dragonx {
namespace ui {
namespace faq {
// ── Wallet group ───────────────────────────────────────────────────────────
const std::vector<FaqSubcategory>& walletFaq()
{
static const std::vector<FaqSubcategory> kWallet = {
{ "faq_w_gs_title", {
{ "faq_w_gs_1_q", "faq_w_gs_1_a" },
{ "faq_w_gs_2_q", "faq_w_gs_2_a" },
{ "faq_w_gs_3_q", "faq_w_gs_3_a" },
{ "faq_w_gs_4_q", "faq_w_gs_4_a" },
}},
{ "faq_w_addr_title", {
{ "faq_w_addr_1_q", "faq_w_addr_1_a" },
{ "faq_w_addr_2_q", "faq_w_addr_2_a" },
{ "faq_w_addr_3_q", "faq_w_addr_3_a" },
{ "faq_w_addr_4_q", "faq_w_addr_4_a" },
}},
{ "faq_w_send_title", {
{ "faq_w_send_1_q", "faq_w_send_1_a" },
{ "faq_w_send_2_q", "faq_w_send_2_a" },
{ "faq_w_send_3_q", "faq_w_send_3_a" },
{ "faq_w_send_4_q", "faq_w_send_4_a" },
}},
{ "faq_w_bal_title", {
{ "faq_w_bal_1_q", "faq_w_bal_1_a" },
{ "faq_w_bal_2_q", "faq_w_bal_2_a" },
{ "faq_w_bal_3_q", "faq_w_bal_3_a" },
}},
{ "faq_w_sec_title", {
{ "faq_w_sec_1_q", "faq_w_sec_1_a" },
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
}},
{ "faq_w_seed_title", {
{ "faq_w_seed_1_q", "faq_w_seed_1_a" },
{ "faq_w_seed_2_q", "faq_w_seed_2_a" },
{ "faq_w_seed_3_q", "faq_w_seed_3_a" },
{ "faq_w_seed_4_q", "faq_w_seed_4_a" },
}},
{ "faq_w_chat_title", {
{ "faq_w_chat_1_q", "faq_w_chat_1_a" },
{ "faq_w_chat_2_q", "faq_w_chat_2_a" },
{ "faq_w_chat_3_q", "faq_w_chat_3_a" },
}},
{ "faq_w_set_title", {
{ "faq_w_set_1_q", "faq_w_set_1_a" },
{ "faq_w_set_2_q", "faq_w_set_2_a" },
{ "faq_w_set_3_q", "faq_w_set_3_a" },
{ "faq_w_set_4_q", "faq_w_set_4_a" },
}},
};
return kWallet;
}
// ── Daemon group (full-node only) ──────────────────────────────────────────
const std::vector<FaqSubcategory>& daemonFaq()
{
static const std::vector<FaqSubcategory> kDaemon = {
{ "faq_d_node_title", {
{ "faq_d_node_1_q", "faq_d_node_1_a" },
{ "faq_d_node_2_q", "faq_d_node_2_a" },
{ "faq_d_node_3_q", "faq_d_node_3_a" },
}},
{ "faq_d_sync_title", {
{ "faq_d_sync_1_q", "faq_d_sync_1_a" },
{ "faq_d_sync_2_q", "faq_d_sync_2_a" },
{ "faq_d_sync_3_q", "faq_d_sync_3_a" },
{ "faq_d_sync_4_q", "faq_d_sync_4_a" },
}},
{ "faq_d_mgmt_title", {
{ "faq_d_mgmt_1_q", "faq_d_mgmt_1_a" },
{ "faq_d_mgmt_2_q", "faq_d_mgmt_2_a" },
{ "faq_d_mgmt_3_q", "faq_d_mgmt_3_a" },
}},
{ "faq_d_upd_title", {
{ "faq_d_upd_1_q", "faq_d_upd_1_a" },
{ "faq_d_upd_2_q", "faq_d_upd_2_a" },
{ "faq_d_upd_3_q", "faq_d_upd_3_a" },
}},
{ "faq_d_mine_title", {
{ "faq_d_mine_1_q", "faq_d_mine_1_a" },
{ "faq_d_mine_2_q", "faq_d_mine_2_a" },
{ "faq_d_mine_3_q", "faq_d_mine_3_a" },
{ "faq_d_mine_4_q", "faq_d_mine_4_a" },
}},
{ "faq_d_net_title", {
{ "faq_d_net_1_q", "faq_d_net_1_a" },
{ "faq_d_net_2_q", "faq_d_net_2_a" },
}},
{ "faq_d_perf_title", {
{ "faq_d_perf_1_q", "faq_d_perf_1_a" },
{ "faq_d_perf_2_q", "faq_d_perf_2_a" },
}},
{ "faq_d_trbl_title", {
{ "faq_d_trbl_1_q", "faq_d_trbl_1_a" },
{ "faq_d_trbl_2_q", "faq_d_trbl_2_a" },
{ "faq_d_trbl_3_q", "faq_d_trbl_3_a" },
{ "faq_d_trbl_4_q", "faq_d_trbl_4_a" },
}},
};
return kDaemon;
}
} // namespace faq
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,41 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// FAQ content model. The FAQ screen is data-driven: this header exposes the two
// top-level groups (Wallet, Daemon) as ordered lists of subcategories, each a list
// of question/answer entries. Every string is an i18n KEY (looked up with TR at
// render time), so wording + translations live in src/util/i18n.cpp + res/lang/*.json
// and never require touching UI code. Add a Q&A by appending a {qKey,aKey} pair here
// and its two strings to loadBuiltinEnglish().
#pragma once
#include <vector>
namespace dragonx {
namespace ui {
namespace faq {
// One question and its answer, both i18n keys. The answer may contain "\n\n"
// paragraph breaks; it is rendered wrapped. Keep answers free of printf specifiers
// (%d/%s/…) — the i18n layer rejects translations whose format signature drifts.
struct FaqEntry {
const char* questionKey;
const char* answerKey;
};
// A named group of questions. titleKey is an i18n key for the subcategory header.
struct FaqSubcategory {
const char* titleKey;
std::vector<FaqEntry> entries;
};
// The two top-level groups. daemonFaq() is full-node material and is only shown when
// the build supports full-node lifecycle actions (see App::supportsFullNodeLifecycleActions()).
const std::vector<FaqSubcategory>& walletFaq();
const std::vector<FaqSubcategory>& daemonFaq();
} // namespace faq
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,201 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_dialog.h"
#include "faq_content.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace dragonx {
namespace ui {
namespace {
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
struct FaqDialogState {
int group = 0;
char search[128] = "";
std::unordered_map<std::string, bool> expanded;
};
FaqDialogState s_faq;
bool entryMatches(const faq::FaqEntry& e, const char* query)
{
return util::containsIgnoreCase(TR(e.questionKey), query) ||
util::containsIgnoreCase(TR(e.answerKey), query);
}
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
void renderAnswer(const char* answerKey)
{
ImGui::Indent(Layout::spacingMd());
ImGui::PushFont(material::Type().body2());
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR(answerKey));
ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::Unindent(Layout::spacingMd());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
} // namespace
void RenderFaqDialog(App* app, bool* p_open)
{
auto& S = schema::UI();
auto win = S.window("dialogs.faq");
const float dp = Layout::dpiScale();
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
// viewport so the content flexes + scrolls on small / HiDPI screens.
const float vpH = ImGui::GetMainViewport()->Size.y;
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
material::OverlayDialogSpec spec;
spec.title = TR("faq_title");
spec.p_open = p_open;
spec.style = material::OverlayStyle::BlurFloat;
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
spec.idSuffix = "faq";
if (!material::BeginOverlayDialog(spec)) {
return;
}
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
// fires when the field isn't capturing it).
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("faq_intro"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const float contentW = ImGui::GetContentRegionAvail().x;
// ── Group tabs: Wallet | Daemon ──
{
const int nTabs = daemonAvailable ? 2 : 1;
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
auto tab = [&](const char* label, int idx) {
const bool active = (s_faq.group == idx);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
}
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
if (active) ImGui::PopStyleColor(2);
};
tab(TR("faq_group_wallet"), 0);
if (daemonAvailable) { ImGui::SameLine(); tab(TR("faq_group_daemon"), 1); }
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ── Search ──
ImGui::SetNextItemWidth(contentW);
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
const bool searching = s_faq.search[0] != '\0';
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
material::ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq();
bool anyShown = false;
bool firstSection = true;
for (const auto& subcat : groups) {
// Collect the entries visible under the current search.
std::vector<const faq::FaqEntry*> visible;
for (const auto& e : subcat.entries) {
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
}
if (visible.empty()) continue;
// Section break: generous space above every section after the first, so topic groups read as
// clearly separated bands rather than one uniform list.
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
firstSection = false;
anyShown = true;
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
// above its (brighter, normal-case) question rows.
material::Type().textColored(material::TypeStyle::Overline,
material::Primary(), TR(subcat.titleKey));
{
const ImVec2 rp = ImGui::GetCursorScreenPos();
const float rw = ImGui::GetContentRegionAvail().x;
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
material::Divider(), 1.0f * dp);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
for (const auto* e : visible) {
const float rowW = ImGui::GetContentRegionAvail().x;
if (searching) {
// Search results: show question + answer directly (no collapsing).
ImGui::PushFont(material::Type().subtitle2());
ImGui::TextWrapped("%s", TR(e->questionKey));
ImGui::PopFont();
renderAnswer(e->answerKey);
} else {
bool& exp = s_faq.expanded[e->questionKey];
std::string id = std::string("##faq_") + e->questionKey;
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
material::Type().subtitle2(), material::OnSurface());
if (exp) renderAnswer(e->answerKey);
}
}
}
if (!anyShown) {
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("faq_no_results"));
ImGui::PopStyleColor();
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
// Close button footer (BlurFloat has no ✕ in the heading).
const float closeW = 120.0f * dp;
material::BeginOverlayDialogFooter(closeW, false);
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
material::EndOverlayDialog();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,18 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
// Renders the Help & FAQ modal. Call every frame while *p_open is true; the dialog
// clears *p_open itself on close (✕ / click-outside / Esc). Content is data-driven
// (see faq_content.h) and grouped into Wallet and Daemon tabs; the Daemon tab is
// hidden on builds without full-node lifecycle support.
void RenderFaqDialog(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx

View File

@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
s_key = found; s_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive s_show_key = wantViewing; // viewing keys are less sensitive
} else { } else {
s_error = r.ok ? std::string("Key not available for this address") : r.error; s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
} }
wallet::secureWipeLiteSecret(found); wallet::secureWipeLiteSecret(found);
s_fetching = false; s_fetching = false;
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
ImGui::TextDisabled("%s", TR("key_export_click_retrieve")); ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
} else { } else {
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ]. // Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
const float gap = 12.0f; const float dp = Layout::dpiScale();
const float gap = 12.0f * dp;
const float avail = ImGui::GetContentRegionAvail().x; const float avail = ImGui::GetContentRegionAvail().x;
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range. // Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
float qrSize = avail * 0.30f; float qrSize = avail * 0.30f;
if (qrSize < 200.0f) qrSize = 200.0f; if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
if (qrSize > 340.0f) qrSize = 340.0f; if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
float keyW = avail - qrSize - gap; float keyW = avail - qrSize - gap;
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
if (!sideBySide) keyW = avail; if (!sideBySide) keyW = avail;
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact // Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact

View File

@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
} }
} else { } else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable"); const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status); DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
if (!market.price_loading && !market.price_error.empty()) { if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error; std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg(); float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
} }
if (errorText.size() < market.price_error.size()) errorText += "..."; if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()), ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
Warning(), errorText.c_str()); Warning(), errorText.c_str());
} }
} }
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
: (tk == ticks - 1) ? plotRight - lblSz.x : (tk == ticks - 1) ? plotRight - lblSz.x
: xpos - lblSz.x * 0.5f; : xpos - lblSz.x * 0.5f;
dl->AddText(capFont, capFont->LegacySize, dl->AddText(capFont, capFont->LegacySize,
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl); ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
} }
} }

View File

@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
} }
}; };
ImFont* stepFont = Type().iconSmall(); ImFont* stepFont = Type().iconSmall();
const float fieldH = capFont->LegacySize + 6.0f * dp; // Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
// capFont+6px, shorter than the box, so they sat misaligned.
const float fieldH = ImGui::GetFrameHeight();
const float sideW = fieldH; // square -/+ buttons const float sideW = fieldH; // square -/+ buttons
const float fieldW = 46.0f * dp; const float fieldW = 46.0f * dp;
const float g = 2.0f * dp; const float g = 2.0f * dp;
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
s_benchConfirm = true; s_benchConfirm = true;
char msg[128]; char msg[128];
snprintf(msg, sizeof(msg), snprintf(msg, sizeof(msg),
"Benchmark takes ~%ds and interrupts mining. Click again to start.", TR("grpc_benchmark_takes_secs"),
(int)(s_benchmark.totalEstimatedSecs() + 0.5f)); (int)(s_benchmark.totalEstimatedSecs() + 0.5f));
Notifications::instance().warning(msg); Notifications::instance().warning(msg);
} else { } else {

View File

@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
if (estActive) if (estActive)
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else else
snprintf(estVal, sizeof(estVal), "N/A"); snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the // Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05) // number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr; const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
else if (totalRAM > 0) else if (totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0); snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
else else
snprintf(sysBuf, sizeof(sysBuf), "N/A"); snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x; float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
float sysTextX = barX + barW - textPadX - sysTextW; float sysTextX = barX + barW - textPadX - sysTextW;

View File

@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
? it->second.feePercent ? it->second.feePercent
: kp.feePercent; : kp.feePercent;
if (feePct >= 0.0) if (feePct >= 0.0)
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(), snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
FormatFeePercent(feePct).c_str()); FormatFeePercent(feePct).c_str());
else else
snprintf(right, sizeof(right), "%s", hrStr.c_str()); snprintf(right, sizeof(right), "%s", hrStr.c_str());
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
// centered ~1000dp band inside the panel's padded content region; the // centered ~1000dp band inside the panel's padded content region; the
// glass panel itself still fills rightW, leftover -> side margin. // glass panel itself still fills rightW, leftover -> side margin.
const float rightContentW = rightW - pad * 2.0f; const float rightContentW = rightW - pad * 2.0f;
const float chartBandW = std::min(rightContentW, 1000.0f * dp); // Fill the panel's content width so the chart extends across the whole panel on a wide window.
const float chartBandX = rightMin.x + pad // (Previously capped at ~1000dp and centered, which left large empty margins either side.)
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f); const float chartBandW = rightContentW;
const float chartBandX = rightMin.x + pad;
// Right panel: live log (if toggled + available) else the sparkline. // Right panel: live log (if toggled + available) else the sparkline.
if (showLogView) { if (showLogView) {

View File

@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
} }
if (benchmarkUpdate.inconclusive) { if (benchmarkUpdate.inconclusive) {
Notifications::instance().warning( Notifications::instance().warning(
"Benchmark inconclusive: no hashrate samples were recorded. " TR("grpc_benchmark_inconclusive"));
"Check the pool connection and try again.");
} }
} }

View File

@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
if (tlsCount == totalPeers) { if (tlsCount == totalPeers) {
ImFont* iconFont = Type().iconSmall(); ImFont* iconFont = Type().iconSmall();
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf); ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
} }
} else { } else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94"); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp); ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp); ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp); dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel); dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
} }
{ {
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
ImU32 tlsBg = WithAlpha(Success(), 25); ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200); ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(badgeX, cy2); ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2); ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp); dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS"); dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
} else { } else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2), dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls")); WithAlpha(Error(), 140), TR("peers_no_tls"));

View File

@@ -259,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s", snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
tag, lblIt->second.c_str(), trunc.c_str(), tag, lblIt->second.c_str(), trunc.c_str(),
addr.balance, DRAGONX_TICKER, addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : ""); isNew ? TR("grpb_new_badge_suffix") : "");
} else { } else {
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s", snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER, tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : ""); isNew ? TR("grpb_new_badge_suffix") : "");
} }
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
@@ -272,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
s_cached_qr_data.clear(); // Force QR regeneration s_cached_qr_data.clear(); // Force QR regeneration
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
material::Tooltip("%s\nBalance: %.8f %s%s", material::Tooltip(TR("grpb_tooltip_address_balance"),
addr.address.c_str(), addr.balance, DRAGONX_TICKER, addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? "\n(selected)" : ""); isCurrent ? TR("grpb_selected_suffix") : "");
} }
ImGui::PopID(); ImGui::PopID();
} }
@@ -716,17 +716,17 @@ void RenderReceiveTab(App* app)
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text); ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f; float ss = iconW * 0.5f;
float cx = startX + ss; float cx = startX + ss;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size; float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size; float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size; float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
float ay1 = cy - ag; float ay1 = cy - ag;
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size); dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled( dl->AddTriangleFilled(
ImVec2(cx + al, ay1), ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss), ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol); ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag; float ay2 = cy + ag;
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size); dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled( dl->AddTriangleFilled(
ImVec2(cx - al, ay2), ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss), ImVec2(cx - al + hss, ay2 - hss),
@@ -744,8 +744,8 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size; float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size; float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size; float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; }; struct Preset { const char* label; double amount; };
Preset presets[] = { Preset presets[] = {
@@ -873,7 +873,7 @@ void RenderReceiveTab(App* app)
RenderQRCode(s_qr_texture, qrSize); RenderQRCode(s_qr_texture, qrSize);
} else { } else {
ImGui::Dummy(ImVec2(qrSize, qrSize)); ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size, ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
qrPanelMin.y + totalQrSize * 0.5f); qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos, dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), TR("qr_unavailable")); OnSurfaceDisabled(), TR("qr_unavailable"));

View File

@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
if (suggestions.empty()) return; if (suggestions.empty()) return;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color))); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size); const float dp = Layout::dpiScale();
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
ImGui::BeginChild(childId, ImVec2(width, sugH), true); ImGui::BeginChild(childId, ImVec2(width, sugH), true);
for (size_t si = 0; si < suggestions.size(); si++) { for (size_t si = 0; si < suggestions.size(); si++) {
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size; int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================ // ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") { static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI(); auto& S = schema::UI();
const float dp = Layout::dpiScale();
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") }; const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH }; const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
for (int fi = 0; fi < 3; fi++) { for (int fi = 0; fi < 3; fi++) {
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size); if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
bool active = (s_fee_tier == fi); bool active = (s_fee_tier == fi);
if (active) { if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
// Max button — use caption font to fit bar height // Max button — use caption font to fit bar height
ImGui::SameLine(0, gap); ImGui::SameLine(0, gap);
char maxId[32]; char maxId[32];
snprintf(maxId, sizeof(maxId), "Max%s", suffix); snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) { if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
s_amount = maxAmount; s_amount = maxAmount;
s_send_max = true; s_send_max = true;
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
bool is_valid_address, double available, bool is_valid_address, double available,
const char* suffix = "") { const char* suffix = "") {
auto& S = schema::UI(); auto& S = schema::UI();
const float dp = Layout::dpiScale();
const auto& state = app->getWalletState(); const auto& state = app->getWalletState();
double total = s_amount + s_fee; double total = s_amount + s_fee;
// Block spending from a view-only source (imported viewing key, no spending key) — it would only // Block spending from a view-only source (imported viewing key, no spending key) — it would only
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (ImGui::BeginPopup(confirmClearId)) { if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("%s", TR("send_clear_fields")); ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing(); ImGui::Spacing();
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) { if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
ClearFormWithUndo(); ClearFormWithUndo();
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
} }
ImGui::SameLine(); ImGui::SameLine();
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) { if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
} }
ImGui::EndPopup(); ImGui::EndPopup();
@@ -1086,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
char undoId[32]; char undoId[32];
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix); snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) { if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot(); RestoreFormSnapshot();
Notifications::instance().info(TR("send_form_restored")); Notifications::instance().info(TR("send_form_restored"));

View File

@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (!skin.valid) { if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true); ImGui::BeginDisabled(true);
std::string label = skin.name + " (invalid)"; std::string label = skin.name + TR("swin_invalid_suffix");
ImGui::Selectable(label.c_str(), false); ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::PushFont(material::Type().iconSmall()); ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) { if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh(); skinMgr.refresh();
Notifications::instance().info("Theme list refreshed"); Notifications::instance().info(TR("swin_theme_list_refreshed"));
} }
ImGui::PopFont(); ImGui::PopFont();
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) { app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) { if (error.empty()) {
std::string version = result.value("version", "unknown"); std::string version = result.value("version", "unknown");
std::string msg = "Connection successful!\ndragonxd version: " + version; std::string msg = std::string(TR("swin_connection_successful")) + version;
Notifications::instance().success(msg); Notifications::instance().success(msg);
} else { } else {
Notifications::instance().error("Connection failed: " + error); Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
} }
}); });
} else { } else {
Notifications::instance().error("RPC client not initialized"); Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
} }
} }
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (error.empty()) { if (error.empty()) {
int start = result.value("start_height", 0); int start = result.value("start_height", 0);
int end = result.value("stop_height", 0); int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) + std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
" to " + std::to_string(end); TR("swin_rescan_to") + std::to_string(end);
Notifications::instance().success(msg); Notifications::instance().success(msg);
} else { } else {
Notifications::instance().error("Rescan failed: " + error); Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
} }
}); });
} else { } else {
Notifications::instance().error("RPC client not initialized"); Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
} }
} }
ImGui::TextDisabled(" %s", TR("settings_rescan_desc")); ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (doConfirm) { if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) { if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared"); Notifications::instance().success(TR("swin_ztx_history_cleared"));
} else { } else {
Notifications::instance().info("No history file found"); Notifications::instance().info(TR("swin_no_history_file_found"));
} }
s_confirm_clear_ztx = false; s_confirm_clear_ztx = false;
} }
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
// Save/Cancel buttons // Save/Cancel buttons
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) { if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings()); saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved"); Notifications::instance().success(TR("swin_settings_saved"));
*p_open = false; *p_open = false;
} }
ImGui::SameLine(); ImGui::SameLine();

View File

@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled) ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly); ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine(); ImGui::SameLine();
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) { if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.txid.c_str()); ImGui::SetClipboardText(tx.txid.c_str());
} }
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly); ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
} }
ImGui::SameLine(); ImGui::SameLine();
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) { if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.address.c_str()); ImGui::SetClipboardText(tx.address.c_str());
} }
} }

View File

@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
int idx_map[] = {-1, 1, 0, 2}; int idx_map[] = {-1, 1, 0, 2};
int idx = idx_map[type_filter]; int idx = idx_map[type_filter];
float xOff = idx * (cardW + cardGap); float xOff = idx * (cardW + cardGap);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3); ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH); ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol; ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
dl->AddRectFilled(acMin, acMax, acCol, 2.0f); dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
} }
ImGui::Dummy(ImVec2(availWidth, cardH)); ImGui::Dummy(ImVec2(availWidth, cardH));

View File

@@ -100,7 +100,7 @@ public:
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap + Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
+ ctrlRow + Layout::spacingSm(); // sort control row + gap + ctrlRow + Layout::spacingSm(); // sort control row + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI // Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall // screen the content-sized card could exceed the window (the framework would clip a too-tall
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls // card, footer and all). When capped, the wallet list flexes + scrolls and the controls