diff --git a/CMakeLists.txt b/CMakeLists.txt index e63529f..232f9cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -553,7 +553,6 @@ set(APP_SOURCES src/ui/windows/console_selection_controller.cpp src/ui/windows/console_tab_helpers.cpp src/ui/windows/console_text_layout.cpp - src/ui/windows/settings_window.cpp src/ui/pages/settings_page.cpp src/ui/windows/about_dialog.cpp src/ui/windows/faq_dialog.cpp @@ -704,7 +703,6 @@ set(APP_HEADERS src/ui/windows/console_selection_controller.h src/ui/windows/console_tab.h 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 diff --git a/src/app.cpp b/src/app.cpp index e8ff838..104fae5 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -36,7 +36,6 @@ #include "ui/windows/explorer_tab.h" #include "ui/windows/seed_display.h" #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" @@ -1963,6 +1962,12 @@ void App::render() } settings_->setWizardCompleted(true); settings_->save(); + // Soften the abrupt handoff: tell the user the node is starting + will keep syncing in the + // background, instead of dropping them onto a busy/blank main window with no explanation. + if (!state_.connected) + ui::Notifications::instance().info( + settings_->getEncryptionPending() ? TR("wiz_handoff_encrypting") : TR("wiz_handoff_starting"), + 10.0f); } // Process deferred encryption from wizard (runs in background) @@ -2528,9 +2533,6 @@ void App::render() // content area, status bar, and every other widget in this window. // ==================================================================== PERF_BEGIN(_perfDialogs); - if (show_settings_) { - ui::RenderSettingsWindow(this, &show_settings_); - } if (show_about_) { ui::RenderAboutDialog(this, &show_about_); @@ -8182,6 +8184,7 @@ void App::restartDaemon() isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load()); if (!decision.allowed) return; if (refuseIfNodeBusy()) return; + daemon_restart_pending_.store(false); // a restart is actually happening — clear the reminder daemon_restarting_ = true; daemon_controller_->prepareLifecycleOperation(decision, settings_.get()); diff --git a/src/app.h b/src/app.h index 7804d24..8f31dc2 100644 --- a/src/app.h +++ b/src/app.h @@ -769,6 +769,9 @@ public: * Shows "Restarting daemon..." in the loading overlay while the daemon cycles. */ void restartDaemon(); + // A newer daemon binary is installed but the running node is still the old one until a restart. + void markDaemonRestartPending() { daemon_restart_pending_.store(true); } + bool daemonRestartPending() const { return daemon_restart_pending_.load(); } // Switch the active wallet: persist the new -wallet=, stop the node, restart on it // (rescan only if it was never synced in this datadir). Per-wallet data follows automatically // via the identity-scoped caches (P1). No-op if that wallet is already active. @@ -1311,6 +1314,7 @@ private: // Daemon restart (e.g. after changing debug log categories) std::atomic daemon_restarting_{false}; + std::atomic daemon_restart_pending_{false}; // updated binary installed, awaiting a restart // Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new // wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to @@ -1401,7 +1405,6 @@ private: // UI State bool quit_requested_ = false; bool show_demo_window_ = false; - bool show_settings_ = false; bool show_about_ = false; bool show_faq_ = false; bool show_import_key_ = false; diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index 774e4c6..5c04dd8 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -570,8 +570,6 @@ void App::buildSweepCatalog() [](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); }); add("modal-about", ui::NavPage::Overview, [](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; }); - add("modal-settings", ui::NavPage::Settings, - [](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; }); // Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC. add("modal-seed-backup", ui::NavPage::Overview, diff --git a/src/app_wizard.cpp b/src/app_wizard.cpp index 6135c91..3a8cfd0 100644 --- a/src/app_wizard.cpp +++ b/src/app_wizard.cpp @@ -1456,7 +1456,8 @@ void App::renderFirstRunWizard() { wizard_phase_ = WizardPhase::Done; settings_->setWizardCompleted(true); settings_->save(); - ui::Notifications::instance().info(TR("wiz_encrypt_bg")); + // The handoff toast is now emitted once at the completion point in app.cpp + // (wiz_handoff_encrypting), so don't also toast here. } ImGui::EndDisabled(); ImGui::PopStyleVar(); diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 3d61dde..181d77f 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -904,7 +904,7 @@ void RenderSettingsPage(App* app) { float cellGap = Layout::spacingLg(); bool twoCol = contentW >= 460.0f * dp; // drop to a single column when too narrow (high font scale) int cols = twoCol ? 2 : 1; - float colW = std::max(160.0f, twoCol ? (contentW - cellGap) * 0.5f : contentW); + float colW = std::max(160.0f * dp, twoCol ? (contentW - cellGap) * 0.5f : contentW); float baseX = ImGui::GetCursorScreenPos().x; const char* cellLabels[4] = { TR("theme"), TR("balance_layout"), TR("language"), TR("clock_format") }; @@ -1346,7 +1346,7 @@ void RenderSettingsPage(App* app) { ImGui::SetCursorScreenPos(ImVec2(cx, last + gp)); 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")); + ImGui::TextColored(material::SuccessVec4(), "%s", TR("autoshield_by_node")); if (!app->daemonAutoShieldAddress().empty()) ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str()); } else { @@ -2595,6 +2595,21 @@ void RenderSettingsPage(App* app) { if (ui::DaemonUpdateDialog::consumeInstalled()) s_settingsState.daemon_info_loaded = false; + // Persistent reminder: a newer daemon binary is installed but the running node is still + // the old one until a restart (easy to miss on the updater's Done screen). + if (app->daemonRestartPending()) { + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + contentW); + Type().textColored(TypeStyle::Caption, material::Warning(), TR("daemon_restart_pending")); + ImGui::PopTextWrapPos(); + if (app->isUsingEmbeddedDaemon()) { + if (material::ActionButton("##drestartpend", TR("daemon_update_restart_now"), + ICON_MD_RESTART_ALT, material::ActionTier::Primary)) + app->restartDaemon(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_daemon_restart_pending")); + } + } + // --- UPDATES --- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_updates_label")); @@ -3001,14 +3016,23 @@ void RenderSettingsPage(App* app) { s_settingsState.debug_gate_verifying = false; } } - if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand")); + if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") + : (s_settingsState.debug_gate_passed ? TR("tt_debug_expand") : TR("tt_debug_expand_gated"))); ImGui::PopStyleColor(3); // Draw overline label + arrow on top of the invisible button { ImFont* ovFont = Type().overline(); float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f; - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(headerPos.x, textY), OnSurfaceMedium(), TR("debug_logging")); + float labelX = headerPos.x; + // Show a lock beside the label while the session gate hasn't been passed, so it's clear the + // section is gated rather than just a bare collapsible header. + if (!s_settingsState.debug_gate_passed) { + ImFont* lockFont = Type().iconSmall(); if (!lockFont) lockFont = ovFont; + dl->AddText(lockFont, lockFont->LegacySize, ImVec2(labelX, textY), OnSurfaceMedium(), ICON_MD_LOCK); + labelX += lockFont->CalcTextSizeA(lockFont->LegacySize, FLT_MAX, 0, ICON_MD_LOCK).x + Layout::spacingXs(); + } + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, textY), OnSurfaceMedium(), TR("debug_logging")); // Use the icon font for the expand/collapse arrow ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = ovFont; @@ -3016,6 +3040,14 @@ void RenderSettingsPage(App* app) { dl->AddText(iconFont, iconFont->LegacySize, ImVec2(headerPos.x + availWidth - arrowW - pad, textY), OnSurfaceMedium(), arrow); } + // One-line explanation under the collapsed header (what it is + that it's gated). + if (!s_settingsState.debug_expanded) { + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), + s_settingsState.debug_gate_passed ? TR("debug_options_caption_unlocked") + : TR("debug_options_caption_gated")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + } + if (s_settingsState.debug_expanded) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); @@ -3059,9 +3091,33 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); #endif - ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select")); - ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note")); + ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium()); + ImGui::TextUnformatted(TR("settings_debug_select")); + ImGui::TextWrapped("%s", TR("settings_debug_restart_note")); // was near-invisible 35% white + ImGui::PopStyleColor(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Restart prompt moved directly under the note so the required action is prominent (appears the + // frame after a category toggles, which sets debug_cats_dirty while rendering the grid below). + if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) { + ImGui::PushStyleColor(ImGuiCol_Text, material::Warning()); + ImFont* iconFont = Type().iconSmall(); + if (iconFont) { + ImGui::PushFont(iconFont); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(ICON_MD_INFO); + ImGui::PopFont(); + ImGui::SameLine(0, Layout::spacingXs()); + } + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(TR("settings_debug_changed")); + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) { + s_settingsState.confirm_restart_daemon = true; + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon")); + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + } // The 22 dragonxd debug categories static const char* debugCats[] = { @@ -3119,27 +3175,7 @@ void RenderSettingsPage(App* app) { } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - - // "Restart daemon" button — only active when categories changed - if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255)); - ImFont* iconFont = Type().iconSmall(); - if (iconFont) { - ImGui::PushFont(iconFont); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(ICON_MD_INFO); - ImGui::PopFont(); - ImGui::SameLine(0, Layout::spacingXs()); - } - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(TR("settings_debug_changed")); - ImGui::PopStyleColor(); - ImGui::SameLine(); - if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) { - s_settingsState.confirm_restart_daemon = true; - } - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon")); - } + // (The "restart daemon" prompt was moved up to directly under the restart note above.) } // ---- Debug-options gate: confirmation + warning (+ re-auth if a PIN/passphrase is set) ---- diff --git a/src/ui/windows/daemon_download_dialog.h b/src/ui/windows/daemon_download_dialog.h index 2525be4..144ce76 100644 --- a/src/ui/windows/daemon_download_dialog.h +++ b/src/ui/windows/daemon_download_dialog.h @@ -445,11 +445,20 @@ private: static void renderDone(float, const Progress& p) { using namespace material; - if (!s_notified) { s_installed_flag = true; s_notified = true; } + if (!s_notified) { + s_installed_flag = true; + s_notified = true; + // Record a persistent "restart pending" reminder so it isn't lost if the user clicks Later. + if (s_app) s_app->markDaemonRestartPending(); + } Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok")); ImGui::Spacing(); ImGui::Text("%s %s", TR("daemon_update_version"), p.latest_tag.c_str()); ImGui::Spacing(); + // Lead with a warning-level headline that a restart is required, then the detail note. + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); + Type().textColored(TypeStyle::Subtitle2, Warning(), TR("daemon_update_restart_needed")); + ImGui::PopTextWrapPos(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_restart_note")); ImGui::Spacing(); if (s_app->isUsingEmbeddedDaemon()) { @@ -459,6 +468,7 @@ private: } ImGui::Spacing(); if (TactileButton(TR("daemon_update_later"), ImVec2(fullW(), 0))) s_open = false; + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_later_hint")); } else { if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false; } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 24619e7..e905388 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -315,6 +315,10 @@ void I18n::loadBuiltinEnglish() strings_["contacts_message_needs_z"] = "Chat needs a shielded (z) address — this contact only has a transparent one"; strings_["chat_copy_address_tip"] = "Click to copy address"; strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify"; + strings_["chat_verify_tip"] = "End-to-end encrypted to this contact's identity key. Click to view the key fingerprint and compare it with your contact over a separate channel (call or in person) to be sure no one is impersonating them."; + strings_["chat_verify_title"] = "Verify contact key"; + strings_["chat_verify_body"] = "Messages in this chat are encrypted to the identity key below, which was learned from this contact's first message. Ask your contact to read you their key fingerprint over a separate channel (a call or in person). If it matches exactly, you are talking to the right person; if not, do not trust this conversation."; + strings_["chat_verify_copy"] = "Copy key"; strings_["chat_awaiting_key"] = "Waiting for reply"; strings_["chat_rename"] = "Rename contact"; strings_["chat_rename_hint"] = "Contact name"; @@ -773,6 +777,8 @@ void I18n::loadBuiltinEnglish() strings_["wiz_pass_spaces"] = "Passphrase has leading/trailing spaces \xe2\x80\x94 remove them"; strings_["wiz_encrypt_continue"] = "Encrypt & Continue"; strings_["wiz_encrypt_bg"] = "Encryption will complete in the background"; + strings_["wiz_handoff_starting"] = "Setting up your wallet. The node is starting and will keep syncing in the background. You can start exploring now, but balances may take a while to appear."; + strings_["wiz_handoff_encrypting"] = "Setting up your wallet. The node is starting, and encryption will finish in the background once it connects. Balances may take a while to appear."; strings_["wiz_skip_confirm"] = "Continue WITHOUT encryption? Keys will be stored unencrypted \xe2\x80\x94 click Skip again to confirm."; strings_["wiz_pin_optional"] = "Quick-Unlock PIN (optional)"; strings_["wiz_pin_label"] = "PIN (4-8 digits):"; @@ -1058,6 +1064,9 @@ void I18n::loadBuiltinEnglish() strings_["tt_reset_settings"] = "Revert this page to the last saved settings (undo unsaved changes)"; strings_["tt_debug_collapse"] = "Collapse debug logging options"; strings_["tt_debug_expand"] = "Expand debug logging options"; + strings_["debug_options_caption_gated"] = "Diagnostics and node tuning for troubleshooting. Locked: opening requires confirmation (and your PIN or passphrase if set)."; + strings_["debug_options_caption_unlocked"] = "Diagnostics and node tuning for troubleshooting. Unlocked for this session."; + strings_["tt_debug_expand_gated"] = "Locked: confirm (and enter your PIN or passphrase if set) to show debug options"; strings_["tt_restart_daemon"] = "Restart the daemon to apply debug logging changes"; // Settings: additional tooltips (keys/data row) @@ -1991,6 +2000,9 @@ void I18n::loadBuiltinEnglish() strings_["toast_stop_mining_failed"] = "Couldn't stop mining: "; strings_["toast_pool_start_failed"] = "Couldn't start the pool miner: "; strings_["toast_ban_failed"] = "Couldn't ban the peer: "; + strings_["toast_peer_banned"] = "Peer banned."; + strings_["toast_peer_unbanned"] = "Peer unbanned."; + strings_["toast_bans_cleared"] = "All bans cleared."; strings_["toast_unban_failed"] = "Couldn't unban the peer: "; strings_["toast_clear_bans_failed"] = "Couldn't clear bans: "; strings_["toast_key_export_failed"] = "Key export failed: "; @@ -2482,6 +2494,10 @@ void I18n::loadBuiltinEnglish() strings_["daemon_update_installed_ok"] = "Node installed"; strings_["daemon_update_restart_note"] = "Restart the daemon to start running the new version."; strings_["daemon_update_restart_now"] = "Restart daemon now"; + strings_["daemon_update_restart_needed"] = "Restart required to use the new version"; + strings_["daemon_update_later_hint"] = "A reminder stays in Settings until you restart the daemon."; + strings_["daemon_restart_pending"] = "A new daemon version is installed but not running yet. Restart the daemon to use it."; + strings_["tt_daemon_restart_pending"] = "Restarts the embedded daemon so the newly installed version takes effect."; strings_["daemon_update_later"] = "Later"; strings_["daemon_update_failed"] = "Update failed"; strings_["daemon_update_unknown_error"] = "Unknown error."; @@ -2538,6 +2554,8 @@ void I18n::loadBuiltinEnglish() strings_["peers_blocks"] = "Blocks"; strings_["peers_blocks_left"] = "%d blocks left"; strings_["peers_clear_all_bans"] = "Clear All Bans"; + strings_["peers_clear_bans_confirm"] = "Clear all %d bans?"; + strings_["peers_clear_bans_confirm_yes"] = "Clear all"; strings_["peers_click_copy"] = "Click to copy"; strings_["peers_connected"] = "Connected"; strings_["peers_connected_count"] = "Connected: %d";