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