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:
@@ -521,6 +521,8 @@ set(APP_SOURCES
|
||||
src/ui/windows/settings_window.cpp
|
||||
src/ui/pages/settings_page.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/transaction_details_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/settings_window.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/transaction_details_dialog.h
|
||||
src/ui/windows/qr_popup_dialog.h
|
||||
|
||||
@@ -965,6 +965,11 @@ edition-label = { position = 120 }
|
||||
link-button = { width = 100, font = "button-sm" }
|
||||
close-button = { width = 120, font = "button", align = "center" }
|
||||
|
||||
[dialogs.faq]
|
||||
width = 860.0
|
||||
height = 660.0
|
||||
window = { width = 860, height = 660 }
|
||||
|
||||
[dialogs.settings]
|
||||
width = 600.0
|
||||
height = 550.0
|
||||
|
||||
266
src/app.cpp
266
src/app.cpp
@@ -37,6 +37,7 @@
|
||||
#include "ui/windows/market_tab.h"
|
||||
#include "ui/windows/settings_window.h"
|
||||
#include "ui/windows/about_dialog.h"
|
||||
#include "ui/windows/faq_dialog.h"
|
||||
#include "embedded/IconsMaterialDesign.h"
|
||||
#include "ui/windows/key_export_dialog.h"
|
||||
#include "ui/windows/transaction_details_dialog.h"
|
||||
@@ -410,8 +411,8 @@ bool App::init()
|
||||
settings_->setActiveWalletFile("wallet.dat");
|
||||
settings_->save();
|
||||
ui::Notifications::instance().warning(
|
||||
"Your last-used wallet file (" + active + ") was not found — opened the default wallet "
|
||||
"instead. If you moved it, restore it and switch back from the wallet list.", 20.0f);
|
||||
std::string(TR("appx_last_used_wallet_not_found_prefix")) + active +
|
||||
TR("appx_last_used_wallet_not_found_suffix"), 20.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,7 +806,7 @@ void App::update()
|
||||
const std::string& err = lite_wallet_->lastOpenError();
|
||||
if (!err.empty() && err != lite_open_error_) {
|
||||
lite_open_error_ = err;
|
||||
ui::Notifications::instance().error(std::string("Wallet open failed: ") + err, 8.0f);
|
||||
ui::Notifications::instance().error(std::string(TR("appx_wallet_open_failed_prefix")) + err, 8.0f);
|
||||
}
|
||||
}
|
||||
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
|
||||
@@ -989,7 +990,7 @@ void App::update()
|
||||
// poll (which hits the still-running pre-restart daemon, rescanning=false)
|
||||
// would fire a false "complete" the instant rescan was clicked.
|
||||
if (user_initiated_rescan_) {
|
||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
|
||||
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
|
||||
}
|
||||
resetWitnessRescanProgress();
|
||||
@@ -1027,8 +1028,8 @@ void App::update()
|
||||
state_.pool_mining.hashrate_15m = 0.0;
|
||||
pool_starting_.store(false, std::memory_order_relaxed);
|
||||
const std::string err = xmrig_manager_->getLastError();
|
||||
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
|
||||
: ("Miner stopped: " + err));
|
||||
ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
|
||||
: (std::string(TR("appx_miner_stopped_prefix")) + err));
|
||||
}
|
||||
|
||||
// Poll xmrig stats every ~2 seconds (use a simple toggle)
|
||||
@@ -1053,7 +1054,7 @@ void App::update()
|
||||
// Pool mining has a connect delay — announce once it's actually connected/hashing.
|
||||
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
|
||||
pool_starting_.store(false, std::memory_order_relaxed);
|
||||
ui::Notifications::instance().success("Pool miner connected and hashing.");
|
||||
ui::Notifications::instance().success(TR("appx_pool_miner_connected_and_hashing"));
|
||||
}
|
||||
// Get memory directly from OS (more reliable than API)
|
||||
double memMB = xmrig_manager_->getMemoryUsageMB();
|
||||
@@ -1095,7 +1096,7 @@ void App::update()
|
||||
const std::string& status = scan.lastStatus;
|
||||
if (scan.finished) {
|
||||
if (state_.sync.rescanning && user_initiated_rescan_) {
|
||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
||||
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
|
||||
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
|
||||
}
|
||||
// Witness rebuild finishes with the rescan it's part of.
|
||||
@@ -1301,7 +1302,7 @@ void App::update()
|
||||
!runtime_rescan_active_ && !bootstrap_downloading_ &&
|
||||
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
|
||||
post_bootstrap_rescan_pending_ = false;
|
||||
ui::Notifications::instance().info("Bootstrap complete — reconciling your wallet with the new chain data.");
|
||||
ui::Notifications::instance().info(TR("appx_bootstrap_complete_reconciling"));
|
||||
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
|
||||
if (ok && !fullHistory) {
|
||||
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
|
||||
@@ -1323,7 +1324,16 @@ void App::update()
|
||||
if (!state_.warming_up && !runtime_rescan_active_) {
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
||||
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
||||
refreshTransactionData();
|
||||
// Throttle the routine new-block full history rescan (z_listreceivedbyaddress —
|
||||
// O(mapWallet), holds cs_main) by its measured cost, so a large wallet doesn't
|
||||
// re-scan every block and starve connection near the tip. Bypass when there's an
|
||||
// explicit need: a dirty set, an in-progress multi-cycle shielded scan (which must
|
||||
// continue to completion), or an in-flight send — all need fresh data immediately.
|
||||
const bool txExplicitNeed = transactions_dirty_ || shielded_history_scan_pending_ ||
|
||||
hasTransactionSendProgress() || !send_txids_.empty();
|
||||
if (txExplicitNeed || txRefreshDue()) {
|
||||
refreshTransactionData();
|
||||
}
|
||||
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
|
||||
refreshRecentTransactionData();
|
||||
}
|
||||
@@ -1339,7 +1349,13 @@ void App::update()
|
||||
fastScanChatMemos();
|
||||
}
|
||||
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
|
||||
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
|
||||
// Explicit need (a changed address set, or an in-flight send tracking its change output)
|
||||
// must refresh now; the routine periodic poll on a wallet page is throttled by the last
|
||||
// address scan's measured cost (addressRefreshDue) — this is the z_listunspent hammering
|
||||
// that was starving cs_main while synced on a large wallet.
|
||||
if (addresses_dirty_ || hasTransactionSendProgress()) {
|
||||
refreshAddressData();
|
||||
} else if (walletDataPage && addressRefreshDue()) {
|
||||
refreshAddressData();
|
||||
}
|
||||
}
|
||||
@@ -1396,7 +1412,7 @@ void App::handleGlobalShortcuts()
|
||||
settings_->setSkinId(skins[cur].id);
|
||||
settings_->save();
|
||||
}
|
||||
ui::Notifications::instance().info("Theme: " + skins[cur].name);
|
||||
ui::Notifications::instance().info(std::string(TR("appx_theme_prefix")) + skins[cur].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1433,7 +1449,7 @@ void App::handleGlobalShortcuts()
|
||||
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
|
||||
}
|
||||
}
|
||||
ui::Notifications::instance().info(newLow ? "Low-spec mode enabled" : "Low-spec mode disabled");
|
||||
ui::Notifications::instance().info(newLow ? TR("appx_low_spec_mode_enabled") : TR("appx_low_spec_mode_disabled"));
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
|
||||
@@ -1445,7 +1461,7 @@ void App::handleGlobalShortcuts()
|
||||
settings_->setThemeEffectsEnabled(newState);
|
||||
settings_->save();
|
||||
}
|
||||
ui::Notifications::instance().info(newState ? "Theme effects enabled" : "Theme effects disabled");
|
||||
ui::Notifications::instance().info(newState ? TR("appx_theme_effects_enabled") : TR("appx_theme_effects_disabled"));
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
|
||||
@@ -1454,7 +1470,7 @@ void App::handleGlobalShortcuts()
|
||||
settings_->setGradientBackground(newGrad);
|
||||
ui::schema::SkinManager::instance().setGradientMode(newGrad);
|
||||
settings_->save();
|
||||
ui::Notifications::instance().info(newGrad ? "Simple background enabled" : "Simple background disabled");
|
||||
ui::Notifications::instance().info(newGrad ? TR("appx_simple_background_enabled") : TR("appx_simple_background_disabled"));
|
||||
}
|
||||
|
||||
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
|
||||
@@ -1595,8 +1611,9 @@ void App::render()
|
||||
{
|
||||
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
|
||||
if (deletedN >= 0) {
|
||||
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
|
||||
" items). The daemon is restarting to re-sync from the network.");
|
||||
char deletedMsg[160];
|
||||
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
|
||||
ui::Notifications::instance().success(deletedMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2138,6 +2155,10 @@ void App::render()
|
||||
ui::RenderAboutDialog(this, &show_about_);
|
||||
}
|
||||
|
||||
if (show_faq_) {
|
||||
ui::RenderFaqDialog(this, &show_faq_);
|
||||
}
|
||||
|
||||
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
|
||||
renderLiteFirstRunPrompt();
|
||||
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
|
||||
@@ -2881,6 +2902,31 @@ void App::renderStatusBar()
|
||||
occupiedX = bellX;
|
||||
}
|
||||
|
||||
// Help (?) — sits just left of the alert bell, on every platform. A plain, subtle question
|
||||
// mark (no circle) that opens the FAQ.
|
||||
{
|
||||
ImFont* helpFont = ui::material::Type().iconSmall();
|
||||
ImGui::PushFont(helpFont);
|
||||
const float helpGlyphW = ImGui::CalcTextSize(ICON_MD_QUESTION_MARK).x;
|
||||
ImGui::PopFont();
|
||||
const float helpW = helpGlyphW + 10.0f * dp;
|
||||
const float helpH = helpFont->LegacySize + 4.0f * dp;
|
||||
const float helpX = occupiedX - helpW - gap;
|
||||
|
||||
ImGui::SameLine(helpX);
|
||||
ui::material::IconButtonStyle hst;
|
||||
hst.color = ui::material::OnSurfaceMedium();
|
||||
hst.hoverColor = ui::material::OnSurface();
|
||||
hst.hoverBg = ui::material::StateHover();
|
||||
hst.bgRounding = 4.0f * dp;
|
||||
hst.tooltip = TR("faq_open_tooltip");
|
||||
if (ui::material::IconButton("##HelpFaq", ICON_MD_QUESTION_MARK, helpFont,
|
||||
ImVec2(helpW, helpH), hst)) {
|
||||
show_faq_ = true;
|
||||
}
|
||||
occupiedX = helpX;
|
||||
}
|
||||
|
||||
// Version always at far right
|
||||
ImGui::SameLine(versionX);
|
||||
ImGui::Text("%s", versionBuf);
|
||||
@@ -3005,7 +3051,8 @@ void App::renderLiteFirstRunPrompt()
|
||||
|
||||
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
const float btnW = 170.0f;
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
const float btnW = 170.0f * dp;
|
||||
|
||||
if (step == 0) {
|
||||
// ── Welcome ──────────────────────────────────────────────────────────
|
||||
@@ -3013,7 +3060,7 @@ void App::renderLiteFirstRunPrompt()
|
||||
ImGui::TextUnformatted(TR("lite_welcome_title"));
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f);
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f * dp);
|
||||
ImGui::TextUnformatted(TR("lite_welcome_msg"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing(); ImGui::Spacing();
|
||||
@@ -3021,7 +3068,7 @@ void App::renderLiteFirstRunPrompt()
|
||||
if (creating) {
|
||||
// Async create (with server failover) is in flight — driven to completion by
|
||||
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
|
||||
ImGui::TextUnformatted("Creating your wallet\xE2\x80\xA6");
|
||||
ImGui::TextUnformatted(TR("appx_creating_your_wallet"));
|
||||
if (lite_wallet_->walletOpen()) {
|
||||
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
|
||||
if (s.ok && !s.seedPhrase.empty()) {
|
||||
@@ -3039,7 +3086,7 @@ void App::renderLiteFirstRunPrompt()
|
||||
} else if (!lite_wallet_->openInProgress() &&
|
||||
!lite_wallet_->lastOpenError().empty()) {
|
||||
ui::Notifications::instance().warning(
|
||||
std::string("Create failed: ") + lite_wallet_->lastOpenError());
|
||||
std::string(TR("appx_create_failed_prefix")) + lite_wallet_->lastOpenError());
|
||||
creating = false; // back to the buttons so the user can retry
|
||||
}
|
||||
} else {
|
||||
@@ -3065,14 +3112,12 @@ void App::renderLiteFirstRunPrompt()
|
||||
} else if (step == 1) {
|
||||
// ── Reveal the seed + birthday with backup warnings ─────────────────────
|
||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||
ImGui::TextUnformatted("Back up your seed phrase");
|
||||
ImGui::TextUnformatted(TR("appx_back_up_seed_phrase_title"));
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f * dp);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||
ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. "
|
||||
"Write them down in order, store them offline, and never share "
|
||||
"them. If you lose them, your funds are gone forever.");
|
||||
ImGui::TextUnformatted(TR("appx_seed_backup_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
@@ -3082,17 +3127,17 @@ void App::renderLiteFirstRunPrompt()
|
||||
char cell[96];
|
||||
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
||||
ImGui::TextUnformatted(cell);
|
||||
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f);
|
||||
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f * dp);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
char bday[80];
|
||||
snprintf(bday, sizeof(bday), "Birthday (block height): %llu — back this up too.", birthday);
|
||||
snprintf(bday, sizeof(bday), TR("appx_birthday_block_height"), birthday);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(bday);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing(); ImGui::Spacing();
|
||||
|
||||
if (ui::material::TactileButton("I've written it down", ImVec2(btnW, 0))) {
|
||||
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
|
||||
chips.clear();
|
||||
for (const auto& w : words) chips.emplace_back(w, false);
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
@@ -3102,9 +3147,9 @@ void App::renderLiteFirstRunPrompt()
|
||||
step = 2;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
|
||||
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
|
||||
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
|
||||
if (!skipConfirm) {
|
||||
skipConfirm = true; // require a second, deliberate click
|
||||
} else {
|
||||
@@ -3116,27 +3161,26 @@ void App::renderLiteFirstRunPrompt()
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
|
||||
"Skip anyway?");
|
||||
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
} else if (step == 2) {
|
||||
// ── Verify: tap the words in order ──────────────────────────────────────
|
||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||
ImGui::TextUnformatted("Confirm your backup");
|
||||
ImGui::TextUnformatted(TR("appx_confirm_your_backup"));
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextUnformatted("Tap the words in the correct order to confirm you saved them.");
|
||||
ImGui::TextUnformatted(TR("appx_tap_words_in_order"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextDisabled("Progress: %d / %d", progress, (int)words.size());
|
||||
ImGui::TextDisabled(TR("appx_progress_n_of_n"), progress, (int)words.size());
|
||||
if (ImGui::GetTime() < wrongFlashUntil) {
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||
ImGui::TextUnformatted(" — that's not the next word");
|
||||
ImGui::TextUnformatted(TR("appx_not_next_word"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
@@ -3145,9 +3189,9 @@ void App::renderLiteFirstRunPrompt()
|
||||
ImGui::PushID((int)i);
|
||||
if (chips[i].second) {
|
||||
ImGui::BeginDisabled();
|
||||
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0));
|
||||
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0));
|
||||
ImGui::EndDisabled();
|
||||
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0))) {
|
||||
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0))) {
|
||||
if (progress < (int)words.size() && chips[i].first == words[progress]) {
|
||||
chips[i].second = true; // correct next word
|
||||
++progress;
|
||||
@@ -3162,15 +3206,15 @@ void App::renderLiteFirstRunPrompt()
|
||||
|
||||
const bool verified = progress == (int)words.size();
|
||||
if (!verified) ImGui::BeginDisabled();
|
||||
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) {
|
||||
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
|
||||
if (ui::material::TactileButton(TR("appx_done"), ImVec2(btnW, 0))) {
|
||||
ui::Notifications::instance().success(TR("appx_wallet_created_and_backed_up"), 6.0f);
|
||||
finish();
|
||||
}
|
||||
if (!verified) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Back", ImVec2(80, 0))) { skipConfirm = false; step = 1; }
|
||||
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) { skipConfirm = false; step = 1; }
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
|
||||
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
|
||||
if (!skipConfirm) {
|
||||
skipConfirm = true; // require a second, deliberate click
|
||||
} else {
|
||||
@@ -3182,8 +3226,7 @@ void App::renderLiteFirstRunPrompt()
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
|
||||
"Skip anyway?");
|
||||
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
@@ -3200,7 +3243,7 @@ void App::renderLiteFirstRunPrompt()
|
||||
|
||||
if (restoring) {
|
||||
// Async restore (with server failover) in flight — driven by pumpAsyncOpen().
|
||||
ImGui::TextUnformatted("Restoring your wallet\xE2\x80\xA6");
|
||||
ImGui::TextUnformatted(TR("appx_restoring_your_wallet"));
|
||||
if (lite_wallet_->walletOpen()) {
|
||||
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
|
||||
finish(); // wipes restoreSeed; the wallet then syncs from the lite server
|
||||
@@ -3212,10 +3255,10 @@ void App::renderLiteFirstRunPrompt()
|
||||
} else {
|
||||
ImGui::TextUnformatted(TR("lite_restore_seed_label"));
|
||||
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
|
||||
ImVec2(380.0f, ImGui::GetTextLineHeight() * 3.2f));
|
||||
ImVec2(380.0f * dp, ImGui::GetTextLineHeight() * 3.2f));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
ImGui::SetNextItemWidth(160.0f * dp);
|
||||
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
|
||||
if (restoreBirthday < 0) restoreBirthday = 0;
|
||||
|
||||
@@ -3242,8 +3285,10 @@ void App::renderLiteFirstRunPrompt()
|
||||
if (!seedTrim.empty() && !seedLenOk) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " +
|
||||
std::to_string(seedWords) + ".").c_str());
|
||||
char recoveryWordsMsg[96];
|
||||
snprintf(recoveryWordsMsg, sizeof(recoveryWordsMsg),
|
||||
TR("appx_recovery_phrase_word_count"), seedWords);
|
||||
ImGui::TextUnformatted(recoveryWordsMsg);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
@@ -3260,13 +3305,13 @@ void App::renderLiteFirstRunPrompt()
|
||||
restoring = true;
|
||||
} else {
|
||||
restoreErr = lite_wallet_->lastOpenError().empty()
|
||||
? std::string("Could not start restore")
|
||||
? std::string(TR("appx_could_not_start_restore"))
|
||||
: lite_wallet_->lastOpenError();
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
|
||||
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) {
|
||||
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
||||
restoreErr.clear();
|
||||
step = 0;
|
||||
@@ -3290,17 +3335,18 @@ void App::renderLiteUnlockPrompt()
|
||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||
ImGui::TextUnformatted(TR("lite_unlock_title"));
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(TR("lite_unlock_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::SetNextItemWidth(280.0f);
|
||||
ImGui::SetNextItemWidth(280.0f * dp);
|
||||
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
ImGui::Spacing();
|
||||
const float btnW = 130.0f;
|
||||
const float btnW = 130.0f * dp;
|
||||
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
|
||||
if (doUnlock) {
|
||||
const bool ok = lite_wallet_->unlockWallet(pass);
|
||||
@@ -5072,7 +5118,7 @@ void App::handlePaymentURI(const std::string& uri)
|
||||
auto payment = util::parsePaymentURI(uri);
|
||||
|
||||
if (!payment.valid) {
|
||||
ui::Notifications::instance().error("Invalid payment URI: " + payment.error);
|
||||
ui::Notifications::instance().error(std::string(TR("appx_invalid_payment_uri_prefix")) + payment.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5087,7 +5133,7 @@ void App::handlePaymentURI(const std::string& uri)
|
||||
setCurrentPage(ui::NavPage::Send);
|
||||
|
||||
// Notify user
|
||||
std::string msg = "Payment request loaded";
|
||||
std::string msg = TR("appx_payment_request_loaded");
|
||||
if (payment.amount > 0) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
|
||||
@@ -5436,7 +5482,7 @@ bool App::stopDaemonForWalletSwitch()
|
||||
void App::rescanBlockchain()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5451,17 +5497,17 @@ void App::rescanBlockchain()
|
||||
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
|
||||
// running would stomp each other — a second confirm must not launch a duplicate operation.
|
||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||
return;
|
||||
}
|
||||
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
|
||||
if (daemon_restarting_) {
|
||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
||||
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
|
||||
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
|
||||
|
||||
// Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
|
||||
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
|
||||
@@ -5491,7 +5537,7 @@ void App::rescanBlockchain()
|
||||
void App::repairWallet()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5504,16 +5550,16 @@ void App::repairWallet()
|
||||
}
|
||||
|
||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||
return;
|
||||
}
|
||||
if (daemon_restarting_) {
|
||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
||||
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
|
||||
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
|
||||
|
||||
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
|
||||
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
|
||||
@@ -5542,11 +5588,11 @@ void App::repairWallet()
|
||||
void App::reinstallBundledDaemon()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
}
|
||||
if (!resources::getBundledDaemonInfo().available) {
|
||||
ui::Notifications::instance().warning("This build has no bundled daemon to install");
|
||||
ui::Notifications::instance().warning(TR("appx_no_bundled_daemon_to_install"));
|
||||
return;
|
||||
}
|
||||
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
|
||||
@@ -5554,17 +5600,17 @@ void App::reinstallBundledDaemon()
|
||||
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
|
||||
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
|
||||
if (!supportsEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning("This build has no embedded daemon to install");
|
||||
ui::Notifications::instance().warning(TR("appx_no_embedded_daemon_to_install"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (async_tasks_.isRunning("reinstall-daemon")) {
|
||||
ui::Notifications::instance().warning("The daemon reinstall is already in progress.");
|
||||
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
|
||||
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
|
||||
|
||||
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
@@ -5596,7 +5642,7 @@ void App::reinstallBundledDaemon()
|
||||
void App::deleteBlockchainData()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5609,12 +5655,12 @@ void App::deleteBlockchainData()
|
||||
}
|
||||
|
||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
||||
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
||||
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
|
||||
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
|
||||
|
||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||
@@ -5719,7 +5765,7 @@ void App::beginShutdown()
|
||||
|
||||
// Stop xmrig pool miner before stopping the daemon
|
||||
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
|
||||
shutdown_status_ = "Stopping pool miner...";
|
||||
shutdown_status_ = TR("appx_stopping_pool_miner");
|
||||
xmrig_manager_->stop(3000);
|
||||
}
|
||||
|
||||
@@ -5731,7 +5777,7 @@ void App::beginShutdown()
|
||||
// Worker join + RPC disconnect happen in shutdown().
|
||||
if (!daemon_controller_) {
|
||||
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
|
||||
shutdown_status_ = "Disconnecting...";
|
||||
shutdown_status_ = TR("appx_disconnecting");
|
||||
if (settings_) {
|
||||
settings_->save();
|
||||
}
|
||||
@@ -5762,15 +5808,15 @@ void App::beginShutdown()
|
||||
// modal "Please wait" dialog).
|
||||
shutdown_thread_ = std::thread([this]() {
|
||||
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
|
||||
shutdown_status_ = "Sending stop command to daemon...";
|
||||
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
|
||||
// Send RPC stop command
|
||||
stopEmbeddedDaemon();
|
||||
|
||||
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
|
||||
shutdown_status_ = "Cleaning up...";
|
||||
shutdown_status_ = TR("appx_cleaning_up");
|
||||
|
||||
DEBUG_LOGF("shutdown thread: complete\n");
|
||||
shutdown_status_ = "Shutdown complete";
|
||||
shutdown_status_ = TR("appx_shutdown_complete");
|
||||
shutdown_complete_ = true;
|
||||
});
|
||||
}
|
||||
@@ -5883,18 +5929,16 @@ void App::renderDaemonStopConfirm()
|
||||
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextUnformatted("Node is rebuilding its witness cache");
|
||||
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
|
||||
if (Type().subtitle1()) ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
|
||||
ImGui::TextUnformatted(
|
||||
"Stopping the node now discards the in-progress rebuild and restarts it (several minutes) "
|
||||
"the next time you open the wallet. You can keep the node running instead.");
|
||||
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (TactileButton("Keep node running & quit", ImVec2(0, 0))) {
|
||||
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
|
||||
shutdown_keep_daemon_override_ = true;
|
||||
shutdown_confirmed_ = true;
|
||||
daemon_stop_confirm_open_ = false;
|
||||
@@ -5905,7 +5949,7 @@ void App::renderDaemonStopConfirm()
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
|
||||
const bool stopAnyway = TactileButton("Stop anyway & quit", ImVec2(0, 0));
|
||||
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
|
||||
ImGui::PopStyleColor(3);
|
||||
if (stopAnyway) {
|
||||
shutdown_confirmed_ = true;
|
||||
@@ -5914,7 +5958,7 @@ void App::renderDaemonStopConfirm()
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Cancel", ImVec2(0, 0))) {
|
||||
if (TactileButton(TR("appx_cancel"), ImVec2(0, 0))) {
|
||||
daemon_stop_confirm_open_ = false; // abort the quit; stay open
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
@@ -5930,6 +5974,7 @@ void App::renderDaemonStopConfirm()
|
||||
void App::renderShutdownScreen()
|
||||
{
|
||||
using namespace ui::material;
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
auto shutElem = [](const char* key, float fb) {
|
||||
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
|
||||
return v >= 0 ? v : fb;
|
||||
@@ -5983,7 +6028,7 @@ void App::renderShutdownScreen()
|
||||
// -------------------------------------------------------------------
|
||||
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
||||
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
|
||||
float spinnerD = shutElem("spinner-radius", 20.0f) * 2.0f + 12.0f;
|
||||
float spinnerD = shutElem("spinner-radius", 20.0f) * dp * 2.0f + 12.0f * dp;
|
||||
float statusH = lineH * 2.0f;
|
||||
float sepH = lineH;
|
||||
float panelH = shutElem("panel-max-height", 160.0f);
|
||||
@@ -6014,10 +6059,10 @@ void App::renderShutdownScreen()
|
||||
// 2. Animated arc spinner
|
||||
// -------------------------------------------------------------------
|
||||
{
|
||||
float r = shutElem("spinner-radius", 20.0f);
|
||||
float thick = shutElem("spinner-thickness", 3.0f);
|
||||
float r = shutElem("spinner-radius", 20.0f) * dp;
|
||||
float thick = shutElem("spinner-thickness", 3.0f) * dp;
|
||||
// Screen-space centre for draw list
|
||||
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f);
|
||||
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f * dp);
|
||||
|
||||
// Background ring (dim)
|
||||
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
|
||||
@@ -6031,7 +6076,7 @@ void App::renderShutdownScreen()
|
||||
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
|
||||
|
||||
// Advance cursor past the spinner
|
||||
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f));
|
||||
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f * dp));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -6056,9 +6101,9 @@ void App::renderShutdownScreen()
|
||||
char elapsed[64];
|
||||
int secs = (int)shutdown_timer_;
|
||||
if (secs < 60)
|
||||
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs);
|
||||
snprintf(elapsed, sizeof(elapsed), TR("appx_n_seconds"), secs);
|
||||
else
|
||||
snprintf(elapsed, sizeof(elapsed), "%d min %d sec", secs / 60, secs % 60);
|
||||
snprintf(elapsed, sizeof(elapsed), TR("appx_n_min_n_sec"), secs / 60, secs % 60);
|
||||
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImVec2 ts = ImGui::CalcTextSize(elapsed);
|
||||
@@ -6078,7 +6123,8 @@ void App::renderShutdownScreen()
|
||||
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
|
||||
// chainstate; say so instead of a bare button.
|
||||
if (shutdownStalled && !curShut.empty()) {
|
||||
std::string stalledMsg = "Still \"" + curShut + "\" — force quitting now may corrupt chain data.";
|
||||
std::string stalledMsg = std::string(TR("appx_still_status_prefix")) + curShut +
|
||||
TR("appx_still_status_suffix");
|
||||
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
|
||||
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
@@ -6087,7 +6133,7 @@ void App::renderShutdownScreen()
|
||||
ImGui::Spacing();
|
||||
}
|
||||
const char* forceLabel = TR("force_quit");
|
||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f * dp, 0);
|
||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||
@@ -6117,7 +6163,7 @@ void App::renderShutdownScreen()
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
float btnW = 120.0f;
|
||||
float btnW = 120.0f * dp;
|
||||
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
|
||||
|
||||
@@ -6149,7 +6195,7 @@ void App::renderShutdownScreen()
|
||||
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
|
||||
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
|
||||
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
|
||||
ImGui::Dummy(ImVec2(0, 4.0f));
|
||||
ImGui::Dummy(ImVec2(0, 4.0f * dp));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -6182,7 +6228,7 @@ void App::renderShutdownScreen()
|
||||
ImGui::GetCursorPosY() + panelPad));
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
|
||||
ImGui::TextUnformatted("dragonxd output");
|
||||
ImGui::TextUnformatted(TR("appx_dragonxd_output"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
|
||||
@@ -6222,7 +6268,7 @@ void App::renderShutdownScreen()
|
||||
|
||||
// Advance cursor past the panel
|
||||
float panelBottom = panelMax.y - wp.y;
|
||||
ImGui::SetCursorPosY(panelBottom + 4.0f);
|
||||
ImGui::SetCursorPosY(panelBottom + 4.0f * dp);
|
||||
ImGui::Dummy(ImVec2(0, 0));
|
||||
}
|
||||
}
|
||||
@@ -6417,14 +6463,14 @@ void App::renderLoadingOverlay(float contentH)
|
||||
|
||||
char wbuf[128];
|
||||
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
|
||||
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
|
||||
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_blocks_left"),
|
||||
progress * 100.0f, state_.sync.witness_remaining);
|
||||
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
|
||||
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f);
|
||||
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_pct"), progress * 100.0f);
|
||||
else if (progress > 0.01f)
|
||||
snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f);
|
||||
snprintf(wbuf, sizeof(wbuf), TR("appx_setting_initial_sapling_witnesses"), progress * 100.0f);
|
||||
else
|
||||
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
|
||||
snprintf(wbuf, sizeof(wbuf), "%s", TR("appx_rebuilding_sapling_note_witnesses"));
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
|
||||
@@ -6462,7 +6508,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
|
||||
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
|
||||
char syncBuf[128];
|
||||
snprintf(syncBuf, sizeof(syncBuf), "Syncing %.1f%% — Block %d / %d",
|
||||
snprintf(syncBuf, sizeof(syncBuf), TR("appx_syncing_pct_block_n_of_n"),
|
||||
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
@@ -6474,7 +6520,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
} else if (!state_.connected && state_.sync.blocks > 0) {
|
||||
// Show last known block height while reconnecting
|
||||
char blockBuf[64];
|
||||
snprintf(blockBuf, sizeof(blockBuf), "Last block: %d", state_.sync.blocks);
|
||||
snprintf(blockBuf, sizeof(blockBuf), TR("appx_last_block_n"), state_.sync.blocks);
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
|
||||
@@ -6493,8 +6539,8 @@ void App::renderLoadingOverlay(float contentH)
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
|
||||
const char* encLabel = encrypt_in_progress_
|
||||
? "Encrypting wallet..."
|
||||
: "Waiting for daemon to encrypt wallet...";
|
||||
? TR("appx_encrypting_wallet")
|
||||
: TR("appx_waiting_for_daemon_to_encrypt_wallet");
|
||||
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
|
||||
ImU32 encCol = IM_COL32(255, 218, 0, 200);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
@@ -6529,7 +6575,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
|
||||
|
||||
// Error title
|
||||
const char* errTitle = "Daemon Error";
|
||||
const char* errTitle = TR("appx_daemon_error");
|
||||
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
|
||||
dl->AddText(bodyFont2, bodyFont2->LegacySize,
|
||||
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
||||
@@ -6549,7 +6595,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
}
|
||||
// Crash count hint
|
||||
if (daemon_controller_->crashCount() >= 3) {
|
||||
const char* hint = "Use Settings > Restart Daemon to try again";
|
||||
const char* hint = TR("appx_use_settings_restart_daemon_hint");
|
||||
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
|
||||
@@ -6822,7 +6868,7 @@ void App::copySecretToClipboard(const std::string& secret)
|
||||
clipboard_secret_hash_ = secret.empty() ? 0 : h;
|
||||
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
|
||||
if (!secret.empty())
|
||||
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
|
||||
ui::Notifications::instance().info(TR("appx_copied_clipboard_autoclears"), 4.0f);
|
||||
}
|
||||
|
||||
void App::clearSecretClipboardIfArmed()
|
||||
@@ -6916,7 +6962,7 @@ std::string App::buildDiagnosticsReport()
|
||||
void App::restartDaemon()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6956,7 +7002,7 @@ void App::restartDaemon()
|
||||
void App::reindexBlockDatabase()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||
return;
|
||||
}
|
||||
if (!daemon_controller_) return;
|
||||
|
||||
21
src/app.h
21
src/app.h
@@ -462,6 +462,7 @@ public:
|
||||
return 0;
|
||||
}
|
||||
void showAboutDialog() { show_about_ = true; }
|
||||
void showFaqDialog() { show_faq_ = true; }
|
||||
|
||||
// Legacy tab compat — maps int to NavPage
|
||||
void setCurrentTab(int tab);
|
||||
@@ -800,6 +801,7 @@ private:
|
||||
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
|
||||
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_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
|
||||
// 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.
|
||||
@@ -809,6 +811,9 @@ private:
|
||||
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
||||
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
||||
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
|
||||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
@@ -877,6 +882,8 @@ private:
|
||||
void fastScanChatMemos();
|
||||
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)
|
||||
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()
|
||||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||
bool lite_firstrun_dismissed_ = false;
|
||||
@@ -1028,6 +1035,7 @@ private:
|
||||
bool show_demo_window_ = false;
|
||||
bool show_settings_ = false;
|
||||
bool show_about_ = false;
|
||||
bool show_faq_ = false;
|
||||
bool show_import_key_ = false;
|
||||
bool show_export_key_ = 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)
|
||||
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
|
||||
// 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.
|
||||
std::uint64_t clipboard_secret_hash_ = 0;
|
||||
double clipboard_clear_deadline_ = 0.0;
|
||||
@@ -1498,6 +1514,11 @@ private:
|
||||
void applyRefreshPolicy(ui::NavPage page);
|
||||
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 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 shouldRunWalletTransactionRefresh() const;
|
||||
bool shouldRefreshTransactions() const;
|
||||
|
||||
@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
||||
});
|
||||
} else {
|
||||
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) {
|
||||
if (!rpc_ || !rpc_->isConnected()) return;
|
||||
encrypt_in_progress_ = true;
|
||||
encrypt_status_ = "Encrypting wallet...";
|
||||
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||
|
||||
if (worker_) {
|
||||
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
|
||||
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
if (result.encrypted) {
|
||||
return [this]() {
|
||||
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");
|
||||
|
||||
// Immediately update local encryption state so the
|
||||
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
}
|
||||
|
||||
ui::Notifications::instance().info(
|
||||
"Wallet encrypted successfully", 5.0f);
|
||||
TR("sec_wallet_encrypted_successfully"), 5.0f);
|
||||
|
||||
// The daemon shuts itself down after encryptwallet.
|
||||
// Update connection_status_ so the loading overlay
|
||||
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
std::string err = result.error;
|
||||
return [this, err]() {
|
||||
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());
|
||||
|
||||
ui::Notifications::instance().error(
|
||||
"Encryption failed: " + err);
|
||||
std::string(TR("sec_encryption_failed_prefix")) + err);
|
||||
|
||||
// Return to passphrase entry on failure
|
||||
if (show_encrypt_dialog_ &&
|
||||
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
|
||||
std::string pin = std::move(deferredEncryption.pin);
|
||||
|
||||
encrypt_in_progress_ = true;
|
||||
encrypt_status_ = "Encrypting wallet...";
|
||||
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||
|
||||
if (worker_) {
|
||||
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) {
|
||||
settings_->setPinEnabled(true);
|
||||
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 {
|
||||
ui::Notifications::instance().warning(
|
||||
"Wallet encrypted but PIN vault failed");
|
||||
TR("sec_wallet_encrypted_but_pin_vault_failed"));
|
||||
}
|
||||
} else {
|
||||
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
|
||||
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
|
||||
}
|
||||
|
||||
wallet_security_.clearDeferredEncryption();
|
||||
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
|
||||
std::string err = result.error;
|
||||
return [this, err]() {
|
||||
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());
|
||||
ui::Notifications::instance().error("Encryption failed: " + err);
|
||||
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
|
||||
wallet_security_.clearDeferredEncryption();
|
||||
};
|
||||
}
|
||||
@@ -531,7 +531,7 @@ void App::lockWallet() {
|
||||
if (!lock_failure_warned_) {
|
||||
lock_failure_warned_ = true;
|
||||
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) {
|
||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||
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* 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_));
|
||||
unlockTransactionHistoryCacheWithPassphrase(newPass);
|
||||
storeTransactionHistoryCacheIfAvailable();
|
||||
ui::Notifications::instance().info("Passphrase changed successfully");
|
||||
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
|
||||
} else {
|
||||
encrypt_status_ = "Failed: " + err_msg;
|
||||
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
|
||||
}
|
||||
util::SecureVault::secureZero(newPass.data(), newPass.size());
|
||||
};
|
||||
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
|
||||
!encryption_incomplete_warned_) {
|
||||
encryption_incomplete_warned_ = true;
|
||||
ui::Notifications::instance().warning(
|
||||
"Wallet encryption did not complete — your wallet is NOT encrypted. "
|
||||
"Open Settings to finish encrypting it.", 30.0f);
|
||||
TR("sec_encryption_did_not_complete"), 30.0f);
|
||||
}
|
||||
if (state_.transactions.empty()) {
|
||||
loadTransactionHistoryCacheIfAvailable();
|
||||
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
|
||||
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);
|
||||
dl->AddText(titleFont, titleFont->LegacySize,
|
||||
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;
|
||||
|
||||
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);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
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
|
||||
if (hasPinVault) {
|
||||
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_
|
||||
? "Use passphrase instead"
|
||||
: "Use PIN instead";
|
||||
? TR("sec_use_passphrase_instead")
|
||||
: TR("sec_use_pin_instead");
|
||||
|
||||
// Current mode indicator — icon with icon font, text with caption font
|
||||
ImFont* iconFont = ui::material::Type().iconSmall();
|
||||
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
|
||||
if (lock_unlock_in_progress_) {
|
||||
// Animated spinner dots
|
||||
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);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
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::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
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::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
|
||||
r->call("walletpassphrase", {passphrase, timeout});
|
||||
rpcOk = true;
|
||||
} else {
|
||||
rpcErr = "Not connected to daemon";
|
||||
rpcErr = TR("sec_not_connected_to_daemon");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
rpcErr = e.what();
|
||||
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
|
||||
// so route through applyUnlockFailure so the lockout curve applies
|
||||
// (this path previously bumped the counter but skipped the lockout math).
|
||||
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());
|
||||
};
|
||||
}
|
||||
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
|
||||
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
|
||||
if (!unlock.ok) {
|
||||
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(
|
||||
"Importing keys & rescanning blockchain — wallet is usable while this runs",
|
||||
TR("sec_importing_keys_rescanning"),
|
||||
8.0f);
|
||||
};
|
||||
});
|
||||
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
|
||||
wallet_security_workflow_.finishImport();
|
||||
ui::Notifications::instance().error(
|
||||
err +
|
||||
"\nEncrypted backup: wallet.dat.encrypted.bak",
|
||||
TR("sec_encrypted_backup_suffix"),
|
||||
12.0f);
|
||||
};
|
||||
});
|
||||
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
|
||||
refreshPeerInfo();
|
||||
|
||||
ui::Notifications::instance().success(
|
||||
"Wallet decrypted successfully! All keys imported.",
|
||||
TR("sec_wallet_decrypted_all_keys_imported"),
|
||||
8.0f);
|
||||
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
||||
};
|
||||
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
|
||||
int tMins = (int)(totalElapsed / 60);
|
||||
int tSecs = (int)(totalElapsed % 60);
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
|
||||
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
|
||||
}
|
||||
|
||||
// ---- Phase 2: Success ----
|
||||
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying passphrase...";
|
||||
pin_status_ = TR("sec_verifying_passphrase");
|
||||
|
||||
// Verify passphrase + store vault on worker thread to avoid
|
||||
// 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 (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
||||
return [this]() {
|
||||
pin_status_ = "Incorrect passphrase";
|
||||
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
|
||||
pin_in_progress_ = false;
|
||||
};
|
||||
}
|
||||
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
|
||||
pin_status_.clear();
|
||||
pin_in_progress_ = false;
|
||||
show_pin_setup_ = false;
|
||||
ui::Notifications::instance().info("PIN set successfully");
|
||||
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
|
||||
} else {
|
||||
pin_status_ = "Failed to create vault";
|
||||
pin_status_ = TR("sec_failed_to_create_vault");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pin_status_ = "Not connected to daemon";
|
||||
pin_status_ = TR("sec_not_connected_to_daemon_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Changing PIN...";
|
||||
pin_status_ = TR("sec_changing_pin");
|
||||
std::string oldPin(pin_old_buf_);
|
||||
std::string newPinCopy = newPin;
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
|
||||
pin_status_.clear();
|
||||
pin_in_progress_ = false;
|
||||
show_pin_change_ = false;
|
||||
ui::Notifications::instance().info("PIN changed successfully");
|
||||
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
|
||||
} else {
|
||||
pin_status_ = "Incorrect current PIN";
|
||||
pin_status_ = TR("sec_incorrect_current_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pin_status_ = "Internal error";
|
||||
pin_status_ = TR("sec_internal_error_change_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying PIN...";
|
||||
pin_status_ = TR("sec_verifying_pin");
|
||||
std::string oldPin(pin_old_buf_);
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
|
||||
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
|
||||
pin_status_.clear();
|
||||
pin_in_progress_ = false;
|
||||
show_pin_remove_ = false;
|
||||
ui::Notifications::instance().info("PIN removed");
|
||||
ui::Notifications::instance().info(TR("sec_pin_removed"));
|
||||
} else {
|
||||
pin_status_ = "Incorrect PIN";
|
||||
pin_status_ = TR("sec_incorrect_pin_remove");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pin_status_ = "Internal error";
|
||||
pin_status_ = TR("sec_internal_error_remove_pin");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
|
||||
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](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).
|
||||
add("modal-encrypt", ui::NavPage::Settings,
|
||||
[](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_)
|
||||
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;
|
||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||
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; }
|
||||
|
||||
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "theme_effects.h"
|
||||
#include "low_spec.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../layout.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
|
||||
|
||||
void ThemeEffects::loadFromTheme() {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
auto eff = [&](const char* name) {
|
||||
return S.drawElement("effects", name);
|
||||
};
|
||||
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
// ---- Shimmer ----
|
||||
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
|
||||
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_.angle = eff("shimmer-angle").sizeOr(30.0f);
|
||||
// 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_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
|
||||
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");
|
||||
if (!glowColorElem.color.empty()) {
|
||||
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_.speed = eff("edge-trace-speed").sizeOr(0.3f);
|
||||
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);
|
||||
auto edgeColorElem = eff("edge-trace-color");
|
||||
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_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
|
||||
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);
|
||||
auto emberColorElem = eff("ember-rise-color");
|
||||
if (!emberColorElem.color.empty()) {
|
||||
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
// 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_.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);
|
||||
auto gbColorA = eff("gradient-border-color-a");
|
||||
if (!gbColorA.color.empty()) {
|
||||
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
|
||||
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
|
||||
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
|
||||
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_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
|
||||
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 {
|
||||
if (!enabled_ || !ember_rise_.enabled) return;
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
float w = pMax.x - pMin.x;
|
||||
float h = pMax.y - pMin.y;
|
||||
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
|
||||
// Simple hash: sin of large prime multiples
|
||||
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 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 {
|
||||
if (!enabled_ || !ember_rise_.enabled) return;
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
float vpW = vp->WorkSize.x;
|
||||
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;
|
||||
xHash = xHash - (int)xHash; // fractional part
|
||||
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 y = vpY + vpH * (1.0f - phase); // rise from bottom to top
|
||||
|
||||
@@ -925,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
|
||||
// Draw a full-height rounded rect with card rounding (left corners)
|
||||
// and clip to stripe width so the shape follows the corner radius.
|
||||
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->AddRectFilled(cMin, cMax, card.accentCol, rnd,
|
||||
ImDrawFlags_RoundCornersLeft);
|
||||
@@ -1282,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 winPos = ImGui::GetWindowPos();
|
||||
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
|
||||
if (!accent_col) {
|
||||
@@ -1306,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
||||
ImFont* titleFont = Type().subtitle1();
|
||||
ImGui::PushFont(titleFont);
|
||||
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;
|
||||
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
|
||||
ImGui::PopFont();
|
||||
|
||||
// Close button (X) on right side
|
||||
if (p_open) {
|
||||
float btnSize = 24.0f;
|
||||
float btnX = barMax.x - btnSize - 12.0f;
|
||||
float btnSize = 24.0f * dp;
|
||||
float btnX = barMax.x - btnSize - 12.0f * dp;
|
||||
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
|
||||
ImVec2 btnMin(btnX, btnY);
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
|
||||
|
||||
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
|
||||
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -1662,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
|
||||
// Content child.
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
|
||||
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)
|
||||
// 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.
|
||||
|
||||
@@ -580,11 +580,11 @@ enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXP
|
||||
static void renderSettingsTabBar(float availWidth) {
|
||||
using namespace material;
|
||||
struct T { int id; const char* label; const char* idstr; };
|
||||
static const T tabs[] = {
|
||||
{TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"},
|
||||
{TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"},
|
||||
{TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"},
|
||||
{TAB_ABOUT, "About", "##stabT"},
|
||||
const T tabs[] = {
|
||||
{TAB_APPEARANCE, TR("grpa_tab_appearance"), "##stabA"}, {TAB_WALLET, TR("grpa_tab_wallet"), "##stabW"},
|
||||
{TAB_BACKUP, TR("grpa_tab_backup_data"), "##stabB"}, {TAB_NODE, TR("grpa_tab_node_security"), "##stabN"},
|
||||
{TAB_EXPLORER, TR("grpa_tab_explorer"), "##stabE"}, {TAB_CHAT, TR("grpa_tab_chat"), "##stabC"},
|
||||
{TAB_ABOUT, TR("grpa_tab_about"), "##stabT"},
|
||||
};
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* f = Type().body2();
|
||||
@@ -811,7 +811,7 @@ void RenderSettingsPage(App* app) {
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::BeginDisabled(true);
|
||||
std::string lbl = skin.name + " (invalid)";
|
||||
std::string lbl = skin.name + TR("grpa_invalid_suffix");
|
||||
ImGui::Selectable(lbl.c_str(), false);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
@@ -1196,14 +1196,25 @@ void RenderSettingsPage(App* app) {
|
||||
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
|
||||
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
|
||||
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
|
||||
if (app && app->daemonAutoShieldProbed()) {
|
||||
// This status must NOT flow with ImGui's cursor: the checkbox grid is absolutely positioned
|
||||
// (SetCursorScreenPos at rowY), so a normal-flow Text would land under the auto-shield checkbox
|
||||
// and the next grid row (Use Tor / Keep daemon) would draw on top of it. Instead, draw it on its
|
||||
// own full-width row at the grid's current rowY, wrapped to the card, then advance rowY past it.
|
||||
if (app && app->daemonAutoShieldProbed() &&
|
||||
(app->daemonAutoShieldActive() || !app->daemonAutoShieldDisabledReason().empty())) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, rowY));
|
||||
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
|
||||
if (app->daemonAutoShieldActive()) {
|
||||
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
|
||||
if (!app->daemonAutoShieldAddress().empty())
|
||||
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
|
||||
} else if (!app->daemonAutoShieldDisabledReason().empty()) {
|
||||
} else {
|
||||
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
last = ImGui::GetCursorScreenPos().y; // cursor is now below the status text
|
||||
rowY = last + gp; // next grid row starts below it
|
||||
c = 0;
|
||||
}
|
||||
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
||||
@@ -1214,26 +1225,7 @@ void RenderSettingsPage(App* app) {
|
||||
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
|
||||
|
||||
// O2: host a RandomX stratum pool from this node. Only offered on daemons that implement
|
||||
// it (v1.3.0+, version encoded major*1e6+minor*1e4+rev*100+build), so we never show a
|
||||
// toggle that does nothing. Takes effect on the next daemon start/restart. Blank allow-IP
|
||||
// = loopback only (safe); a subnet opens it to that LAN.
|
||||
if (app->state().daemon_version >= 1030000) {
|
||||
if (CB(TrId("stratum_host", "strat_host"), &s_settingsState.stratum_host))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
|
||||
if (s_settingsState.stratum_host) {
|
||||
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
|
||||
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
|
||||
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
|
||||
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (s_settingsState.stratum_allowip[0] != '\0')
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
|
||||
TR("stratum_expose_warn"));
|
||||
}
|
||||
}
|
||||
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
|
||||
}
|
||||
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
|
||||
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
|
||||
@@ -1697,7 +1689,7 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("lite_birthday_label"));
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
|
||||
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
|
||||
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
||||
@@ -1709,7 +1701,7 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(TR("lite_account_label"));
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
|
||||
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
|
||||
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
||||
@@ -1889,7 +1881,7 @@ void RenderSettingsPage(App* app) {
|
||||
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
|
||||
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
|
||||
if (liteKey.empty()) {
|
||||
s_settingsState.lite_backup_status = "Enter a private key to import.";
|
||||
s_settingsState.lite_backup_status = TR("grpa_enter_private_key_to_import");
|
||||
} else {
|
||||
const auto r = app->liteWallet()->importKey(liteKey);
|
||||
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
||||
@@ -2650,6 +2642,10 @@ void RenderSettingsPage(App* app) {
|
||||
if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
|
||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
||||
lf.next(material::ActionButtonWidth(TR("faq"), ICON_MD_QUESTION_MARK));
|
||||
if (material::ActionButton("##aboutfaq", TR("faq"), ICON_MD_QUESTION_MARK, AT::Secondary))
|
||||
app->showFaqDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("faq_open_tooltip"));
|
||||
}
|
||||
const float rightBottom = ImGui::GetCursorScreenPos().y;
|
||||
|
||||
@@ -2676,6 +2672,33 @@ void RenderSettingsPage(App* app) {
|
||||
if (s_settingsState.current_tab == TAB_NODE)
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
// MINING POOL HOSTING — run this node's built-in RandomX stratum server so other miners can point
|
||||
// at this machine. It's a node feature, so it belongs here (not the Wallet tab). Full-node only, and
|
||||
// only on a daemon new enough to implement -stratum (v1.3.0+, version encoded
|
||||
// major*1e6+minor*1e4+rev*100+build). Takes effect on the next daemon start/restart; a blank allow-IP
|
||||
// keeps it loopback-only (safe), a subnet opens it to that LAN.
|
||||
// ====================================================================
|
||||
if (app->supportsFullNodeLifecycleActions() && s_settingsState.current_tab == TAB_NODE &&
|
||||
app->state().daemon_version >= 1030000) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("stratum_host_section"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||
if (ImGui::Checkbox(TR("stratum_host"), &s_settingsState.stratum_host))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
|
||||
if (s_settingsState.stratum_host) {
|
||||
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
|
||||
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
|
||||
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
|
||||
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (s_settingsState.stratum_allowip[0] != '\0')
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
|
||||
TR("stratum_expose_warn"));
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
|
||||
// daemon debug= categories written to DRAGONX.conf; lite has no daemon). Shown on the Node tab.
|
||||
@@ -2738,7 +2761,7 @@ void RenderSettingsPage(App* app) {
|
||||
if (chat::hushChatFeatureEnabledAtBuild()) {
|
||||
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
|
||||
if (TactileButton(TR("grpa_seed_demo_chat"), ImVec2(0, 0), S.resolveFont("button")))
|
||||
app->seedChatDemoData();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
|
||||
}
|
||||
@@ -2765,29 +2788,29 @@ void RenderSettingsPage(App* app) {
|
||||
"paymentdisclosure", "pow", "proxy", "prune", "rand",
|
||||
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
|
||||
};
|
||||
static const char* debugTips[] = {
|
||||
"Peer address tracking and management",
|
||||
"Alert system messages",
|
||||
"Benchmark timings for operations",
|
||||
"Coin database read/write operations",
|
||||
"Berkeley DB operations",
|
||||
"Fee estimation algorithm",
|
||||
"HTTP RPC server activity",
|
||||
"Libevent networking library",
|
||||
"Lock contention debugging",
|
||||
"Transaction memory pool activity",
|
||||
"Network connections and messages",
|
||||
"Payment disclosure protocol",
|
||||
"Proof-of-work mining activity",
|
||||
"SOCKS5 proxy connections",
|
||||
"Block pruning operations",
|
||||
"Random number generation",
|
||||
"Blockchain reindexing progress",
|
||||
"RPC command processing",
|
||||
"Coin selection for transactions",
|
||||
"Tor integration and circuit info",
|
||||
"ZeroMQ notification system",
|
||||
"Shielded (z-addr) RPC operations"
|
||||
const char* debugTips[] = {
|
||||
TR("grpa_dbg_addrman"),
|
||||
TR("grpa_dbg_alert"),
|
||||
TR("grpa_dbg_bench"),
|
||||
TR("grpa_dbg_coindb"),
|
||||
TR("grpa_dbg_db"),
|
||||
TR("grpa_dbg_estimatefee"),
|
||||
TR("grpa_dbg_http"),
|
||||
TR("grpa_dbg_libevent"),
|
||||
TR("grpa_dbg_lock"),
|
||||
TR("grpa_dbg_mempool"),
|
||||
TR("grpa_dbg_net"),
|
||||
TR("grpa_dbg_paymentdisclosure"),
|
||||
TR("grpa_dbg_pow"),
|
||||
TR("grpa_dbg_proxy"),
|
||||
TR("grpa_dbg_prune"),
|
||||
TR("grpa_dbg_rand"),
|
||||
TR("grpa_dbg_reindex"),
|
||||
TR("grpa_dbg_rpc"),
|
||||
TR("grpa_dbg_selectcoins"),
|
||||
TR("grpa_dbg_tor"),
|
||||
TR("grpa_dbg_zmq"),
|
||||
TR("grpa_dbg_zrpc")
|
||||
};
|
||||
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
|
||||
|
||||
@@ -3037,7 +3060,7 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s", TR("rescan_from_height"));
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
ImGui::SetNextItemWidth(160.0f * dp);
|
||||
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
|
||||
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
|
||||
} else {
|
||||
@@ -3051,12 +3074,12 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::Spacing();
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
|
||||
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40 * dp))) {
|
||||
s_settingsState.confirm_rescan = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(detecting);
|
||||
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40 * dp))) {
|
||||
if (bootstrapped) {
|
||||
app->runtimeRescan(s_settingsState.rescan_start_height);
|
||||
} else {
|
||||
|
||||
@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
float lineW = s_cc.lineW;
|
||||
|
||||
// --- 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;
|
||||
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
|
||||
// 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 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; };
|
||||
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
|
||||
};
|
||||
|
||||
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
}
|
||||
}
|
||||
if (depth > s_ic.threshold) {
|
||||
float baseInset = s_ic.inset;
|
||||
float baseInset = s_ic.inset * Layout::dpiScale();
|
||||
int shadowMax = (int)(s_ic.maxAlpha * depth);
|
||||
float fadeRatio = s_ic.fadeRatio;
|
||||
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
|
||||
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
||||
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
|
||||
|
||||
float baseFlexH = baseNavGap;
|
||||
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
if (showLabels) {
|
||||
curY += sectionGap;
|
||||
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
|
||||
curY += olFsz + 2.0f + sectionLabelPadBot;
|
||||
curY += olFsz + 2.0f * dp + sectionLabelPadBot;
|
||||
} else {
|
||||
curY += sectionGap * 0.4f;
|
||||
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.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);
|
||||
buttonRects.push_back({indMin, indMax, btnRnd});
|
||||
}
|
||||
@@ -684,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
bool itemHasBadge =
|
||||
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
|
||||
(item.page == NavPage::Mining && status.miningActive) ||
|
||||
(item.page == NavPage::Peers && status.peerCount > 0) ||
|
||||
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
|
||||
float badgeReserve = 0.0f;
|
||||
if (itemHasBadge) {
|
||||
@@ -732,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
badgeCol = Warning(); badgeTextCol = OnWarning();
|
||||
} else if (item.page == NavPage::Mining && status.miningActive) {
|
||||
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) {
|
||||
badgeCount = status.chatUnreadCount;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public:
|
||||
// Arrow
|
||||
{
|
||||
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f);
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float fsz = ScaledFontSize(iconFont);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
@@ -312,7 +312,8 @@ private:
|
||||
|
||||
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);
|
||||
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
|
||||
s_open = false;
|
||||
|
||||
@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
} else if (rows.empty()) {
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
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");
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||
|
||||
@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
|
||||
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
|
||||
|
||||
// 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 valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
|
||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.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) * dp;
|
||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
|
||||
float marketContentH = cardPadLg
|
||||
+ ovFont->LegacySize + ovGap
|
||||
+ 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.
|
||||
// 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.
|
||||
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) {
|
||||
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
|
||||
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
|
||||
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
|
||||
ImU32 bd = WithAlpha(fg, 90);
|
||||
snprintf(buf, sizeof(buf), "%s %s",
|
||||
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.
|
||||
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
|
||||
Tooltip("%s", TR("data_stale_tooltip"));
|
||||
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow
|
||||
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)),
|
||||
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)(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());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
WithAlpha(Success(), 160), buf);
|
||||
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
|
||||
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
||||
ImVec2 ts = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, buf);
|
||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
|
||||
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").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) * dp;
|
||||
ImVec2 bMin(cMax.x - ts.x - bp * 3,
|
||||
cMin.y + cardPadLg);
|
||||
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
|
||||
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Receive
|
||||
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)),
|
||||
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);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
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());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceDisabled(), buf);
|
||||
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Receive
|
||||
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)),
|
||||
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);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
float textW = std::max(pSz.x + tickGap + usdSz.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 sparkRight = cMax.x - cardPadLg;
|
||||
|
||||
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool pos = market.change_24h >= 0;
|
||||
ImU32 chgCol = pos ? Success()
|
||||
: Error();
|
||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
|
||||
pos ? "+" : "", market.change_24h);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx, cy), chgCol, buf);
|
||||
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Market
|
||||
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)),
|
||||
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);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Market);
|
||||
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* body2 = Type().body2();
|
||||
|
||||
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
|
||||
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
|
||||
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
|
||||
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.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) * dp;
|
||||
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) * dp;
|
||||
|
||||
// Shielded legend
|
||||
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);
|
||||
legendY += capFont->LegacySize + legendLineGap;
|
||||
|
||||
// Transparent legend
|
||||
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);
|
||||
legendY += capFont->LegacySize + legendSectionGap;
|
||||
|
||||
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
|
||||
const auto& market = state.market;
|
||||
if (market.price_usd > 0) {
|
||||
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
|
||||
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),
|
||||
OnSurfaceMedium(), buf);
|
||||
legendY += capFont->LegacySize + 4 * dp;
|
||||
|
||||
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),
|
||||
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);
|
||||
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)),
|
||||
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
|
||||
float barY = divY + Layout::spacingSm();
|
||||
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
|
||||
// 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->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
|
||||
ImDrawFlags_RoundCornersLeft);
|
||||
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
// Click
|
||||
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)),
|
||||
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);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(tiles[i].nav);
|
||||
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Font-content floor per row: icon + label + value must fit
|
||||
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
|
||||
+ 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 vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
|
||||
float stackH;
|
||||
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
};
|
||||
|
||||
RowInfo rowInfos[4] = {
|
||||
{"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},
|
||||
{"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_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
||||
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
||||
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
||||
{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++) {
|
||||
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
|
||||
if (i == 1 || i == 2) {
|
||||
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 barH = std::max(
|
||||
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
|
||||
if (state.market.price_history.size() >= 2) {
|
||||
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 sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.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) * dp;
|
||||
float sparkLeft = px + labelSz.x + sparkGap;
|
||||
float sparkRight = chgX - sparkGap;
|
||||
if (sparkLeft < sparkRight) {
|
||||
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
// Font-content floor per row: caption text + vertical padding
|
||||
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
|
||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
|
||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
|
||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
|
||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
|
||||
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
|
||||
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
|
||||
float stackH;
|
||||
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
CellInfo cells[2][2] = {
|
||||
// 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},
|
||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
||||
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||
},
|
||||
// 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},
|
||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
||||
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
||||
{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
|
||||
if (state.market.price_history.size() >= 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
|
||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = px + labelSz.x + sparkGap;
|
||||
float sparkRight = chgX - sparkGap;
|
||||
if (sparkLeft < sparkRight) {
|
||||
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
|
||||
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
|
||||
gaugeCy + sinf(needleAngle) * needleLen);
|
||||
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
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
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));
|
||||
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);
|
||||
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
|
||||
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
|
||||
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
|
||||
// Sparkline between price and percentage
|
||||
if (market.price_history.size() >= 2) {
|
||||
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
|
||||
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.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) * dp;
|
||||
float sparkLeft = cx + priceSz.x + sparkGap;
|
||||
float sparkRightEdge = sparkRight - sparkGap;
|
||||
if (sparkLeft < sparkRightEdge) {
|
||||
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImVec2 sepPos = ImGui::GetCursorScreenPos();
|
||||
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
|
||||
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.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) * dp;
|
||||
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 endX = sepPos.x + availW;
|
||||
while (x < endX) {
|
||||
|
||||
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
||||
s_loading = false;
|
||||
|
||||
if (!error.empty()) {
|
||||
s_error = "Error: " + error;
|
||||
s_error = std::string(TR("grpa_error_prefix")) + error;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
||||
|
||||
s_has_data = true;
|
||||
} 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
|
||||
if (state.sync.blocks > 0) {
|
||||
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
|
||||
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
|
||||
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
||||
auto hashResult = rpc->call("getblockhash", {height});
|
||||
if (!hashResult.is_string()) {
|
||||
error = "unexpected getblockhash result";
|
||||
error = TR("grpa_unexpected_getblockhash_result");
|
||||
} else {
|
||||
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ private:
|
||||
|
||||
if (!s_bootstrap) {
|
||||
s_state = State::Failed;
|
||||
s_errorMsg = "Bootstrap not initialized";
|
||||
s_errorMsg = TR("grpc_bootstrap_not_initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ private:
|
||||
s_state = State::Done;
|
||||
} else {
|
||||
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_bootstrap.reset();
|
||||
|
||||
@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
|
||||
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
|
||||
const std::string peer = "Ava";
|
||||
const PMsg msgs[] = {
|
||||
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
|
||||
{ u8"Yep — just 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_payment_through"), false, true, true, peer + " " + t1 },
|
||||
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
|
||||
};
|
||||
const int N = 3;
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
float outputH = ComputeConsoleOutputHeight(
|
||||
availHeight,
|
||||
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);
|
||||
|
||||
ImDrawList* dlOut = ImGui::GetWindowDrawList();
|
||||
|
||||
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
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)
|
||||
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)
|
||||
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
txDL->AddRectFilled(rowStart,
|
||||
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
|
||||
WithAlpha(OnSurface(), 10),
|
||||
S.drawElement("tabs.explorer", "row-rounding").size);
|
||||
S.drawElement("tabs.explorer", "row-rounding").size * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", txid.c_str());
|
||||
}
|
||||
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
115
src/ui/windows/faq_content.cpp
Normal file
115
src/ui/windows/faq_content.cpp
Normal 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
|
||||
41
src/ui/windows/faq_content.h
Normal file
41
src/ui/windows/faq_content.h
Normal 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
|
||||
201
src/ui/windows/faq_dialog.cpp
Normal file
201
src/ui/windows/faq_dialog.cpp
Normal 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
|
||||
18
src/ui/windows/faq_dialog.h
Normal file
18
src/ui/windows/faq_dialog.h
Normal 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
|
||||
@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
|
||||
s_key = found;
|
||||
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||
} 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);
|
||||
s_fetching = false;
|
||||
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
||||
} else {
|
||||
// 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;
|
||||
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
|
||||
float qrSize = avail * 0.30f;
|
||||
if (qrSize < 200.0f) qrSize = 200.0f;
|
||||
if (qrSize > 340.0f) qrSize = 340.0f;
|
||||
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
|
||||
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
|
||||
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;
|
||||
|
||||
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
|
||||
|
||||
@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
}
|
||||
} else {
|
||||
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()) {
|
||||
std::string errorText = market.price_error;
|
||||
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 += "...";
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
: (tk == ticks - 1) ? plotRight - lblSz.x
|
||||
: xpos - lblSz.x * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
||||
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
}
|
||||
};
|
||||
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 fieldW = 46.0f * dp;
|
||||
const float g = 2.0f * dp;
|
||||
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
s_benchConfirm = true;
|
||||
char msg[128];
|
||||
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));
|
||||
Notifications::instance().warning(msg);
|
||||
} else {
|
||||
|
||||
@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
if (estActive)
|
||||
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
||||
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
|
||||
// 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;
|
||||
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
else if (totalRAM > 0)
|
||||
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
|
||||
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 sysTextX = barX + barW - textPadX - sysTextW;
|
||||
|
||||
@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
? it->second.feePercent
|
||||
: kp.feePercent;
|
||||
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());
|
||||
else
|
||||
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
|
||||
// glass panel itself still fills rightW, leftover -> side margin.
|
||||
const float rightContentW = rightW - pad * 2.0f;
|
||||
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
|
||||
const float chartBandX = rightMin.x + pad
|
||||
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
|
||||
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
|
||||
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
|
||||
const float chartBandW = rightContentW;
|
||||
const float chartBandX = rightMin.x + pad;
|
||||
|
||||
// Right panel: live log (if toggled + available) else the sparkline.
|
||||
if (showLogView) {
|
||||
|
||||
@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
|
||||
}
|
||||
if (benchmarkUpdate.inconclusive) {
|
||||
Notifications::instance().warning(
|
||||
"Benchmark inconclusive: no hashrate samples were recorded. "
|
||||
"Check the pool connection and try again.");
|
||||
TR("grpc_benchmark_inconclusive"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
|
||||
if (tlsCount == totalPeers) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
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 {
|
||||
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 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->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 tlsFg = WithAlpha(Success(), 200);
|
||||
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->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 {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
|
||||
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
||||
|
||||
@@ -259,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
||||
tag, lblIt->second.c_str(), trunc.c_str(),
|
||||
addr.balance, DRAGONX_TICKER,
|
||||
isNew ? " [NEW]" : "");
|
||||
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isNew ? " [NEW]" : "");
|
||||
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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,
|
||||
isCurrent ? "\n(selected)" : "");
|
||||
isCurrent ? TR("grpb_selected_suffix") : "");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -716,17 +716,17 @@ void RenderReceiveTab(App* app)
|
||||
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
float ss = iconW * 0.5f;
|
||||
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 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;
|
||||
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(
|
||||
ImVec2(cx + al, ay1),
|
||||
ImVec2(cx + al - hss, ay1 - hss),
|
||||
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
||||
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(
|
||||
ImVec2(cx - al, ay2),
|
||||
ImVec2(cx - al + hss, ay2 - hss),
|
||||
@@ -744,8 +744,8 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
|
||||
|
||||
struct Preset { const char* label; double amount; };
|
||||
Preset presets[] = {
|
||||
@@ -873,7 +873,7 @@ void RenderReceiveTab(App* app)
|
||||
RenderQRCode(s_qr_texture, qrSize);
|
||||
} else {
|
||||
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);
|
||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||
OnSurfaceDisabled(), TR("qr_unavailable"));
|
||||
|
||||
@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
||||
if (suggestions.empty()) return;
|
||||
|
||||
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);
|
||||
for (size_t si = 0; si < suggestions.size(); si++) {
|
||||
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 = "") {
|
||||
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 double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
|
||||
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);
|
||||
if (active) {
|
||||
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
|
||||
ImGui::SameLine(0, gap);
|
||||
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)) {
|
||||
s_amount = maxAmount;
|
||||
s_send_max = true;
|
||||
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
bool is_valid_address, double available,
|
||||
const char* suffix = "") {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto& state = app->getWalletState();
|
||||
double total = s_amount + s_fee;
|
||||
// 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)) {
|
||||
ImGui::Text("%s", TR("send_clear_fields"));
|
||||
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();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
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::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_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
|
||||
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"))) {
|
||||
RestoreFormSnapshot();
|
||||
Notifications::instance().info(TR("send_form_restored"));
|
||||
|
||||
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
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::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
|
||||
skinMgr.refresh();
|
||||
Notifications::instance().info("Theme list refreshed");
|
||||
Notifications::instance().info(TR("swin_theme_list_refreshed"));
|
||||
}
|
||||
ImGui::PopFont();
|
||||
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) {
|
||||
if (error.empty()) {
|
||||
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);
|
||||
} else {
|
||||
Notifications::instance().error("Connection failed: " + error);
|
||||
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
|
||||
}
|
||||
});
|
||||
} 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()) {
|
||||
int start = result.value("start_height", 0);
|
||||
int end = result.value("stop_height", 0);
|
||||
std::string msg = "Rescan started from block " + std::to_string(start) +
|
||||
" to " + std::to_string(end);
|
||||
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
|
||||
TR("swin_rescan_to") + std::to_string(end);
|
||||
Notifications::instance().success(msg);
|
||||
} else {
|
||||
Notifications::instance().error("Rescan failed: " + error);
|
||||
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().error("RPC client not initialized");
|
||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
||||
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (doConfirm) {
|
||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||
if (util::Platform::deleteFile(ztx_file)) {
|
||||
Notifications::instance().success("Z-transaction history cleared");
|
||||
Notifications::instance().success(TR("swin_ztx_history_cleared"));
|
||||
} else {
|
||||
Notifications::instance().info("No history file found");
|
||||
Notifications::instance().info(TR("swin_no_history_file_found"));
|
||||
}
|
||||
s_confirm_clear_ztx = false;
|
||||
}
|
||||
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
// Save/Cancel buttons
|
||||
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
||||
saveSettingsFromUI(app->settings());
|
||||
Notifications::instance().success("Settings saved");
|
||||
Notifications::instance().success(TR("swin_settings_saved"));
|
||||
*p_open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -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::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
|
||||
int idx_map[] = {-1, 1, 0, 2};
|
||||
int idx = idx_map[type_filter];
|
||||
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);
|
||||
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));
|
||||
|
||||
@@ -100,7 +100,7 @@ public:
|
||||
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
||||
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + 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
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user