From 3ff62ca24838eda8f25368fde3df83cc6c3a3a52 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 4 Apr 2026 19:10:58 -0500 Subject: [PATCH] =?UTF-8?q?v1.2.0:=20UX=20audit=20=E2=80=94=20security=20f?= =?UTF-8?q?ixes,=20accessibility,=20and=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security (P0): - Fix sidebar remaining interactive behind lock screen - Extend auto-lock idle detection to include active widget interactions - Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase Blocking UX (P1): - Add 15s timeout for encryption state check to prevent indefinite loading - Show restart reason in loading overlay after wallet encryption - Add Force Quit button on shutdown screen after 10s - Warn user if embedded daemon fails to start during wizard completion Polish (P2): - Use configured explorer URL in Receive tab instead of hardcoded URL - Increase request memo buffer from 256 to 512 bytes to match Send tab - Extend notification duration to 5s for critical operations (tx sent, wallet encrypted, key import, backup, export) - Add Reduce Motion accessibility setting (disables page fade + balance lerp) - Show estimated remaining time during mining thread benchmark - Add staleness indicator to market price data (warning after 5 min) New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set, restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion, ago, wizard_daemon_start_failed --- CMakeLists.txt | 2 +- src/app.cpp | 48 ++++++++++++++++--- src/app.h | 3 ++ src/app_network.cpp | 1 + src/app_security.cpp | 26 ++++++---- src/app_wizard.cpp | 10 +++- src/config/settings.cpp | 2 + src/config/settings.h | 5 ++ src/config/version.h | 6 +-- src/data/wallet_state.h | 1 + src/ui/pages/settings_page.cpp | 16 +++++++ src/ui/sidebar.h | 12 +++-- src/ui/windows/backup_wallet_dialog.cpp | 2 +- src/ui/windows/balance_tab.cpp | 4 +- src/ui/windows/export_all_keys_dialog.cpp | 2 +- src/ui/windows/export_transactions_dialog.cpp | 2 +- src/ui/windows/import_key_dialog.cpp | 2 +- src/ui/windows/market_tab.cpp | 20 ++++++++ src/ui/windows/mining_tab.cpp | 26 ++++++++-- src/ui/windows/receive_tab.cpp | 19 +++----- src/ui/windows/send_tab.cpp | 2 +- src/util/i18n.cpp | 9 ++++ 22 files changed, 173 insertions(+), 47 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b574d05..69835c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ if(APPLE) endif() project(ObsidianDragon - VERSION 1.1.2 + VERSION 1.2.0 LANGUAGES C CXX DESCRIPTION "DragonX Cryptocurrency Wallet" ) diff --git a/src/app.cpp b/src/app.cpp index 32329bf..dcac25b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -316,7 +316,8 @@ void App::update() // Track user interaction for auto-lock if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 || io.MouseClicked[0] || io.MouseClicked[1] || - io.InputQueueCharacters.Size > 0) { + io.InputQueueCharacters.Size > 0 || + ImGui::IsAnyItemActive()) { last_interaction_ = std::chrono::steady_clock::now(); } @@ -1024,7 +1025,8 @@ void App::render() bool prevCollapsed = sidebar_collapsed_; { PERF_SCOPE("Render.Sidebar"); - ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_); + ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_, + state_.isLocked()); } if (sbStatus.exitClicked) { requestQuit(); @@ -1039,7 +1041,7 @@ void App::render() // Page transition: detect change, ramp alpha if (current_page_ != prev_page_) { - page_alpha_ = ui::effects::isLowSpecMode() ? 1.0f : 0.0f; + page_alpha_ = (ui::effects::isLowSpecMode() || (settings_ && settings_->getReduceMotion())) ? 1.0f : 0.0f; prev_page_ = current_page_; } if (page_alpha_ < 1.0f) { @@ -1094,6 +1096,18 @@ void App::render() // Lock screen — covers tab content just like the loading overlay renderLockScreen(); } else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) { + // Track how long we've been waiting for encryption state + if (state_.connected && !state_.encryption_state_known) { + encryption_check_timer_ += ImGui::GetIO().DeltaTime; + if (encryption_check_timer_ >= 15.0f) { + DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n"); + state_.encryption_state_known = true; + state_.encrypted = false; + encryption_check_timer_ = 0.0f; + } + } else { + encryption_check_timer_ = 0.0f; + } // Reset lock screen focus flag so it auto-focuses next time lock_screen_was_visible_ = false; // Show loading overlay instead of tab content @@ -2403,11 +2417,14 @@ void App::renderShutdownScreen() ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::Begin("##ShutdownOverlay", nullptr, + ImGuiWindowFlags shutdownFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav | - ImGuiWindowFlags_NoSavedSettings); + ImGuiWindowFlags_NoSavedSettings; + // Allow input after 10s so Force Quit button is clickable + if (shutdown_timer_ < 10.0f) + shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; + ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags); ImDrawList* dl = ImGui::GetWindowDrawList(); // Convert local centre to screen coords for draw-list primitives @@ -2505,6 +2522,25 @@ void App::renderShutdownScreen() ImGui::PopFont(); } + // ------------------------------------------------------------------- + // 4b. Force Quit button — appears after 10 seconds + // ------------------------------------------------------------------- + if (shutdown_timer_ >= 10.0f) { + ImGui::Spacing(); + ImGui::Spacing(); + const char* forceLabel = TR("force_quit"); + ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 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)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button(forceLabel, btnSize)) { + DEBUG_LOGF("Force quit requested by user after %.0fs\n", shutdown_timer_); + shutdown_complete_ = true; + } + ImGui::PopStyleColor(3); + } + ImGui::Spacing(); ImGui::Spacing(); diff --git a/src/app.h b/src/app.h index 6d0e9b3..7def3e5 100644 --- a/src/app.h +++ b/src/app.h @@ -378,6 +378,9 @@ private: std::atomic daemon_restarting_{false}; std::thread daemon_restart_thread_; + // Encryption state check timeout + float encryption_check_timer_ = 0.0f; + // UI State bool quit_requested_ = false; bool show_demo_window_ = false; diff --git a/src/app_network.cpp b/src/app_network.cpp index cb4d37a..5f54e39 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1186,6 +1186,7 @@ void App::refreshPrice() state_.market.volume_24h = market.volume_24h; state_.market.market_cap = market.market_cap; state_.market.last_updated = market.last_updated; + state_.market.last_fetch_time = std::chrono::steady_clock::now(); state_.market.price_history.push_back(market.price_usd); if (state_.market.price_history.size() > MarketInfo::MAX_HISTORY) { diff --git a/src/app_security.cpp b/src/app_security.cpp index 4162c06..1b854d6 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -69,10 +69,13 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) { } ui::Notifications::instance().info( - "Wallet encrypted successfully"); + "Wallet encrypted successfully", 5.0f); - // The daemon shuts itself down after encryptwallet + // The daemon shuts itself down after encryptwallet. + // Update connection_status_ so the loading overlay + // explains why the daemon is restarting. if (isUsingEmbeddedDaemon()) { + connection_status_ = TR("restarting_after_encryption"); // Give daemon a moment to shut down, then restart // (do this off the main thread to avoid stalling the UI) std::thread([this]() { @@ -164,13 +167,13 @@ void App::processDeferredEncryption() { if (pinStored) { settings_->setPinEnabled(true); settings_->save(); - ui::Notifications::instance().info("Wallet encrypted & PIN set"); + ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f); } else { ui::Notifications::instance().warning( "Wallet encrypted but PIN vault failed"); } } else { - ui::Notifications::instance().info("Wallet encrypted successfully"); + ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f); } // Securely clear deferred state @@ -264,7 +267,7 @@ void App::unlockWallet(const std::string& passphrase, int timeout) { state_.unlocked_until = std::time(nullptr) + timeout; } else { lock_attempts_++; - lock_error_msg_ = "Incorrect passphrase"; + lock_error_msg_ = TR("incorrect_passphrase"); lock_error_timer_ = 3.0f; memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_)); @@ -788,10 +791,17 @@ void App::renderLockScreen() { bool vaultOk = vault_ && vault_->retrieve(pin, passphrase); if (!vaultOk) { - return [this]() { + bool noVault = !vault_ || !vault_->hasVault(); + return [this, noVault]() { lock_unlock_in_progress_ = false; - lock_attempts_++; - lock_error_msg_ = "Incorrect PIN"; + if (noVault) { + // Vault file missing — switch to passphrase mode + lock_error_msg_ = TR("pin_not_set"); + lock_use_pin_ = false; + } else { + lock_attempts_++; + lock_error_msg_ = TR("incorrect_pin"); + } lock_error_timer_ = 3.0f; float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f); diff --git a/src/app_wizard.cpp b/src/app_wizard.cpp index 78db8f1..58f4ca5 100644 --- a/src/app_wizard.cpp +++ b/src/app_wizard.cpp @@ -1294,7 +1294,10 @@ void App::renderFirstRunWizard() { // Start daemon + finish wizard immediately if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { - startEmbeddedDaemon(); + if (!startEmbeddedDaemon()) { + ui::Notifications::instance().warning( + TR("wizard_daemon_start_failed")); + } } tryConnect(); wizard_phase_ = WizardPhase::Done; @@ -1313,7 +1316,10 @@ void App::renderFirstRunWizard() { settings_->setWizardCompleted(true); settings_->save(); if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { - startEmbeddedDaemon(); + if (!startEmbeddedDaemon()) { + ui::Notifications::instance().warning( + TR("wizard_daemon_start_failed")); + } } tryConnect(); } diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 1ec9a73..bf733d7 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -140,6 +140,7 @@ bool Settings::load(const std::string& path) } if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get(); if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get(); + if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get(); if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get(); if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get(); if (j.contains("pool_url")) pool_url_ = j["pool_url"].get(); @@ -240,6 +241,7 @@ bool Settings::save(const std::string& path) j["debug_categories"].push_back(cat); j["theme_effects_enabled"] = theme_effects_enabled_; j["low_spec_mode"] = low_spec_mode_; + j["reduce_motion"] = reduce_motion_; j["selected_exchange"] = selected_exchange_; j["selected_pair"] = selected_pair_; j["pool_url"] = pool_url_; diff --git a/src/config/settings.h b/src/config/settings.h index b83f9d0..392be39 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -186,6 +186,10 @@ public: bool getLowSpecMode() const { return low_spec_mode_; } void setLowSpecMode(bool v) { low_spec_mode_ = v; } + // Reduce motion — disables animated transitions for accessibility + bool getReduceMotion() const { return reduce_motion_; } + void setReduceMotion(bool v) { reduce_motion_ = v; } + // Market — last selected exchange + pair std::string getSelectedExchange() const { return selected_exchange_; } void setSelectedExchange(const std::string& v) { selected_exchange_ = v; } @@ -303,6 +307,7 @@ private: std::set debug_categories_; bool theme_effects_enabled_ = true; bool low_spec_mode_ = false; + bool reduce_motion_ = false; std::string selected_exchange_ = "TradeOgre"; std::string selected_pair_ = "DRGX/BTC"; diff --git a/src/config/version.h b/src/config/version.h index 35fca6a..2ee30ec 100644 --- a/src/config/version.h +++ b/src/config/version.h @@ -7,10 +7,10 @@ // !! DO NOT EDIT version.h — it is generated from version.h.in by CMake. // !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) -#define DRAGONX_VERSION "1.1.2" +#define DRAGONX_VERSION "1.2.0" #define DRAGONX_VERSION_MAJOR 1 -#define DRAGONX_VERSION_MINOR 1 -#define DRAGONX_VERSION_PATCH 2 +#define DRAGONX_VERSION_MINOR 2 +#define DRAGONX_VERSION_PATCH 0 #define DRAGONX_APP_NAME "ObsidianDragon" #define DRAGONX_ORG_NAME "Hush" diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 1b1dd51..716bf9a 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -134,6 +134,7 @@ struct MarketInfo { double change_24h = 0.0; double market_cap = 0.0; std::string last_updated; + std::chrono::steady_clock::time_point last_fetch_time{}; // Price history for chart std::vector price_history; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 1dedce0..31f4ede 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -93,6 +93,9 @@ static bool sp_gradient_background = false; // Low-spec mode static bool sp_low_spec_mode = false; +// Reduce motion (accessibility) +static bool sp_reduce_motion = false; + // Font scale (user accessibility, 1.0–3.0) static float sp_font_scale = 1.0f; @@ -179,6 +182,7 @@ static void loadSettingsPageState(config::Settings* settings) { sp_theme_effects_enabled = settings->getThemeEffectsEnabled(); sp_low_spec_mode = settings->getLowSpecMode(); effects::setLowSpecMode(sp_low_spec_mode); + sp_reduce_motion = settings->getReduceMotion(); sp_font_scale = settings->getFontScale(); Layout::setUserFontScale(sp_font_scale); // sync with Layout on load sp_keep_daemon_running = settings->getKeepDaemonRunning(); @@ -232,6 +236,7 @@ static void saveSettingsPageState(config::Settings* settings) { settings->setScanlineEnabled(sp_scanline_enabled); settings->setThemeEffectsEnabled(sp_theme_effects_enabled); settings->setLowSpecMode(sp_low_spec_mode); + settings->setReduceMotion(sp_reduce_motion); settings->setFontScale(sp_font_scale); settings->setKeepDaemonRunning(sp_keep_daemon_running); settings->setStopExternalDaemon(sp_stop_external_daemon); @@ -672,6 +677,12 @@ void RenderSettingsPage(App* app) { } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg")); + ImGui::SameLine(0, Layout::spacingLg()); + if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) { + saveSettingsPageState(app->settings()); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion")); + ImGui::BeginDisabled(sp_low_spec_mode); ImGui::SameLine(0, Layout::spacingLg()); @@ -962,6 +973,11 @@ void RenderSettingsPage(App* app) { } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt")); + if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) { + saveSettingsPageState(app->settings()); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion")); + ImGui::BeginDisabled(sp_low_spec_mode); if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) { diff --git a/src/ui/sidebar.h b/src/ui/sidebar.h index 1e80fd9..6fdaa54 100644 --- a/src/ui/sidebar.h +++ b/src/ui/sidebar.h @@ -380,7 +380,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx, // collapsed: when true, sidebar is in icon-only mode (narrow width). // The caller can toggle collapsed via a reference if a toggle button is desired. inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight, - SidebarStatus& status, bool& collapsed) + SidebarStatus& status, bool& collapsed, bool locked = false) { using namespace material; bool changed = false; @@ -610,8 +610,12 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } - // Click detection - if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + // Click detection — block pages that require unlock when locked + bool pageNeedsUnlock = locked && + item.page != NavPage::Console && + item.page != NavPage::Peers && + item.page != NavPage::Settings; + if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) { current = item.page; changed = true; } @@ -620,7 +624,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei float iconS = iconHalfSize; // icon half-size in pixels float iconCY = cursor.y + btnH * 0.5f; float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f; - ImU32 textCol = selected ? Primary() : OnSurfaceMedium(); + ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium()); if (showLabels) { // Measure total width of icon + gap + label, then center. diff --git a/src/ui/windows/backup_wallet_dialog.cpp b/src/ui/windows/backup_wallet_dialog.cpp index f6de20c..47d6a51 100644 --- a/src/ui/windows/backup_wallet_dialog.cpp +++ b/src/ui/windows/backup_wallet_dialog.cpp @@ -144,7 +144,7 @@ void BackupWalletDialog::render(App* app) s_status = statusMsg; s_backing_up = false; if (success) { - Notifications::instance().success(TR("backup_created")); + Notifications::instance().success(TR("backup_created"), 5.0f); } else { Notifications::instance().warning(statusMsg); } diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index f2e7a49..e900f85 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -326,7 +326,7 @@ static void RenderBalanceClassic(App* app) // Lerp displayed balances toward actual values { float dt = ImGui::GetIO().DeltaTime; - float speed = kBalanceLerpSpeed; + float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed; auto lerp = [](double& disp, double target, float dt, float spd) { double diff = target - disp; if (std::abs(diff) < 1e-9) { disp = target; return; } @@ -1278,7 +1278,7 @@ static void UpdateBalanceLerp(App* app) { const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f); const auto& state = app->state(); float dt = ImGui::GetIO().DeltaTime; - float speed = kBalanceLerpSpeed; + float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed; auto lerp = [](double& disp, double target, float dt, float spd) { double diff = target - disp; if (std::abs(diff) < 1e-9) { disp = target; return; } diff --git a/src/ui/windows/export_all_keys_dialog.cpp b/src/ui/windows/export_all_keys_dialog.cpp index 02b27dd..aefc5c7 100644 --- a/src/ui/windows/export_all_keys_dialog.cpp +++ b/src/ui/windows/export_all_keys_dialog.cpp @@ -213,7 +213,7 @@ void ExportAllKeysDialog::render(App* app) s_exporting = false; if (writeOk) { s_status = "Exported to: " + filepath; - Notifications::instance().success(TR("export_keys_success")); + Notifications::instance().success(TR("export_keys_success"), 5.0f); } else { s_status = "Failed to write file"; Notifications::instance().error("Failed to save key file"); diff --git a/src/ui/windows/export_transactions_dialog.cpp b/src/ui/windows/export_transactions_dialog.cpp index 8905a36..e1b8b1e 100644 --- a/src/ui/windows/export_transactions_dialog.cpp +++ b/src/ui/windows/export_transactions_dialog.cpp @@ -142,7 +142,7 @@ void ExportTransactionsDialog::render(App* app) s_status = "Exported " + std::to_string(state.transactions.size()) + " transactions to: " + filepath; - Notifications::instance().success(TR("export_tx_success")); + Notifications::instance().success(TR("export_tx_success"), 5.0f); } } } diff --git a/src/ui/windows/import_key_dialog.cpp b/src/ui/windows/import_key_dialog.cpp index 8074d04..7b37701 100644 --- a/src/ui/windows/import_key_dialog.cpp +++ b/src/ui/windows/import_key_dialog.cpp @@ -363,7 +363,7 @@ void ImportKeyDialog::render(App* app) imported, failed); s_status = buf; if (imported > 0) { - Notifications::instance().success(TR("import_key_success")); + Notifications::instance().success(TR("import_key_success"), 5.0f); } }; }); diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 0f7f495..1f3282e 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -237,6 +237,26 @@ void RenderMarketTab(App* app) ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str()); } + // ---- STALENESS INDICATOR ---- + { + auto fetchTime = market.last_fetch_time; + if (fetchTime.time_since_epoch().count() > 0) { + auto elapsed = std::chrono::steady_clock::now() - fetchTime; + int ageSecs = (int)std::chrono::duration_cast(elapsed).count(); + bool stale = ageSecs > 300; // 5 minutes + if (ageSecs < 60) + snprintf(buf, sizeof(buf), "%s %ds %s", stale ? ICON_MD_WARNING : "", ageSecs, TR("ago")); + else + snprintf(buf, sizeof(buf), "%s %dm %s", stale ? ICON_MD_WARNING : "", ageSecs / 60, TR("ago")); + ImFont* staleFont = capFont; + ImU32 staleCol = stale ? Warning() : WithAlpha(OnSurface(), 100); + float staleY = statsY + ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm(); + ImVec2 staleSz = staleFont->CalcTextSizeA(staleFont->LegacySize, FLT_MAX, 0, buf); + dl->AddText(staleFont, staleFont->LegacySize, + ImVec2(cardMin.x + Layout::spacingLg(), staleY), staleCol, buf); + } + } + // ---- TRADE BUTTON (top-right of card) ---- if (!currentExchange.pairs.empty()) { const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str(); diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index b149d68..f23a41a 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -1297,10 +1297,28 @@ static void RenderMiningTabContent(App* app) // Status text above bar int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size() ? s_benchmark.candidates[s_benchmark.current_index] : 0; - snprintf(buf, sizeof(buf), "%s %d/%d (%dt)", - TR("mining_benchmark_testing"), - s_benchmark.current_index + 1, - (int)s_benchmark.candidates.size(), ct); + // Estimated remaining time + int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index; + float elapsed_in_phase = s_benchmark.phase_timer; + float phase_total = (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp) + ? ThreadBenchmark::WARMUP_SECS + : ThreadBenchmark::MEASURE_SECS; + float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase); + // Remaining tests after current each need warmup + measure + float est_secs = remaining_in_current + + (remaining_tests - 1) * (ThreadBenchmark::WARMUP_SECS + ThreadBenchmark::MEASURE_SECS); + int est_min = (int)(est_secs / 60.0f); + int est_sec = (int)est_secs % 60; + if (est_min > 0) + snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds", + TR("mining_benchmark_testing"), + s_benchmark.current_index + 1, + (int)s_benchmark.candidates.size(), ct, est_min, est_sec); + else + snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds", + TR("mining_benchmark_testing"), + s_benchmark.current_index + 1, + (int)s_benchmark.candidates.size(), ct, est_sec); ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp), diff --git a/src/ui/windows/receive_tab.cpp b/src/ui/windows/receive_tab.cpp index 2aa3106..72c3b46 100644 --- a/src/ui/windows/receive_tab.cpp +++ b/src/ui/windows/receive_tab.cpp @@ -11,7 +11,9 @@ #include "receive_tab.h" #include "send_tab.h" #include "../../app.h" +#include "../../config/settings.h" #include "../../util/i18n.h" +#include "../../util/platform.h" #include "../../config/version.h" #include "../../data/wallet_state.h" #include "../../ui/widgets/qr_code.h" @@ -45,7 +47,7 @@ static std::string TrId(const char* key, const char* id) { // ============================================================================ static int s_selected_address_idx = -1; static double s_request_amount = 0.0; -static char s_request_memo[256] = ""; +static char s_request_memo[512] = ""; static double s_request_usd_amount = 0.0; static bool s_request_usd_mode = false; static std::string s_cached_qr_data; @@ -76,16 +78,9 @@ static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen); } -static void OpenExplorerURL(const std::string& address) { - std::string url = "https://explorer.dragonx.com/address/" + address; -#ifdef _WIN32 - std::string cmd = "start \"\" \"" + url + "\""; -#elif __APPLE__ - std::string cmd = "open \"" + url + "\""; -#else - std::string cmd = "xdg-open \"" + url + "\""; -#endif - system(cmd.c_str()); +static void OpenExplorerURL(App* app, const std::string& address) { + std::string url = app->settings()->getAddressExplorerUrl() + address; + dragonx::util::Platform::openUrl(url); } // ============================================================================ @@ -978,7 +973,7 @@ void RenderReceiveTab(App* app) ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight())); if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) { - OpenExplorerURL(selected.address); + OpenExplorerURL(app, selected.address); } ImGui::PopStyleVar(); // FrameBorderSize ImGui::PopStyleColor(4); diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 498acfb..2ca0b23 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -804,7 +804,7 @@ void RenderSendConfirmPopup(App* app) { s_tx_status = TR("send_tx_sent"); s_result_txid = result; s_status_success = true; - Notifications::instance().success(TR("send_tx_success")); + Notifications::instance().success(TR("send_tx_success"), 5.0f); s_to_address[0] = '\0'; s_amount = 0.0; s_memo[0] = '\0'; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 8203499..05ff80f 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -256,6 +256,15 @@ void I18n::loadBuiltinEnglish() strings_["settings_quick_unlock_pin"] = "Quick-unlock PIN"; strings_["settings_pin_active"] = "PIN"; strings_["settings_encrypt_first_pin"] = "Encrypt wallet first to enable PIN"; + strings_["incorrect_pin"] = "Incorrect PIN"; + strings_["incorrect_passphrase"] = "Incorrect passphrase"; + strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock."; + strings_["restarting_after_encryption"] = "Restarting daemon after encryption..."; + strings_["force_quit"] = "Force Quit"; + strings_["reduce_motion"] = "Reduce Motion"; + strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility"; + strings_["ago"] = "ago"; + strings_["wizard_daemon_start_failed"] = "Failed to start daemon \xe2\x80\x94 it will be retried automatically"; strings_["settings_data_dir"] = "Data Dir:"; strings_["settings_wallet_size_label"] = "Wallet Size:"; strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";