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/windows/settings_window.cpp
|
||||||
src/ui/pages/settings_page.cpp
|
src/ui/pages/settings_page.cpp
|
||||||
src/ui/windows/about_dialog.cpp
|
src/ui/windows/about_dialog.cpp
|
||||||
|
src/ui/windows/faq_dialog.cpp
|
||||||
|
src/ui/windows/faq_content.cpp
|
||||||
src/ui/windows/key_export_dialog.cpp
|
src/ui/windows/key_export_dialog.cpp
|
||||||
src/ui/windows/transaction_details_dialog.cpp
|
src/ui/windows/transaction_details_dialog.cpp
|
||||||
src/ui/windows/qr_popup_dialog.cpp
|
src/ui/windows/qr_popup_dialog.cpp
|
||||||
@@ -657,6 +659,8 @@ set(APP_HEADERS
|
|||||||
src/ui/windows/console_tab_helpers.h
|
src/ui/windows/console_tab_helpers.h
|
||||||
src/ui/windows/settings_window.h
|
src/ui/windows/settings_window.h
|
||||||
src/ui/windows/about_dialog.h
|
src/ui/windows/about_dialog.h
|
||||||
|
src/ui/windows/faq_dialog.h
|
||||||
|
src/ui/windows/faq_content.h
|
||||||
src/ui/windows/key_export_dialog.h
|
src/ui/windows/key_export_dialog.h
|
||||||
src/ui/windows/transaction_details_dialog.h
|
src/ui/windows/transaction_details_dialog.h
|
||||||
src/ui/windows/qr_popup_dialog.h
|
src/ui/windows/qr_popup_dialog.h
|
||||||
|
|||||||
@@ -965,6 +965,11 @@ edition-label = { position = 120 }
|
|||||||
link-button = { width = 100, font = "button-sm" }
|
link-button = { width = 100, font = "button-sm" }
|
||||||
close-button = { width = 120, font = "button", align = "center" }
|
close-button = { width = 120, font = "button", align = "center" }
|
||||||
|
|
||||||
|
[dialogs.faq]
|
||||||
|
width = 860.0
|
||||||
|
height = 660.0
|
||||||
|
window = { width = 860, height = 660 }
|
||||||
|
|
||||||
[dialogs.settings]
|
[dialogs.settings]
|
||||||
width = 600.0
|
width = 600.0
|
||||||
height = 550.0
|
height = 550.0
|
||||||
|
|||||||
264
src/app.cpp
264
src/app.cpp
@@ -37,6 +37,7 @@
|
|||||||
#include "ui/windows/market_tab.h"
|
#include "ui/windows/market_tab.h"
|
||||||
#include "ui/windows/settings_window.h"
|
#include "ui/windows/settings_window.h"
|
||||||
#include "ui/windows/about_dialog.h"
|
#include "ui/windows/about_dialog.h"
|
||||||
|
#include "ui/windows/faq_dialog.h"
|
||||||
#include "embedded/IconsMaterialDesign.h"
|
#include "embedded/IconsMaterialDesign.h"
|
||||||
#include "ui/windows/key_export_dialog.h"
|
#include "ui/windows/key_export_dialog.h"
|
||||||
#include "ui/windows/transaction_details_dialog.h"
|
#include "ui/windows/transaction_details_dialog.h"
|
||||||
@@ -410,8 +411,8 @@ bool App::init()
|
|||||||
settings_->setActiveWalletFile("wallet.dat");
|
settings_->setActiveWalletFile("wallet.dat");
|
||||||
settings_->save();
|
settings_->save();
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
"Your last-used wallet file (" + active + ") was not found — opened the default wallet "
|
std::string(TR("appx_last_used_wallet_not_found_prefix")) + active +
|
||||||
"instead. If you moved it, restore it and switch back from the wallet list.", 20.0f);
|
TR("appx_last_used_wallet_not_found_suffix"), 20.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -805,7 +806,7 @@ void App::update()
|
|||||||
const std::string& err = lite_wallet_->lastOpenError();
|
const std::string& err = lite_wallet_->lastOpenError();
|
||||||
if (!err.empty() && err != lite_open_error_) {
|
if (!err.empty() && err != lite_open_error_) {
|
||||||
lite_open_error_ = err;
|
lite_open_error_ = err;
|
||||||
ui::Notifications::instance().error(std::string("Wallet open failed: ") + err, 8.0f);
|
ui::Notifications::instance().error(std::string(TR("appx_wallet_open_failed_prefix")) + err, 8.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
|
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
|
||||||
@@ -989,7 +990,7 @@ void App::update()
|
|||||||
// poll (which hits the still-running pre-restart daemon, rescanning=false)
|
// poll (which hits the still-running pre-restart daemon, rescanning=false)
|
||||||
// would fire a false "complete" the instant rescan was clicked.
|
// would fire a false "complete" the instant rescan was clicked.
|
||||||
if (user_initiated_rescan_) {
|
if (user_initiated_rescan_) {
|
||||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
|
||||||
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
|
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
|
||||||
}
|
}
|
||||||
resetWitnessRescanProgress();
|
resetWitnessRescanProgress();
|
||||||
@@ -1027,8 +1028,8 @@ void App::update()
|
|||||||
state_.pool_mining.hashrate_15m = 0.0;
|
state_.pool_mining.hashrate_15m = 0.0;
|
||||||
pool_starting_.store(false, std::memory_order_relaxed);
|
pool_starting_.store(false, std::memory_order_relaxed);
|
||||||
const std::string err = xmrig_manager_->getLastError();
|
const std::string err = xmrig_manager_->getLastError();
|
||||||
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
|
ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
|
||||||
: ("Miner stopped: " + err));
|
: (std::string(TR("appx_miner_stopped_prefix")) + err));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll xmrig stats every ~2 seconds (use a simple toggle)
|
// Poll xmrig stats every ~2 seconds (use a simple toggle)
|
||||||
@@ -1053,7 +1054,7 @@ void App::update()
|
|||||||
// Pool mining has a connect delay — announce once it's actually connected/hashing.
|
// Pool mining has a connect delay — announce once it's actually connected/hashing.
|
||||||
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
|
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
|
||||||
pool_starting_.store(false, std::memory_order_relaxed);
|
pool_starting_.store(false, std::memory_order_relaxed);
|
||||||
ui::Notifications::instance().success("Pool miner connected and hashing.");
|
ui::Notifications::instance().success(TR("appx_pool_miner_connected_and_hashing"));
|
||||||
}
|
}
|
||||||
// Get memory directly from OS (more reliable than API)
|
// Get memory directly from OS (more reliable than API)
|
||||||
double memMB = xmrig_manager_->getMemoryUsageMB();
|
double memMB = xmrig_manager_->getMemoryUsageMB();
|
||||||
@@ -1095,7 +1096,7 @@ void App::update()
|
|||||||
const std::string& status = scan.lastStatus;
|
const std::string& status = scan.lastStatus;
|
||||||
if (scan.finished) {
|
if (scan.finished) {
|
||||||
if (state_.sync.rescanning && user_initiated_rescan_) {
|
if (state_.sync.rescanning && user_initiated_rescan_) {
|
||||||
ui::Notifications::instance().success("Blockchain rescan complete");
|
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
|
||||||
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
|
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
|
||||||
}
|
}
|
||||||
// Witness rebuild finishes with the rescan it's part of.
|
// Witness rebuild finishes with the rescan it's part of.
|
||||||
@@ -1301,7 +1302,7 @@ void App::update()
|
|||||||
!runtime_rescan_active_ && !bootstrap_downloading_ &&
|
!runtime_rescan_active_ && !bootstrap_downloading_ &&
|
||||||
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
|
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
|
||||||
post_bootstrap_rescan_pending_ = false;
|
post_bootstrap_rescan_pending_ = false;
|
||||||
ui::Notifications::instance().info("Bootstrap 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) {
|
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
|
||||||
if (ok && !fullHistory) {
|
if (ok && !fullHistory) {
|
||||||
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
|
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
|
||||||
@@ -1323,7 +1324,16 @@ void App::update()
|
|||||||
if (!state_.warming_up && !runtime_rescan_active_) {
|
if (!state_.warming_up && !runtime_rescan_active_) {
|
||||||
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
|
||||||
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
|
||||||
|
// Throttle the routine new-block full history rescan (z_listreceivedbyaddress —
|
||||||
|
// O(mapWallet), holds cs_main) by its measured cost, so a large wallet doesn't
|
||||||
|
// re-scan every block and starve connection near the tip. Bypass when there's an
|
||||||
|
// explicit need: a dirty set, an in-progress multi-cycle shielded scan (which must
|
||||||
|
// continue to completion), or an in-flight send — all need fresh data immediately.
|
||||||
|
const bool txExplicitNeed = transactions_dirty_ || shielded_history_scan_pending_ ||
|
||||||
|
hasTransactionSendProgress() || !send_txids_.empty();
|
||||||
|
if (txExplicitNeed || txRefreshDue()) {
|
||||||
refreshTransactionData();
|
refreshTransactionData();
|
||||||
|
}
|
||||||
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
|
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
|
||||||
refreshRecentTransactionData();
|
refreshRecentTransactionData();
|
||||||
}
|
}
|
||||||
@@ -1339,7 +1349,13 @@ void App::update()
|
|||||||
fastScanChatMemos();
|
fastScanChatMemos();
|
||||||
}
|
}
|
||||||
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
|
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
|
||||||
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
|
// Explicit need (a changed address set, or an in-flight send tracking its change output)
|
||||||
|
// must refresh now; the routine periodic poll on a wallet page is throttled by the last
|
||||||
|
// address scan's measured cost (addressRefreshDue) — this is the z_listunspent hammering
|
||||||
|
// that was starving cs_main while synced on a large wallet.
|
||||||
|
if (addresses_dirty_ || hasTransactionSendProgress()) {
|
||||||
|
refreshAddressData();
|
||||||
|
} else if (walletDataPage && addressRefreshDue()) {
|
||||||
refreshAddressData();
|
refreshAddressData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1396,7 +1412,7 @@ void App::handleGlobalShortcuts()
|
|||||||
settings_->setSkinId(skins[cur].id);
|
settings_->setSkinId(skins[cur].id);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
}
|
}
|
||||||
ui::Notifications::instance().info("Theme: " + skins[cur].name);
|
ui::Notifications::instance().info(std::string(TR("appx_theme_prefix")) + skins[cur].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1433,7 +1449,7 @@ void App::handleGlobalShortcuts()
|
|||||||
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
|
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui::Notifications::instance().info(newLow ? "Low-spec mode enabled" : "Low-spec mode disabled");
|
ui::Notifications::instance().info(newLow ? TR("appx_low_spec_mode_enabled") : TR("appx_low_spec_mode_disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
|
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
|
||||||
@@ -1445,7 +1461,7 @@ void App::handleGlobalShortcuts()
|
|||||||
settings_->setThemeEffectsEnabled(newState);
|
settings_->setThemeEffectsEnabled(newState);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
}
|
}
|
||||||
ui::Notifications::instance().info(newState ? "Theme effects enabled" : "Theme effects disabled");
|
ui::Notifications::instance().info(newState ? TR("appx_theme_effects_enabled") : TR("appx_theme_effects_disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
|
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
|
||||||
@@ -1454,7 +1470,7 @@ void App::handleGlobalShortcuts()
|
|||||||
settings_->setGradientBackground(newGrad);
|
settings_->setGradientBackground(newGrad);
|
||||||
ui::schema::SkinManager::instance().setGradientMode(newGrad);
|
ui::schema::SkinManager::instance().setGradientMode(newGrad);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
ui::Notifications::instance().info(newGrad ? "Simple background enabled" : "Simple background disabled");
|
ui::Notifications::instance().info(newGrad ? TR("appx_simple_background_enabled") : TR("appx_simple_background_disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
|
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
|
||||||
@@ -1595,8 +1611,9 @@ void App::render()
|
|||||||
{
|
{
|
||||||
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
|
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
|
||||||
if (deletedN >= 0) {
|
if (deletedN >= 0) {
|
||||||
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
|
char deletedMsg[160];
|
||||||
" items). The daemon is restarting to re-sync from the network.");
|
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
|
||||||
|
ui::Notifications::instance().success(deletedMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2138,6 +2155,10 @@ void App::render()
|
|||||||
ui::RenderAboutDialog(this, &show_about_);
|
ui::RenderAboutDialog(this, &show_about_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (show_faq_) {
|
||||||
|
ui::RenderFaqDialog(this, &show_faq_);
|
||||||
|
}
|
||||||
|
|
||||||
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
|
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
|
||||||
renderLiteFirstRunPrompt();
|
renderLiteFirstRunPrompt();
|
||||||
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
|
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
|
||||||
@@ -2881,6 +2902,31 @@ void App::renderStatusBar()
|
|||||||
occupiedX = bellX;
|
occupiedX = bellX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Help (?) — sits just left of the alert bell, on every platform. A plain, subtle question
|
||||||
|
// mark (no circle) that opens the FAQ.
|
||||||
|
{
|
||||||
|
ImFont* helpFont = ui::material::Type().iconSmall();
|
||||||
|
ImGui::PushFont(helpFont);
|
||||||
|
const float helpGlyphW = ImGui::CalcTextSize(ICON_MD_QUESTION_MARK).x;
|
||||||
|
ImGui::PopFont();
|
||||||
|
const float helpW = helpGlyphW + 10.0f * dp;
|
||||||
|
const float helpH = helpFont->LegacySize + 4.0f * dp;
|
||||||
|
const float helpX = occupiedX - helpW - gap;
|
||||||
|
|
||||||
|
ImGui::SameLine(helpX);
|
||||||
|
ui::material::IconButtonStyle hst;
|
||||||
|
hst.color = ui::material::OnSurfaceMedium();
|
||||||
|
hst.hoverColor = ui::material::OnSurface();
|
||||||
|
hst.hoverBg = ui::material::StateHover();
|
||||||
|
hst.bgRounding = 4.0f * dp;
|
||||||
|
hst.tooltip = TR("faq_open_tooltip");
|
||||||
|
if (ui::material::IconButton("##HelpFaq", ICON_MD_QUESTION_MARK, helpFont,
|
||||||
|
ImVec2(helpW, helpH), hst)) {
|
||||||
|
show_faq_ = true;
|
||||||
|
}
|
||||||
|
occupiedX = helpX;
|
||||||
|
}
|
||||||
|
|
||||||
// Version always at far right
|
// Version always at far right
|
||||||
ImGui::SameLine(versionX);
|
ImGui::SameLine(versionX);
|
||||||
ImGui::Text("%s", versionBuf);
|
ImGui::Text("%s", versionBuf);
|
||||||
@@ -3005,7 +3051,8 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
|
|
||||||
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
|
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
|
||||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||||
const float btnW = 170.0f;
|
const float dp = ui::Layout::dpiScale();
|
||||||
|
const float btnW = 170.0f * dp;
|
||||||
|
|
||||||
if (step == 0) {
|
if (step == 0) {
|
||||||
// ── Welcome ──────────────────────────────────────────────────────────
|
// ── Welcome ──────────────────────────────────────────────────────────
|
||||||
@@ -3013,7 +3060,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::TextUnformatted(TR("lite_welcome_title"));
|
ImGui::TextUnformatted(TR("lite_welcome_title"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f * dp);
|
||||||
ImGui::TextUnformatted(TR("lite_welcome_msg"));
|
ImGui::TextUnformatted(TR("lite_welcome_msg"));
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
ImGui::Spacing(); ImGui::Spacing();
|
ImGui::Spacing(); ImGui::Spacing();
|
||||||
@@ -3021,7 +3068,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
if (creating) {
|
if (creating) {
|
||||||
// Async create (with server failover) is in flight — driven to completion by
|
// Async create (with server failover) is in flight — driven to completion by
|
||||||
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
|
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
|
||||||
ImGui::TextUnformatted("Creating your wallet\xE2\x80\xA6");
|
ImGui::TextUnformatted(TR("appx_creating_your_wallet"));
|
||||||
if (lite_wallet_->walletOpen()) {
|
if (lite_wallet_->walletOpen()) {
|
||||||
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
|
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
|
||||||
if (s.ok && !s.seedPhrase.empty()) {
|
if (s.ok && !s.seedPhrase.empty()) {
|
||||||
@@ -3039,7 +3086,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
} else if (!lite_wallet_->openInProgress() &&
|
} else if (!lite_wallet_->openInProgress() &&
|
||||||
!lite_wallet_->lastOpenError().empty()) {
|
!lite_wallet_->lastOpenError().empty()) {
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
std::string("Create failed: ") + lite_wallet_->lastOpenError());
|
std::string(TR("appx_create_failed_prefix")) + lite_wallet_->lastOpenError());
|
||||||
creating = false; // back to the buttons so the user can retry
|
creating = false; // back to the buttons so the user can retry
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -3065,14 +3112,12 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
} else if (step == 1) {
|
} else if (step == 1) {
|
||||||
// ── Reveal the seed + birthday with backup warnings ─────────────────────
|
// ── Reveal the seed + birthday with backup warnings ─────────────────────
|
||||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||||
ImGui::TextUnformatted("Back up your seed phrase");
|
ImGui::TextUnformatted(TR("appx_back_up_seed_phrase_title"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f * dp);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||||
ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. "
|
ImGui::TextUnformatted(TR("appx_seed_backup_warning"));
|
||||||
"Write them down in order, store them offline, and never share "
|
|
||||||
"them. If you lose them, your funds are gone forever.");
|
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3082,17 +3127,17 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
char cell[96];
|
char cell[96];
|
||||||
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
||||||
ImGui::TextUnformatted(cell);
|
ImGui::TextUnformatted(cell);
|
||||||
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f);
|
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f * dp);
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
char bday[80];
|
char bday[80];
|
||||||
snprintf(bday, sizeof(bday), "Birthday (block height): %llu — back this up too.", birthday);
|
snprintf(bday, sizeof(bday), TR("appx_birthday_block_height"), birthday);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||||
ImGui::TextUnformatted(bday);
|
ImGui::TextUnformatted(bday);
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::Spacing(); ImGui::Spacing();
|
ImGui::Spacing(); ImGui::Spacing();
|
||||||
|
|
||||||
if (ui::material::TactileButton("I've written it down", ImVec2(btnW, 0))) {
|
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
|
||||||
chips.clear();
|
chips.clear();
|
||||||
for (const auto& w : words) chips.emplace_back(w, false);
|
for (const auto& w : words) chips.emplace_back(w, false);
|
||||||
std::mt19937 rng{std::random_device{}()};
|
std::mt19937 rng{std::random_device{}()};
|
||||||
@@ -3102,9 +3147,9 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
step = 2;
|
step = 2;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
|
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
|
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
|
||||||
if (!skipConfirm) {
|
if (!skipConfirm) {
|
||||||
skipConfirm = true; // require a second, deliberate click
|
skipConfirm = true; // require a second, deliberate click
|
||||||
} else {
|
} else {
|
||||||
@@ -3116,27 +3161,26 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||||
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
|
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
|
||||||
"Skip anyway?");
|
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
}
|
}
|
||||||
} else if (step == 2) {
|
} else if (step == 2) {
|
||||||
// ── Verify: tap the words in order ──────────────────────────────────────
|
// ── Verify: tap the words in order ──────────────────────────────────────
|
||||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||||
ImGui::TextUnformatted("Confirm your backup");
|
ImGui::TextUnformatted(TR("appx_confirm_your_backup"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||||
ImGui::TextUnformatted("Tap the words in the correct order to confirm you saved them.");
|
ImGui::TextUnformatted(TR("appx_tap_words_in_order"));
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImGui::TextDisabled("Progress: %d / %d", progress, (int)words.size());
|
ImGui::TextDisabled(TR("appx_progress_n_of_n"), progress, (int)words.size());
|
||||||
if (ImGui::GetTime() < wrongFlashUntil) {
|
if (ImGui::GetTime() < wrongFlashUntil) {
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||||
ImGui::TextUnformatted(" — that's not the next word");
|
ImGui::TextUnformatted(TR("appx_not_next_word"));
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3145,9 +3189,9 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::PushID((int)i);
|
ImGui::PushID((int)i);
|
||||||
if (chips[i].second) {
|
if (chips[i].second) {
|
||||||
ImGui::BeginDisabled();
|
ImGui::BeginDisabled();
|
||||||
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0));
|
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0));
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0))) {
|
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0))) {
|
||||||
if (progress < (int)words.size() && chips[i].first == words[progress]) {
|
if (progress < (int)words.size() && chips[i].first == words[progress]) {
|
||||||
chips[i].second = true; // correct next word
|
chips[i].second = true; // correct next word
|
||||||
++progress;
|
++progress;
|
||||||
@@ -3162,15 +3206,15 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
|
|
||||||
const bool verified = progress == (int)words.size();
|
const bool verified = progress == (int)words.size();
|
||||||
if (!verified) ImGui::BeginDisabled();
|
if (!verified) ImGui::BeginDisabled();
|
||||||
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) {
|
if (ui::material::TactileButton(TR("appx_done"), ImVec2(btnW, 0))) {
|
||||||
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
|
ui::Notifications::instance().success(TR("appx_wallet_created_and_backed_up"), 6.0f);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
if (!verified) ImGui::EndDisabled();
|
if (!verified) ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Back", ImVec2(80, 0))) { skipConfirm = false; step = 1; }
|
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) { skipConfirm = false; step = 1; }
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
|
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
|
||||||
if (!skipConfirm) {
|
if (!skipConfirm) {
|
||||||
skipConfirm = true; // require a second, deliberate click
|
skipConfirm = true; // require a second, deliberate click
|
||||||
} else {
|
} else {
|
||||||
@@ -3182,8 +3226,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||||
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
|
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
|
||||||
"Skip anyway?");
|
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
}
|
}
|
||||||
@@ -3200,7 +3243,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
|
|
||||||
if (restoring) {
|
if (restoring) {
|
||||||
// Async restore (with server failover) in flight — driven by pumpAsyncOpen().
|
// Async restore (with server failover) in flight — driven by pumpAsyncOpen().
|
||||||
ImGui::TextUnformatted("Restoring your wallet\xE2\x80\xA6");
|
ImGui::TextUnformatted(TR("appx_restoring_your_wallet"));
|
||||||
if (lite_wallet_->walletOpen()) {
|
if (lite_wallet_->walletOpen()) {
|
||||||
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
|
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
|
||||||
finish(); // wipes restoreSeed; the wallet then syncs from the lite server
|
finish(); // wipes restoreSeed; the wallet then syncs from the lite server
|
||||||
@@ -3212,10 +3255,10 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
} else {
|
} else {
|
||||||
ImGui::TextUnformatted(TR("lite_restore_seed_label"));
|
ImGui::TextUnformatted(TR("lite_restore_seed_label"));
|
||||||
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
|
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
|
||||||
ImVec2(380.0f, ImGui::GetTextLineHeight() * 3.2f));
|
ImVec2(380.0f * dp, ImGui::GetTextLineHeight() * 3.2f));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
|
ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
|
||||||
ImGui::SetNextItemWidth(160.0f);
|
ImGui::SetNextItemWidth(160.0f * dp);
|
||||||
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
|
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
|
||||||
if (restoreBirthday < 0) restoreBirthday = 0;
|
if (restoreBirthday < 0) restoreBirthday = 0;
|
||||||
|
|
||||||
@@ -3242,8 +3285,10 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
if (!seedTrim.empty() && !seedLenOk) {
|
if (!seedTrim.empty() && !seedLenOk) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||||
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " +
|
char recoveryWordsMsg[96];
|
||||||
std::to_string(seedWords) + ".").c_str());
|
snprintf(recoveryWordsMsg, sizeof(recoveryWordsMsg),
|
||||||
|
TR("appx_recovery_phrase_word_count"), seedWords);
|
||||||
|
ImGui::TextUnformatted(recoveryWordsMsg);
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3260,13 +3305,13 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
restoring = true;
|
restoring = true;
|
||||||
} else {
|
} else {
|
||||||
restoreErr = lite_wallet_->lastOpenError().empty()
|
restoreErr = lite_wallet_->lastOpenError().empty()
|
||||||
? std::string("Could not start restore")
|
? std::string(TR("appx_could_not_start_restore"))
|
||||||
: lite_wallet_->lastOpenError();
|
: lite_wallet_->lastOpenError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
|
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) {
|
||||||
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
||||||
restoreErr.clear();
|
restoreErr.clear();
|
||||||
step = 0;
|
step = 0;
|
||||||
@@ -3290,17 +3335,18 @@ void App::renderLiteUnlockPrompt()
|
|||||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||||
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
|
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
|
||||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||||
|
const float dp = ui::Layout::dpiScale();
|
||||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||||
ImGui::TextUnformatted(TR("lite_unlock_title"));
|
ImGui::TextUnformatted(TR("lite_unlock_title"));
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextUnformatted(TR("lite_unlock_msg"));
|
ImGui::TextUnformatted(TR("lite_unlock_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::SetNextItemWidth(280.0f);
|
ImGui::SetNextItemWidth(280.0f * dp);
|
||||||
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
|
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
|
||||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
const float btnW = 130.0f;
|
const float btnW = 130.0f * dp;
|
||||||
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
|
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
|
||||||
if (doUnlock) {
|
if (doUnlock) {
|
||||||
const bool ok = lite_wallet_->unlockWallet(pass);
|
const bool ok = lite_wallet_->unlockWallet(pass);
|
||||||
@@ -5072,7 +5118,7 @@ void App::handlePaymentURI(const std::string& uri)
|
|||||||
auto payment = util::parsePaymentURI(uri);
|
auto payment = util::parsePaymentURI(uri);
|
||||||
|
|
||||||
if (!payment.valid) {
|
if (!payment.valid) {
|
||||||
ui::Notifications::instance().error("Invalid payment URI: " + payment.error);
|
ui::Notifications::instance().error(std::string(TR("appx_invalid_payment_uri_prefix")) + payment.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5087,7 +5133,7 @@ void App::handlePaymentURI(const std::string& uri)
|
|||||||
setCurrentPage(ui::NavPage::Send);
|
setCurrentPage(ui::NavPage::Send);
|
||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
std::string msg = "Payment request loaded";
|
std::string msg = TR("appx_payment_request_loaded");
|
||||||
if (payment.amount > 0) {
|
if (payment.amount > 0) {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
|
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
|
||||||
@@ -5436,7 +5482,7 @@ bool App::stopDaemonForWalletSwitch()
|
|||||||
void App::rescanBlockchain()
|
void App::rescanBlockchain()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5451,17 +5497,17 @@ void App::rescanBlockchain()
|
|||||||
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
|
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
|
||||||
// running would stomp each other — a second confirm must not launch a duplicate operation.
|
// running would stomp each other — a second confirm must not launch a duplicate operation.
|
||||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
|
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
|
||||||
if (daemon_restarting_) {
|
if (daemon_restarting_) {
|
||||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
||||||
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
|
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
|
||||||
|
|
||||||
// Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
|
// Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
|
||||||
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
|
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
|
||||||
@@ -5491,7 +5537,7 @@ void App::rescanBlockchain()
|
|||||||
void App::repairWallet()
|
void App::repairWallet()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5504,16 +5550,16 @@ void App::repairWallet()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (daemon_restarting_) {
|
if (daemon_restarting_) {
|
||||||
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
|
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
|
||||||
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
|
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
|
||||||
|
|
||||||
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
|
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
|
||||||
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
|
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
|
||||||
@@ -5542,11 +5588,11 @@ void App::repairWallet()
|
|||||||
void App::reinstallBundledDaemon()
|
void App::reinstallBundledDaemon()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!resources::getBundledDaemonInfo().available) {
|
if (!resources::getBundledDaemonInfo().available) {
|
||||||
ui::Notifications::instance().warning("This build has no bundled daemon to install");
|
ui::Notifications::instance().warning(TR("appx_no_bundled_daemon_to_install"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
|
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
|
||||||
@@ -5554,17 +5600,17 @@ void App::reinstallBundledDaemon()
|
|||||||
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
|
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
|
||||||
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
|
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
|
||||||
if (!supportsEmbeddedDaemon()) {
|
if (!supportsEmbeddedDaemon()) {
|
||||||
ui::Notifications::instance().warning("This build has no embedded daemon to install");
|
ui::Notifications::instance().warning(TR("appx_no_embedded_daemon_to_install"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (async_tasks_.isRunning("reinstall-daemon")) {
|
if (async_tasks_.isRunning("reinstall-daemon")) {
|
||||||
ui::Notifications::instance().warning("The daemon reinstall is already in progress.");
|
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
||||||
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
|
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
|
||||||
|
|
||||||
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
AppDaemonLifecycleRuntime runtime(*this);
|
AppDaemonLifecycleRuntime runtime(*this);
|
||||||
@@ -5596,7 +5642,7 @@ void App::reinstallBundledDaemon()
|
|||||||
void App::deleteBlockchainData()
|
void App::deleteBlockchainData()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5609,12 +5655,12 @@ void App::deleteBlockchainData()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
|
||||||
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
|
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
|
||||||
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
|
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
|
||||||
|
|
||||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||||
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
|
||||||
@@ -5719,7 +5765,7 @@ void App::beginShutdown()
|
|||||||
|
|
||||||
// Stop xmrig pool miner before stopping the daemon
|
// Stop xmrig pool miner before stopping the daemon
|
||||||
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
|
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
|
||||||
shutdown_status_ = "Stopping pool miner...";
|
shutdown_status_ = TR("appx_stopping_pool_miner");
|
||||||
xmrig_manager_->stop(3000);
|
xmrig_manager_->stop(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5731,7 +5777,7 @@ void App::beginShutdown()
|
|||||||
// Worker join + RPC disconnect happen in shutdown().
|
// Worker join + RPC disconnect happen in shutdown().
|
||||||
if (!daemon_controller_) {
|
if (!daemon_controller_) {
|
||||||
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
|
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
|
||||||
shutdown_status_ = "Disconnecting...";
|
shutdown_status_ = TR("appx_disconnecting");
|
||||||
if (settings_) {
|
if (settings_) {
|
||||||
settings_->save();
|
settings_->save();
|
||||||
}
|
}
|
||||||
@@ -5762,15 +5808,15 @@ void App::beginShutdown()
|
|||||||
// modal "Please wait" dialog).
|
// modal "Please wait" dialog).
|
||||||
shutdown_thread_ = std::thread([this]() {
|
shutdown_thread_ = std::thread([this]() {
|
||||||
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
|
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
|
||||||
shutdown_status_ = "Sending stop command to daemon...";
|
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
|
||||||
// Send RPC stop command
|
// Send RPC stop command
|
||||||
stopEmbeddedDaemon();
|
stopEmbeddedDaemon();
|
||||||
|
|
||||||
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
|
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
|
||||||
shutdown_status_ = "Cleaning up...";
|
shutdown_status_ = TR("appx_cleaning_up");
|
||||||
|
|
||||||
DEBUG_LOGF("shutdown thread: complete\n");
|
DEBUG_LOGF("shutdown thread: complete\n");
|
||||||
shutdown_status_ = "Shutdown complete";
|
shutdown_status_ = TR("appx_shutdown_complete");
|
||||||
shutdown_complete_ = true;
|
shutdown_complete_ = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5883,18 +5929,16 @@ void App::renderDaemonStopConfirm()
|
|||||||
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
|
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
|
||||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||||
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
|
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
|
||||||
ImGui::TextUnformatted("Node is rebuilding its witness cache");
|
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
|
||||||
if (Type().subtitle1()) ImGui::PopFont();
|
if (Type().subtitle1()) ImGui::PopFont();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
|
||||||
ImGui::TextUnformatted(
|
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
|
||||||
"Stopping the node now discards the in-progress rebuild and restarts it (several minutes) "
|
|
||||||
"the next time you open the wallet. You can keep the node running instead.");
|
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
if (TactileButton("Keep node running & quit", ImVec2(0, 0))) {
|
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
|
||||||
shutdown_keep_daemon_override_ = true;
|
shutdown_keep_daemon_override_ = true;
|
||||||
shutdown_confirmed_ = true;
|
shutdown_confirmed_ = true;
|
||||||
daemon_stop_confirm_open_ = false;
|
daemon_stop_confirm_open_ = false;
|
||||||
@@ -5905,7 +5949,7 @@ void App::renderDaemonStopConfirm()
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
|
||||||
const bool stopAnyway = TactileButton("Stop anyway & quit", ImVec2(0, 0));
|
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
if (stopAnyway) {
|
if (stopAnyway) {
|
||||||
shutdown_confirmed_ = true;
|
shutdown_confirmed_ = true;
|
||||||
@@ -5914,7 +5958,7 @@ void App::renderDaemonStopConfirm()
|
|||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton("Cancel", ImVec2(0, 0))) {
|
if (TactileButton(TR("appx_cancel"), ImVec2(0, 0))) {
|
||||||
daemon_stop_confirm_open_ = false; // abort the quit; stay open
|
daemon_stop_confirm_open_ = false; // abort the quit; stay open
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
@@ -5930,6 +5974,7 @@ void App::renderDaemonStopConfirm()
|
|||||||
void App::renderShutdownScreen()
|
void App::renderShutdownScreen()
|
||||||
{
|
{
|
||||||
using namespace ui::material;
|
using namespace ui::material;
|
||||||
|
const float dp = ui::Layout::dpiScale();
|
||||||
auto shutElem = [](const char* key, float fb) {
|
auto shutElem = [](const char* key, float fb) {
|
||||||
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
|
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
|
||||||
return v >= 0 ? v : fb;
|
return v >= 0 ? v : fb;
|
||||||
@@ -5983,7 +6028,7 @@ void App::renderShutdownScreen()
|
|||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
||||||
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
|
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
|
||||||
float spinnerD = shutElem("spinner-radius", 20.0f) * 2.0f + 12.0f;
|
float spinnerD = shutElem("spinner-radius", 20.0f) * dp * 2.0f + 12.0f * dp;
|
||||||
float statusH = lineH * 2.0f;
|
float statusH = lineH * 2.0f;
|
||||||
float sepH = lineH;
|
float sepH = lineH;
|
||||||
float panelH = shutElem("panel-max-height", 160.0f);
|
float panelH = shutElem("panel-max-height", 160.0f);
|
||||||
@@ -6014,10 +6059,10 @@ void App::renderShutdownScreen()
|
|||||||
// 2. Animated arc spinner
|
// 2. Animated arc spinner
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
float r = shutElem("spinner-radius", 20.0f);
|
float r = shutElem("spinner-radius", 20.0f) * dp;
|
||||||
float thick = shutElem("spinner-thickness", 3.0f);
|
float thick = shutElem("spinner-thickness", 3.0f) * dp;
|
||||||
// Screen-space centre for draw list
|
// Screen-space centre for draw list
|
||||||
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f);
|
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f * dp);
|
||||||
|
|
||||||
// Background ring (dim)
|
// Background ring (dim)
|
||||||
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
|
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
|
||||||
@@ -6031,7 +6076,7 @@ void App::renderShutdownScreen()
|
|||||||
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
|
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
|
||||||
|
|
||||||
// Advance cursor past the spinner
|
// Advance cursor past the spinner
|
||||||
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f));
|
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f * dp));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -6056,9 +6101,9 @@ void App::renderShutdownScreen()
|
|||||||
char elapsed[64];
|
char elapsed[64];
|
||||||
int secs = (int)shutdown_timer_;
|
int secs = (int)shutdown_timer_;
|
||||||
if (secs < 60)
|
if (secs < 60)
|
||||||
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs);
|
snprintf(elapsed, sizeof(elapsed), TR("appx_n_seconds"), secs);
|
||||||
else
|
else
|
||||||
snprintf(elapsed, sizeof(elapsed), "%d min %d sec", secs / 60, secs % 60);
|
snprintf(elapsed, sizeof(elapsed), TR("appx_n_min_n_sec"), secs / 60, secs % 60);
|
||||||
|
|
||||||
ImGui::PushFont(Type().caption());
|
ImGui::PushFont(Type().caption());
|
||||||
ImVec2 ts = ImGui::CalcTextSize(elapsed);
|
ImVec2 ts = ImGui::CalcTextSize(elapsed);
|
||||||
@@ -6078,7 +6123,8 @@ void App::renderShutdownScreen()
|
|||||||
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
|
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
|
||||||
// chainstate; say so instead of a bare button.
|
// chainstate; say so instead of a bare button.
|
||||||
if (shutdownStalled && !curShut.empty()) {
|
if (shutdownStalled && !curShut.empty()) {
|
||||||
std::string stalledMsg = "Still \"" + curShut + "\" — force quitting now may corrupt chain data.";
|
std::string stalledMsg = std::string(TR("appx_still_status_prefix")) + curShut +
|
||||||
|
TR("appx_still_status_suffix");
|
||||||
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
|
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
|
||||||
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||||
@@ -6087,7 +6133,7 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
const char* forceLabel = TR("force_quit");
|
const char* forceLabel = TR("force_quit");
|
||||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f * dp, 0);
|
||||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||||
@@ -6117,7 +6163,7 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = 120.0f;
|
float btnW = 120.0f * dp;
|
||||||
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
||||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
|
||||||
|
|
||||||
@@ -6149,7 +6195,7 @@ void App::renderShutdownScreen()
|
|||||||
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
|
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
|
||||||
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
|
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
|
||||||
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
|
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
|
||||||
ImGui::Dummy(ImVec2(0, 4.0f));
|
ImGui::Dummy(ImVec2(0, 4.0f * dp));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -6182,7 +6228,7 @@ void App::renderShutdownScreen()
|
|||||||
ImGui::GetCursorPosY() + panelPad));
|
ImGui::GetCursorPosY() + panelPad));
|
||||||
ImGui::PushFont(Type().caption());
|
ImGui::PushFont(Type().caption());
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
|
||||||
ImGui::TextUnformatted("dragonxd output");
|
ImGui::TextUnformatted(TR("appx_dragonxd_output"));
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
|
|
||||||
@@ -6222,7 +6268,7 @@ void App::renderShutdownScreen()
|
|||||||
|
|
||||||
// Advance cursor past the panel
|
// Advance cursor past the panel
|
||||||
float panelBottom = panelMax.y - wp.y;
|
float panelBottom = panelMax.y - wp.y;
|
||||||
ImGui::SetCursorPosY(panelBottom + 4.0f);
|
ImGui::SetCursorPosY(panelBottom + 4.0f * dp);
|
||||||
ImGui::Dummy(ImVec2(0, 0));
|
ImGui::Dummy(ImVec2(0, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6417,14 +6463,14 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
|
|
||||||
char wbuf[128];
|
char wbuf[128];
|
||||||
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
|
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
|
||||||
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
|
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_blocks_left"),
|
||||||
progress * 100.0f, state_.sync.witness_remaining);
|
progress * 100.0f, state_.sync.witness_remaining);
|
||||||
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
|
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
|
||||||
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f);
|
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_pct"), progress * 100.0f);
|
||||||
else if (progress > 0.01f)
|
else if (progress > 0.01f)
|
||||||
snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f);
|
snprintf(wbuf, sizeof(wbuf), TR("appx_setting_initial_sapling_witnesses"), progress * 100.0f);
|
||||||
else
|
else
|
||||||
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses…");
|
snprintf(wbuf, sizeof(wbuf), "%s", TR("appx_rebuilding_sapling_note_witnesses"));
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
if (!capFont) capFont = ImGui::GetFont();
|
if (!capFont) capFont = ImGui::GetFont();
|
||||||
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
|
||||||
@@ -6462,7 +6508,7 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
|
|
||||||
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
|
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
|
||||||
char syncBuf[128];
|
char syncBuf[128];
|
||||||
snprintf(syncBuf, sizeof(syncBuf), "Syncing %.1f%% — Block %d / %d",
|
snprintf(syncBuf, sizeof(syncBuf), TR("appx_syncing_pct_block_n_of_n"),
|
||||||
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
|
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
if (!capFont) capFont = ImGui::GetFont();
|
if (!capFont) capFont = ImGui::GetFont();
|
||||||
@@ -6474,7 +6520,7 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
} else if (!state_.connected && state_.sync.blocks > 0) {
|
} else if (!state_.connected && state_.sync.blocks > 0) {
|
||||||
// Show last known block height while reconnecting
|
// Show last known block height while reconnecting
|
||||||
char blockBuf[64];
|
char blockBuf[64];
|
||||||
snprintf(blockBuf, sizeof(blockBuf), "Last block: %d", state_.sync.blocks);
|
snprintf(blockBuf, sizeof(blockBuf), TR("appx_last_block_n"), state_.sync.blocks);
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
if (!capFont) capFont = ImGui::GetFont();
|
if (!capFont) capFont = ImGui::GetFont();
|
||||||
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
|
||||||
@@ -6493,8 +6539,8 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
if (!capFont) capFont = ImGui::GetFont();
|
if (!capFont) capFont = ImGui::GetFont();
|
||||||
|
|
||||||
const char* encLabel = encrypt_in_progress_
|
const char* encLabel = encrypt_in_progress_
|
||||||
? "Encrypting wallet..."
|
? TR("appx_encrypting_wallet")
|
||||||
: "Waiting for daemon to encrypt wallet...";
|
: TR("appx_waiting_for_daemon_to_encrypt_wallet");
|
||||||
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
|
||||||
ImU32 encCol = IM_COL32(255, 218, 0, 200);
|
ImU32 encCol = IM_COL32(255, 218, 0, 200);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
@@ -6529,7 +6575,7 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
|
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
|
||||||
|
|
||||||
// Error title
|
// Error title
|
||||||
const char* errTitle = "Daemon Error";
|
const char* errTitle = TR("appx_daemon_error");
|
||||||
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
|
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
|
||||||
dl->AddText(bodyFont2, bodyFont2->LegacySize,
|
dl->AddText(bodyFont2, bodyFont2->LegacySize,
|
||||||
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
||||||
@@ -6549,7 +6595,7 @@ void App::renderLoadingOverlay(float contentH)
|
|||||||
}
|
}
|
||||||
// Crash count hint
|
// Crash count hint
|
||||||
if (daemon_controller_->crashCount() >= 3) {
|
if (daemon_controller_->crashCount() >= 3) {
|
||||||
const char* hint = "Use Settings > Restart Daemon to try again";
|
const char* hint = TR("appx_use_settings_restart_daemon_hint");
|
||||||
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
|
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
|
||||||
@@ -6822,7 +6868,7 @@ void App::copySecretToClipboard(const std::string& secret)
|
|||||||
clipboard_secret_hash_ = secret.empty() ? 0 : h;
|
clipboard_secret_hash_ = secret.empty() ? 0 : h;
|
||||||
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
|
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
|
||||||
if (!secret.empty())
|
if (!secret.empty())
|
||||||
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
|
ui::Notifications::instance().info(TR("appx_copied_clipboard_autoclears"), 4.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::clearSecretClipboardIfArmed()
|
void App::clearSecretClipboardIfArmed()
|
||||||
@@ -6916,7 +6962,7 @@ std::string App::buildDiagnosticsReport()
|
|||||||
void App::restartDaemon()
|
void App::restartDaemon()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6956,7 +7002,7 @@ void App::restartDaemon()
|
|||||||
void App::reindexBlockDatabase()
|
void App::reindexBlockDatabase()
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!daemon_controller_) return;
|
if (!daemon_controller_) return;
|
||||||
|
|||||||
21
src/app.h
21
src/app.h
@@ -462,6 +462,7 @@ public:
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
void showAboutDialog() { show_about_ = true; }
|
void showAboutDialog() { show_about_ = true; }
|
||||||
|
void showFaqDialog() { show_faq_ = true; }
|
||||||
|
|
||||||
// Legacy tab compat — maps int to NavPage
|
// Legacy tab compat — maps int to NavPage
|
||||||
void setCurrentTab(int tab);
|
void setCurrentTab(int tab);
|
||||||
@@ -800,6 +801,7 @@ private:
|
|||||||
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
|
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
|
||||||
bool chat_note_scan_in_flight_ = false;
|
bool chat_note_scan_in_flight_ = false;
|
||||||
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
|
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
|
||||||
|
double chat_note_scan_ms_ = 0.0; // measured cost of the last note scan (adaptive back-off)
|
||||||
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
|
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
|
||||||
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
|
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
|
||||||
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
|
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
|
||||||
@@ -809,6 +811,9 @@ private:
|
|||||||
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
||||||
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
||||||
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
|
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
|
||||||
|
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
|
||||||
|
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
|
||||||
|
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
|
||||||
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
||||||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||||
@@ -877,6 +882,8 @@ private:
|
|||||||
void fastScanChatMemos();
|
void fastScanChatMemos();
|
||||||
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
|
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
|
||||||
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
|
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
|
||||||
|
double chat_fast_scan_last_ = 0.0; // ImGui time of the last 0-conf fast scan (adaptive back-off)
|
||||||
|
double chat_fast_scan_ms_ = 0.0; // measured cost of the last fast scan (z_listreceivedbyaddress)
|
||||||
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
|
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
|
||||||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||||
bool lite_firstrun_dismissed_ = false;
|
bool lite_firstrun_dismissed_ = false;
|
||||||
@@ -1028,6 +1035,7 @@ private:
|
|||||||
bool show_demo_window_ = false;
|
bool show_demo_window_ = false;
|
||||||
bool show_settings_ = false;
|
bool show_settings_ = false;
|
||||||
bool show_about_ = false;
|
bool show_about_ = false;
|
||||||
|
bool show_faq_ = false;
|
||||||
bool show_import_key_ = false;
|
bool show_import_key_ = false;
|
||||||
bool show_export_key_ = false;
|
bool show_export_key_ = false;
|
||||||
bool show_backup_ = false;
|
bool show_backup_ = false;
|
||||||
@@ -1143,6 +1151,14 @@ private:
|
|||||||
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
|
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
|
||||||
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
|
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
|
||||||
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
|
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
|
||||||
|
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
|
||||||
|
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
|
||||||
|
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
|
||||||
|
// starving block connection near the tip (where effectivelySyncing() reads false). See
|
||||||
|
// addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
|
||||||
|
// refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
|
||||||
|
double last_address_scan_ms_ = 0.0; // measured cost of the last address scan
|
||||||
|
double last_tx_scan_ms_ = 0.0; // measured cost of the last history scan
|
||||||
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
||||||
std::uint64_t clipboard_secret_hash_ = 0;
|
std::uint64_t clipboard_secret_hash_ = 0;
|
||||||
double clipboard_clear_deadline_ = 0.0;
|
double clipboard_clear_deadline_ = 0.0;
|
||||||
@@ -1498,6 +1514,11 @@ private:
|
|||||||
void applyRefreshPolicy(ui::NavPage page);
|
void applyRefreshPolicy(ui::NavPage page);
|
||||||
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
|
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
|
||||||
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
|
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
|
||||||
|
bool addressRefreshDue() const; // same adaptive back-off for the address scan (z_listunspent)
|
||||||
|
bool txRefreshDue() const; // same adaptive back-off for the history scan (z_listreceivedbyaddress)
|
||||||
|
// Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
|
||||||
|
// lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
|
||||||
|
bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
|
||||||
bool currentPageNeedsWalletDataRefresh() const;
|
bool currentPageNeedsWalletDataRefresh() const;
|
||||||
bool shouldRunWalletTransactionRefresh() const;
|
bool shouldRunWalletTransactionRefresh() const;
|
||||||
bool shouldRefreshTransactions() const;
|
bool shouldRefreshTransactions() const;
|
||||||
|
|||||||
@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
"Please restart your daemon for encryption to take effect.");
|
TR("sec_restart_daemon_for_encryption"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||||
if (!rpc_ || !rpc_->isConnected()) return;
|
if (!rpc_ || !rpc_->isConnected()) return;
|
||||||
encrypt_in_progress_ = true;
|
encrypt_in_progress_ = true;
|
||||||
encrypt_status_ = "Encrypting wallet...";
|
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||||
|
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
|
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
|
||||||
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
|||||||
if (result.encrypted) {
|
if (result.encrypted) {
|
||||||
return [this]() {
|
return [this]() {
|
||||||
encrypt_in_progress_ = false;
|
encrypt_in_progress_ = false;
|
||||||
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
|
encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
|
||||||
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
|
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
|
||||||
|
|
||||||
// Immediately update local encryption state so the
|
// Immediately update local encryption state so the
|
||||||
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui::Notifications::instance().info(
|
ui::Notifications::instance().info(
|
||||||
"Wallet encrypted successfully", 5.0f);
|
TR("sec_wallet_encrypted_successfully"), 5.0f);
|
||||||
|
|
||||||
// The daemon shuts itself down after encryptwallet.
|
// The daemon shuts itself down after encryptwallet.
|
||||||
// Update connection_status_ so the loading overlay
|
// Update connection_status_ so the loading overlay
|
||||||
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
|||||||
std::string err = result.error;
|
std::string err = result.error;
|
||||||
return [this, err]() {
|
return [this, err]() {
|
||||||
encrypt_in_progress_ = false;
|
encrypt_in_progress_ = false;
|
||||||
encrypt_status_ = "Encryption failed: " + err;
|
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
|
||||||
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
|
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
|
||||||
|
|
||||||
ui::Notifications::instance().error(
|
ui::Notifications::instance().error(
|
||||||
"Encryption failed: " + err);
|
std::string(TR("sec_encryption_failed_prefix")) + err);
|
||||||
|
|
||||||
// Return to passphrase entry on failure
|
// Return to passphrase entry on failure
|
||||||
if (show_encrypt_dialog_ &&
|
if (show_encrypt_dialog_ &&
|
||||||
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
|
|||||||
std::string pin = std::move(deferredEncryption.pin);
|
std::string pin = std::move(deferredEncryption.pin);
|
||||||
|
|
||||||
encrypt_in_progress_ = true;
|
encrypt_in_progress_ = true;
|
||||||
encrypt_status_ = "Encrypting wallet...";
|
encrypt_status_ = TR("sec_encrypting_wallet");
|
||||||
|
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
|
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
|
||||||
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
|
|||||||
if (result.pinStored) {
|
if (result.pinStored) {
|
||||||
settings_->setPinEnabled(true);
|
settings_->setPinEnabled(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
|
ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
|
||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
"Wallet encrypted but PIN vault failed");
|
TR("sec_wallet_encrypted_but_pin_vault_failed"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
|
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
wallet_security_.clearDeferredEncryption();
|
wallet_security_.clearDeferredEncryption();
|
||||||
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
|
|||||||
std::string err = result.error;
|
std::string err = result.error;
|
||||||
return [this, err]() {
|
return [this, err]() {
|
||||||
encrypt_in_progress_ = false;
|
encrypt_in_progress_ = false;
|
||||||
encrypt_status_ = "Encryption failed: " + err;
|
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
|
||||||
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
|
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
|
||||||
ui::Notifications::instance().error("Encryption failed: " + err);
|
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
|
||||||
wallet_security_.clearDeferredEncryption();
|
wallet_security_.clearDeferredEncryption();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -531,7 +531,7 @@ void App::lockWallet() {
|
|||||||
if (!lock_failure_warned_) {
|
if (!lock_failure_warned_) {
|
||||||
lock_failure_warned_ = true;
|
lock_failure_warned_ = true;
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
|
TR("sec_couldnt_lock_wallet"), 12.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -541,7 +541,7 @@ void App::lockWallet() {
|
|||||||
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
|
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
|
||||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||||
encrypt_in_progress_ = true;
|
encrypt_in_progress_ = true;
|
||||||
encrypt_status_ = "Changing passphrase...";
|
encrypt_status_ = TR("sec_changing_passphrase");
|
||||||
|
|
||||||
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
|
|||||||
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
|
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
|
||||||
unlockTransactionHistoryCacheWithPassphrase(newPass);
|
unlockTransactionHistoryCacheWithPassphrase(newPass);
|
||||||
storeTransactionHistoryCacheIfAvailable();
|
storeTransactionHistoryCacheIfAvailable();
|
||||||
ui::Notifications::instance().info("Passphrase changed successfully");
|
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
|
||||||
} else {
|
} else {
|
||||||
encrypt_status_ = "Failed: " + err_msg;
|
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
|
||||||
}
|
}
|
||||||
util::SecureVault::secureZero(newPass.data(), newPass.size());
|
util::SecureVault::secureZero(newPass.data(), newPass.size());
|
||||||
};
|
};
|
||||||
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
|
|||||||
!encryption_incomplete_warned_) {
|
!encryption_incomplete_warned_) {
|
||||||
encryption_incomplete_warned_ = true;
|
encryption_incomplete_warned_ = true;
|
||||||
ui::Notifications::instance().warning(
|
ui::Notifications::instance().warning(
|
||||||
"Wallet encryption did not complete — your wallet is NOT encrypted. "
|
TR("sec_encryption_did_not_complete"), 30.0f);
|
||||||
"Open Settings to finish encrypting it.", 30.0f);
|
|
||||||
}
|
}
|
||||||
if (state_.transactions.empty()) {
|
if (state_.transactions.empty()) {
|
||||||
loadTransactionHistoryCacheIfAvailable();
|
loadTransactionHistoryCacheIfAvailable();
|
||||||
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
|
|||||||
ImU32 textCol = ui::material::OnSurface();
|
ImU32 textCol = ui::material::OnSurface();
|
||||||
|
|
||||||
{
|
{
|
||||||
const char* title = "Wallet Locked";
|
const char* title = TR("sec_wallet_locked_title");
|
||||||
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
|
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
|
||||||
dl->AddText(titleFont, titleFont->LegacySize,
|
dl->AddText(titleFont, titleFont->LegacySize,
|
||||||
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
|
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
|
||||||
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
|
|||||||
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
|
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
|
||||||
|
|
||||||
char msg[128];
|
char msg[128];
|
||||||
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
|
snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
|
||||||
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
|
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
|
||||||
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
|
|||||||
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
||||||
if (hasPinVault) {
|
if (hasPinVault) {
|
||||||
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
|
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
|
||||||
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
|
const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
|
||||||
const char* switchLabel = lock_use_pin_
|
const char* switchLabel = lock_use_pin_
|
||||||
? "Use passphrase instead"
|
? TR("sec_use_passphrase_instead")
|
||||||
: "Use PIN instead";
|
: TR("sec_use_pin_instead");
|
||||||
|
|
||||||
// Current mode indicator — icon with icon font, text with caption font
|
// Current mode indicator — icon with icon font, text with caption font
|
||||||
ImFont* iconFont = ui::material::Type().iconSmall();
|
ImFont* iconFont = ui::material::Type().iconSmall();
|
||||||
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
|
|||||||
if (lock_unlock_in_progress_) {
|
if (lock_unlock_in_progress_) {
|
||||||
// Animated spinner dots
|
// Animated spinner dots
|
||||||
char msg[64];
|
char msg[64];
|
||||||
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
|
snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
|
||||||
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
|
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
|
||||||
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!canSubmit);
|
ImGui::BeginDisabled(!canSubmit);
|
||||||
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
|
bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
|
|||||||
r->call("walletpassphrase", {passphrase, timeout});
|
r->call("walletpassphrase", {passphrase, timeout});
|
||||||
rpcOk = true;
|
rpcOk = true;
|
||||||
} else {
|
} else {
|
||||||
rpcErr = "Not connected to daemon";
|
rpcErr = TR("sec_not_connected_to_daemon");
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
rpcErr = e.what();
|
rpcErr = e.what();
|
||||||
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
|
|||||||
// so route through applyUnlockFailure so the lockout curve applies
|
// so route through applyUnlockFailure so the lockout curve applies
|
||||||
// (this path previously bumped the counter but skipped the lockout math).
|
// (this path previously bumped the counter but skipped the lockout math).
|
||||||
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
|
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
|
||||||
applyUnlockFailure("Unlock failed: " + rpcErr);
|
applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
|
||||||
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
|
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
|
||||||
if (!unlock.ok) {
|
if (!unlock.ok) {
|
||||||
return [this]() {
|
return [this]() {
|
||||||
wallet_security_workflow_.failEntry("Incorrect passphrase");
|
wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ui::Notifications::instance().info(
|
ui::Notifications::instance().info(
|
||||||
"Importing keys & rescanning blockchain — wallet is usable while this runs",
|
TR("sec_importing_keys_rescanning"),
|
||||||
8.0f);
|
8.0f);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
wallet_security_workflow_.finishImport();
|
wallet_security_workflow_.finishImport();
|
||||||
ui::Notifications::instance().error(
|
ui::Notifications::instance().error(
|
||||||
err +
|
err +
|
||||||
"\nEncrypted backup: wallet.dat.encrypted.bak",
|
TR("sec_encrypted_backup_suffix"),
|
||||||
12.0f);
|
12.0f);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
refreshPeerInfo();
|
refreshPeerInfo();
|
||||||
|
|
||||||
ui::Notifications::instance().success(
|
ui::Notifications::instance().success(
|
||||||
"Wallet decrypted successfully! All keys imported.",
|
TR("sec_wallet_decrypted_all_keys_imported"),
|
||||||
8.0f);
|
8.0f);
|
||||||
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
||||||
};
|
};
|
||||||
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
int tMins = (int)(totalElapsed / 60);
|
int tMins = (int)(totalElapsed / 60);
|
||||||
int tSecs = (int)(totalElapsed % 60);
|
int tSecs = (int)(totalElapsed % 60);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
|
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Phase 2: Success ----
|
// ---- Phase 2: Success ----
|
||||||
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = "Verifying passphrase...";
|
pin_status_ = TR("sec_verifying_passphrase");
|
||||||
|
|
||||||
// Verify passphrase + store vault on worker thread to avoid
|
// Verify passphrase + store vault on worker thread to avoid
|
||||||
// blocking the UI with Argon2id key derivation.
|
// blocking the UI with Argon2id key derivation.
|
||||||
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
|
|||||||
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
|
||||||
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
|
||||||
return [this]() {
|
return [this]() {
|
||||||
pin_status_ = "Incorrect passphrase";
|
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
|
|||||||
pin_status_.clear();
|
pin_status_.clear();
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
show_pin_setup_ = false;
|
show_pin_setup_ = false;
|
||||||
ui::Notifications::instance().info("PIN set successfully");
|
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
|
||||||
} else {
|
} else {
|
||||||
pin_status_ = "Failed to create vault";
|
pin_status_ = TR("sec_failed_to_create_vault");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pin_status_ = "Not connected to daemon";
|
pin_status_ = TR("sec_not_connected_to_daemon_pin");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = "Changing PIN...";
|
pin_status_ = TR("sec_changing_pin");
|
||||||
std::string oldPin(pin_old_buf_);
|
std::string oldPin(pin_old_buf_);
|
||||||
std::string newPinCopy = newPin;
|
std::string newPinCopy = newPin;
|
||||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||||
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
|
|||||||
pin_status_.clear();
|
pin_status_.clear();
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
show_pin_change_ = false;
|
show_pin_change_ = false;
|
||||||
ui::Notifications::instance().info("PIN changed successfully");
|
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
|
||||||
} else {
|
} else {
|
||||||
pin_status_ = "Incorrect current PIN";
|
pin_status_ = TR("sec_incorrect_current_pin");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pin_status_ = "Internal error";
|
pin_status_ = TR("sec_internal_error_change_pin");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = "Verifying PIN...";
|
pin_status_ = TR("sec_verifying_pin");
|
||||||
std::string oldPin(pin_old_buf_);
|
std::string oldPin(pin_old_buf_);
|
||||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||||
|
|
||||||
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
|
|||||||
pin_status_.clear();
|
pin_status_.clear();
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
show_pin_remove_ = false;
|
show_pin_remove_ = false;
|
||||||
ui::Notifications::instance().info("PIN removed");
|
ui::Notifications::instance().info(TR("sec_pin_removed"));
|
||||||
} else {
|
} else {
|
||||||
pin_status_ = "Incorrect PIN";
|
pin_status_ = TR("sec_incorrect_pin_remove");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pin_status_ = "Internal error";
|
pin_status_ = TR("sec_internal_error_remove_pin");
|
||||||
pin_in_progress_ = false;
|
pin_in_progress_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
|
|||||||
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
|
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
|
||||||
add("modal-backup", ui::NavPage::Overview,
|
add("modal-backup", ui::NavPage::Overview,
|
||||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||||
|
add("modal-faq", ui::NavPage::Overview,
|
||||||
|
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
|
||||||
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
|
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
|
||||||
add("modal-encrypt", ui::NavPage::Settings,
|
add("modal-encrypt", ui::NavPage::Settings,
|
||||||
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
|
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
|
||||||
@@ -708,9 +710,22 @@ void App::startSweepImpl(bool full)
|
|||||||
if (sweep_current_theme_only_)
|
if (sweep_current_theme_only_)
|
||||||
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
|
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
|
||||||
|
|
||||||
|
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
|
||||||
|
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
|
||||||
|
// under review instead of all surfaces x every skin.
|
||||||
|
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
|
||||||
|
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
|
||||||
|
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
|
||||||
|
|
||||||
sweep_full_ = full;
|
sweep_full_ = full;
|
||||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||||
buildSweepCatalog();
|
buildSweepCatalog();
|
||||||
|
if (!sweepOnlyStr.empty()) {
|
||||||
|
std::vector<SweepTarget> keep;
|
||||||
|
for (const auto& t : sweep_targets_)
|
||||||
|
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
|
||||||
|
sweep_targets_.swap(keep);
|
||||||
|
}
|
||||||
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
||||||
|
|
||||||
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "theme_effects.h"
|
#include "theme_effects.h"
|
||||||
#include "low_spec.h"
|
#include "low_spec.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../layout.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
|
|||||||
|
|
||||||
void ThemeEffects::loadFromTheme() {
|
void ThemeEffects::loadFromTheme() {
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
auto eff = [&](const char* name) {
|
auto eff = [&](const char* name) {
|
||||||
return S.drawElement("effects", name);
|
return S.drawElement("effects", name);
|
||||||
};
|
};
|
||||||
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
// ---- Shimmer ----
|
// ---- Shimmer ----
|
||||||
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
|
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
|
||||||
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
|
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
|
||||||
shimmer_.width = eff("shimmer-width").sizeOr(80.0f);
|
shimmer_.width = eff("shimmer-width").sizeOr(80.0f) * dp;
|
||||||
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
|
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
|
||||||
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
|
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
|
||||||
// Shimmer color: read from the schema's color resolver
|
// Shimmer color: read from the schema's color resolver
|
||||||
@@ -124,7 +126,7 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
|
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
|
||||||
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
|
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
|
||||||
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
|
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
|
||||||
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f);
|
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
|
||||||
auto glowColorElem = eff("glow-pulse-color");
|
auto glowColorElem = eff("glow-pulse-color");
|
||||||
if (!glowColorElem.color.empty()) {
|
if (!glowColorElem.color.empty()) {
|
||||||
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
|
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
|
||||||
@@ -136,7 +138,7 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
|
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
|
||||||
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
|
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
|
||||||
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
|
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
|
||||||
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f);
|
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
|
||||||
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
|
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
|
||||||
auto edgeColorElem = eff("edge-trace-color");
|
auto edgeColorElem = eff("edge-trace-color");
|
||||||
if (!edgeColorElem.color.empty()) {
|
if (!edgeColorElem.color.empty()) {
|
||||||
@@ -149,7 +151,7 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
|
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
|
||||||
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
|
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
|
||||||
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
|
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
|
||||||
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f);
|
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
|
||||||
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
|
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
|
||||||
auto emberColorElem = eff("ember-rise-color");
|
auto emberColorElem = eff("ember-rise-color");
|
||||||
if (!emberColorElem.color.empty()) {
|
if (!emberColorElem.color.empty()) {
|
||||||
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
|
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
|
||||||
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
|
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
|
||||||
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
||||||
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
|
||||||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
||||||
auto gbColorA = eff("gradient-border-color-a");
|
auto gbColorA = eff("gradient-border-color-a");
|
||||||
if (!gbColorA.color.empty()) {
|
if (!gbColorA.color.empty()) {
|
||||||
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
|
|||||||
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
|
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
|
||||||
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
|
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
|
||||||
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
|
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
|
||||||
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f);
|
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
|
||||||
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
|
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
|
||||||
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
|
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
|
||||||
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
|
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
|
||||||
@@ -694,6 +696,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
|||||||
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
|
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
|
||||||
if (!enabled_ || !ember_rise_.enabled) return;
|
if (!enabled_ || !ember_rise_.enabled) return;
|
||||||
|
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
float w = pMax.x - pMin.x;
|
float w = pMax.x - pMin.x;
|
||||||
float h = pMax.y - pMin.y;
|
float h = pMax.y - pMin.y;
|
||||||
if (w <= 0 || h <= 0) return;
|
if (w <= 0 || h <= 0) return;
|
||||||
@@ -707,7 +710,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
|
|||||||
// Deterministic pseudo-random x position per particle
|
// Deterministic pseudo-random x position per particle
|
||||||
// Simple hash: sin of large prime multiples
|
// Simple hash: sin of large prime multiples
|
||||||
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
|
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
|
||||||
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway
|
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway
|
||||||
|
|
||||||
float x = pMin.x + w * xHash + xDrift;
|
float x = pMin.x + w * xHash + xDrift;
|
||||||
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
|
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
|
||||||
@@ -745,6 +748,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
|
|||||||
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
||||||
if (!enabled_ || !ember_rise_.enabled) return;
|
if (!enabled_ || !ember_rise_.enabled) return;
|
||||||
|
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
float vpW = vp->WorkSize.x;
|
float vpW = vp->WorkSize.x;
|
||||||
float vpH = vp->WorkSize.y;
|
float vpH = vp->WorkSize.y;
|
||||||
@@ -765,7 +769,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
|||||||
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
|
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
|
||||||
xHash = xHash - (int)xHash; // fractional part
|
xHash = xHash - (int)xHash; // fractional part
|
||||||
if (xHash < 0) xHash += 1.0f;
|
if (xHash < 0) xHash += 1.0f;
|
||||||
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f;
|
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;
|
||||||
|
|
||||||
float x = vpX + vpW * xHash + xDrift;
|
float x = vpX + vpW * xHash + xDrift;
|
||||||
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
|
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
|
||||||
|
|||||||
@@ -925,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
|
|||||||
// Draw a full-height rounded rect with card rounding (left corners)
|
// Draw a full-height rounded rect with card rounding (left corners)
|
||||||
// and clip to stripe width so the shape follows the corner radius.
|
// and clip to stripe width so the shape follows the corner radius.
|
||||||
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
|
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
|
||||||
float stripeW = 4.0f;
|
float stripeW = 4.0f * Layout::dpiScale();
|
||||||
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
|
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
|
||||||
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
|
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
|
||||||
ImDrawFlags_RoundCornersLeft);
|
ImDrawFlags_RoundCornersLeft);
|
||||||
@@ -1282,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
|||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImVec2 winPos = ImGui::GetWindowPos();
|
ImVec2 winPos = ImGui::GetWindowPos();
|
||||||
float winWidth = ImGui::GetWindowWidth();
|
float winWidth = ImGui::GetWindowWidth();
|
||||||
float barHeight = 36.0f;
|
const float dp = Layout::dpiScale();
|
||||||
|
float barHeight = 36.0f * dp;
|
||||||
|
|
||||||
// Get accent color from theme if not provided
|
// Get accent color from theme if not provided
|
||||||
if (!accent_col) {
|
if (!accent_col) {
|
||||||
@@ -1306,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
|||||||
ImFont* titleFont = Type().subtitle1();
|
ImFont* titleFont = Type().subtitle1();
|
||||||
ImGui::PushFont(titleFont);
|
ImGui::PushFont(titleFont);
|
||||||
ImVec2 titleSize = ImGui::CalcTextSize(title);
|
ImVec2 titleSize = ImGui::CalcTextSize(title);
|
||||||
float titleX = barMin.x + 16.0f;
|
float titleX = barMin.x + 16.0f * dp;
|
||||||
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
|
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
|
||||||
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
|
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
|
|
||||||
// Close button (X) on right side
|
// Close button (X) on right side
|
||||||
if (p_open) {
|
if (p_open) {
|
||||||
float btnSize = 24.0f;
|
float btnSize = 24.0f * dp;
|
||||||
float btnX = barMax.x - btnSize - 12.0f;
|
float btnX = barMax.x - btnSize - 12.0f * dp;
|
||||||
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
|
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
|
||||||
ImVec2 btnMin(btnX, btnY);
|
ImVec2 btnMin(btnX, btnY);
|
||||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||||
@@ -1327,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
|||||||
|
|
||||||
// Button background on hover
|
// Button background on hover
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
|
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f * dp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw X icon
|
// Draw X icon
|
||||||
@@ -1348,7 +1349,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reserve space for title bar so content starts below it
|
// Reserve space for title bar so content starts below it
|
||||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
|
||||||
|
|
||||||
return closeClicked;
|
return closeClicked;
|
||||||
}
|
}
|
||||||
@@ -1648,7 +1649,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
|
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
|
||||||
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
|
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
|
||||||
// tiles, chat) while staying translucent rather than opaque.
|
// tiles, chat) while staying translucent rather than opaque.
|
||||||
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
|
cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1662,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
|
|
||||||
// Content child.
|
// Content child.
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28 * dp, 20 * dp) : ImVec2(28 * dp, 24 * dp));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
||||||
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
|
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
|
||||||
// content overflowed the viewport); otherwise the child auto-resizes to its content.
|
// content overflowed the viewport); otherwise the child auto-resizes to its content.
|
||||||
|
|||||||
@@ -580,11 +580,11 @@ enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXP
|
|||||||
static void renderSettingsTabBar(float availWidth) {
|
static void renderSettingsTabBar(float availWidth) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
struct T { int id; const char* label; const char* idstr; };
|
struct T { int id; const char* label; const char* idstr; };
|
||||||
static const T tabs[] = {
|
const T tabs[] = {
|
||||||
{TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"},
|
{TAB_APPEARANCE, TR("grpa_tab_appearance"), "##stabA"}, {TAB_WALLET, TR("grpa_tab_wallet"), "##stabW"},
|
||||||
{TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"},
|
{TAB_BACKUP, TR("grpa_tab_backup_data"), "##stabB"}, {TAB_NODE, TR("grpa_tab_node_security"), "##stabN"},
|
||||||
{TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"},
|
{TAB_EXPLORER, TR("grpa_tab_explorer"), "##stabE"}, {TAB_CHAT, TR("grpa_tab_chat"), "##stabC"},
|
||||||
{TAB_ABOUT, "About", "##stabT"},
|
{TAB_ABOUT, TR("grpa_tab_about"), "##stabT"},
|
||||||
};
|
};
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImFont* f = Type().body2();
|
ImFont* f = Type().body2();
|
||||||
@@ -811,7 +811,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (!skin.valid) {
|
if (!skin.valid) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||||
ImGui::BeginDisabled(true);
|
ImGui::BeginDisabled(true);
|
||||||
std::string lbl = skin.name + " (invalid)";
|
std::string lbl = skin.name + TR("grpa_invalid_suffix");
|
||||||
ImGui::Selectable(lbl.c_str(), false);
|
ImGui::Selectable(lbl.c_str(), false);
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
@@ -1196,14 +1196,25 @@ void RenderSettingsPage(App* app) {
|
|||||||
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
|
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
|
||||||
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
|
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
|
||||||
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
|
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
|
||||||
if (app && app->daemonAutoShieldProbed()) {
|
// This status must NOT flow with ImGui's cursor: the checkbox grid is absolutely positioned
|
||||||
|
// (SetCursorScreenPos at rowY), so a normal-flow Text would land under the auto-shield checkbox
|
||||||
|
// and the next grid row (Use Tor / Keep daemon) would draw on top of it. Instead, draw it on its
|
||||||
|
// own full-width row at the grid's current rowY, wrapped to the card, then advance rowY past it.
|
||||||
|
if (app && app->daemonAutoShieldProbed() &&
|
||||||
|
(app->daemonAutoShieldActive() || !app->daemonAutoShieldDisabledReason().empty())) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cx, rowY));
|
||||||
|
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
|
||||||
if (app->daemonAutoShieldActive()) {
|
if (app->daemonAutoShieldActive()) {
|
||||||
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
|
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
|
||||||
if (!app->daemonAutoShieldAddress().empty())
|
if (!app->daemonAutoShieldAddress().empty())
|
||||||
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
|
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
|
||||||
} else if (!app->daemonAutoShieldDisabledReason().empty()) {
|
} else {
|
||||||
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
|
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
|
||||||
}
|
}
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
last = ImGui::GetCursorScreenPos().y; // cursor is now below the status text
|
||||||
|
rowY = last + gp; // next grid row starts below it
|
||||||
|
c = 0;
|
||||||
}
|
}
|
||||||
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
|
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
||||||
@@ -1214,26 +1225,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
|
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
|
||||||
|
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
|
||||||
// O2: host a RandomX stratum pool from this node. Only offered on daemons that implement
|
|
||||||
// it (v1.3.0+, version encoded major*1e6+minor*1e4+rev*100+build), so we never show a
|
|
||||||
// toggle that does nothing. Takes effect on the next daemon start/restart. Blank allow-IP
|
|
||||||
// = loopback only (safe); a subnet opens it to that LAN.
|
|
||||||
if (app->state().daemon_version >= 1030000) {
|
|
||||||
if (CB(TrId("stratum_host", "strat_host"), &s_settingsState.stratum_host))
|
|
||||||
saveSettingsPageState(app->settings());
|
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
|
|
||||||
if (s_settingsState.stratum_host) {
|
|
||||||
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
|
|
||||||
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
|
|
||||||
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
|
|
||||||
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
|
|
||||||
saveSettingsPageState(app->settings());
|
|
||||||
if (s_settingsState.stratum_allowip[0] != '\0')
|
|
||||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
|
|
||||||
TR("stratum_expose_warn"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
|
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
|
||||||
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
|
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
|
||||||
@@ -1697,7 +1689,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted(TR("lite_birthday_label"));
|
ImGui::TextUnformatted(TR("lite_birthday_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
|
||||||
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
|
||||||
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
||||||
@@ -1709,7 +1701,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted(TR("lite_account_label"));
|
ImGui::TextUnformatted(TR("lite_account_label"));
|
||||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
|
||||||
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
|
||||||
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
||||||
@@ -1889,7 +1881,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
|
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
|
||||||
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
|
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
|
||||||
if (liteKey.empty()) {
|
if (liteKey.empty()) {
|
||||||
s_settingsState.lite_backup_status = "Enter a private key to import.";
|
s_settingsState.lite_backup_status = TR("grpa_enter_private_key_to_import");
|
||||||
} else {
|
} else {
|
||||||
const auto r = app->liteWallet()->importKey(liteKey);
|
const auto r = app->liteWallet()->importKey(liteKey);
|
||||||
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
||||||
@@ -2650,6 +2642,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
|
if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
|
||||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
||||||
|
lf.next(material::ActionButtonWidth(TR("faq"), ICON_MD_QUESTION_MARK));
|
||||||
|
if (material::ActionButton("##aboutfaq", TR("faq"), ICON_MD_QUESTION_MARK, AT::Secondary))
|
||||||
|
app->showFaqDialog();
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("faq_open_tooltip"));
|
||||||
}
|
}
|
||||||
const float rightBottom = ImGui::GetCursorScreenPos().y;
|
const float rightBottom = ImGui::GetCursorScreenPos().y;
|
||||||
|
|
||||||
@@ -2676,6 +2672,33 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (s_settingsState.current_tab == TAB_NODE)
|
if (s_settingsState.current_tab == TAB_NODE)
|
||||||
ImGui::Dummy(ImVec2(0, gap));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// MINING POOL HOSTING — run this node's built-in RandomX stratum server so other miners can point
|
||||||
|
// at this machine. It's a node feature, so it belongs here (not the Wallet tab). Full-node only, and
|
||||||
|
// only on a daemon new enough to implement -stratum (v1.3.0+, version encoded
|
||||||
|
// major*1e6+minor*1e4+rev*100+build). Takes effect on the next daemon start/restart; a blank allow-IP
|
||||||
|
// keeps it loopback-only (safe), a subnet opens it to that LAN.
|
||||||
|
// ====================================================================
|
||||||
|
if (app->supportsFullNodeLifecycleActions() && s_settingsState.current_tab == TAB_NODE &&
|
||||||
|
app->state().daemon_version >= 1030000) {
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("stratum_host_section"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
||||||
|
if (ImGui::Checkbox(TR("stratum_host"), &s_settingsState.stratum_host))
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
|
||||||
|
if (s_settingsState.stratum_host) {
|
||||||
|
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
|
||||||
|
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
|
||||||
|
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
|
||||||
|
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
|
if (s_settingsState.stratum_allowip[0] != '\0')
|
||||||
|
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
|
||||||
|
TR("stratum_expose_warn"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
|
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
|
||||||
// daemon debug= categories written to DRAGONX.conf; lite has no daemon). Shown on the Node tab.
|
// daemon debug= categories written to DRAGONX.conf; lite has no daemon). Shown on the Node tab.
|
||||||
@@ -2738,7 +2761,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (chat::hushChatFeatureEnabledAtBuild()) {
|
if (chat::hushChatFeatureEnabledAtBuild()) {
|
||||||
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
|
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
|
if (TactileButton(TR("grpa_seed_demo_chat"), ImVec2(0, 0), S.resolveFont("button")))
|
||||||
app->seedChatDemoData();
|
app->seedChatDemoData();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
|
||||||
}
|
}
|
||||||
@@ -2765,29 +2788,29 @@ void RenderSettingsPage(App* app) {
|
|||||||
"paymentdisclosure", "pow", "proxy", "prune", "rand",
|
"paymentdisclosure", "pow", "proxy", "prune", "rand",
|
||||||
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
|
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
|
||||||
};
|
};
|
||||||
static const char* debugTips[] = {
|
const char* debugTips[] = {
|
||||||
"Peer address tracking and management",
|
TR("grpa_dbg_addrman"),
|
||||||
"Alert system messages",
|
TR("grpa_dbg_alert"),
|
||||||
"Benchmark timings for operations",
|
TR("grpa_dbg_bench"),
|
||||||
"Coin database read/write operations",
|
TR("grpa_dbg_coindb"),
|
||||||
"Berkeley DB operations",
|
TR("grpa_dbg_db"),
|
||||||
"Fee estimation algorithm",
|
TR("grpa_dbg_estimatefee"),
|
||||||
"HTTP RPC server activity",
|
TR("grpa_dbg_http"),
|
||||||
"Libevent networking library",
|
TR("grpa_dbg_libevent"),
|
||||||
"Lock contention debugging",
|
TR("grpa_dbg_lock"),
|
||||||
"Transaction memory pool activity",
|
TR("grpa_dbg_mempool"),
|
||||||
"Network connections and messages",
|
TR("grpa_dbg_net"),
|
||||||
"Payment disclosure protocol",
|
TR("grpa_dbg_paymentdisclosure"),
|
||||||
"Proof-of-work mining activity",
|
TR("grpa_dbg_pow"),
|
||||||
"SOCKS5 proxy connections",
|
TR("grpa_dbg_proxy"),
|
||||||
"Block pruning operations",
|
TR("grpa_dbg_prune"),
|
||||||
"Random number generation",
|
TR("grpa_dbg_rand"),
|
||||||
"Blockchain reindexing progress",
|
TR("grpa_dbg_reindex"),
|
||||||
"RPC command processing",
|
TR("grpa_dbg_rpc"),
|
||||||
"Coin selection for transactions",
|
TR("grpa_dbg_selectcoins"),
|
||||||
"Tor integration and circuit info",
|
TR("grpa_dbg_tor"),
|
||||||
"ZeroMQ notification system",
|
TR("grpa_dbg_zmq"),
|
||||||
"Shielded (z-addr) RPC operations"
|
TR("grpa_dbg_zrpc")
|
||||||
};
|
};
|
||||||
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
|
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
|
||||||
|
|
||||||
@@ -3037,7 +3060,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
|
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Text("%s", TR("rescan_from_height"));
|
ImGui::Text("%s", TR("rescan_from_height"));
|
||||||
ImGui::SetNextItemWidth(160.0f);
|
ImGui::SetNextItemWidth(160.0f * dp);
|
||||||
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
|
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
|
||||||
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
|
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -3051,12 +3074,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
|
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40 * dp))) {
|
||||||
s_settingsState.confirm_rescan = false;
|
s_settingsState.confirm_rescan = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::BeginDisabled(detecting);
|
ImGui::BeginDisabled(detecting);
|
||||||
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40 * dp))) {
|
||||||
if (bootstrapped) {
|
if (bootstrapped) {
|
||||||
app->runtimeRescan(s_settingsState.rescan_start_height);
|
app->runtimeRescan(s_settingsState.rescan_start_height);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|||||||
float lineW = s_cc.lineW;
|
float lineW = s_cc.lineW;
|
||||||
|
|
||||||
// --- Outer glow pass: wider, softer dark edge on top-left ---
|
// --- Outer glow pass: wider, softer dark edge on top-left ---
|
||||||
float glowExpand = s_cc.glowExpand;
|
float glowExpand = s_cc.glowExpand * Layout::dpiScale();
|
||||||
int glowA = (int)s_cc.glowAlpha;
|
int glowA = (int)s_cc.glowAlpha;
|
||||||
float glowLineW = s_cc.glowLineW;
|
float glowLineW = s_cc.glowLineW;
|
||||||
{
|
{
|
||||||
@@ -289,6 +289,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|||||||
// inner pass gives crisp bevel edge. All use AddRect with rounding so
|
// inner pass gives crisp bevel edge. All use AddRect with rounding so
|
||||||
// every layer follows the rounded corners perfectly — no clip rects needed.
|
// every layer follows the rounded corners perfectly — no clip rects needed.
|
||||||
{
|
{
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
float cx = (mn.x + mx.x) * 0.5f;
|
float cx = (mn.x + mx.x) * 0.5f;
|
||||||
float cy = (mn.y + mx.y) * 0.5f;
|
float cy = (mn.y + mx.y) * 0.5f;
|
||||||
|
|
||||||
@@ -304,7 +305,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|||||||
|
|
||||||
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
|
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
|
||||||
BevelPass passes[] = {
|
BevelPass passes[] = {
|
||||||
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
|
{ 0.5f * dp, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
|
||||||
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
|
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (depth > s_ic.threshold) {
|
if (depth > s_ic.threshold) {
|
||||||
float baseInset = s_ic.inset;
|
float baseInset = s_ic.inset * Layout::dpiScale();
|
||||||
int shadowMax = (int)(s_ic.maxAlpha * depth);
|
int shadowMax = (int)(s_ic.maxAlpha * depth);
|
||||||
float fadeRatio = s_ic.fadeRatio;
|
float fadeRatio = s_ic.fadeRatio;
|
||||||
float bW = mx.x - mn.x - baseInset * 2.0f;
|
float bW = mx.x - mn.x - baseInset * 2.0f;
|
||||||
@@ -487,7 +488,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
float fixedH = stripH; // collapse strip
|
float fixedH = stripH; // collapse strip
|
||||||
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
||||||
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
|
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
|
||||||
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
|
fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
|
||||||
fixedH += bottomPadding + stripH; // exit area
|
fixedH += bottomPadding + stripH; // exit area
|
||||||
|
|
||||||
float baseFlexH = baseNavGap;
|
float baseFlexH = baseNavGap;
|
||||||
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
curY += sectionGap;
|
curY += sectionGap;
|
||||||
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
|
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
|
||||||
curY += olFsz + 2.0f + sectionLabelPadBot;
|
curY += olFsz + 2.0f * dp + sectionLabelPadBot;
|
||||||
} else {
|
} else {
|
||||||
curY += sectionGap * 0.4f;
|
curY += sectionGap * 0.4f;
|
||||||
if (nSeparators < 4) separatorY[nSeparators++] = curY;
|
if (nSeparators < 4) separatorY[nSeparators++] = curY;
|
||||||
@@ -653,7 +654,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
fx.drawShimmer(dl, indMin, indMax, btnRnd);
|
fx.drawShimmer(dl, indMin, indMax, btnRnd);
|
||||||
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
|
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
|
||||||
}
|
}
|
||||||
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
|
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f * dp);
|
||||||
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
|
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
|
||||||
buttonRects.push_back({indMin, indMax, btnRnd});
|
buttonRects.push_back({indMin, indMax, btnRnd});
|
||||||
}
|
}
|
||||||
@@ -684,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
bool itemHasBadge =
|
bool itemHasBadge =
|
||||||
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
|
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
|
||||||
(item.page == NavPage::Mining && status.miningActive) ||
|
(item.page == NavPage::Mining && status.miningActive) ||
|
||||||
(item.page == NavPage::Peers && status.peerCount > 0) ||
|
|
||||||
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
|
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
|
||||||
float badgeReserve = 0.0f;
|
float badgeReserve = 0.0f;
|
||||||
if (itemHasBadge) {
|
if (itemHasBadge) {
|
||||||
@@ -732,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
badgeCol = Warning(); badgeTextCol = OnWarning();
|
badgeCol = Warning(); badgeTextCol = OnWarning();
|
||||||
} else if (item.page == NavPage::Mining && status.miningActive) {
|
} else if (item.page == NavPage::Mining && status.miningActive) {
|
||||||
dotOnly = true; badgeCol = Success();
|
dotOnly = true; badgeCol = Success();
|
||||||
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
|
|
||||||
badgeCount = status.peerCount;
|
|
||||||
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
|
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
|
||||||
badgeCount = status.chatUnreadCount;
|
badgeCount = status.chatUnreadCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public:
|
|||||||
// Arrow
|
// Arrow
|
||||||
{
|
{
|
||||||
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
|
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
|
||||||
ImGui::SetCursorPosX(arrowCX - 8.0f);
|
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
|
||||||
ImFont* iconFont = Type().iconMed();
|
ImFont* iconFont = Type().iconMed();
|
||||||
float fsz = ScaledFontSize(iconFont);
|
float fsz = ScaledFontSize(iconFont);
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||||
@@ -312,7 +312,8 @@ private:
|
|||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
float btnW = 120.0f;
|
const float dp = Layout::dpiScale();
|
||||||
|
float btnW = 120.0f * dp;
|
||||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
|
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
|
||||||
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
|
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
|
||||||
s_open = false;
|
s_open = false;
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
|||||||
} else if (rows.empty()) {
|
} else if (rows.empty()) {
|
||||||
float cw = ImGui::GetContentRegionAvail().x;
|
float cw = ImGui::GetContentRegionAvail().x;
|
||||||
float ch = ImGui::GetContentRegionAvail().y;
|
float ch = ImGui::GetContentRegionAvail().y;
|
||||||
if (ch < 60) ch = 60;
|
if (ch < 60.0f * dp) ch = 60.0f * dp;
|
||||||
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
||||||
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
||||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||||
|
|||||||
@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
|
|||||||
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
|
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
|
||||||
|
|
||||||
// Card height: must fit the Market card's content (overline + price + 24h)
|
// Card height: must fit the Market card's content (overline + price + 24h)
|
||||||
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f);
|
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
|
||||||
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
|
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
|
||||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f);
|
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
|
||||||
float marketContentH = cardPadLg
|
float marketContentH = cardPadLg
|
||||||
+ ovFont->LegacySize + ovGap
|
+ ovFont->LegacySize + ovGap
|
||||||
+ sub1->LegacySize + 2.0f * dp
|
+ sub1->LegacySize + 2.0f * dp
|
||||||
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
|
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
|
||||||
// We draw a full-size rounded rect (left corners only) and clip it to the
|
// We draw a full-size rounded rect (left corners only) and clip it to the
|
||||||
// stripe width so the shape itself follows the card rounding.
|
// stripe width so the shape itself follows the card rounding.
|
||||||
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
|
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
|
||||||
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
|
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
|
||||||
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
|
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
|
||||||
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
|
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
|
||||||
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
ImU32 bd = WithAlpha(fg, 90);
|
ImU32 bd = WithAlpha(fg, 90);
|
||||||
snprintf(buf, sizeof(buf), "%s %s",
|
snprintf(buf, sizeof(buf), "%s %s",
|
||||||
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
|
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
|
||||||
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
|
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
|
||||||
// Hover → explain what stale means and how old the data actually is.
|
// Hover → explain what stale means and how old the data actually is.
|
||||||
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
|
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
|
||||||
Tooltip("%s", TR("data_stale_tooltip"));
|
Tooltip("%s", TR("data_stale_tooltip"));
|
||||||
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Hover glow
|
// Hover glow
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
if (material::IsRectHovered(cMin, cMax)) {
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
{
|
{
|
||||||
float privPct = (s_dispTotal > 1e-9)
|
float privPct = (s_dispTotal > 1e-9)
|
||||||
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
|
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
|
||||||
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr",
|
snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
|
||||||
privPct, (int)state.z_addresses.size());
|
privPct, (int)state.z_addresses.size());
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||||
WithAlpha(Success(), 160), buf);
|
WithAlpha(Success(), 160), buf);
|
||||||
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
|
|||||||
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
||||||
ImVec2 ts = capFont->CalcTextSizeA(
|
ImVec2 ts = capFont->CalcTextSizeA(
|
||||||
capFont->LegacySize, 10000, 0, buf);
|
capFont->LegacySize, 10000, 0, buf);
|
||||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
|
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
|
||||||
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f);
|
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
|
||||||
ImVec2 bMin(cMax.x - ts.x - bp * 3,
|
ImVec2 bMin(cMax.x - ts.x - bp * 3,
|
||||||
cMin.y + cardPadLg);
|
cMin.y + cardPadLg);
|
||||||
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
|
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
|
||||||
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Hover glow + click to Receive
|
// Hover glow + click to Receive
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
if (material::IsRectHovered(cMin, cMax)) {
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(NavPage::Receive);
|
app->setCurrentPage(NavPage::Receive);
|
||||||
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||||
cy += sub1->LegacySize + valGap;
|
cy += sub1->LegacySize + valGap;
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "%d T-addresses",
|
snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
|
||||||
(int)state.t_addresses.size());
|
(int)state.t_addresses.size());
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||||
OnSurfaceDisabled(), buf);
|
OnSurfaceDisabled(), buf);
|
||||||
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Hover glow + click to Receive
|
// Hover glow + click to Receive
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
if (material::IsRectHovered(cMin, cMax)) {
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(NavPage::Receive);
|
app->setCurrentPage(NavPage::Receive);
|
||||||
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
[](unsigned char c){ return (char)std::toupper(c); });
|
[](unsigned char c){ return (char)std::toupper(c); });
|
||||||
float textW = std::max(pSz.x + tickGap + usdSz.x,
|
float textW = std::max(pSz.x + tickGap + usdSz.x,
|
||||||
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
|
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
|
||||||
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
|
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||||
float sparkLeft = cx + textW + sparkGap;
|
float sparkLeft = cx + textW + sparkGap;
|
||||||
float sparkRight = cMax.x - cardPadLg;
|
float sparkRight = cMax.x - cardPadLg;
|
||||||
|
|
||||||
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
bool pos = market.change_24h >= 0;
|
bool pos = market.change_24h >= 0;
|
||||||
ImU32 chgCol = pos ? Success()
|
ImU32 chgCol = pos ? Success()
|
||||||
: Error();
|
: Error();
|
||||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
|
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
|
||||||
pos ? "+" : "", market.change_24h);
|
pos ? "+" : "", market.change_24h);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(cx, cy), chgCol, buf);
|
ImVec2(cx, cy), chgCol, buf);
|
||||||
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
// Hover glow + click to Market
|
// Hover glow + click to Market
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
if (material::IsRectHovered(cMin, cMax)) {
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(NavPage::Market);
|
app->setCurrentPage(NavPage::Market);
|
||||||
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
|
|||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
ImFont* body2 = Type().body2();
|
ImFont* body2 = Type().body2();
|
||||||
|
|
||||||
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
|
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
|
||||||
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
|
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
|
||||||
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
|
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
|
||||||
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f);
|
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
|
||||||
|
|
||||||
// Shielded legend
|
// Shielded legend
|
||||||
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
|
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
|
||||||
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded);
|
snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
|
||||||
legendY += capFont->LegacySize + legendLineGap;
|
legendY += capFont->LegacySize + legendLineGap;
|
||||||
|
|
||||||
// Transparent legend
|
// Transparent legend
|
||||||
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
|
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
|
||||||
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent);
|
snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
|
||||||
legendY += capFont->LegacySize + legendSectionGap;
|
legendY += capFont->LegacySize + legendSectionGap;
|
||||||
|
|
||||||
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
|
|||||||
const auto& market = state.market;
|
const auto& market = state.market;
|
||||||
if (market.price_usd > 0) {
|
if (market.price_usd > 0) {
|
||||||
if (market.price_usd >= 0.01)
|
if (market.price_usd >= 0.01)
|
||||||
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd);
|
snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
|
||||||
else
|
else
|
||||||
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd);
|
snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
||||||
OnSurfaceMedium(), buf);
|
OnSurfaceMedium(), buf);
|
||||||
legendY += capFont->LegacySize + 4 * dp;
|
legendY += capFont->LegacySize + 4 * dp;
|
||||||
|
|
||||||
bool pos = market.change_24h >= 0;
|
bool pos = market.change_24h >= 0;
|
||||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h);
|
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
||||||
pos ? Success() : Error(), buf);
|
pos ? Success() : Error(), buf);
|
||||||
}
|
}
|
||||||
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
|
|||||||
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
|
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
|
||||||
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
|
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
|
||||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
|
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
|
||||||
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f));
|
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
|
||||||
|
|
||||||
// Bottom half: proportion bars
|
// Bottom half: proportion bars
|
||||||
float barY = divY + Layout::spacingSm();
|
float barY = divY + Layout::spacingSm();
|
||||||
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
|
|||||||
|
|
||||||
// Accent stripe — clipped to tile rounded corners
|
// Accent stripe — clipped to tile rounded corners
|
||||||
{
|
{
|
||||||
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
|
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
|
||||||
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
|
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
|
||||||
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
|
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
|
||||||
ImDrawFlags_RoundCornersLeft);
|
ImDrawFlags_RoundCornersLeft);
|
||||||
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
|
|||||||
// Click
|
// Click
|
||||||
if (material::IsRectHovered(tMin, tMax)) {
|
if (material::IsRectHovered(tMin, tMax)) {
|
||||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||||
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(tiles[i].nav);
|
app->setCurrentPage(tiles[i].nav);
|
||||||
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
|||||||
// Font-content floor per row: icon + label + value must fit
|
// Font-content floor per row: icon + label + value must fit
|
||||||
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
|
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
|
||||||
+ Layout::spacingSm() * 2;
|
+ Layout::spacingSm() * 2;
|
||||||
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f);
|
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
|
||||||
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
|
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
|
||||||
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
|
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
|
||||||
float stackH;
|
float stackH;
|
||||||
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RowInfo rowInfos[4] = {
|
RowInfo rowInfos[4] = {
|
||||||
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
||||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
||||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
||||||
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
|
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
|||||||
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
|
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
|
||||||
if (i == 1 || i == 2) {
|
if (i == 1 || i == 2) {
|
||||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
||||||
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
|
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||||
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
||||||
float barH = std::max(
|
float barH = std::max(
|
||||||
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
|
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
|
||||||
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
|
|||||||
// Sparkline in the gap between label and 24h change
|
// Sparkline in the gap between label and 24h change
|
||||||
if (state.market.price_history.size() >= 2) {
|
if (state.market.price_history.size() >= 2) {
|
||||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
||||||
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
|
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||||
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
|
||||||
float sparkLeft = px + labelSz.x + sparkGap;
|
float sparkLeft = px + labelSz.x + sparkGap;
|
||||||
float sparkRight = chgX - sparkGap;
|
float sparkRight = chgX - sparkGap;
|
||||||
if (sparkLeft < sparkRight) {
|
if (sparkLeft < sparkRight) {
|
||||||
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
|||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
// Font-content floor per row: caption text + vertical padding
|
// Font-content floor per row: caption text + vertical padding
|
||||||
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
|
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
|
||||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
|
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
|
||||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
|
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
|
||||||
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
|
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
|
||||||
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
|
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
|
||||||
float stackH;
|
float stackH;
|
||||||
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
|
|||||||
CellInfo cells[2][2] = {
|
CellInfo cells[2][2] = {
|
||||||
// Row 0: Total Balance (left), Shielded (right)
|
// Row 0: Total Balance (left), Shielded (right)
|
||||||
{
|
{
|
||||||
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
||||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||||
},
|
},
|
||||||
// Row 1: Market (left), Transparent (right)
|
// Row 1: Market (left), Transparent (right)
|
||||||
{
|
{
|
||||||
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
||||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
|||||||
// Sparkline between label and 24h change
|
// Sparkline between label and 24h change
|
||||||
if (state.market.price_history.size() >= 2) {
|
if (state.market.price_history.size() >= 2) {
|
||||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
|
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
|
||||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
|
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
|
||||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
|
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
|
||||||
float sparkLeft = px + labelSz.x + sparkGap;
|
float sparkLeft = px + labelSz.x + sparkGap;
|
||||||
float sparkRight = chgX - sparkGap;
|
float sparkRight = chgX - sparkGap;
|
||||||
if (sparkLeft < sparkRight) {
|
if (sparkLeft < sparkRight) {
|
||||||
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
|
|||||||
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
|
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
|
||||||
gaugeCy + sinf(needleAngle) * needleLen);
|
gaugeCy + sinf(needleAngle) * needleLen);
|
||||||
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
|
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
|
||||||
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f));
|
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
|
||||||
|
|
||||||
// Center text: percentage
|
// Center text: percentage
|
||||||
ImFont* sub1 = Type().subtitle1();
|
ImFont* sub1 = Type().subtitle1();
|
||||||
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RenderSyncBar(app, dl, vs);
|
RenderSyncBar(app, dl, vs);
|
||||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f)));
|
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
|
||||||
|
|
||||||
// Row 2: 3 mini-cards inline
|
// Row 2: 3 mini-cards inline
|
||||||
{
|
{
|
||||||
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
|
|||||||
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
|
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
|
||||||
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
|
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f);
|
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
|
||||||
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
|
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
|
||||||
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
|
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
|
||||||
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
|
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
|
||||||
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
|
|||||||
|
|
||||||
// Sparkline between price and percentage
|
// Sparkline between price and percentage
|
||||||
if (market.price_history.size() >= 2) {
|
if (market.price_history.size() >= 2) {
|
||||||
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
|
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
|
||||||
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f);
|
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
|
||||||
float sparkLeft = cx + priceSz.x + sparkGap;
|
float sparkLeft = cx + priceSz.x + sparkGap;
|
||||||
float sparkRightEdge = sparkRight - sparkGap;
|
float sparkRightEdge = sparkRight - sparkGap;
|
||||||
if (sparkLeft < sparkRightEdge) {
|
if (sparkLeft < sparkRightEdge) {
|
||||||
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
|
|||||||
{
|
{
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
ImVec2 sepPos = ImGui::GetCursorScreenPos();
|
ImVec2 sepPos = ImGui::GetCursorScreenPos();
|
||||||
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
|
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
|
||||||
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f);
|
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
|
||||||
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
|
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
|
||||||
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f);
|
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
|
||||||
float x = sepPos.x;
|
float x = sepPos.x;
|
||||||
float endX = sepPos.x + availW;
|
float endX = sepPos.x + availW;
|
||||||
while (x < endX) {
|
while (x < endX) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
|||||||
s_loading = false;
|
s_loading = false;
|
||||||
|
|
||||||
if (!error.empty()) {
|
if (!error.empty()) {
|
||||||
s_error = "Error: " + error;
|
s_error = std::string(TR("grpa_error_prefix")) + error;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
|||||||
|
|
||||||
s_has_data = true;
|
s_has_data = true;
|
||||||
} else {
|
} else {
|
||||||
s_error = "Invalid response from daemon";
|
s_error = TR("grpa_invalid_response_from_daemon");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
|
|
||||||
// Current block info
|
// Current block info
|
||||||
if (state.sync.blocks > 0) {
|
if (state.sync.blocks > 0) {
|
||||||
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
|
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
|
|||||||
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
||||||
auto hashResult = rpc->call("getblockhash", {height});
|
auto hashResult = rpc->call("getblockhash", {height});
|
||||||
if (!hashResult.is_string()) {
|
if (!hashResult.is_string()) {
|
||||||
error = "unexpected getblockhash result";
|
error = TR("grpa_unexpected_getblockhash_result");
|
||||||
} else {
|
} else {
|
||||||
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ private:
|
|||||||
|
|
||||||
if (!s_bootstrap) {
|
if (!s_bootstrap) {
|
||||||
s_state = State::Failed;
|
s_state = State::Failed;
|
||||||
s_errorMsg = "Bootstrap not initialized";
|
s_errorMsg = TR("grpc_bootstrap_not_initialized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ private:
|
|||||||
s_state = State::Done;
|
s_state = State::Done;
|
||||||
} else {
|
} else {
|
||||||
s_errorMsg = finalProg.error;
|
s_errorMsg = finalProg.error;
|
||||||
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
|
if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
|
||||||
s_state = State::Failed;
|
s_state = State::Failed;
|
||||||
}
|
}
|
||||||
s_bootstrap.reset();
|
s_bootstrap.reset();
|
||||||
|
|||||||
@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
|
|||||||
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
|
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
|
||||||
const std::string peer = "Ava";
|
const std::string peer = "Ava";
|
||||||
const PMsg msgs[] = {
|
const PMsg msgs[] = {
|
||||||
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
|
{ TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
|
||||||
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||||
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
|
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
|
||||||
};
|
};
|
||||||
const int N = 3;
|
const int N = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
|||||||
float outputH = ComputeConsoleOutputHeight(
|
float outputH = ComputeConsoleOutputHeight(
|
||||||
availHeight,
|
availHeight,
|
||||||
input_height,
|
input_height,
|
||||||
schema::UI().drawElement("tabs.console", "output-min-height").size,
|
schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
|
||||||
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
|
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
|
||||||
|
|
||||||
ImDrawList* dlOut = ImGui::GetWindowDrawList();
|
ImDrawList* dlOut = ImGui::GetWindowDrawList();
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
|
|||||||
int64_t diff = now - timestamp;
|
int64_t diff = now - timestamp;
|
||||||
if (diff < 0) diff = 0;
|
if (diff < 0) diff = 0;
|
||||||
if (diff < 60)
|
if (diff < 60)
|
||||||
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
|
snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
|
||||||
else if (diff < 3600)
|
else if (diff < 3600)
|
||||||
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
|
snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
|
||||||
else if (diff < 86400)
|
else if (diff < 86400)
|
||||||
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
|
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
|
||||||
else
|
else
|
||||||
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
|
snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
txDL->AddRectFilled(rowStart,
|
txDL->AddRectFilled(rowStart,
|
||||||
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
|
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
|
||||||
WithAlpha(OnSurface(), 10),
|
WithAlpha(OnSurface(), 10),
|
||||||
S.drawElement("tabs.explorer", "row-rounding").size);
|
S.drawElement("tabs.explorer", "row-rounding").size * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
material::Tooltip("%s", txid.c_str());
|
material::Tooltip("%s", txid.c_str());
|
||||||
}
|
}
|
||||||
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (s_detail_txids.size() > 100) {
|
if (s_detail_txids.size() > 100) {
|
||||||
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
|
snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
|
||||||
ImGui::TextDisabled("%s", buf);
|
ImGui::TextDisabled("%s", buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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_key = found;
|
||||||
s_show_key = wantViewing; // viewing keys are less sensitive
|
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||||
} else {
|
} else {
|
||||||
s_error = r.ok ? std::string("Key not available for this address") : r.error;
|
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
|
||||||
}
|
}
|
||||||
wallet::secureWipeLiteSecret(found);
|
wallet::secureWipeLiteSecret(found);
|
||||||
s_fetching = false;
|
s_fetching = false;
|
||||||
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
|
|||||||
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
||||||
} else {
|
} else {
|
||||||
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
|
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
|
||||||
const float gap = 12.0f;
|
const float dp = Layout::dpiScale();
|
||||||
|
const float gap = 12.0f * dp;
|
||||||
const float avail = ImGui::GetContentRegionAvail().x;
|
const float avail = ImGui::GetContentRegionAvail().x;
|
||||||
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
|
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
|
||||||
float qrSize = avail * 0.30f;
|
float qrSize = avail * 0.30f;
|
||||||
if (qrSize < 200.0f) qrSize = 200.0f;
|
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
|
||||||
if (qrSize > 340.0f) qrSize = 340.0f;
|
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
|
||||||
float keyW = avail - qrSize - gap;
|
float keyW = avail - qrSize - gap;
|
||||||
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
|
const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
|
||||||
if (!sideBySide) keyW = avail;
|
if (!sideBySide) keyW = avail;
|
||||||
|
|
||||||
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
|
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
|
||||||
|
|||||||
@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
|
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
|
||||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
|
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
|
||||||
if (!market.price_loading && !market.price_error.empty()) {
|
if (!market.price_loading && !market.price_error.empty()) {
|
||||||
std::string errorText = market.price_error;
|
std::string errorText = market.price_error;
|
||||||
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
|
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
|
||||||
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
|||||||
}
|
}
|
||||||
if (errorText.size() < market.price_error.size()) errorText += "...";
|
if (errorText.size() < market.price_error.size()) errorText += "...";
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
|
ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
|
||||||
Warning(), errorText.c_str());
|
Warning(), errorText.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
|||||||
: (tk == ticks - 1) ? plotRight - lblSz.x
|
: (tk == ticks - 1) ? plotRight - lblSz.x
|
||||||
: xpos - lblSz.x * 0.5f;
|
: xpos - lblSz.x * 0.5f;
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
ImFont* stepFont = Type().iconSmall();
|
ImFont* stepFont = Type().iconSmall();
|
||||||
const float fieldH = capFont->LegacySize + 6.0f * dp;
|
// Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
|
||||||
|
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
|
||||||
|
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
|
||||||
|
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
|
||||||
|
// capFont+6px, shorter than the box, so they sat misaligned.
|
||||||
|
const float fieldH = ImGui::GetFrameHeight();
|
||||||
const float sideW = fieldH; // square -/+ buttons
|
const float sideW = fieldH; // square -/+ buttons
|
||||||
const float fieldW = 46.0f * dp;
|
const float fieldW = 46.0f * dp;
|
||||||
const float g = 2.0f * dp;
|
const float g = 2.0f * dp;
|
||||||
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
s_benchConfirm = true;
|
s_benchConfirm = true;
|
||||||
char msg[128];
|
char msg[128];
|
||||||
snprintf(msg, sizeof(msg),
|
snprintf(msg, sizeof(msg),
|
||||||
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
|
TR("grpc_benchmark_takes_secs"),
|
||||||
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
|
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
|
||||||
Notifications::instance().warning(msg);
|
Notifications::instance().warning(msg);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
if (estActive)
|
if (estActive)
|
||||||
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
||||||
else
|
else
|
||||||
snprintf(estVal, sizeof(estVal), "N/A");
|
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
|
||||||
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
|
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
|
||||||
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
|
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
|
||||||
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
|
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
|
||||||
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
else if (totalRAM > 0)
|
else if (totalRAM > 0)
|
||||||
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
|
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
|
||||||
else
|
else
|
||||||
snprintf(sysBuf, sizeof(sysBuf), "N/A");
|
snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
|
||||||
|
|
||||||
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
|
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
|
||||||
float sysTextX = barX + barW - textPadX - sysTextW;
|
float sysTextX = barX + barW - textPadX - sysTextW;
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
|||||||
? it->second.feePercent
|
? it->second.feePercent
|
||||||
: kp.feePercent;
|
: kp.feePercent;
|
||||||
if (feePct >= 0.0)
|
if (feePct >= 0.0)
|
||||||
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
|
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
|
||||||
FormatFeePercent(feePct).c_str());
|
FormatFeePercent(feePct).c_str());
|
||||||
else
|
else
|
||||||
snprintf(right, sizeof(right), "%s", hrStr.c_str());
|
snprintf(right, sizeof(right), "%s", hrStr.c_str());
|
||||||
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
|||||||
// centered ~1000dp band inside the panel's padded content region; the
|
// centered ~1000dp band inside the panel's padded content region; the
|
||||||
// glass panel itself still fills rightW, leftover -> side margin.
|
// glass panel itself still fills rightW, leftover -> side margin.
|
||||||
const float rightContentW = rightW - pad * 2.0f;
|
const float rightContentW = rightW - pad * 2.0f;
|
||||||
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
|
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
|
||||||
const float chartBandX = rightMin.x + pad
|
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
|
||||||
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
|
const float chartBandW = rightContentW;
|
||||||
|
const float chartBandX = rightMin.x + pad;
|
||||||
|
|
||||||
// Right panel: live log (if toggled + available) else the sparkline.
|
// Right panel: live log (if toggled + available) else the sparkline.
|
||||||
if (showLogView) {
|
if (showLogView) {
|
||||||
|
|||||||
@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
|
|||||||
}
|
}
|
||||||
if (benchmarkUpdate.inconclusive) {
|
if (benchmarkUpdate.inconclusive) {
|
||||||
Notifications::instance().warning(
|
Notifications::instance().warning(
|
||||||
"Benchmark inconclusive: no hashrate samples were recorded. "
|
TR("grpc_benchmark_inconclusive"));
|
||||||
"Check the pool connection and try again.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
|
|||||||
if (tlsCount == totalPeers) {
|
if (tlsCount == totalPeers) {
|
||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
|
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
|
||||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||||
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
|
|||||||
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
|
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
|
||||||
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
|
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
|
||||||
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
|
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
|
|||||||
ImU32 tlsBg = WithAlpha(Success(), 25);
|
ImU32 tlsBg = WithAlpha(Success(), 25);
|
||||||
ImU32 tlsFg = WithAlpha(Success(), 200);
|
ImU32 tlsFg = WithAlpha(Success(), 200);
|
||||||
ImVec2 tlsMin(badgeX, cy2);
|
ImVec2 tlsMin(badgeX, cy2);
|
||||||
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
|
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
|
||||||
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
|
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
|
||||||
} else {
|
} else {
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
|
||||||
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
||||||
|
|||||||
@@ -259,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
||||||
tag, lblIt->second.c_str(), trunc.c_str(),
|
tag, lblIt->second.c_str(), trunc.c_str(),
|
||||||
addr.balance, DRAGONX_TICKER,
|
addr.balance, DRAGONX_TICKER,
|
||||||
isNew ? " [NEW]" : "");
|
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||||
} else {
|
} else {
|
||||||
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
|
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
|
||||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
|
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
|
||||||
isNew ? " [NEW]" : "");
|
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
@@ -272,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
s_cached_qr_data.clear(); // Force QR regeneration
|
s_cached_qr_data.clear(); // Force QR regeneration
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
material::Tooltip("%s\nBalance: %.8f %s%s",
|
material::Tooltip(TR("grpb_tooltip_address_balance"),
|
||||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||||
isCurrent ? "\n(selected)" : "");
|
isCurrent ? TR("grpb_selected_suffix") : "");
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
@@ -716,17 +716,17 @@ void RenderReceiveTab(App* app)
|
|||||||
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||||
float ss = iconW * 0.5f;
|
float ss = iconW * 0.5f;
|
||||||
float cx = startX + ss;
|
float cx = startX + ss;
|
||||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size;
|
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
|
||||||
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
|
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
|
||||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size;
|
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
|
||||||
float ay1 = cy - ag;
|
float ay1 = cy - ag;
|
||||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
|
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||||
dl->AddTriangleFilled(
|
dl->AddTriangleFilled(
|
||||||
ImVec2(cx + al, ay1),
|
ImVec2(cx + al, ay1),
|
||||||
ImVec2(cx + al - hss, ay1 - hss),
|
ImVec2(cx + al - hss, ay1 - hss),
|
||||||
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
||||||
float ay2 = cy + ag;
|
float ay2 = cy + ag;
|
||||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
|
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||||
dl->AddTriangleFilled(
|
dl->AddTriangleFilled(
|
||||||
ImVec2(cx - al, ay2),
|
ImVec2(cx - al, ay2),
|
||||||
ImVec2(cx - al + hss, ay2 - hss),
|
ImVec2(cx - al + hss, ay2 - hss),
|
||||||
@@ -744,8 +744,8 @@ void RenderReceiveTab(App* app)
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
||||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
|
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
|
||||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
|
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
|
||||||
|
|
||||||
struct Preset { const char* label; double amount; };
|
struct Preset { const char* label; double amount; };
|
||||||
Preset presets[] = {
|
Preset presets[] = {
|
||||||
@@ -873,7 +873,7 @@ void RenderReceiveTab(App* app)
|
|||||||
RenderQRCode(s_qr_texture, qrSize);
|
RenderQRCode(s_qr_texture, qrSize);
|
||||||
} else {
|
} else {
|
||||||
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
|
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
|
||||||
qrPanelMin.y + totalQrSize * 0.5f);
|
qrPanelMin.y + totalQrSize * 0.5f);
|
||||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||||
OnSurfaceDisabled(), TR("qr_unavailable"));
|
OnSurfaceDisabled(), TR("qr_unavailable"));
|
||||||
|
|||||||
@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
|||||||
if (suggestions.empty()) return;
|
if (suggestions.empty()) return;
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
|
||||||
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size);
|
const float dp = Layout::dpiScale();
|
||||||
|
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
|
||||||
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
|
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
|
||||||
for (size_t si = 0; si < suggestions.size(); si++) {
|
for (size_t si = 0; si < suggestions.size(); si++) {
|
||||||
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
|
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
|
||||||
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
static void RenderFeeTierSelector(const char* suffix = "") {
|
static void RenderFeeTierSelector(const char* suffix = "") {
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
|
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
|
||||||
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
|
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
|
||||||
for (int fi = 0; fi < 3; fi++) {
|
for (int fi = 0; fi < 3; fi++) {
|
||||||
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size);
|
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
|
||||||
bool active = (s_fee_tier == fi);
|
bool active = (s_fee_tier == fi);
|
||||||
if (active) {
|
if (active) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
|
||||||
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
|
|||||||
// Max button — use caption font to fit bar height
|
// Max button — use caption font to fit bar height
|
||||||
ImGui::SameLine(0, gap);
|
ImGui::SameLine(0, gap);
|
||||||
char maxId[32];
|
char maxId[32];
|
||||||
snprintf(maxId, sizeof(maxId), "Max%s", suffix);
|
snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
|
||||||
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
|
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
|
||||||
s_amount = maxAmount;
|
s_amount = maxAmount;
|
||||||
s_send_max = true;
|
s_send_max = true;
|
||||||
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
bool is_valid_address, double available,
|
bool is_valid_address, double available,
|
||||||
const char* suffix = "") {
|
const char* suffix = "") {
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
const auto& state = app->getWalletState();
|
const auto& state = app->getWalletState();
|
||||||
double total = s_amount + s_fee;
|
double total = s_amount + s_fee;
|
||||||
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
|
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
|
||||||
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
if (ImGui::BeginPopup(confirmClearId)) {
|
if (ImGui::BeginPopup(confirmClearId)) {
|
||||||
ImGui::Text("%s", TR("send_clear_fields"));
|
ImGui::Text("%s", TR("send_clear_fields"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
|
||||||
ClearFormWithUndo();
|
ClearFormWithUndo();
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
@@ -1086,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
|
||||||
char undoId[32];
|
char undoId[32];
|
||||||
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
|
snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
|
||||||
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
|
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
|
||||||
RestoreFormSnapshot();
|
RestoreFormSnapshot();
|
||||||
Notifications::instance().info(TR("send_form_restored"));
|
Notifications::instance().info(TR("send_form_restored"));
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
if (!skin.valid) {
|
if (!skin.valid) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||||
ImGui::BeginDisabled(true);
|
ImGui::BeginDisabled(true);
|
||||||
std::string label = skin.name + " (invalid)";
|
std::string label = skin.name + TR("swin_invalid_suffix");
|
||||||
ImGui::Selectable(label.c_str(), false);
|
ImGui::Selectable(label.c_str(), false);
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
ImGui::PushFont(material::Type().iconSmall());
|
ImGui::PushFont(material::Type().iconSmall());
|
||||||
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
|
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
|
||||||
skinMgr.refresh();
|
skinMgr.refresh();
|
||||||
Notifications::instance().info("Theme list refreshed");
|
Notifications::instance().info(TR("swin_theme_list_refreshed"));
|
||||||
}
|
}
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
||||||
if (error.empty()) {
|
if (error.empty()) {
|
||||||
std::string version = result.value("version", "unknown");
|
std::string version = result.value("version", "unknown");
|
||||||
std::string msg = "Connection successful!\ndragonxd version: " + version;
|
std::string msg = std::string(TR("swin_connection_successful")) + version;
|
||||||
Notifications::instance().success(msg);
|
Notifications::instance().success(msg);
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().error("Connection failed: " + error);
|
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().error("RPC client not initialized");
|
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
if (error.empty()) {
|
if (error.empty()) {
|
||||||
int start = result.value("start_height", 0);
|
int start = result.value("start_height", 0);
|
||||||
int end = result.value("stop_height", 0);
|
int end = result.value("stop_height", 0);
|
||||||
std::string msg = "Rescan started from block " + std::to_string(start) +
|
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
|
||||||
" to " + std::to_string(end);
|
TR("swin_rescan_to") + std::to_string(end);
|
||||||
Notifications::instance().success(msg);
|
Notifications::instance().success(msg);
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().error("Rescan failed: " + error);
|
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().error("RPC client not initialized");
|
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
||||||
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
if (doConfirm) {
|
if (doConfirm) {
|
||||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||||
if (util::Platform::deleteFile(ztx_file)) {
|
if (util::Platform::deleteFile(ztx_file)) {
|
||||||
Notifications::instance().success("Z-transaction history cleared");
|
Notifications::instance().success(TR("swin_ztx_history_cleared"));
|
||||||
} else {
|
} else {
|
||||||
Notifications::instance().info("No history file found");
|
Notifications::instance().info(TR("swin_no_history_file_found"));
|
||||||
}
|
}
|
||||||
s_confirm_clear_ztx = false;
|
s_confirm_clear_ztx = false;
|
||||||
}
|
}
|
||||||
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
|||||||
// Save/Cancel buttons
|
// Save/Cancel buttons
|
||||||
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
||||||
saveSettingsFromUI(app->settings());
|
saveSettingsFromUI(app->settings());
|
||||||
Notifications::instance().success("Settings saved");
|
Notifications::instance().success(TR("swin_settings_saved"));
|
||||||
*p_open = false;
|
*p_open = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
|
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
|
||||||
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
|
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||||
ImGui::SetClipboardText(tx.txid.c_str());
|
ImGui::SetClipboardText(tx.txid.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||||
ImGui::SetClipboardText(tx.address.c_str());
|
ImGui::SetClipboardText(tx.address.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
|
|||||||
int idx_map[] = {-1, 1, 0, 2};
|
int idx_map[] = {-1, 1, 0, 2};
|
||||||
int idx = idx_map[type_filter];
|
int idx = idx_map[type_filter];
|
||||||
float xOff = idx * (cardW + cardGap);
|
float xOff = idx * (cardW + cardGap);
|
||||||
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
|
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
|
||||||
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
|
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
|
||||||
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
|
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
|
||||||
dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
|
dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(availWidth, cardH));
|
ImGui::Dummy(ImVec2(availWidth, cardH));
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public:
|
|||||||
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
||||||
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
|
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
|
||||||
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
|
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
|
||||||
const float padV = 48.0f; // content-child padding (top+bottom) + margin
|
const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
|
||||||
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
|
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
|
||||||
// screen the content-sized card could exceed the window (the framework would clip a too-tall
|
// screen the content-sized card could exceed the window (the framework would clip a too-tall
|
||||||
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
|
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
|
||||||
|
|||||||
Reference in New Issue
Block a user