// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "settings_page.h" #include "../../app.h" #include "../../config/version.h" #include "../../config/settings.h" #include "../../wallet/lite_wallet_lifecycle_ui_adapter.h" #include "../../wallet/lite_wallet_server_selection_adapter.h" #include "../../wallet/lite_wallet_controller.h" #include #include "../../util/logger.h" #include "../windows/balance_tab.h" #include "../windows/chat_tab.h" // RenderChatSettingsControls (shared Chat & Contacts controls) #include "../windows/console_tab.h" #include "../../util/i18n.h" #include "../../util/platform.h" #include "../../util/seed_phrase.h" #include "../../resources/embedded_resources.h" #include #include "../../rpc/rpc_client.h" #include "../../rpc/connection.h" #include "../../rpc/rpc_worker.h" #include "../theme.h" #include "../layout.h" #include "../schema/ui_schema.h" #include "../schema/skin_manager.h" #include "../notifications.h" #include "../effects/imgui_acrylic.h" #include "../effects/theme_effects.h" #include "../effects/low_spec.h" #include "../effects/scroll_fade_shader.h" #include "../material/draw_helpers.h" #include "../material/settings_controls.h" #include "../material/type.h" #include "../material/colors.h" #include "../windows/validate_address_dialog.h" #include "../windows/shield_dialog.h" #include "../windows/request_payment_dialog.h" #include "../windows/block_info_dialog.h" #include "../windows/export_all_keys_dialog.h" #include "../windows/export_transactions_dialog.h" #include "../windows/bootstrap_download_dialog.h" #include "../windows/daemon_download_dialog.h" #include "../windows/wallets_dialog.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; // Top of the Acrylic slider's range; the slider reads 0–100% of it (shared with App::applyVisualSettings). static constexpr float kAcrylicMaxBlur = effects::kAcrylicMaxBlur; // Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID static std::string TrId(const char* tr_key, const char* id) { std::string s = TR(tr_key); s += "##"; s += id; return s; } // ============================================================================ // Settings state loaded from config::Settings on first render // ============================================================================ struct LowSpecSnapshot { bool valid = false; bool acrylic_enabled; float blur_amount; float ui_opacity; float window_opacity; bool theme_effects_enabled; bool scanline_enabled; }; struct SettingsPageState { bool initialized = false; int language_index = 0; bool save_ztxs = true; bool allow_custom_fees = false; bool auto_shield = false; bool fetch_prices = true; bool use_tor = false; char rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST; char rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT; char rpc_user[64] = ""; char rpc_password[64] = ""; bool rpc_plaintext_remote = false; char tx_explorer[256] = "https://explorer.dragonx.is/tx/"; char addr_explorer[256] = "https://explorer.dragonx.is/address/"; bool acrylic_enabled = true; float blur_amount = 1.5f; float noise_opacity = 0.5f; float ui_opacity = 1.0f; float window_opacity = 1.0f; float favorite_opacity = 1.0f; int earnings_format = 1; std::string balance_layout = "classic"; bool scanline_enabled = true; bool theme_effects_enabled = true; bool gradient_background = false; bool low_spec_mode = false; bool reduce_motion = false; float font_scale = 1.0f; LowSpecSnapshot low_spec_snapshot; bool keep_daemon_running = false; bool stop_external_daemon = false; bool stratum_host = false; // O2: host a RandomX stratum pool from the node (v1.3.0+) char stratum_allowip[64] = ""; // -stratumallowip subnet (blank = loopback only) bool lite_lifecycle_expanded = false; int lite_lifecycle_operation = 0; char lite_wallet_path[256] = ""; char lite_lifecycle_passphrase[128] = ""; // Confirmation of a NEW Create/Restore passphrase (SECRET: wiped with the passphrase). Open never uses it. char lite_lifecycle_passphrase_confirm[128] = ""; char lite_restore_seed[512] = ""; int lite_restore_birthday = 0; int lite_restore_account = 0; bool lite_restore_overwrite = false; std::string lite_lifecycle_status; std::string lite_lifecycle_summary; // True while an async create/open/restore is in flight (Settings drives the controller's // async lifecycle path so a flaky server never freezes the UI; the result is polled each frame). bool lite_lifecycle_pending = false; // Backup & keys (only populated for an open lite wallet). lite_export_secret holds the // revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export / // import. lite_import_key is the import input buffer (wiped right after submission). std::string lite_export_secret; std::string lite_export_label; bool lite_export_is_seed = false; // revealed secret is a seed (show birthday + save-to-file) unsigned long long lite_export_birthday = 0; // seed birthday (shown with the seed) char lite_import_key[512] = ""; std::string lite_backup_status; // Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is // reused for Encrypt (unencrypted wallet) and Unlock (locked wallet); lite_dec_pass for Decrypt. // lite_enc_confirm confirms a NEW passphrase for Encrypt only; Unlock never uses it. char lite_enc_pass[128] = ""; char lite_enc_confirm[128] = ""; char lite_dec_pass[128] = ""; std::string lite_encryption_status; bool mine_when_idle = false; int mine_idle_delay = 120; bool idle_thread_scaling = false; int idle_threads_active = 0; int idle_threads_idle = 0; bool verbose_logging = false; std::set debug_categories; bool debug_cats_dirty = false; bool debug_expanded = false; // Debug-options gate: a confirmation + warning (and, when a PIN/passphrase is set, re-auth) is // required before the debug dropdown reveals its options — once per session (passing it once // unlocks the dropdown until the app restarts). bool debug_gate_open = false; bool debug_gate_passed = false; bool debug_gate_verifying = false; char debug_gate_buf[128] = {0}; std::string debug_gate_err; float debug_gate_err_timer = 0.0f; bool effects_expanded = false; bool tools_expanded = false; bool rpc_expanded = false; // Node & Security: reveal the RPC connection fields int current_tab = 0; // active settings category tab (see SettingsTab enum) bool confirm_clear_ztx = false; bool confirm_delete_blockchain = false; bool confirm_rescan = false; // Rescan dialog: probe the node's available block range so a bootstrapped/pruned node gets a // runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis. bool rescan_height_detecting = false; bool rescan_height_detected = false; bool rescan_full_history = true; // genesis present → traditional -rescan restart int rescan_start_height = 0; // editable pre-fill for the runtime rescan bool rescan_probe_failed = false; // the probe could not tell (node busy): warn before the -rescan restart int rescan_probe_seq = 0; // the probe is async: a late result for an earlier opening is dropped bool confirm_repair_wallet = false; bool confirm_reinstall_daemon = false; // Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh, // since reading the installed binary to scan its version is a one-off disk read). bool daemon_info_loaded = false; dragonx::resources::DaemonBinaryInfo installed_daemon; std::future daemon_version_scan; // streamed version scan, off the UI thread (C123) dragonx::resources::BundledDaemonInfo bundled_daemon; bool confirm_restart_daemon = false; bool confirm_lite_redownload = false; effects::ScrollFadeShader fade_shader; }; static SettingsPageState s_settingsState; // Low-spec mode enter: snapshot the current effects prefs, then force the // low-cost overrides. When applyEffects is true, also push the overrides to // the live effects subsystems (the runtime-sync path passes false since it // only mirrors an already-applied hotkey toggle into the checkbox state). static void enterLowSpec(bool applyEffects) { s_settingsState.low_spec_snapshot.valid = true; s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled; s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount; s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity; s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity; s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled; s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled; s_settingsState.acrylic_enabled = false; s_settingsState.blur_amount = 0.0f; s_settingsState.ui_opacity = 1.0f; s_settingsState.window_opacity = 1.0f; s_settingsState.theme_effects_enabled = false; s_settingsState.scanline_enabled = false; if (applyEffects) { effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); effects::ImGuiAcrylic::SetUIOpacity(1.0f); effects::ThemeEffects::instance().setEnabled(false); effects::ThemeEffects::instance().setReducedTransparency(true); ConsoleTab::s_scanline_enabled = false; } } // Low-spec mode exit: restore the previously snapshotted effects prefs and // clear the snapshot. When applyEffects is true, also push them to the live // effects subsystems. Callers guard this on low_spec_snapshot.valid. static void exitLowSpec(bool applyEffects) { s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount; s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity; s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity; s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled; s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled; if (applyEffects) { effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount); effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled); effects::ThemeEffects::instance().setReducedTransparency(!s_settingsState.theme_effects_enabled); ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled; } s_settingsState.low_spec_snapshot.valid = false; } // Count words in a (seed) buffer — used to validate/guide restore input. Normalizes exotic Unicode // whitespace (NBSP etc.) first so the count matches the phrase actually submitted (shared with the // first-run restore gate via util::seed_phrase). static int liteSeedWordCount(const char* s) { return dragonx::util::seedPhraseWordCount( dragonx::util::normalizeSeedPhrase(s ? std::string(s) : std::string())); } // Why a NEW passphrase is not accepted (empty = nothing to report), with the full-node wizard's // wording. While typing, an empty passphrase or confirmation is not reported yet; on submit // (`submitted`), a missing confirmation counts as a mismatch. static std::string liteNewPassphraseIssue(const char* pass, const char* confirm, bool submitted) { const size_t len = std::strlen(pass); if (len < wallet::kLiteMinNewPassphraseLength && (len > 0 || submitted)) { char buf[256]; snprintf(buf, sizeof(buf), TR("wiz_pass_too_short"), len); return buf; } if (len >= wallet::kLiteMinNewPassphraseLength && (confirm[0] != '\0' || submitted) && std::strcmp(pass, confirm) != 0) return TR("wiz_pass_mismatch"); return std::string(); } static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() { switch (s_settingsState.lite_lifecycle_operation) { case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting; case 2: return wallet::LiteWalletLifecycleOperation::RestoreFromSeed; default: return wallet::LiteWalletLifecycleOperation::CreateNew; } } static void evaluateLiteLifecycleRequestFromPageState(App* app) { if (!app || !app->settings()) return; wallet::LiteWalletLifecycleUiExecutionInput input; input.capabilities = app->walletCapabilities(); input.settingsLoaded = true; input.requirePersistedServerSelectionIntent = true; input.ui.selectedServerDisplayReady = true; input.ui.lifecycleUiOwnerReady = true; input.ui.operationConfirmed = true; input.ui.privateDataRedactionReady = true; input.ui.syncPlannerFeedReady = true; input.request.requestProvided = true; input.request.operation = liteLifecycleOperationFromPageState(); switch (input.request.operation) { case wallet::LiteWalletLifecycleOperation::CreateNew: input.request.createRequest.passphrase = s_settingsState.lite_lifecycle_passphrase; break; case wallet::LiteWalletLifecycleOperation::OpenExisting: input.request.openRequest.walletPath = s_settingsState.lite_wallet_path; input.request.openRequest.passphrase = s_settingsState.lite_lifecycle_passphrase; break; case wallet::LiteWalletLifecycleOperation::RestoreFromSeed: input.request.restoreRequest.walletPath = s_settingsState.lite_wallet_path; // Normalize (fold NBSP/exotic whitespace to plain spaces) so an NBSP-pasted phrase the // gate counted as 24 words also restores correctly at the backend. input.request.restoreRequest.seedPhrase = dragonx::util::normalizeSeedPhrase(s_settingsState.lite_restore_seed); input.request.restoreRequest.passphrase = s_settingsState.lite_lifecycle_passphrase; input.request.restoreRequest.birthday = static_cast(std::max(0, s_settingsState.lite_restore_birthday)); input.request.restoreRequest.account = static_cast(std::max(0, s_settingsState.lite_restore_account)); input.request.restoreRequest.overwrite = s_settingsState.lite_restore_overwrite; break; } // Wipe ALL secret material when leaving this function, on every path (real execution, // validation-only fallback, or early return): the UI char buffers AND the std::string // copies inside `input.request.*Request`. The controller wipes its own by-value request // copy, but these page-local copies are separate; leaving them would defeat the wipe. struct LiteSecretScrubber { wallet::LiteWalletLifecycleUiExecutionInput& in; ~LiteSecretScrubber() { sodium_memzero(s_settingsState.lite_lifecycle_passphrase, sizeof(s_settingsState.lite_lifecycle_passphrase)); sodium_memzero(s_settingsState.lite_lifecycle_passphrase_confirm, sizeof(s_settingsState.lite_lifecycle_passphrase_confirm)); sodium_memzero(s_settingsState.lite_restore_seed, sizeof(s_settingsState.lite_restore_seed)); wallet::secureWipeLiteSecret(in.request.createRequest.passphrase); wallet::secureWipeLiteSecret(in.request.openRequest.passphrase); wallet::secureWipeLiteSecret(in.request.restoreRequest.seedPhrase); wallet::secureWipeLiteSecret(in.request.restoreRequest.passphrase); } } liteSecretScrubber{input}; // Open/Restore both target a wallet path; reject an empty/whitespace-only one early so we // never dispatch an unusable request (the scrubber above still wipes secrets on this return). if (input.request.operation == wallet::LiteWalletLifecycleOperation::OpenExisting || input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) { std::string trimmedPath(s_settingsState.lite_wallet_path); const auto first = trimmedPath.find_first_not_of(" \t\r\n"); if (first == std::string::npos) { s_settingsState.lite_lifecycle_status = TR("lite_enter_wallet_path"); s_settingsState.lite_lifecycle_summary.clear(); return; } } // Restore needs a complete 24-word seed; reject early (the scrubber above still wipes the // entered secret on this return path). if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) { const int words = liteSeedWordCount(s_settingsState.lite_restore_seed); if (!dragonx::util::isCompleteRecoveryPhrase(words)) { char seedBuf[128]; snprintf(seedBuf, sizeof(seedBuf), TR("lite_enter_all_seed_words"), words); s_settingsState.lite_lifecycle_status = seedBuf; s_settingsState.lite_lifecycle_summary.clear(); return; } } // A NEW passphrase (Create/Restore with one entered) must be 8+ characters and confirmed, so a // typo can't lock the user out. Open is never checked: its passphrase unlocks an existing // wallet, which may predate the rule. (The scrubber above still wipes both fields here.) if (!wallet::liteLifecyclePassphraseAcceptable(input.request.operation, s_settingsState.lite_lifecycle_passphrase, s_settingsState.lite_lifecycle_passphrase_confirm)) { s_settingsState.lite_lifecycle_status = liteNewPassphraseIssue(s_settingsState.lite_lifecycle_passphrase, s_settingsState.lite_lifecycle_passphrase_confirm, /*submitted=*/true); s_settingsState.lite_lifecycle_summary.clear(); return; } // When a linked lite backend is present, execute the operation for real through the App-owned // controller — ASYNCHRONOUSLY, with server failover, so an unreachable/flaky server never // freezes the UI. The request (with its secrets) is moved into the controller; the page polls // the outcome each frame in renderLiteLifecyclePending() below. (Secret wiping of the page char // buffers + the moved-from request copies is handled by liteSecretScrubber at function exit.) if (auto* lite = app->liteWallet()) { bool started = false; switch (input.request.operation) { case wallet::LiteWalletLifecycleOperation::CreateNew: started = lite->beginCreateWalletAsync(std::move(input.request.createRequest)); break; case wallet::LiteWalletLifecycleOperation::OpenExisting: started = lite->beginOpenWalletAsync(std::move(input.request.openRequest)); break; case wallet::LiteWalletLifecycleOperation::RestoreFromSeed: started = lite->beginRestoreWalletAsync(std::move(input.request.restoreRequest)); break; } if (started) { s_settingsState.lite_lifecycle_pending = true; s_settingsState.lite_lifecycle_status = TR("lite_working"); s_settingsState.lite_lifecycle_summary.clear(); } else { // Rejected before any thread launched (wallet already open, an attempt in flight, or no // usable server). The controller's status carries the reason. s_settingsState.lite_lifecycle_status = lite->status().message.empty() ? TR("lite_could_not_start") : lite->status().message; Notifications::instance().warning(s_settingsState.lite_lifecycle_status); } return; } // No linked lite backend to execute against (full-node build, or the lite backend // failed to load / rollout-disabled). The live path above returns when a backend is // present, so reaching here means there is nothing to run. s_settingsState.lite_lifecycle_summary.clear(); s_settingsState.lite_lifecycle_status = TR("lite_backend_unavailable"); Notifications::instance().warning(s_settingsState.lite_lifecycle_status); } // (APPEARANCE card now uses ChannelsSplit like all other cards) static void loadSettingsPageState(App* app) { config::Settings* settings = app ? app->settings() : nullptr; if (!settings) return; s_settingsState.save_ztxs = settings->getSaveZtxs(); s_settingsState.allow_custom_fees = settings->getAllowCustomFees(); s_settingsState.auto_shield = settings->getAutoShield(); s_settingsState.fetch_prices = settings->getFetchPrices(); s_settingsState.use_tor = settings->getUseTor(); strncpy(s_settingsState.tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_settingsState.tx_explorer) - 1); strncpy(s_settingsState.addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_settingsState.addr_explorer) - 1); auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); std::string current_lang = settings->getLanguage(); if (current_lang.empty()) current_lang = "en"; s_settingsState.language_index = 0; int idx = 0; for (const auto& lang : languages) { if (lang.first == current_lang) { s_settingsState.language_index = idx; break; } idx++; } // Load blur amount directly from saved multiplier, clamped to the (now gentler) slider max so a // value saved under the old 0–4 range maps into 0–100% instead of pinning far past the top. s_settingsState.blur_amount = std::min(settings->getBlurMultiplier(), kAcrylicMaxBlur); s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); s_settingsState.ui_opacity = settings->getUIOpacity(); s_settingsState.window_opacity = settings->getWindowOpacity(); s_settingsState.favorite_opacity = settings->getFavoriteOpacity(); s_settingsState.earnings_format = settings->getEarningsFormat(); s_settingsState.noise_opacity = settings->getNoiseOpacity(); s_settingsState.gradient_background = settings->getGradientBackground(); s_settingsState.balance_layout = settings->getBalanceLayout(); s_settingsState.scanline_enabled = settings->getScanlineEnabled(); s_settingsState.theme_effects_enabled = settings->getThemeEffectsEnabled(); s_settingsState.low_spec_mode = settings->getLowSpecMode(); s_settingsState.reduce_motion = settings->getReduceMotion(); s_settingsState.font_scale = settings->getFontScale(); Layout::setUserFontScale(s_settingsState.font_scale); // sync with Layout on load s_settingsState.keep_daemon_running = settings->getKeepDaemonRunning(); s_settingsState.stop_external_daemon = settings->getStopExternalDaemon(); s_settingsState.stratum_host = settings->getStratumHost(); std::snprintf(s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip), "%s", settings->getStratumAllowIp().c_str()); // Lite-server selection is managed entirely by the Network tab (not the Settings page). s_settingsState.mine_when_idle = settings->getMineWhenIdle(); s_settingsState.mine_idle_delay = settings->getMineIdleDelay(); s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling(); s_settingsState.idle_threads_active = settings->getIdleThreadsActive(); s_settingsState.idle_threads_idle = settings->getIdleThreadsIdle(); s_settingsState.verbose_logging = settings->getVerboseLogging(); s_settingsState.debug_categories = settings->getDebugCategories(); s_settingsState.debug_cats_dirty = false; // Populate the RPC fields from the auto-detected daemon config so they show the REAL connection // (host/port/user/pass) instead of compile-time defaults. These are read-only in the UI: the RPC // credentials come from the daemon's DRAGONX.conf, so this is an accurate display, not an editor. const auto rpcCfg = rpc::Connection::autoDetectConfig(); snprintf(s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), "%s", rpcCfg.host.c_str()); snprintf(s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), "%s", rpcCfg.port.c_str()); snprintf(s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), "%s", rpcCfg.rpcuser.c_str()); snprintf(s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), "%s", rpcCfg.rpcpassword.c_str()); s_settingsState.rpc_plaintext_remote = rpc::Connection::usesPlaintextRemote(rpcCfg); // Apply the loaded appearance prefs through the same low-spec-aware path as startup (it used to push // the raw prefs, so opening Settings turned theme effects and blur back on under low-spec). Under // low-spec the controls show the overrides and the snapshot keeps the saved prefs, so unticking // low-spec restores them; enterLowSpec(false) only mirrors what applyVisualSettings applied. app->applyVisualSettings(); if (!s_settingsState.low_spec_mode) { s_settingsState.low_spec_snapshot.valid = false; } else { // Keep a snapshot already taken this session (a Reload after ticking low-spec here): it still // holds the prefs from before low-spec, e.g. the window opacity low-spec saved as 1.0. const LowSpecSnapshot kept = s_settingsState.low_spec_snapshot; enterLowSpec(false); if (kept.valid) s_settingsState.low_spec_snapshot = kept; } s_settingsState.initialized = true; } static void saveSettingsPageState(config::Settings* settings) { if (!settings) return; settings->setTheme(settings->getSkinId()); settings->setSaveZtxs(s_settingsState.save_ztxs); settings->setAllowCustomFees(s_settingsState.allow_custom_fees); settings->setAutoShield(s_settingsState.auto_shield); settings->setFetchPrices(s_settingsState.fetch_prices); settings->setUseTor(s_settingsState.use_tor); settings->setTxExplorerUrl(s_settingsState.tx_explorer); settings->setAddressExplorerUrl(s_settingsState.addr_explorer); auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); auto it = languages.begin(); std::advance(it, s_settingsState.language_index); if (it != languages.end()) { settings->setLanguage(it->first); } // Visual effects settings. Under low-spec the controls show the low-spec overrides; persist the // user's real prefs (held in the snapshot) instead, so low-spec stays a runtime-only override and // turning it off restores them, the same as the hotkey. const bool lowSpecOverrides = s_settingsState.low_spec_mode && s_settingsState.low_spec_snapshot.valid; const LowSpecSnapshot& realPrefs = s_settingsState.low_spec_snapshot; const float blur = lowSpecOverrides ? realPrefs.blur_amount : s_settingsState.blur_amount; settings->setAcrylicEnabled(lowSpecOverrides ? realPrefs.acrylic_enabled : s_settingsState.acrylic_enabled); settings->setAcrylicQuality(blur > 0.001f ? static_cast(effects::AcrylicQuality::Low) : static_cast(effects::AcrylicQuality::Off)); settings->setBlurMultiplier(blur); settings->setUIOpacity(lowSpecOverrides ? realPrefs.ui_opacity : s_settingsState.ui_opacity); settings->setWindowOpacity(s_settingsState.window_opacity); // 1.0 under low-spec, as the hotkey saves it settings->setFavoriteOpacity(s_settingsState.favorite_opacity); // not low-spec gated — cosmetic settings->setEarningsFormat(s_settingsState.earnings_format); settings->setNoiseOpacity(s_settingsState.noise_opacity); settings->setGradientBackground(s_settingsState.gradient_background); settings->setScanlineEnabled(lowSpecOverrides ? realPrefs.scanline_enabled : s_settingsState.scanline_enabled); settings->setThemeEffectsEnabled(lowSpecOverrides ? realPrefs.theme_effects_enabled : s_settingsState.theme_effects_enabled); settings->setLowSpecMode(s_settingsState.low_spec_mode); settings->setReduceMotion(s_settingsState.reduce_motion); settings->setFontScale(s_settingsState.font_scale); settings->setKeepDaemonRunning(s_settingsState.keep_daemon_running); settings->setStopExternalDaemon(s_settingsState.stop_external_daemon); settings->setStratumHost(s_settingsState.stratum_host); settings->setStratumAllowIp(s_settingsState.stratum_allowip); // Lite-server selection is owned by the Network tab; the Settings page no longer writes it. settings->setMineWhenIdle(s_settingsState.mine_when_idle); settings->setMineIdleDelay(s_settingsState.mine_idle_delay); settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling); settings->setIdleThreadsActive(s_settingsState.idle_threads_active); settings->setIdleThreadsIdle(s_settingsState.idle_threads_idle); settings->setVerboseLogging(s_settingsState.verbose_logging); settings->setDebugCategories(s_settingsState.debug_categories); settings->save(); } // Console output color toggles (accent bars + per-channel text color), shown on their own row under // the effects checkboxes. They mirror the console toolbar buttons and carry no GPU cost, so — unlike // scanline/theme-effects — they stay enabled in low-spec: the caller has an open // BeginDisabled(low_spec) around the effects row, so close it, draw these live-bound to the ConsoleTab // statics, then reopen it. Persisted immediately (the startup restore in App loads them on launch). static void renderConsoleColorToggles(App* app) { ImGui::EndDisabled(); bool accents = ConsoleTab::s_line_accents_enabled; if (ImGui::Checkbox(TrId("console_accents", "con_accents").c_str(), &accents)) { ConsoleTab::s_line_accents_enabled = accents; app->settings()->setConsoleLineAccents(accents); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_accents")); ImGui::SameLine(0, Layout::spacingLg()); bool textColor = ConsoleTab::s_line_text_color_enabled; if (ImGui::Checkbox(TrId("console_text_colors", "con_textcol").c_str(), &textColor)) { ConsoleTab::s_line_text_color_enabled = textColor; app->settings()->setConsoleTextColor(textColor); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_text_color")); ImGui::SameLine(0, Layout::spacingLg()); // Console behavior (not a GPU effect): focus the command input when the tab opens. Bound straight to // settings — the App reads it at the page transition; no ConsoleTab static needed. bool autoFocus = app->settings()->getConsoleAutoFocus(); if (ImGui::Checkbox(TrId("console_auto_focus", "con_autofocus").c_str(), &autoFocus)) { app->settings()->setConsoleAutoFocus(autoFocus); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_toggle_auto_focus")); ImGui::BeginDisabled(s_settingsState.low_spec_mode); } // ============================================================================ // Settings Page Renderer // ============================================================================ // A full-card-width, left-aligned, solid button (icon + label) drawn at an explicit (x,y). // Used by the side-by-side "column card" tabs (Backup, Wallet) where content is positioned // manually because ImGui's Indent (which GlassCardScope uses) is window-relative. static bool renderCardButton(ImDrawList* dl, float x, float y, float w, float h, const char* id, const char* label, const char* icon) { using namespace material; ImGui::SetCursorScreenPos(ImVec2(x, y)); ImFont* lf = Type().button(); ImFont* icf = Type().iconSmall(); const float dpp = Layout::dpiScale(); const float padX = 12.0f * dpp, ig = 6.0f * dpp; const float bh = h; const bool pressed = ImGui::InvisibleButton(id, ImVec2(w, bh)); const bool hov = ImGui::IsItemHovered(); if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); const ImVec2 pmin(x, y), pmax(x + w, y + bh); const float round = 7.0f * dpp; // match ActionButton chips (mockup .btn radius: 7px) dl->AddRectFilled(pmin, pmax, WithAlpha(OnSurface(), hov ? 30 : 20), round); dl->AddRect(pmin, pmax, WithAlpha(OnSurface(), 48), round, 0, 1.0f); const ImU32 fg = ImGui::GetColorU32(OnSurface()); dl->PushClipRect(pmin, pmax, true); float tx = x + padX; if (icon && icon[0] && icf) { dl->AddText(icf, icf->LegacySize, ImVec2(tx, y + (bh - icf->LegacySize) * 0.5f), fg, icon); tx += icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x + ig; } dl->AddText(lf, lf->LegacySize, ImVec2(tx, y + (bh - lf->LegacySize) * 0.5f), fg, label); dl->PopClipRect(); return pressed; } // ---- Category tabs (top-level settings navigation) ------------------------- enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXPLORER, TAB_CHAT, TAB_ABOUT, TAB_COUNT }; // Pinned horizontal category tab bar, drawn above the settings scroll region. Each category is a // pill; the active one gets an accent fill. Advances the ImGui cursor past the bar + a divider so // the scrollable content begins below it. static void renderSettingsTabBar(float availWidth) { using namespace material; struct T { int id; const char* label; const char* idstr; }; 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(); const float dp = Layout::dpiScale(); const float padX = 13.0f * dp, padY = 7.0f * dp, gap = 6.0f * dp, rnd = 8.0f * dp; const float h = f->LegacySize + padY * 2.0f; const ImVec2 origin = ImGui::GetCursorScreenPos(); float x = origin.x, y = origin.y; for (const T& t : tabs) { ImVec2 ts = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, t.label); float w = ts.x + padX * 2.0f; if (x > origin.x && x + w > origin.x + availWidth) { x = origin.x; y += h + gap; } // wrap ImGui::SetCursorScreenPos(ImVec2(x, y)); if (ImGui::InvisibleButton(t.idstr, ImVec2(w, h))) s_settingsState.current_tab = t.id; const bool hovered = ImGui::IsItemHovered(); const bool active = (s_settingsState.current_tab == t.id); if (active) dl->AddRectFilled(ImVec2(x, y), ImVec2(x + w, y + h), WithAlpha(Primary(), 34), rnd); else if (hovered) dl->AddRectFilled(ImVec2(x, y), ImVec2(x + w, y + h), IM_COL32(255, 255, 255, 12), rnd); dl->AddText(f, f->LegacySize, ImVec2(x + (w - ts.x) * 0.5f, y + (h - ts.y) * 0.5f), ImGui::GetColorU32((active || hovered) ? OnSurface() : OnSurfaceMedium()), t.label); x += w + gap; } const float bottom = y + h; dl->AddLine(ImVec2(origin.x, bottom + 5.0f * dp), ImVec2(origin.x + availWidth, bottom + 5.0f * dp), ImGui::GetColorU32(Divider()), 1.0f); ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom + 12.0f * dp)); } void RenderSettingsPage(App* app) { // Load settings state on first render if (!s_settingsState.initialized && app->settings()) { loadSettingsPageState(app); } // Sync low-spec / theme-effects state from runtime each frame // so that hotkey toggles are reflected in the checkboxes. { bool runtimeLowSpec = effects::isLowSpecMode(); if (s_settingsState.low_spec_mode != runtimeLowSpec) { if (runtimeLowSpec) { // Hotkey turned low-spec ON — save snapshot, override statics enterLowSpec(false); } else if (s_settingsState.low_spec_snapshot.valid) { // Hotkey turned low-spec OFF — restore snapshot exitLowSpec(false); } else if (app->settings()) { // No snapshot — read prefs from settings file s_settingsState.blur_amount = app->settings()->getBlurMultiplier(); s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); s_settingsState.ui_opacity = app->settings()->getUIOpacity(); s_settingsState.window_opacity = app->settings()->getWindowOpacity(); s_settingsState.theme_effects_enabled = app->settings()->getThemeEffectsEnabled(); s_settingsState.scanline_enabled = app->settings()->getScanlineEnabled(); } s_settingsState.low_spec_mode = runtimeLowSpec; } bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled(); if (s_settingsState.theme_effects_enabled != runtimeThemeEffects) { s_settingsState.theme_effects_enabled = runtimeThemeEffects; } } auto& S = schema::UI(); // Responsive layout — matches other tabs ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float scrollbarMargin = ImGui::GetStyle().ScrollbarSize + Layout::spacingSm(); float availWidth = contentAvail.x - scrollbarMargin; // Settings fills the full content width (the global content-max-width cap is disabled). float settingsLeftOffset = 0.0f; float hs = Layout::hScale(availWidth); float vs = Layout::vScale(contentAvail.y); float pad = Layout::cardInnerPadding(); float bottomPad = std::max(0.0f, pad - ImGui::GetStyle().ItemSpacing.y); float gap = Layout::cardGap(); float glassRound = Layout::glassRounding(); char buf[256]; // Label column position — adaptive to width float labelW = std::max(S.drawElement("components.settings-page", "label-min-width").size, S.drawElement("components.settings-page", "label-width").size * hs); // Widen labelW if any translated label is wider (prevents overflow) { ImFont* mf = Type().body2(); const char* labelKeys[] = {"theme", "balance_layout", "language", "acrylic", "noise", "ui_opacity", "window_opacity", "font_scale", "rpc_host", "rpc_port", "rpc_user", "rpc_pass", "transaction_url", "address_url"}; for (const char* k : labelKeys) { float tw = mf->CalcTextSizeA(mf->LegacySize, FLT_MAX, 0, TR(k)).x + Layout::spacingMd(); if (tw > labelW) labelW = tw; } } // Input field width — fill remaining space in card float inputW = std::max(S.drawElement("components.settings-page", "input-min-width").size, availWidth - labelW - pad * 2); (void)inputW; // used by some sections; may be unused depending on active tab // Category tab bar — pinned above the scrollable content area (not part of the scroll). renderSettingsTabBar(availWidth); // Scrollable content area — NoBackground matches other tabs if (settingsLeftOffset > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + settingsLeftOffset); ImGui::BeginChild("##SettingsPageScroll", ImVec2(settingsLeftOffset > 0.0f ? availWidth + scrollbarMargin : 0.0f, 0), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse); ApplySmoothScroll(); // Capture the ACTUAL clip boundaries from inside the child window ImVec2 childClipMin = ImGui::GetWindowPos(); ImVec2 childClipMax(childClipMin.x + ImGui::GetWindowSize().x, childClipMin.y + ImGui::GetWindowSize().y); const float dp = Layout::dpiScale(); const float fadeH = schema::UI().drawElement("components.settings-page", "edge-fade-zone").size * dp; const float fadeOffTop = schema::UI().drawElement("components.settings-page", "edge-fade-offset-top").size * dp; const float fadeOffBot = schema::UI().drawElement("components.settings-page", "edge-fade-offset-bottom").size * dp; // Get draw list AFTER BeginChild so we draw on the child window's list ImDrawList* dl = ImGui::GetWindowDrawList(); (void)dl; // used by cards below // Capture ForegroundDrawList vertex start — DrawGlassPanel draws // theme effects (rainbow border, shimmer, specular glare, edge trace) // on the ForegroundDrawList; those bypass the shader fade so we'll // apply a vertex-based alpha fade to them after rendering. ImDrawList* fgDL = ImGui::GetForegroundDrawList(); int fgVtxStart = fgDL->VtxBuffer.Size; // --- Shader-based scroll fade: bind custom fragment shader --- // The shader multiplies output alpha by a smoothstep gradient based // on screen Y, giving a true per-pixel alpha mask at scroll edges. float settingsScrollY_pre = ImGui::GetScrollY(); float settingsScrollMaxY_pre = ImGui::GetScrollMaxY(); float settingsFadeTopY = childClipMin.y + fadeOffTop; float settingsFadeBottomY = childClipMax.y - fadeOffBot; float settingsFadeZoneTop = (settingsScrollY_pre > 1.0f) ? fadeH : 0.0f; float settingsFadeZoneBot = (settingsScrollMaxY_pre > 0 && settingsScrollY_pre < settingsScrollMaxY_pre - 1.0f) ? fadeH : 0.0f; if (fadeH > 0.0f && !s_settingsState.low_spec_mode && s_settingsState.fade_shader.init()) { s_settingsState.fade_shader.fadeTopY = settingsFadeTopY; s_settingsState.fade_shader.fadeBottomY = settingsFadeBottomY; s_settingsState.fade_shader.fadeZoneTop = settingsFadeZoneTop; s_settingsState.fade_shader.fadeZoneBottom = settingsFadeZoneBot; s_settingsState.fade_shader.addBind(dl); } // Top margin from schema float topMargin = schema::UI().drawElement("components.settings-page", "top-margin").size; if (topMargin > 0.0f) ImGui::Dummy(ImVec2(0, topMargin)); GlassPanelSpec glassSpec; glassSpec.rounding = glassRound; glassSpec.fillAlpha = 26; // lift the settings cards off the background (closer to the mockup's flat cards) glassSpec.borderAlpha = 50; // crisper, more defined card border (mockup uses a visible 1px line) ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); ImFont* sub1 = Type().subtitle1(); // ==================================================================== // APPEARANCE — two stacked cards: THEME & LANGUAGE (2x2 dropdown grid) // then SCALE & EFFECTS (font scale + effect toggles + Advanced sliders). // ==================================================================== if (s_settingsState.current_tab == TAB_APPEARANCE) { float refreshBtnW = S.drawElement("components.settings-page", "refresh-btn-width").size; // --- Skin data --- auto& skinMgr = schema::SkinManager::instance(); const auto& skins = skinMgr.available(); std::string active_preview = "DragonX"; bool active_is_custom = false; for (const auto& skin : skins) { if (skin.id == skinMgr.activeSkinId()) { active_preview = skin.name; active_is_custom = !skin.bundled; break; } } (void)active_is_custom; // --- Language data --- auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); std::vector lang_names; lang_names.reserve(languages.size()); for (const auto& lang : languages) { lang_names.push_back(lang.second.c_str()); } // --- Balance layout data --- const auto& layouts = GetBalanceLayouts(); std::string balPreview = s_settingsState.balance_layout; for (const auto& l : layouts) { if (l.id == s_settingsState.balance_layout) { balPreview = l.name; break; } } // --- Theme combo popup (shared) --- auto renderThemeComboPopup = [&]() { ImGui::TextDisabled("%s", TR("settings_builtin")); ImGui::Separator(); for (size_t i = 0; i < skins.size(); i++) { const auto& skin = skins[i]; if (!skin.bundled) continue; bool is_selected = (skin.id == skinMgr.activeSkinId()); if (ImGui::Selectable(skin.name.c_str(), is_selected)) { skinMgr.setActiveSkin(skin.id); if (app->settings()) { app->settings()->setSkinId(skin.id); app->settings()->save(); } } if (is_selected) ImGui::SetItemDefaultFocus(); } bool has_custom = false; for (const auto& skin : skins) { if (!skin.bundled) { has_custom = true; break; } } if (has_custom) { ImGui::Spacing(); ImGui::TextDisabled("%s", TR("settings_custom")); ImGui::Separator(); for (size_t i = 0; i < skins.size(); i++) { const auto& skin = skins[i]; if (skin.bundled) continue; bool is_selected = (skin.id == skinMgr.activeSkinId()); if (!skin.valid) { ImGui::PushStyleColor(ImGuiCol_Text, material::ErrorVec4()); ImGui::BeginDisabled(true); std::string lbl = skin.name + TR("grpa_invalid_suffix"); ImGui::Selectable(lbl.c_str(), false); ImGui::EndDisabled(); ImGui::PopStyleColor(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", skin.validationError.c_str()); } else { std::string lbl = skin.name; if (!skin.author.empty()) lbl += " (" + skin.author + ")"; if (ImGui::Selectable(lbl.c_str(), is_selected)) { skinMgr.setActiveSkin(skin.id); if (app->settings()) { app->settings()->setSkinId(skin.id); app->settings()->save(); } } if (is_selected) ImGui::SetItemDefaultFocus(); } } } }; // ============================================================ // Card 1 — THEME & LANGUAGE (2x2 grid of labeled dropdowns) // ============================================================ { material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language")); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); float contentW = availWidth - pad * 2; 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 * 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") }; ImGui::PushFont(body2); float lblW = 0.0f; // label column — mockup puts the label BESIDE the control (.row), not above for (int i = 0; i < 4; ++i) lblW = std::max(lblW, ImGui::CalcTextSize(cellLabels[i]).x); lblW += Layout::spacingMd(); float rowTop = ImGui::GetCursorScreenPos().y; float rowBottom = rowTop; for (int i = 0; i < 4; ++i) { int col = i % cols; if (col == 0 && i > 0) rowTop = rowBottom; // start a new grid row float cx = baseX + col * (colW + cellGap); // Field label on the left, control filling the rest of the cell (mockup .row layout). ImGui::SetCursorScreenPos(ImVec2(cx, rowTop)); ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); ImGui::TextUnformatted(cellLabels[i]); ImGui::PopStyleColor(); ImGui::SetCursorScreenPos(ImVec2(cx + lblW, rowTop)); ImGui::SetNextItemWidth(colW - lblW); switch (i) { case 0: // Theme if (ImGui::BeginCombo("##Theme", active_preview.c_str())) { renderThemeComboPopup(); ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_hotkey")); break; case 1: // Balance layout if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) { for (const auto& l : layouts) { if (!l.enabled) continue; bool selected = (l.id == s_settingsState.balance_layout); if (ImGui::Selectable(l.name.c_str(), selected)) { s_settingsState.balance_layout = l.id; if (app->settings()) { app->settings()->setBalanceLayout(l.id); app->settings()->save(); } } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_layout_hotkey")); break; case 2: // Language if (ImGui::Combo("##Language", &s_settingsState.language_index, lang_names.data(), static_cast(lang_names.size()))) { auto it = languages.begin(); std::advance(it, s_settingsState.language_index); i18n.loadLanguage(it->first); if (app->settings()) { app->settings()->setLanguage(it->first); app->settings()->save(); } } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_language")); break; case 3: { // Clock format int cf = app->settings() ? app->settings()->getTimeFormat() : 0; const char* cfItems[] = { TR("chat_ts_24h"), TR("chat_ts_12h") }; if (ImGui::Combo("##ClockFormat", &cf, cfItems, 2) && app->settings()) { app->settings()->setTimeFormat(cf); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clock_format")); break; } } float cellBottom = ImGui::GetCursorScreenPos().y; rowBottom = (col == 0) ? cellBottom : std::max(rowBottom, cellBottom); if (col == cols - 1 || i == 3) { ImGui::SetCursorScreenPos(ImVec2(baseX, rowBottom + 11.0f * dp)); rowBottom = ImGui::GetCursorScreenPos().y; } } ImGui::PopFont(); // Rescan the theme folder — minor action, tucked below the grid. ImGui::SetCursorScreenPos(ImVec2(baseX, rowBottom)); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); Notifications::instance().info(TR("settings_theme_refreshed")); } if (ImGui::IsItemHovered()) material::Tooltip(TR("tt_scan_themes"), schema::SkinManager::getUserSkinsDirectory().c_str()); } ImGui::Dummy(ImVec2(0, gap)); // ============================================================ // Card 2 — SCALE & EFFECTS (font scale + effect toggles + Advanced sliders) // ============================================================ { material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("scale_effects")); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); float contentW = availWidth - pad * 2; // --- Font Scale slider --- { ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); ImGui::TextUnformatted(TR("font_scale")); ImGui::PopStyleColor(); float fontSliderW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW), 360.0f * dp); ImGui::SetNextItemWidth(fontSliderW); s_settingsState.font_scale = Layout::userFontScale(); float prev_font_scale = s_settingsState.font_scale; { char fs_fmt[16]; snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", s_settingsState.font_scale); ImGui::SliderFloat("##FontScale", &s_settingsState.font_scale, 1.0f, 1.5f, fs_fmt, ImGuiSliderFlags_AlwaysClamp); } s_settingsState.font_scale = std::max(1.0f, std::min(1.5f, std::round(s_settingsState.font_scale * 20.0f) / 20.0f)); if (s_settingsState.font_scale != prev_font_scale) Layout::setUserFontScaleVisual(s_settingsState.font_scale); if (ImGui::IsItemDeactivatedAfterEdit()) { Layout::setUserFontScale(s_settingsState.font_scale); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // --- Favorite highlight opacity (gold outline + star button in the Balance address list) --- // Pure color-alpha preference; not low-spec gated (unlike the transparency sliders below). { ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); ImGui::TextUnformatted(TR("favorite_opacity")); ImGui::PopStyleColor(); float favSliderW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW), 360.0f * dp); ImGui::SetNextItemWidth(favSliderW); { char favop_fmt[16]; snprintf(favop_fmt, sizeof(favop_fmt), "%.0f%%%%", s_settingsState.favorite_opacity * 100.0f); if (ImGui::SliderFloat("##FavoriteOpacity", &s_settingsState.favorite_opacity, 0.15f, 1.0f, favop_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (app->settings()) app->settings()->setFavoriteOpacity(s_settingsState.favorite_opacity); // live; persisted on release } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_favorite_opacity")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // --- Earnings display format (Balance tab per-address + total earnings figures) --- { ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); ImGui::TextUnformatted(TR("earnings_format")); ImGui::PopStyleColor(); float efW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW), 360.0f * dp); ImGui::SetNextItemWidth(efW); const char* efItems[] = { TR("earnings_format_coin"), TR("earnings_format_coin_fiat"), TR("earnings_format_fiat") }; if (ImGui::Combo("##EarningsFormat", &s_settingsState.earnings_format, efItems, IM_ARRAYSIZE(efItems))) { s_settingsState.earnings_format = std::max(0, std::min(2, s_settingsState.earnings_format)); if (app->settings()) { app->settings()->setEarningsFormat(s_settingsState.earnings_format); app->settings()->save(); } } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_earnings_format")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // --- Effect toggles (always visible, horizontal wrapping flow) --- { ImGui::PushFont(body2); const float efFh = ImGui::GetFrameHeight(); const float efInner = ImGui::GetStyle().ItemInnerSpacing.x; float efX = 0.0f; bool efFirst = true; auto efFlow = [&](const char* label) { const float w = efFh + efInner + ImGui::CalcTextSize(label).x; if (efFirst) { efFirst = false; efX = w; } else if (efX + Layout::spacingLg() + w <= contentW) { ImGui::SameLine(0, Layout::spacingLg()); efX += Layout::spacingLg() + w; } else { efX = w; } }; efFlow(TR("low_spec_mode")); if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) { effects::setLowSpecMode(s_settingsState.low_spec_mode); if (s_settingsState.low_spec_mode) { enterLowSpec(true); } else if (s_settingsState.low_spec_snapshot.valid) { exitLowSpec(true); } saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec")); efFlow(TR("simple_background")); if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_settingsState.gradient_background)) { schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg")); efFlow(TR("reduce_motion")); if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) { saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion")); ImGui::BeginDisabled(s_settingsState.low_spec_mode); efFlow(TR("console_scanline")); if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &s_settingsState.scanline_enabled)) { ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled; app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline")); efFlow(TR("theme_effects")); if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &s_settingsState.theme_effects_enabled)) { effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled); // Paired with the enable flag everywhere (startup, hotkeys): off also drops the // translucent rainbow/shimmer/glare layers, so turning effects on has to restore them. effects::ThemeEffects::instance().setReducedTransparency(!s_settingsState.theme_effects_enabled); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects")); efFlow(TR("animate_avatars")); { bool anim = app->settings()->getAnimateAvatars(); if (ImGui::Checkbox(TrId("animate_avatars", "animate_avatars").c_str(), &anim)) { app->settings()->setAnimateAvatars(anim); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_animate_avatars")); } ImGui::EndDisabled(); // low-spec ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // --- Collapsible: Advanced Effects... (console colors + 2x2 opacity/blur sliders) --- material::CollapsibleHeader(dl, "##EffectsToggle", TR("advanced_effects"), s_settingsState.effects_expanded, contentW, body2, OnSurfaceMedium()); if (s_settingsState.effects_expanded) { ImGui::PushFont(body2); ImGui::BeginDisabled(s_settingsState.low_spec_mode); // Console output color toggles (own row — no GPU cost, enabled even in low-spec). // renderConsoleColorToggles() temporarily End/BeginDisabled()s so its own checkboxes // stay enabled — it MUST be called while exactly one BeginDisabled is active. renderConsoleColorToggles(app); // Row 1: Acrylic preset slider + Noise slider (side by side, labels above) float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size; float halfW = (contentW - Layout::spacingLg()) * 0.5f; float ctrlW = std::min(std::max(effCtrlMinW, halfW), 360.0f * dp); float baseX = ImGui::GetCursorScreenPos().x; float rightX = baseX + ctrlW + Layout::spacingLg(); ImGui::TextUnformatted(TR("acrylic")); float row1Y = ImGui::GetCursorScreenPos().y; ImGui::SetNextItemWidth(ctrlW); { char blur_fmt[16]; if (s_settingsState.blur_amount < 0.01f) snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off")); else snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_settingsState.blur_amount / kAcrylicMaxBlur * 100.0f); if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, kAcrylicMaxBlur, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < kAcrylicMaxBlur * 0.04f) s_settingsState.blur_amount = 0.0f; s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur")); float afterRow1Y = ImGui::GetCursorScreenPos().y; float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y; ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH)); ImGui::TextUnformatted(TR("noise")); ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y)); ImGui::SetNextItemWidth(ctrlW); { char noise_fmt[16]; if (s_settingsState.noise_opacity < 0.01f) snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off")); else snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_settingsState.noise_opacity * 100.0f); if (ImGui::SliderFloat("##NoiseOpacity", &s_settingsState.noise_opacity, 0.0f, 1.0f, noise_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity); if (app->settings()) app->settings()->setNoiseOpacity(s_settingsState.noise_opacity); // live; persisted on release } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise")); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y)); ImGui::TextUnformatted(TR("ui_opacity")); float row2Y = ImGui::GetCursorScreenPos().y; ImGui::SetNextItemWidth(ctrlW); { char uiop_fmt[16]; snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", s_settingsState.ui_opacity * 100.0f); if (ImGui::SliderFloat("##UIOpacity", &s_settingsState.ui_opacity, 0.3f, 1.0f, uiop_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); if (app->settings()) app->settings()->setUIOpacity(s_settingsState.ui_opacity); // live; persisted on release } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity")); float afterRow2Y = ImGui::GetCursorScreenPos().y; ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH)); ImGui::TextUnformatted(TR("window_opacity")); ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y)); ImGui::SetNextItemWidth(ctrlW); { char winop_fmt[16]; snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", s_settingsState.window_opacity * 100.0f); if (ImGui::SliderFloat("##WindowOpacity", &s_settingsState.window_opacity, 0.3f, 1.0f, winop_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (app->settings()) app->settings()->setWindowOpacity(s_settingsState.window_opacity); // live; persisted on release } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity")); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y)); ImGui::EndDisabled(); // low-spec ImGui::PopFont(); } // s_settingsState.effects_expanded } } // ==================================================================== // WALLET — card (privacy/daemon toggles + collapsible tools) // ==================================================================== if (s_settingsState.current_tab == TAB_WALLET) { const bool showDaemonOptions = app->supportsFullNodeLifecycleActions(); // Two side-by-side glass cards: OPTIONS (toggles) | DIAGNOSTICS (log + tools). const float ccGap = Layout::cardGap(); const float ccW = (availWidth - ccGap) * 0.5f; const float cw = ccW - pad * 2; const float ccTop = ImGui::GetCursorScreenPos().y; const float ccBaseX = ImGui::GetCursorScreenPos().x; float ccBottom = ccTop; // One foreground channel for both cards; panels painted afterwards at equal (tallest) height. float cardBot[2] = { ccTop, ccTop }; dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); auto cardHeader = [&](int col, const char* header) -> float { const float cx = ccBaseX + col * (ccW + ccGap); ImGui::SetCursorScreenPos(ImVec2(cx + pad, ccTop + pad)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), header); return ImGui::GetCursorScreenPos().y + Layout::spacingMd(); }; auto cardClose = [&](int col, float lastBottom) { cardBot[col] = lastBottom + pad; ccBottom = std::max(ccBottom, cardBot[col]); }; ImGui::PushFont(body2); const float fh = ImGui::GetFrameHeight(); // checkbox row height const float bh = ImGui::GetFrameHeight() + 12.0f * dp; // taller, airier buttons (match mockup) const float gp = Layout::spacingSm(); // roomier gap // ---- Card 0: OPTIONS (checkboxes in a 2-column grid — mockup .chks.two) ---- { const float cx = ccBaseX + pad; const float col2W = (cw - Layout::spacingLg()) * 0.5f; float rowY = cardHeader(0, TR("wallet_options_hdr")); int c = 0; float last = rowY; auto CB = [&](const std::string& id, bool* val) -> bool { ImGui::SetCursorScreenPos(ImVec2(cx + c * (col2W + Layout::spacingLg()), rowY)); const bool changed = ImGui::Checkbox(id.c_str(), val); last = rowY + fh; if (c == 1) { rowY += fh + gp; c = 0; } else { c = 1; } return changed; }; // These persist immediately on toggle (like the daemon/verbose options below). The old // code discarded CB()'s return and relied on a Save button that only renders on the About // tab, so a user who enabled Auto-shield (a money/privacy intent) silently lost it on relaunch. if (CB(TrId("save_z_transactions", "save_ztx"), &s_settingsState.save_ztxs)) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx")); if (CB(TrId("auto_shield", "auto_shld"), &s_settingsState.auto_shield)) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield")); { // Not implemented: disabled and never shown as "on", even for a stored use_tor=true. bool torOff = false; ImGui::BeginDisabled(); CB(TrId("use_tor", "tor"), &torOff); ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { const float wrapW = S.drawElement("components.settings-page", "tor-tooltip-wrap-width") .sizeOr(420.0f) * Layout::dpiScale(); material::BeginTooltip(); ImGui::PushTextWrapPos(wrapW); ImGui::TextUnformatted(TR(app->isLiteBuild() ? "tt_tor_unavailable_lite" : "tt_tor_unavailable")); ImGui::PopTextWrapPos(); material::EndTooltip(); } } if (showDaemonOptions) { if (CB(TrId("keep_daemon", "keep_dmn"), &s_settingsState.keep_daemon_running)) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_keep_daemon")); 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")); // (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); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_verbose")); // O1: node coinbase auto-shield status (v1.3.0+). Rendered as a full-width note BELOW the // checkbox grid (not wedged between checkboxes) with friendly wording — the daemon's raw // technical reason is available on hover. Nothing renders on pre-1.3.0 daemons (never probed). if (app && app->daemonAutoShieldProbed() && (app->daemonAutoShieldActive() || !app->daemonAutoShieldDisabledReason().empty())) { ImGui::SetCursorScreenPos(ImVec2(cx, last + gp)); ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width if (app->daemonAutoShieldActive()) { ImGui::TextColored(material::SuccessVec4(), "%s", TR("autoshield_by_node")); if (!app->daemonAutoShieldAddress().empty()) ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str()); } else { // Friendly, actionable wording. The seed-not-recoverable case is the common one and has a // clear fix (back up the seed); anything else gets a generic line. Raw daemon text on hover. ImGui::TextDisabled("%s", app->daemonAutoShieldSeedRecoverable() ? TR("autoshield_off_generic") : TR("autoshield_off_backup_seed")); if (ImGui::IsItemHovered()) material::Tooltip("%s", app->daemonAutoShieldDisabledReason().c_str()); } ImGui::PopTextWrapPos(); last = ImGui::GetCursorScreenPos().y; // grow the card to include the note } cardClose(0, last); } // ---- Card 1: DIAGNOSTICS + Tools & Actions (2-column button grids) ---- { const float cx = ccBaseX + (ccW + ccGap) + pad; const float col2W = (cw - Layout::spacingLg()) * 0.5f; float rowY = cardHeader(1, TR("wallet_diagnostics_hdr")); int c = 0; float last = rowY; auto BTN = [&](const char* id, const char* label, const char* icon) -> bool { const float bx = cx + c * (col2W + Layout::spacingLg()); const bool p = renderCardButton(dl, bx, rowY, col2W, bh, id, label, icon); last = rowY + bh; if (c == 1) { rowY += bh + gp; c = 0; } else { c = 1; } return p; }; auto rowBreak = [&]() { if (c == 1) { rowY += bh + gp; c = 0; } }; if (BTN("##wlog", TR("settings_open_log_folder"), ICON_MD_FOLDER)) dragonx::util::Platform::openFolder(dragonx::util::Platform::getObsidianDragonDir()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_log_folder")); if (BTN("##wdiag", TR("settings_copy_diagnostics"), ICON_MD_CONTENT_COPY)) { ImGui::SetClipboardText(app->buildDiagnosticsReport().c_str()); ui::Notifications::instance().info(TR("settings_diagnostics_copied"), 4.0f); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_copy_diagnostics")); rowBreak(); rowY += Layout::spacingSm(); ImGui::SetCursorScreenPos(ImVec2(cx, rowY)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("tools_actions_hdr")); rowY = ImGui::GetCursorScreenPos().y + Layout::spacingMd(); c = 0; if (BTN("##waddr", TR("settings_address_book"), ICON_MD_CONTACTS)) app->setCurrentPage(ui::NavPage::Contacts); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book")); if (BTN("##wval", TR("settings_validate_address"), ICON_MD_CHECK_CIRCLE)) ValidateAddressDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_validate")); if (BTN("##wreq", TR("settings_request_payment"), ICON_MD_QR_CODE)) RequestPaymentDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_request_payment")); // z_shieldcoinbase / z_mergetoaddress are full-node RPCs: Lite has no node to send them to. if (app->supportsFullNodeLifecycleActions()) { if (BTN("##wshield", TR("settings_shield_mining"), ICON_MD_SHIELD)) ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining")); if (BTN("##wmerge", TR("settings_merge_to_address"), ICON_MD_CALL_MERGE)) ShieldDialog::showMerge(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge")); } if (BTN("##wclear", TR("settings_clear_ztx"), ICON_MD_DELETE_SWEEP)) s_settingsState.confirm_clear_ztx = true; if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clear_ztx")); rowBreak(); cardClose(1, last); } // Paint both cards at the same (tallest) height, then merge the channels. { const float eq = std::max(cardBot[0], cardBot[1]); dl->ChannelsSetCurrent(0); for (int col = 0; col < 2; ++col) { const float cx = ccBaseX + col * (ccW + ccGap); material::DrawGlassPanel(dl, ImVec2(cx, ccTop), ImVec2(cx + ccW, eq), glassSpec); } dl->ChannelsMerge(); } ImGui::PopFont(); // Reserve the full multi-card footprint with a Dummy so the scroll region grows to include // the manually-positioned cards (a bare SetCursorScreenPos past content warns in ImGui). ImGui::SetCursorScreenPos(ImVec2(ccBaseX, ccTop)); ImGui::Dummy(ImVec2(availWidth, (ccBottom - ccTop) + Layout::spacingSm())); } // ==================================================================== // BACKUP & DATA — card (own category tab; split out of Wallet) // ==================================================================== if (s_settingsState.current_tab == TAB_BACKUP) { const bool fullNode = app->supportsFullNodeLifecycleActions(); // Three side-by-side glass cards, each with its header inside (mockup-style grouping). // Content is positioned manually (SetCursorScreenPos) because ImGui's Indent — which // GlassCardScope relies on — is window-relative and would pull offset columns back to x0. const float ccGap = Layout::cardGap(); const int ccN = 3; const float ccW = (availWidth - ccGap * (ccN - 1)) / (float)ccN; const float cw = ccW - pad * 2; const float ccTop = ImGui::GetCursorScreenPos().y; const float ccBaseX = ImGui::GetCursorScreenPos().x; float ccBottom = ccTop; ImGui::PushFont(body2); const float bh = ImGui::GetFrameHeight() + 12.0f * dp; // taller, airier buttons (match mockup) const float bgp = Layout::spacingSm(); // roomier gap between buttons // A full-card-width, left-aligned, solid button drawn at an explicit (x,y). auto cardBtn = [&](float x, float y, float w, const char* id, const char* label, const char* icon) -> bool { return renderCardButton(dl, x, y, w, bh, id, label, icon); }; // All cards render onto one foreground channel; the glass panels are painted afterwards at a // single equal height (the tallest card) so side-by-side cards match — mockup grid-stretch look. float cardBot[3] = { ccTop, ccTop, ccTop }; dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); auto cardHeader = [&](int col, const char* header) -> float { const float cx = ccBaseX + col * (ccW + ccGap); float cy = ccTop + pad; ImGui::SetCursorScreenPos(ImVec2(cx + pad, cy)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), header); return ImGui::GetCursorScreenPos().y + Layout::spacingMd(); }; auto cardClose = [&](int col, float lastBottom) { cardBot[col] = lastBottom + pad; ccBottom = std::max(ccBottom, cardBot[col]); }; // ---- Card 0: Import & Restore ---- { const float cx = ccBaseX + pad; float cy = cardHeader(0, TR("backup_col_import")); float last = cy; if (cardBtn(cx, cy, cw, "##imp_key", TR("settings_import_key"), ICON_MD_KEY)) app->showImportKeyDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_key")); last = cy + bh; cy += bh + bgp; if (cardBtn(cx, cy, cw, "##imp_vk", TR("settings_import_viewkey"), ICON_MD_VISIBILITY)) app->showImportViewingKeyDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey")); last = cy + bh; if (fullNode) { cy += bh + bgp; if (cardBtn(cx, cy, cw, "##wallets", TR("wallets_button"), ICON_MD_ACCOUNT_BALANCE_WALLET)) ui::WalletsDialog::show(app); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wallets_button")); last = cy + bh; cy += bh + bgp; if (cardBtn(cx, cy, cw, "##bootstrap", TR("download_bootstrap"), ICON_MD_CLOUD_DOWNLOAD)) BootstrapDownloadDialog::show(app); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_download_bootstrap")); last = cy + bh; } cardClose(0, last); } // ---- Card 1: Backup ---- { const float cx = ccBaseX + (ccW + ccGap) + pad; float cy = cardHeader(1, TR("backup_col_backup")); float last = cy; if (fullNode) { if (cardBtn(cx, cy, cw, "##seed", TR("seed_backup_button"), ICON_MD_VPN_KEY)) app->showSeedBackupDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_backup")); last = cy + bh; cy += bh + bgp; } if (cardBtn(cx, cy, cw, "##backup", TR("settings_backup"), ICON_MD_BACKUP)) app->showBackupDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_backup")); last = cy + bh; if (fullNode) { cy += bh + bgp; const bool migrateGlow = app->isPreSeedWallet(); if (cardBtn(cx, cy, cw, "##migrate", TR("seed_migrate_button"), ICON_MD_SWAP_HORIZ)) app->showSeedMigrationDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate")); if (migrateGlow) { const ImVec2 gmn = ImGui::GetItemRectMin(), gmx = ImGui::GetItemRectMax(); const float gdp = Layout::dpiScale(); const float pulse = 0.5f + 0.5f * std::sin((float)ImGui::GetTime() * 3.2f); for (int g = 3; g >= 1; --g) { const float e = (float)g * 2.2f * gdp; const int a = (int)((70.0f + pulse * 95.0f) / (float)g); dl->AddRect(ImVec2(gmn.x - e, gmn.y - e), ImVec2(gmx.x + e, gmx.y + e), material::WithAlpha(material::Primary(), a), 6.0f * gdp + e, 0, 1.6f * gdp); } } last = cy + bh; cy += bh + bgp; if (cardBtn(cx, cy, cw, "##wizard", TR("setup_wizard"), ICON_MD_AUTO_FIX_HIGH)) app->restartWizard(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wizard")); last = cy + bh; } cardClose(1, last); } // ---- Card 2: Export ---- { const float cx = ccBaseX + (ccW + ccGap) * 2.0f + pad; float cy = cardHeader(2, TR("backup_col_export")); float last = cy; if (cardBtn(cx, cy, cw, "##exp_key", TR("settings_export_key"), ICON_MD_LOGOUT)) app->showExportKeyDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_key")); last = cy + bh; cy += bh + bgp; // Export-all only works over the full-node RPC; hide it in lite (which has its own key export // in the lite wallet section) rather than dead-ending on "Not connected to the daemon". if (fullNode) { if (cardBtn(cx, cy, cw, "##exp_all", TR("settings_export_all"), ICON_MD_ARCHIVE)) ExportAllKeysDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_all")); last = cy + bh; cy += bh + bgp; } if (cardBtn(cx, cy, cw, "##exp_csv", TR("settings_export_csv"), ICON_MD_DESCRIPTION)) ExportTransactionsDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_csv")); last = cy + bh; cardClose(2, last); } // Paint all three cards at the same (tallest) height, then merge the channels. { const float eq = std::max({cardBot[0], cardBot[1], cardBot[2]}); dl->ChannelsSetCurrent(0); for (int col = 0; col < 3; ++col) { const float cx = ccBaseX + col * (ccW + ccGap); material::DrawGlassPanel(dl, ImVec2(cx, ccTop), ImVec2(cx + ccW, eq), glassSpec); } dl->ChannelsMerge(); } ImGui::PopFont(); // Reserve the full multi-card footprint with a Dummy so the scroll region grows to include // the manually-positioned cards (a bare SetCursorScreenPos past content warns in ImGui). ImGui::SetCursorScreenPos(ImVec2(ccBaseX, ccTop)); ImGui::Dummy(ImVec2(availWidth, (ccBottom - ccTop) + Layout::spacingSm())); } // ==================================================================== // NODE & SECURITY — card // ==================================================================== if (s_settingsState.current_tab == TAB_NODE) { // Two side-by-side glass cards (NODE/SECURITY | DAEMON BINARY), drawn manually because // GlassCardScope's Indent is window-relative and can't offset the right card. All the // column *content* below is unchanged; only the card wrapper differs. const float ndTop = ImGui::GetCursorScreenPos().y; const float ndBaseX = ImGui::GetCursorScreenPos().x; bool ndTwoCol = false; float ndColW = 0.0f, ndColGap = 0.0f, ndLeftBottom = 0.0f, ndRightBottom = 0.0f, ndSingleBottom = ndTop; dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(ndBaseX, ndTop + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f); float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f); auto rowBtnW = [&](std::initializer_list labels) -> float { float maxTextW = 0; for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x); return std::max(minBtnW, maxTextW + btnPad * 2); }; // --- NODE & SECURITY: one full-width card, controls laid out horizontally --- // A single vertical flow of sub-sections (Node, RPC, Security, Daemon binary), // each with its overline header and its controls flowing left-to-right to fill // the card width. No 2x2 grid, no column clips, no masonry placement. { ImVec2 sectionOrigin = ImGui::GetCursorScreenPos(); // The lite WALLET block below still positions its rows against a "left" // column origin/width; in the single-column layout that is simply the full // content column at the section origin. const float leftX = sectionOrigin.x; const float leftColW = contentW; ImFont* body2Info = S.resolveFont("body2"); if (!body2Info) body2Info = Type().body2(); // Shared SECURITY controls, rendered full-width with horizontal rows. Used by // both the lite branch (auto-lock + PIN only) and the full-node branch (which // also gets the RPC-backed encrypt/change/lock/remove block, includeRpcEncrypt). // `secX` is the row start X (== content-column left); `secColW` its width. auto renderSecuritySection = [&](float secX, float secColW, bool includeRpcEncrypt) { // Narrow (two-column) card: the encrypt buttons, auto-lock and PIN // controls can't all share one row, so wrap groups onto fresh rows. const bool secNarrow = secColW < 740.0f * Layout::dpiScale(); // Encrypt / Change passphrase / Lock / Remove — RPC-backed, full-node only. // In lite the wallet's encryption controls live in the WALLET block above. if (includeRpcEncrypt) { bool isEncrypted = app->state().isEncrypted(); bool isLocked = app->state().isLocked(); float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (secColW - Layout::spacingMd()) * 0.5f); ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); if (!isEncrypted) { if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showEncryptDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_encrypt")); ImGui::SameLine(0, Layout::spacingMd()); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted")); } else { if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showChangePassphraseDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pass")); ImGui::SameLine(0, Layout::spacingMd()); if (isLocked) { ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(material::WarningVec4(), ICON_MD_LOCK); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(material::WarningVec4(), "%s", TR("settings_locked")); } else { if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->lockWallet(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock")); ImGui::SameLine(0, Layout::spacingSm()); ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(material::SuccessVec4(), ICON_MD_LOCK_OPEN); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(material::SuccessVec4(), "%s", TR("settings_unlocked")); } // Remove Encryption button — trails the row, or wraps below // when the column is too narrow for three buttons + status. if (secNarrow) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); } else { ImGui::SameLine(0, Layout::spacingMd()); } if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showDecryptDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt")); } } // Auto-Lock timeout + PIN controls — continue on the same row as the encrypt // controls (full-node) so the whole SECURITY band fills the width; on lite // (no encrypt row) they start a fresh row at the section's left edge. { float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f); int timeout = app->settings()->getAutoLockTimeout(); const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") }; int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 }; int selTimeout = 0; for (int i = 0; i < 6; i++) { if (timeoutValues[i] == timeout) { selTimeout = i; break; } } // Auto-lock gets its own full-width row (label left, dropdown filling — mockup). (void)secNarrow; if (includeRpcEncrypt) ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock")); const float alLblW = ImGui::CalcTextSize(TR("settings_auto_lock")).x; ImGui::SameLine(0, Layout::spacingMd()); ImGui::SetNextItemWidth(std::max(comboW, secColW - alLblW - Layout::spacingMd())); if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) { app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]); app->settings()->save(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock")); // PIN unlock controls, trailing the auto-lock combo on the same row. bool isEncryptedPIN = app->state().isEncrypted(); // Lite has no PIN quick-unlock: its unlock paths (Settings, send-time prompt) take the passphrase // directly and never consult a PIN vault, so these controls would be dead. Hide them. if (app->isLiteBuild()) { // nothing to show } else if (isEncryptedPIN) { bool hasPIN = app->hasPinVault(); float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (secColW - Layout::spacingSm()) * 0.5f); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); // Not while a passphrase change runs: PIN setup could seal the old passphrase. const bool pinBusy = app->isEncryptInProgress(); if (!hasPIN) { ImGui::BeginDisabled(pinBusy); if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinSetupDialog(); ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_set_pin")); ImGui::SameLine(0, Layout::spacingMd()); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin")); } else { ImGui::BeginDisabled(pinBusy); if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinChangeDialog(); ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_change_pin")); ImGui::SameLine(0, Layout::spacingSm()); ImGui::BeginDisabled(pinBusy); if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinRemoveDialog(); ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_remove_pin")); ImGui::SameLine(0, Layout::spacingMd()); if (app->isPinVaultStale()) { // The vault seals a passphrase the wallet no longer accepts (C87). const ImVec4 staleCol = ImGui::ColorConvertU32ToFloat4(Warning()); ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(staleCol, ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(staleCol, "%s", TR("settings_pin_out_of_date")); if (ImGui::IsItemHovered()) { const float wrapW = S.drawElement("components.settings-page", "pin-tooltip-wrap-width") .sizeOr(420.0f) * Layout::dpiScale(); material::BeginTooltip(); ImGui::PushTextWrapPos(wrapW); ImGui::TextUnformatted(TR("tt_pin_out_of_date")); ImGui::PopTextWrapPos(); material::EndTooltip(); } } else { ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(material::SuccessVec4(), ICON_MD_DIALPAD); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(material::SuccessVec4(), "%s", TR("settings_pin_active")); } } } else { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin")); } } }; if (app->isLiteBuild()) { // ============================ LITE ============================ // Lite has no daemon: this card holds wallet lifecycle/encryption // (the "WALLET" block) followed by the shared SECURITY block, both // rendered full-width in a single column exactly as before. Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { ImGui::PushFont(body2Info); { float liteLabelW = std::min(leftColW * 0.35f, 132.0f); float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm()); // Lite-server selection lives in the dedicated Network tab now. Type().textColored(TypeStyle::Body2, OnSurfaceMedium(), TR("lite_servers_network_tab")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (material::TactileButton(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) { s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded; } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_toggle")); if (s_settingsState.lite_lifecycle_expanded) { const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")}; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_action")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); if (ImGui::BeginCombo("##LiteLifecycleOperation", lifecycleLabels[std::max(0, std::min(2, s_settingsState.lite_lifecycle_operation))])) { for (int operationIndex = 0; operationIndex < 3; ++operationIndex) { const bool selected = s_settingsState.lite_lifecycle_operation == operationIndex; if (ImGui::Selectable(lifecycleLabels[operationIndex], selected)) { s_settingsState.lite_lifecycle_operation = operationIndex; s_settingsState.lite_lifecycle_status.clear(); s_settingsState.lite_lifecycle_summary.clear(); } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_op")); if (s_settingsState.lite_lifecycle_operation == 1 || s_settingsState.lite_lifecycle_operation == 2) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_wallet_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path, sizeof(s_settingsState.lite_wallet_path)); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_wallet_path")); } if (s_settingsState.lite_lifecycle_operation == 2) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_seed_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed, sizeof(s_settingsState.lite_restore_seed), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_seed")); // Live 24-word count so the user knows the seed is complete before restoring. { const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed); char wbuf[32]; snprintf(wbuf, sizeof(wbuf), TR("lite_word_count"), wc); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_birthday_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); 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; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("lite_birthday_hint")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_account_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); 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; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(), &s_settingsState.lite_restore_overwrite); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_overwrite")); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_passphrase_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteLifecyclePassphrase", s_settingsState.lite_lifecycle_passphrase, sizeof(s_settingsState.lite_lifecycle_passphrase), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_pass")); // Create/Restore with a passphrase sets a NEW one: confirm it (8+ characters). // Open unlocks an existing wallet and is never checked. const bool liteNewPassphrase = s_settingsState.lite_lifecycle_operation != 1 && s_settingsState.lite_lifecycle_passphrase[0] != '\0'; if (liteNewPassphrase) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_confirm_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteLifecyclePassphraseConfirm", s_settingsState.lite_lifecycle_passphrase_confirm, sizeof(s_settingsState.lite_lifecycle_passphrase_confirm), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_confirm_pass")); const std::string issue = liteNewPassphraseIssue( s_settingsState.lite_lifecycle_passphrase, s_settingsState.lite_lifecycle_passphrase_confirm, /*submitted=*/false); if (!issue.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, Error(), issue.c_str()); } } else { // Hidden (Open, or the passphrase was cleared): always wipe the whole buffer; an // emptied field can still hold an old passphrase past its first NUL. sodium_memzero(s_settingsState.lite_lifecycle_passphrase_confirm, sizeof(s_settingsState.lite_lifecycle_passphrase_confirm)); } // Poll a completed async create/open/restore (driven by the App-owned // controller; finalized on the main thread by App::update's // pumpLifecycleResult()). While in flight the button is disabled. if (s_settingsState.lite_lifecycle_pending) { if (auto* lite = app->liteWallet()) { if (!lite->lifecycleRequestInProgress()) { s_settingsState.lite_lifecycle_pending = false; const auto& result = lite->lastLifecycleResult(); s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted; if (result.walletReady) { s_settingsState.lite_lifecycle_status = TR("lite_wallet_ready"); } else { s_settingsState.lite_lifecycle_status = result.error.empty() ? result.status.message : result.error; Notifications::instance().warning(s_settingsState.lite_lifecycle_status); } } } else { s_settingsState.lite_lifecycle_pending = false; // backend went away } } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); // Also unavailable while a NEW passphrase is unconfirmed or too short (the hint // above says why), so a click never wipes a typed seed over a passphrase typo. const bool liteLifecycleBlocked = s_settingsState.lite_lifecycle_pending || !wallet::liteLifecyclePassphraseAcceptable(liteLifecycleOperationFromPageState(), s_settingsState.lite_lifecycle_passphrase, s_settingsState.lite_lifecycle_passphrase_confirm); if (liteLifecycleBlocked) ImGui::BeginDisabled(); if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { evaluateLiteLifecycleRequestFromPageState(app); } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_lite_lifecycle_run")); if (liteLifecycleBlocked) ImGui::EndDisabled(); if (!s_settingsState.lite_lifecycle_status.empty()) { ImGui::SameLine(0, Layout::spacingSm()); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_lifecycle_status.c_str()); } if (!s_settingsState.lite_lifecycle_summary.empty()) { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_lifecycle_summary.c_str()); } } // Open the lite wallet data folder in the OS file manager (always available in // lite). The lite backend stores its wallet in its OWN dir (silentdragonxlite), // NOT the full-node getDragonXDataDir(). ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (material::ActionButton("##liteopendir", TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN, material::ActionTier::Secondary)) { util::Platform::openFolder(util::Platform::getLiteWalletDataDir()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir")); // ---- Backup & keys (open wallet only) ---------------------------------- if (app->liteWallet() && app->liteWallet()->walletOpen()) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_backup_keys")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (material::ActionButton("##LiteExportSeed", TR("lite_show_seed"), ICON_MD_VPN_KEY, material::ActionTier::Secondary)) { auto r = app->liteWallet()->exportSeed(); wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); if (r.ok) { s_settingsState.lite_export_secret = r.seedPhrase; s_settingsState.lite_export_label = TR("lite_seed_warning"); s_settingsState.lite_export_is_seed = true; s_settingsState.lite_export_birthday = r.birthday; s_settingsState.lite_backup_status.clear(); } else { s_settingsState.lite_export_label.clear(); s_settingsState.lite_export_is_seed = false; s_settingsState.lite_backup_status = r.error; } wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_seed")); ImGui::SameLine(0, Layout::spacingSm()); if (material::ActionButton("##LiteExportKeys", TR("lite_show_private_keys"), ICON_MD_KEY, material::ActionTier::Secondary)) { auto r = app->liteWallet()->exportPrivateKeys(); wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_is_seed = false; // keys, not a seed if (r.ok) { s_settingsState.lite_export_secret = r.privateKeysJson; s_settingsState.lite_export_label = TR("lite_private_keys_warning"); s_settingsState.lite_backup_status.clear(); } else { s_settingsState.lite_export_label.clear(); s_settingsState.lite_backup_status = r.error; } wallet::secureWipeLiteSecret(r.privateKeysJson); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_keys")); // Revealed secret: shown read-only (no extra copies), with copy + wipe. if (!s_settingsState.lite_export_secret.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_export_label.c_str()); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::PushTextWrapPos(0.0f); ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str()); ImGui::PopTextWrapPos(); // The seed's birthday is needed to restore quickly — show + back it up too. if (s_settingsState.lite_export_is_seed) { char bday[64]; snprintf(bday, sizeof(bday), TR("lite_birthday_backup"), s_settingsState.lite_export_birthday); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { app->copySecretToClipboard(s_settingsState.lite_export_secret); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_copy")); ImGui::SameLine(0, Layout::spacingSm()); // Save the seed (+ birthday) to an owner-only (0600) file in the config dir. if (s_settingsState.lite_export_is_seed && TactileButton(TrId("lite_save_to_file", "LiteExportSave").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { std::string path = util::Platform::getConfigDir() + "/lite-seed-backup.txt"; std::string content = s_settingsState.lite_export_secret + "\nBirthday: " + std::to_string(s_settingsState.lite_export_birthday) + "\n"; const bool ok = util::Platform::writeFileAtomically( path, content, /*restrictPermissions=*/true); wallet::secureWipeLiteSecret(content); s_settingsState.lite_backup_status = ok ? (std::string(TR("lite_saved_to")) + path) : (std::string(TR("lite_could_not_write")) + path); } if (s_settingsState.lite_export_is_seed) { if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_save_seed_file")); ImGui::SameLine(0, Layout::spacingSm()); } if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); s_settingsState.lite_export_is_seed = false; } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_hide_wipe")); } // Import a spending/viewing key (history appears after the next sync). ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_import_key_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); // Import / Encrypt / Remove encryption end in a backend save that waits on the scan lock (held for a // whole scan): they run on the controller's wallet-op lane and are unavailable while a scan runs. const bool liteScanning = app->liteWallet()->scanInProgress(); const bool liteOpBusy = app->liteWallet()->walletOpInProgress(); ImGui::BeginDisabled(liteScanning || liteOpBusy); if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { std::string liteKey(s_settingsState.lite_import_key); 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 = TR("grpa_enter_private_key_to_import"); } else { s_settingsState.lite_backup_status = TR("lite_working"); app->liteWallet()->importKeyAsync(liteKey, [](const wallet::LiteImportResult& r) { // Generic text: the reason (non-secret) is in the lite log. s_settingsState.lite_backup_status = r.ok ? TR("lite_key_imported") : TR("lite_key_import_failed"); }); sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); } wallet::secureWipeLiteSecret(liteKey); // the trimmed copy (was never wiped before) } ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", liteScanning ? TR("send_tooltip_syncing") : TR("tt_lite_import_key_btn")); if (!s_settingsState.lite_backup_status.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_backup_status.c_str()); } // ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ---- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_security")); const auto& wstate = app->getWalletState(); const float encLabelX = leftX - sectionOrigin.x + liteLabelW; if (!wstate.isEncrypted()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_passphrase_label")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt_pass")); // A NEW passphrase: confirm it and require 8+ characters (a typo here locks // the wallet at once). Unlock below never applies this rule. ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_confirm_label")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteEncryptConfirm", s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_confirm_pass")); const std::string encIssue = liteNewPassphraseIssue( s_settingsState.lite_enc_pass, s_settingsState.lite_enc_confirm, /*submitted=*/false); if (!encIssue.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, Error(), encIssue.c_str()); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::PushTextWrapPos((leftX + leftColW) - ImGui::GetWindowPos().x); Type().textColored(TypeStyle::Caption, Warning(), TR("lite_encrypt_offline_warning")); ImGui::PopTextWrapPos(); const bool encPassOk = wallet::liteEncryptPassphraseValid(s_settingsState.lite_enc_pass, s_settingsState.lite_enc_confirm); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::BeginDisabled(liteScanning || app->liteWallet()->walletOpInProgress() || !encPassOk); if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { s_settingsState.lite_encryption_status = TR("lite_working"); app->liteWallet()->encryptWalletAsync(s_settingsState.lite_enc_pass, [](const wallet::LiteEncryptionResult& r) { s_settingsState.lite_encryption_status = r.ok ? std::string(TR("lite_wallet_encrypted")) : r.error; }); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); sodium_memzero(s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm)); } ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", liteScanning ? TR("send_tooltip_syncing") : TR("tt_lite_encrypt")); } else { // Encrypted: the Encrypt confirmation is hidden; wipe the whole buffer (an emptied field // can still hold an old passphrase past its first NUL). sodium_memzero(s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm)); if (wstate.isLocked()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_unlock")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock_pass")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { std::string unlockErr; const bool ok = app->unlockLiteWallet(std::string(s_settingsState.lite_enc_pass), &unlockErr); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); s_settingsState.lite_encryption_status = ok ? std::string(TR("lite_wallet_unlocked")) : unlockErr; } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock")); } else { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { // Route through App so the chat session is torn down immediately on lock. s_settingsState.lite_encryption_status = app->lockLiteWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed"); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lock")); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("lite_passphrase_label")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_decrypt_pass")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::BeginDisabled(liteScanning || app->liteWallet()->walletOpInProgress()); if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { s_settingsState.lite_encryption_status = TR("lite_working"); app->liteWallet()->decryptWalletAsync(s_settingsState.lite_dec_pass, [](const wallet::LiteEncryptionResult& r) { s_settingsState.lite_encryption_status = r.ok ? std::string(TR("lite_encryption_removed")) : r.error; }); sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); } ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", liteScanning ? TR("send_tooltip_syncing") : TR("tt_lite_remove_encrypt")); } if (!s_settingsState.lite_encryption_status.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_encryption_status.c_str()); } // ---- Maintenance: re-download blocks from the lite server ---- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_maintenance")); const bool scanning = app->liteWallet()->scanInProgress(); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); // Not while a wallet op is queued: its save would wait out the whole re-scan. ImGui::BeginDisabled(scanning || app->liteWallet()->walletOpInProgress()); if (material::ActionButton("##literedl", TR("lite_redownload_blocks"), ICON_MD_CLOUD_DOWNLOAD, material::ActionTier::Secondary)) { s_settingsState.confirm_lite_redownload = true; } ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_lite_redownload")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc")); } else { // Wallet closed: don't leave a revealed backup/secret or a typed passphrase in memory. if (!s_settingsState.lite_export_secret.empty()) { wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); } // Unconditional: an emptied field can still hold the old tail past its first NUL. sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); sodium_memzero(s_settingsState.lite_enc_confirm, sizeof(s_settingsState.lite_enc_confirm)); sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); } } // lite WALLET block ImGui::PopFont(); } // lite WALLET push-font scope // ---- SECURITY (lite): auto-lock + PIN only ---- // The wallet's passphrase encryption controls live in the WALLET block // above (lite backend), so this section shows only the shared auto-lock // timeout + PIN controls. ImGui::Dummy(ImVec2(0, Layout::spacingSm())); renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/false); // Advance to the true bottom of the single column. ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y)); ndSingleBottom = ImGui::GetCursorScreenPos().y; // lite = single card } else { // ========================= FULL NODE ========================= // One full-width column; each sub-section's controls flow left-to-right. ImGui::PushFont(body2Info); const float spMd = Layout::spacingMd(); // Two-column layout when wide enough: Node / RPC / Security on the left, Daemon binary on // the right (fills the empty right side + shortens the card). One column when narrow. const bool nsHasDaemon = app->supportsFullNodeLifecycleActions(); const bool nsTwoCol = nsHasDaemon && contentW > 760.0f * Layout::dpiScale(); // The two columns become two SEPARATE glass panels: left [x0, x0+cardW], // right [x0+cardW+cardGap, x0+availWidth]. For the panels to keep a clean cardGap // between them, the content column must be cardW-2*pad and the right-column indent // (nsColW+nsColGap) must equal cardW+cardGap — so nsColGap = cardGap + 2*pad. const float nsColGap = Layout::cardGap() + 2.0f * pad; const float nsColW = nsTwoCol ? ((availWidth - Layout::cardGap()) * 0.5f - 2.0f * pad) : contentW; ndTwoCol = nsTwoCol; ndColW = nsColW; ndColGap = nsColGap; // hoist geometry for the two-panel draw const ImVec2 nsColTop = ImGui::GetCursorScreenPos(); // Window-local anchor for the right column. We shift it with Indent() (not a one-shot // SetCursorScreenPos): ImGui resets the cursor X to the window's left indent on every // line-advance, so only an indent holds a column across multiple rows/widgets. const float nsColTopLocalX = ImGui::GetCursorPosX(); const float nsColTopLocalY = ImGui::GetCursorPosY(); { // ---- left column (Node / RPC / Security) ---- const float contentW = nsColW; const ImVec2 sectionOrigin(nsColTop.x, nsColTop.y); // -------------------- NODE / DATA -------------------- Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); { const std::string dirPath = util::Platform::getDragonXDataDir(); const std::string walletPath = dirPath + "wallet.dat"; const uint64_t wallet_size = util::Platform::getFileSize(walletPath); const std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : std::string(TR("settings_not_found")); const float leftX = ImGui::GetCursorPosX(); const float labelW = std::max(ImGui::CalcTextSize(TR("settings_data_dir")).x, ImGui::CalcTextSize(TR("settings_wallet_size_label")).x) + Layout::spacingLg(); const ImU32 metaCol = OnSurfaceMedium(); // Row 1: Data directory — label left; clickable path + copy button RIGHT-aligned // (mockup .kv space-between ledger look). The path middle-ellipsizes to fit. ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, metaCol); ImGui::TextUnformatted(TR("settings_data_dir")); ImGui::PopStyleColor(); ImFont* pathFont = ImGui::GetFont(); const float copyW = ImGui::GetFrameHeight(); const float pathAvailW = contentW - labelW - copyW - Layout::spacingSm() * 2.0f; const std::string dirShown = material::TruncateToWidth(dirPath, pathFont, pathFont->LegacySize, pathAvailW); const float pathW = ImGui::CalcTextSize(dirShown.c_str()).x; ImGui::SameLine(0, 0); ImGui::SetCursorPosX(leftX + contentW - copyW - Layout::spacingSm() - pathW); ImGui::AlignTextToFramePadding(); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", dirShown.c_str()); if (ImGui::IsItemHovered()) { const ImVec2 tmn = ImGui::GetItemRectMin(), tmx = ImGui::GetItemRectMax(); dl->AddLine(ImVec2(tmn.x, tmx.y), ImVec2(tmx.x, tmx.y), Primary()); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s\n%s", dirPath.c_str(), TR("tt_open_dir")); } if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath); ImGui::SameLine(0, Layout::spacingSm()); { material::IconButtonStyle cs; cs.hoverBg = material::WithAlpha(OnSurface(), 30); cs.tooltip = TR("copy"); const float ih = ImGui::GetFrameHeight(); if (material::IconButton("##copydir", ICON_MD_CONTENT_COPY, Type().iconSmall(), ImVec2(ih, ih), cs)) { ImGui::SetClipboardText(dirPath.c_str()); Notifications::instance().info(TR("copied_to_clipboard")); } } // Row 2: Wallet size — label left, value RIGHT-aligned. ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, metaCol); ImGui::TextUnformatted(TR("settings_wallet_size_label")); ImGui::PopStyleColor(); { const char* wv = (wallet_size > 0) ? size_str.c_str() : TR("settings_not_found"); const float wvW = ImGui::CalcTextSize(wv).x; ImGui::SameLine(0, 0); ImGui::SetCursorPosX(leftX + contentW - wvW); ImGui::AlignTextToFramePadding(); if (wallet_size > 0) ImGui::TextUnformatted(wv); else ImGui::TextDisabled("%s", wv); } // Large-wallet nudge: the BDB wallet.dat bloats with shielded-note witness data and // never shrinks in place. Past a threshold, hint the user toward consolidating notes // (Merge to Address) to curb further growth. Full-node only (lite has no wallet.dat here). static constexpr uint64_t kWalletBloatWarnBytes = 500ull * 1024 * 1024; // 500 MB if (app->supportsFullNodeLifecycleActions() && wallet_size > kWalletBloatWarnBytes) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushStyleColor(ImGuiCol_Text, Warning()); ImGui::PushTextWrapPos(leftX + contentW); ImGui::TextWrapped("%s", TR("wallet_size_warn")); ImGui::PopTextWrapPos(); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wallet_size_warn")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); if (material::ActionButton("##walletconsolidate", TR("wallet_size_consolidate"), ICON_MD_CALL_MERGE, material::ActionTier::Secondary)) ShieldDialog::showConsolidate(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge")); } // Row 3: folder buttons (their own row so the path gets the full width). ImGui::Dummy(ImVec2(0, Layout::spacingXs())); material::ButtonFlow ff(contentW); ff.next(material::ActionButtonWidth(TR("settings_open_app_dir"), ICON_MD_FOLDER)); if (material::ActionButton("##openapp", TR("settings_open_app_dir"), ICON_MD_FOLDER, material::ActionTier::Secondary)) util::Platform::openFolder(util::Platform::getObsidianDragonDir()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_app_dir")); ff.next(material::ActionButtonWidth(TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN)); if (material::ActionButton("##opendata", TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN, material::ActionTier::Secondary)) util::Platform::openFolder(dirPath); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir")); } // -------------------- RPC (collapsible, like Tools & Actions) -------------------- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); { ImFont* rpcHdrFont = body2; // match the "Tools & Actions..." toggle style const char* rpcArrow = s_settingsState.rpc_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button("##RpcToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) s_settingsState.rpc_expanded = !s_settingsState.rpc_expanded; if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("tt_rpc_toggle")); } float textY = hdrPos.y + (ImGui::GetFrameHeight() - rpcHdrFont->LegacySize) * 0.5f; dl->AddText(rpcHdrFont, rpcHdrFont->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("rpc_connection")); ImFont* rpcIconFont = Type().iconSmall(); if (!rpcIconFont) rpcIconFont = body2; float rpcArrowW = rpcIconFont->CalcTextSizeA(rpcIconFont->LegacySize, FLT_MAX, 0, rpcArrow).x; dl->AddText(rpcIconFont, rpcIconFont->LegacySize, ImVec2(hdrPos.x + contentW - rpcArrowW, textY), OnSurfaceMedium(), rpcArrow); ImGui::PopStyleColor(3); ImGui::PopFont(); } if (s_settingsState.rpc_expanded) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // Four fields (Host, Port, Username, Password) across one row when the // card is wide enough; otherwise 2x2. Each field is "label [input]". const char* hostLbl = TR("rpc_host"); const char* portLbl = TR("rpc_port"); const char* userLbl = TR("rpc_user"); const char* passLbl = TR("rpc_pass"); // Two rows, two column-aligned cells each: Host | Port, then Username | Password. // Each input fills to its column's right edge so the two columns line up vertically. const float colGap = spMd; const float colW = std::floor((contentW - colGap) * 0.5f); const float startX = ImGui::GetCursorPosX(); const float leftColRight = startX + colW; const float rightColRight = startX + contentW; // Read-only: the RPC credentials are auto-detected from the daemon's DRAGONX.conf, // so these fields DISPLAY the live connection (editing them here did nothing). auto cell = [&](const char* label, const char* id, char* buf, size_t bufSz, float cellX, float cellRight, bool password) { ImGui::SetCursorPosX(cellX); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(label); ImGui::SameLine(0, Layout::spacingXs()); ImGui::SetNextItemWidth(std::max(60.0f, cellRight - ImGui::GetCursorPosX())); ImGui::InputText(id, buf, bufSz, ImGuiInputTextFlags_ReadOnly | (password ? ImGuiInputTextFlags_Password : 0)); }; // Row 1: Host | Port cell(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), startX, leftColRight, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host")); ImGui::SameLine(); cell(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), leftColRight + colGap, rightColRight, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port")); // Row 2: Username | Password cell(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), startX, leftColRight, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); ImGui::SameLine(); cell(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), leftColRight + colGap, rightColRight, true); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass")); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("settings_auto_detected")); if (s_settingsState.rpc_plaintext_remote) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); ImGui::PushTextWrapPos(sectionOrigin.x + contentW); ImGui::TextWrapped("%s", TR("rpc_plaintext_remote_warning")); ImGui::PopTextWrapPos(); ImGui::PopStyleColor(); } } // -------------------- SECURITY -------------------- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/true); } // ---- end left column ---- const float nsLeftBottom = ImGui::GetCursorScreenPos().y; ndLeftBottom = nsLeftBottom; if (nsTwoCol) { // Reset to the top, then indent so every line in the right column starts at the // column X (the indent persists across line-advances; the explicit SetCursorPosX // places the very first widget on the same line). ImGui::SetCursorPosY(nsColTopLocalY); ImGui::Indent(nsColW + nsColGap); ImGui::SetCursorPosX(nsColTopLocalX + nsColW + nsColGap); } { // ---- right column (Daemon binary) ---- const float contentW = nsColW; // -------------------- DAEMON BINARY -------------------- if (app->supportsFullNodeLifecycleActions()) { // A std::async future BLOCKS in its destructor until the task finishes. The Refresh/ // post-install buttons set daemon_info_loaded=false; don't start (and so reassign) a new // scan while the previous one is still running — defer the reload until it lands (C123). const bool scanBusy = s_settingsState.daemon_version_scan.valid() && s_settingsState.daemon_version_scan.wait_for(std::chrono::seconds(0)) != std::future_status::ready; if (!s_settingsState.daemon_info_loaded && !scanBusy) { // Stat on the UI thread; the version stamp is a streamed read of a ~200 MB binary on // POSIX, so it runs on a worker and fills in when done (C123). s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(/*scanVersion=*/false); s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo(); s_settingsState.daemon_info_loaded = true; if (s_settingsState.installed_daemon.exists) s_settingsState.daemon_version_scan = std::async(std::launch::async, [] { return dragonx::resources::getInstalledDaemonInfo(/*scanVersion=*/true); }); } if (s_settingsState.daemon_version_scan.valid()) { if (s_settingsState.daemon_version_scan.wait_for(std::chrono::seconds(0)) == std::future_status::ready) s_settingsState.installed_daemon = s_settingsState.daemon_version_scan.get(); else ui::material::RequestProgressFrame(); // keep polling until the scan lands } const auto& inst = s_settingsState.installed_daemon; const auto& bun = s_settingsState.bundled_daemon; auto fmtDate = [](std::int64_t epoch) -> std::string { if (epoch <= 0) return "—"; std::time_t t = static_cast(epoch); std::tm tmv{}; #ifdef _WIN32 localtime_s(&tmv, &t); #else localtime_r(&t, &tmv); #endif char buf[32]; std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv); return std::string(buf); }; ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Heading row: "DAEMON BINARY" on the left, a compact colored status right-aligned // on the same line (moved up out of the status box, and shortened). { const ImVec2 hp = ImGui::GetCursorScreenPos(); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); if (bun.available) { const bool sameSize = inst.exists && inst.size == bun.size; const char* stTxt = !inst.exists ? TR("daemon_status_none") : sameSize ? TR("daemon_status_ok") : TR("daemon_status_diff"); const ImU32 stCol = (inst.exists && sameSize) ? Success() : Warning(); ImFont* ov = Type().overline(); const float stW = ov->CalcTextSizeA(ov->LegacySize, FLT_MAX, 0, stTxt).x; dl->AddText(ov, ov->LegacySize, ImVec2(hp.x + contentW - stW, hp.y), stCol, stTxt); } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); const float ddp = Layout::dpiScale(); // --- Status, grouped in a filled box (mockup .statusbox) --- const float boxPad = Layout::spacingMd(); // roomier inner padding (mockup ~9-11px) const float boxLeftX = ImGui::GetCursorScreenPos().x; ImGui::Dummy(ImVec2(0, boxPad)); ImGui::BeginGroup(); ImGui::Indent(boxPad); const float dLeftX = ImGui::GetCursorPosX(); const float dLabelW = std::max(ImGui::CalcTextSize(TR("daemon_installed")).x, ImGui::CalcTextSize(TR("daemon_bundled")).x) + Layout::spacingLg(); auto dkv = [&](const char* label, const std::string& value, bool dim) { ImGui::AlignTextToFramePadding(); ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium()); ImGui::TextUnformatted(label); ImGui::PopStyleColor(); ImGui::SameLine(0, 0); // Right-align the value to the box edge (mockup .kv). If it's too long to fit // (e.g. the installed version+size+date), fall back to left-packing after the label. const float dvW = ImGui::CalcTextSize(value.c_str()).x; ImGui::SetCursorPosX(std::max(dLeftX + dLabelW, dLeftX + (contentW - 2.0f * boxPad) - dvW)); ImGui::AlignTextToFramePadding(); if (dim) ImGui::TextDisabled("%s", value.c_str()); else ImGui::TextUnformatted(value.c_str()); }; if (inst.exists) { char ibuf[176]; std::snprintf(ibuf, sizeof(ibuf), "%s \xC2\xB7 %s \xC2\xB7 %s", inst.version.empty() ? TR("unknown") : inst.version.c_str(), util::Platform::formatFileSize(inst.size).c_str(), fmtDate(inst.modifiedEpoch).c_str()); dkv(TR("daemon_installed"), ibuf, false); } else { dkv(TR("daemon_installed"), TR("daemon_not_installed"), true); } if (bun.available) { char bbuf[144]; std::snprintf(bbuf, sizeof(bbuf), "%s \xC2\xB7 %s", bun.version.empty() ? TR("unknown") : bun.version.c_str(), util::Platform::formatFileSize(bun.size).c_str()); dkv(TR("daemon_bundled"), bbuf, false); } else { dkv(TR("daemon_bundled"), TR("daemon_none_bundled"), true); } ImGui::Unindent(boxPad); ImGui::EndGroup(); { const ImVec2 gmn = ImGui::GetItemRectMin(), gmx = ImGui::GetItemRectMax(); const ImVec2 bmn(boxLeftX, gmn.y - boxPad), bmx(boxLeftX + contentW, gmx.y + boxPad); // Subtle lifted fill (mockup .statusbox #26262a on the #121317 card) + near-invisible border. dl->AddRectFilled(bmn, bmx, material::WithAlpha(material::OnSurface(), 8), 8.0f * ddp); dl->AddRect(bmn, bmx, material::WithAlpha(material::OnSurface(), 20), 8.0f * ddp, 0, 1.0f); } ImGui::Dummy(ImVec2(0, boxPad)); // Refresh the cached daemon info once an in-app install has completed. 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")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); { using AT = material::ActionTier; material::ButtonFlow uf(contentW); uf.next(material::ActionButtonWidth(TR("daemon_update_check"), ICON_MD_SYSTEM_UPDATE)); if (material::ActionButton("##dupd", TR("daemon_update_check"), ICON_MD_SYSTEM_UPDATE, AT::Primary)) ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check")); uf.next(material::ActionButtonWidth(TR("refresh"), ICON_MD_REFRESH)); if (material::ActionButton("##drefresh", TR("refresh"), ICON_MD_REFRESH, AT::Secondary)) s_settingsState.daemon_info_loaded = false; if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_daemon_refresh")); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); uf.next(material::ActionButtonWidth(TR("daemon_install_bundled"), ICON_MD_DOWNLOAD)); if (material::ActionButton("##dinst", TR("daemon_install_bundled"), ICON_MD_DOWNLOAD, AT::Secondary)) s_settingsState.confirm_reinstall_daemon = true; if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); ImGui::EndDisabled(); } // --- MAINTENANCE --- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_maintenance_label")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); { using AT = material::ActionTier; material::ButtonFlow mf(contentW); ImGui::BeginDisabled(!app->isConnected()); mf.next(material::ActionButtonWidth(TR("test_connection"), ICON_MD_LINK)); if (material::ActionButton("##dtest", TR("test_connection"), ICON_MD_LINK, AT::Secondary)) { if (app->rpc() && app->rpc()->isConnected() && app->worker()) { app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb { try { rpc::RPCClient::TraceScope trace("Settings / Test connection"); rpc->call("getinfo"); return []() { Notifications::instance().success(TR("settings_rpc_ok")); }; } catch (const std::exception& e) { std::string err = e.what(); return [err]() { Notifications::instance().error(std::string(TR("settings_rpc_error_prefix")) + err); }; } }); } else { Notifications::instance().warning(TR("settings_not_connected")); } } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); mf.next(material::ActionButtonWidth(TR("rescan"), ICON_MD_REFRESH)); if (material::ActionButton("##drescan", TR("rescan"), ICON_MD_REFRESH, AT::Secondary)) { s_settingsState.confirm_rescan = true; s_settingsState.rescan_height_detecting = false; s_settingsState.rescan_height_detected = false; s_settingsState.rescan_probe_failed = false; } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan")); ImGui::EndDisabled(); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); mf.next(material::ActionButtonWidth(TR("repair_wallet"), ICON_MD_HEALING)); if (material::ActionButton("##drepair", TR("repair_wallet"), ICON_MD_HEALING, AT::Secondary)) s_settingsState.confirm_repair_wallet = true; if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); ImGui::EndDisabled(); } // --- Danger zone: Delete Blockchain, fenced off below a divider --- ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // mockup .danger margin-top: 8px { const ImVec2 dvp = ImGui::GetCursorScreenPos(); // Neutral hairline (mockup .danger border-top #26262b) — not an alarming red rule. dl->AddLine(dvp, ImVec2(dvp.x + contentW, dvp.y), material::WithAlpha(material::OnSurface(), 22), 1.0f); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // mockup .danger padding-top: 11px { using AT = material::ActionTier; material::ButtonFlow df(contentW); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); df.next(material::ActionButtonWidth(TR("delete_blockchain"), ICON_MD_DELETE)); if (material::ActionButton("##ddelete", TR("delete_blockchain"), ICON_MD_DELETE, AT::Destructive)) s_settingsState.confirm_delete_blockchain = true; if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); ImGui::EndDisabled(); } } } // ---- end right column ---- const float nsRightBottom = ImGui::GetCursorScreenPos().y; ndRightBottom = nsRightBottom; ndSingleBottom = nsRightBottom; if (nsTwoCol) ImGui::Unindent(nsColW + nsColGap); // restore indent before the rest of the page ImGui::SetCursorScreenPos(ImVec2(nsColTop.x, nsTwoCol ? std::max(nsLeftBottom, nsRightBottom) : nsRightBottom)); ImGui::PopFont(); } // ---- Draw the glass card(s) behind the content, then merge the channels ---- ImGui::Unindent(pad); dl->ChannelsSetCurrent(0); if (ndTwoCol) { const float ndEq = std::max(ndLeftBottom, ndRightBottom); // equal-height cards (mockup grid stretch) material::DrawGlassPanel(dl, ImVec2(ndBaseX, ndTop), ImVec2(ndBaseX + 2.0f * pad + ndColW, ndEq + bottomPad), glassSpec); material::DrawGlassPanel(dl, ImVec2(ndBaseX + ndColW + ndColGap, ndTop), ImVec2(ndBaseX + availWidth, ndEq + bottomPad), glassSpec); } else { material::DrawGlassPanel(dl, ImVec2(ndBaseX, ndTop), ImVec2(ndBaseX + availWidth, ndSingleBottom + bottomPad), glassSpec); } dl->ChannelsMerge(); const float ndBot = ndTwoCol ? std::max(ndLeftBottom, ndRightBottom) : ndSingleBottom; ImGui::SetCursorScreenPos(ImVec2(ndBaseX, ndBot + bottomPad)); } } // ==================================================================== // EXPLORER & OPTIONS — full-width card // ==================================================================== if (s_settingsState.current_tab == TAB_EXPLORER) { // Card 1 — URLS { material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); float contentW = availWidth - pad * 2; Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_urls_hdr")); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); ImGui::PushFont(body2); // Transaction URL and Address URL — stacked rows, label left, input filling the card (mockup .row). const float urlLblW = std::max(ImGui::CalcTextSize(TR("transaction_url")).x, ImGui::CalcTextSize(TR("address_url")).x) + Layout::spacingMd(); const float urlRowX = ImGui::GetCursorPosX(); const float urlInputW = contentW - urlLblW; ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("transaction_url")); ImGui::SameLine(urlRowX + urlLblW); ImGui::SetNextItemWidth(urlInputW); // These Explorer-tab controls persist on edit — they were the only ones still relying on the // About-only Save button, which a user on this tab never sees. ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer)); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("address_url")); ImGui::SameLine(urlRowX + urlLblW); ImGui::SetNextItemWidth(urlInputW); ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer)); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, gap)); // Card 2 — OPTIONS { material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); float contentW = availWidth - pad * 2; Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet_options_hdr")); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); ImGui::PushFont(body2); // Checkboxes + Block Explorer button (button wraps to its own row when it won't fit). const float expRowRight = ImGui::GetCursorScreenPos().x + contentW; if (ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees)) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees")); ImGui::SameLine(0, Layout::spacingLg()); if (ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices)) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices")); const float expBtnW = ImGui::CalcTextSize(TR("block_explorer")).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg(); if (ImGui::GetItemRectMax().x + expBtnW <= expRowRight) ImGui::SameLine(0, Layout::spacingLg()); else ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://explorer.dragonx.is"); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer")); ImGui::PopFont(); } } // ==================================================================== // CHAT & CONTACTS — card (same controls as the Chat tab's settings notch) // ==================================================================== if (s_settingsState.current_tab == TAB_CHAT) { // The shared control paints its own two cards (Appearance | Messaging) when drawCards=true. ImGui::PushFont(body2); RenderChatSettingsControls(app, availWidth, /*drawCards=*/true); ImGui::PopFont(); } // ==================================================================== // ABOUT — card // ==================================================================== if (s_settingsState.current_tab == TAB_ABOUT) { material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); const float contentW = availWidth - pad * 2; const float adp = Layout::dpiScale(); const float baseX = ImGui::GetCursorScreenPos().x; // --- Header: small logo + title / tagline / tech line --- const ImVec2 logoTop = ImGui::GetCursorScreenPos(); const float logoSz = 60.0f * adp; ImTextureID logoTex = app->getLogoTexture(); const float logoAspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f; float logoAreaW = 0.0f; if (logoTex != 0) { logoAreaW = logoSz + Layout::spacingLg(); ImGui::Indent(logoAreaW); } ImGui::PushFont(sub1); ImGui::TextUnformatted(DRAGONX_APP_NAME); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingSm()); ImGui::PushFont(body2); snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION); ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "%s", buf); ImGui::PopFont(); ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(OnSurfaceMedium())); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + (contentW - logoAreaW)); ImGui::TextUnformatted(TR("settings_about_text")); ImGui::PopTextWrapPos(); ImGui::PopStyleColor(); ImGui::PopFont(); ImGui::PushFont(capFont); snprintf(buf, sizeof(buf), "SDL3 \xC2\xB7 Dear ImGui %s \xC2\xB7 GPL-3.0", IMGUI_VERSION); ImGui::TextColored(ImVec4(1, 1, 1, 0.4f), "%s", buf); ImGui::PopFont(); if (logoTex != 0) ImGui::Unindent(logoAreaW); // Make the header at least as tall as the logo, then draw the logo centered in it. float headerH = ImGui::GetCursorScreenPos().y - logoTop.y; if (headerH < logoSz) { ImGui::Dummy(ImVec2(0, logoSz - headerH)); headerH = logoSz; } if (logoTex != 0) { float lw = logoSz, lh = logoSz; if (logoAspect >= 1.0f) lh = logoSz / logoAspect; else lw = logoSz * logoAspect; const float lx = logoTop.x + (logoSz - lw) * 0.5f; const float ly = logoTop.y + (headerH - lh) * 0.5f; dl->AddImage(logoTex, ImVec2(lx, ly), ImVec2(lx + lw, ly + lh)); } // --- Divider --- ImGui::Dummy(ImVec2(0, Layout::spacingMd())); { const ImVec2 dv = ImGui::GetCursorScreenPos(); dl->AddLine(dv, ImVec2(dv.x + contentW, dv.y), ImGui::GetColorU32(material::Divider()), 1.0f); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // --- Two columns: Credits (bullets) | License (paragraph + links) --- const float colGap = Layout::spacingLg(); const float colW = (contentW - colGap) * 0.5f; const float colTop = ImGui::GetCursorScreenPos().y; const float rx = baseX + colW + colGap; // Left column — Credits ImGui::SetCursorScreenPos(ImVec2(baseX, colTop)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("about_credits")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); { ImGui::PushFont(body2); static const char* kCredits[] = { "The Hush Developers", "The DragonX Developers", "ObsidianDragon Community", "Dear ImGui \xE2\x80\x94 Omar Cornut", "SDL3 \xE2\x80\x94 Sam Lantinga", "HushChat \xC2\xB7 librustzcash \xC2\xB7 libsodium", }; for (size_t i = 0; i < std::size(kCredits); ++i) { const char* c = kCredits[i]; const ImVec2 p = ImGui::GetCursorScreenPos(); const float r = 2.5f * adp; dl->AddCircleFilled(ImVec2(p.x + r, p.y + ImGui::GetTextLineHeight() * 0.5f), r, material::WithAlpha(material::Primary(), 210)); ImGui::SetCursorScreenPos(ImVec2(p.x + r * 2.0f + 8.0f * adp, p.y)); ImGui::TextUnformatted(c); if (i < std::size(kCredits) - 1) ImGui::Dummy(ImVec2(0, Layout::spacingSm())); } ImGui::PopFont(); } const float leftBottom = ImGui::GetCursorScreenPos().y; // Right column — License + links ImGui::SetCursorScreenPos(ImVec2(rx, colTop)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("about_license")); { ImGui::SetCursorScreenPos(ImVec2(rx, ImGui::GetCursorScreenPos().y + Layout::spacingSm())); ImGui::PushFont(capFont); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 0.6f)); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + colW); ImGui::TextUnformatted(TR("about_license_text")); ImGui::PopTextWrapPos(); ImGui::PopStyleColor(); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingLg())); { using AT = material::ActionTier; ImGui::SetCursorScreenPos(ImVec2(rx, ImGui::GetCursorScreenPos().y)); material::ButtonFlow lf(colW); lf.next(material::ActionButtonWidth(TR("website"), ICON_MD_PUBLIC)); if (material::ActionButton("##aboutweb", TR("website"), ICON_MD_PUBLIC, AT::Secondary)) util::Platform::openUrl("https://dragonx.is"); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_website")); lf.next(material::ActionButtonWidth(TR("about_source"), ICON_MD_CODE)); if (material::ActionButton("##aboutsrc", TR("about_source"), ICON_MD_CODE, AT::Secondary)) util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon"); lf.next(material::ActionButtonWidth(TR("report_bug"), ICON_MD_BUG_REPORT)); 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; // Reconcile the two columns, then a card-wide settings-actions row. ImGui::SetCursorScreenPos(ImVec2(baseX, std::max(leftBottom, rightBottom))); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); { using AT = material::ActionTier; material::ButtonFlow af(contentW); af.next(material::ActionButtonWidth(TR("save_settings"), ICON_MD_SAVE)); if (material::ActionButton("##aboutsave", TR("save_settings"), ICON_MD_SAVE, AT::Secondary)) { saveSettingsPageState(app->settings()); Notifications::instance().success(TR("settings_saved")); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings")); // This only reloads the last-saved values from disk (it doesn't apply factory defaults), so // label it honestly as "Discard unsaved changes" rather than the misleading "Reset to Defaults". af.next(material::ActionButtonWidth(TR("discard_changes"), ICON_MD_UNDO)); if (material::ActionButton("##aboutreset", TR("discard_changes"), ICON_MD_UNDO, AT::Tertiary)) { if (app->settings()) { loadSettingsPageState(app); Notifications::instance().info(TR("settings_reloaded")); } } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings")); } } 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(material::WarningVec4(), " %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. // ==================================================================== if (app->supportsFullNodeLifecycleActions() && s_settingsState.current_tab == TAB_NODE) { // Clickable header row ImVec2 headerPos = ImGui::GetCursorScreenPos(); const char* arrow = s_settingsState.debug_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) { if (s_settingsState.debug_expanded) { s_settingsState.debug_expanded = false; // collapsing needs no gate } else if (s_settingsState.debug_gate_passed) { s_settingsState.debug_expanded = true; // already unlocked this session } else { // First expand this session is gated: confirmation + (if secured) re-auth. s_settingsState.debug_gate_open = true; s_settingsState.debug_gate_buf[0] = '\0'; s_settingsState.debug_gate_err.clear(); s_settingsState.debug_gate_verifying = false; } } 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; 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; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; 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())); material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); #if DRAGONX_ENABLE_UI_SWEEP // Dev builds only (both sweeps, open-dir, demo chat and the theme checkbox): the tab sweep // screenshots real balances and addresses, and the demo chat fabricates a chat identity. // Screenshot sweep — capture every theme x every tab (above the daemon debug categories). ImGui::TextWrapped("%s", TR("screenshot_sweep_desc")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button"))) app->startScreenshotSweep(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep")); ImGui::SameLine(); // Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline). if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button"))) app->startFullUiSweep(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep_full")); ImGui::SameLine(); if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button"))) util::Platform::openFolder(app->screenshotDir()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_open_dir")); if (chat::hushChatFeatureEnabledAtBuild()) { // Populate the Chat tab with demo conversations so the sweep captures its real UI. ImGui::SameLine(); 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")); } // Restrict either sweep to just the active theme instead of cycling every skin. ImGui::SameLine(); { bool only = app->sweepCurrentThemeOnly(); if (ImGui::Checkbox(TR("sweep_current_theme_only"), &only)) app->setSweepCurrentThemeOnly(only); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_sweep_current_theme_only")); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Separator(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); #endif 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[] = { "addrman", "alert", "bench", "coindb", "db", "estimatefee", "http", "libevent", "lock", "mempool", "net", "paymentdisclosure", "pow", "proxy", "prune", "rand", "reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc" }; 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]); // Render as a 4-column grid of checkboxes int columns = 4; float dbgContentW = availWidth - pad * 2.0f; float colW = dbgContentW / columns; for (int i = 0; i < numCats; i++) { if (i > 0 && (i % columns) != 0) { ImGui::SameLine(pad + colW * (i % columns)); } bool enabled = s_settingsState.debug_categories.count(debugCats[i]) > 0; if (ImGui::Checkbox(debugCats[i], &enabled)) { if (enabled) { s_settingsState.debug_categories.insert(debugCats[i]); } else { s_settingsState.debug_categories.erase(debugCats[i]); } s_settingsState.debug_cats_dirty = true; saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", debugTips[i]); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // (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) ---- if (s_settingsState.debug_gate_open) { auto& st = s_settingsState; if (st.debug_gate_err_timer > 0.0f) { st.debug_gate_err_timer -= app->logicDeltaTime(); if (st.debug_gate_err_timer <= 0.0f) st.debug_gate_err.clear(); } const bool needAuth = app->debugGateRequiresAuth(); const bool hasPin = app->hasPinVault(); const float gdp = Layout::dpiScale(); material::OverlayDialogSpec ov; ov.title = TR("debug_gate_title"); ov.p_open = &st.debug_gate_open; ov.style = material::OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.idSuffix = "debuggate"; if (material::BeginOverlayDialog(ov)) { if (ImGui::IsKeyPressed(ImGuiKey_Escape)) st.debug_gate_open = false; ImGui::TextWrapped("%s", TR("debug_gate_warning")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); bool submit = false; if (needAuth) { Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), hasPin ? TR("debug_gate_pin_prompt") : TR("debug_gate_pass_prompt")); ImGui::SetNextItemWidth(-FLT_MIN); ImGuiInputTextFlags f = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue; if (hasPin) f |= ImGuiInputTextFlags_CharsDecimal; // PIN is numeric if (ImGui::InputText("##debugGateSecret", st.debug_gate_buf, sizeof(st.debug_gate_buf), f)) submit = true; if (!st.debug_gate_err.empty()) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); Type().textColored(TypeStyle::Caption, Error(), st.debug_gate_err.c_str()); } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::BeginDisabled(st.debug_gate_verifying); if (TactileButton(TR("debug_gate_confirm"), ImVec2(170.0f * gdp, 0)) || submit) { if (!needAuth) { st.debug_expanded = true; st.debug_gate_passed = true; st.debug_gate_open = false; } else if (strlen(st.debug_gate_buf) > 0) { st.debug_gate_verifying = true; std::string secret = st.debug_gate_buf; memset(st.debug_gate_buf, 0, sizeof(st.debug_gate_buf)); app->verifyDebugCredential(secret, [](bool ok) { auto& s = s_settingsState; s.debug_gate_verifying = false; if (ok) { s.debug_expanded = true; s.debug_gate_passed = true; s.debug_gate_open = false; } else { s.debug_gate_err = TR("debug_gate_incorrect"); s.debug_gate_err_timer = 4.0f; } }); if (!secret.empty()) memset(&secret[0], 0, secret.size()); } } ImGui::EndDisabled(); ImGui::SameLine(); if (TactileButton(TR("cancel"), ImVec2(120.0f * gdp, 0))) { memset(st.debug_gate_buf, 0, sizeof(st.debug_gate_buf)); st.debug_gate_open = false; } if (st.debug_gate_verifying) { ImGui::SameLine(); ImGui::AlignTextToFramePadding(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("debug_gate_verifying")); } material::EndOverlayDialog(); } } } // --- Shader-based scroll fade: unbind (restore ImGui's default shader) --- if (fadeH > 0.0f && !s_settingsState.low_spec_mode && s_settingsState.fade_shader.ready) { effects::ScrollFadeShader::addUnbind(dl); } // --- Vertex-based alpha fade for ForegroundDrawList theme effects --- // DrawGlassPanel draws rainbow borders, shimmer, specular glare, and // edge traces on the ForegroundDrawList which bypasses the shader. // Apply the same fade boundaries via vertex alpha manipulation. if (fadeH > 0.0f) { int fgVtxEnd = fgDL->VtxBuffer.Size; float safeTop = settingsFadeTopY + settingsFadeZoneTop; float safeBot = settingsFadeBottomY - settingsFadeZoneBot; for (int vi = fgVtxStart; vi < fgVtxEnd; vi++) { ImDrawVert& v = fgDL->VtxBuffer[vi]; if (v.pos.y >= safeTop && v.pos.y <= safeBot) continue; float alpha = 1.0f; if (settingsFadeZoneTop > 0.0f) { float dTop = v.pos.y - settingsFadeTopY; if (dTop < settingsFadeZoneTop) alpha = std::min(alpha, std::max(0.0f, dTop / settingsFadeZoneTop)); } if (settingsFadeZoneBot > 0.0f) { float dBot = settingsFadeBottomY - v.pos.y; if (dBot < settingsFadeZoneBot) alpha = std::min(alpha, std::max(0.0f, dBot / settingsFadeZoneBot)); } if (alpha < 1.0f) { int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF; a = static_cast(a * alpha); v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } } } ImGui::EndChild(); // ##SettingsPageScroll // Confirmation dialog for clearing z-tx history if (s_settingsState.confirm_clear_ztx) { if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1")); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool doCancel = false, doConfirm = false; material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_btn").c_str(), true, doCancel, doConfirm); if (doCancel) { s_settingsState.confirm_clear_ztx = false; } if (doConfirm) { std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; if (util::Platform::deleteFile(ztx_file)) { Notifications::instance().success(TR("settings_ztx_cleared")); } else { Notifications::instance().info(TR("settings_ztx_not_found")); } s_settingsState.confirm_clear_ztx = false; } EndOverlayDialog(); } } // Confirmation dialog for deleting blockchain data if (s_settingsState.confirm_delete_blockchain) { if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::ErrorVec4()); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg")); ImGui::Spacing(); ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_delete_blockchain_safe")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool doCancel = false, doConfirm = false; material::DialogConfirmFooter(TR("cancel"), TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), true, doCancel, doConfirm); if (doCancel) { s_settingsState.confirm_delete_blockchain = false; } if (doConfirm) { if (app->supportsFullNodeLifecycleActions()) app->deleteBlockchainData(); s_settingsState.confirm_delete_blockchain = false; } EndOverlayDialog(); } } // Confirm: rescan blockchain. On a normal (full-history) node this restarts the daemon with // -rescan; on a bootstrapped/pruned node that would fail (pre-snapshot blocks are absent), so we // probe the lowest available block height and run a runtime rescanblockchain from a confirmed, // editable height instead. if (s_settingsState.confirm_rescan) { // Kick off the one-shot block-range probe the first frame the dialog is open. if (!s_settingsState.rescan_height_detecting && !s_settingsState.rescan_height_detected) { s_settingsState.rescan_height_detecting = true; // Asynchronous now (worker_): the callback runs on the UI thread some frames later, or at once when // there is nothing to probe. A result for an earlier opening of the dialog is dropped. const int probeSeq = ++s_settingsState.rescan_probe_seq; app->detectLowestAvailableBlockHeight([probeSeq](bool ok, int lowest, bool fullHistory) { if (probeSeq != s_settingsState.rescan_probe_seq) return; s_settingsState.rescan_height_detecting = false; s_settingsState.rescan_height_detected = true; s_settingsState.rescan_probe_failed = !ok && !fullHistory; // (false, 0, true): nothing to probe s_settingsState.rescan_full_history = (!ok) || fullHistory; s_settingsState.rescan_start_height = (ok && !fullHistory) ? lowest : 0; }); } if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::WarningVec4()); ImGui::Spacing(); const bool detecting = s_settingsState.rescan_height_detecting; const bool bootstrapped = s_settingsState.rescan_height_detected && !s_settingsState.rescan_full_history; if (detecting) { ImGui::TextWrapped("%s", TR("rescan_detecting")); } else if (bootstrapped) { ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg")); ImGui::Spacing(); ImGui::Text("%s", TR("rescan_from_height")); 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 { if (s_settingsState.rescan_probe_failed) { // A long line: TextColored does not wrap, so colour a wrapped one. ImGui::PushStyleColor(ImGuiCol_Text, material::WarningVec4()); ImGui::TextWrapped("%s", TR("rescan_probe_failed")); ImGui::PopStyleColor(); ImGui::Spacing(); } ImGui::TextWrapped("%s", TR("confirm_rescan_msg")); ImGui::Spacing(); ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_rescan_safe")); } ImGui::Spacing(); ImGui::Separator(); 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 * dp))) { s_settingsState.confirm_rescan = false; } ImGui::SameLine(); ImGui::BeginDisabled(detecting); if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40 * dp))) { if (bootstrapped) { app->runtimeRescan(s_settingsState.rescan_start_height); } else { app->rescanBlockchain(); } s_settingsState.confirm_rescan = false; } ImGui::EndDisabled(); EndOverlayDialog(); } } // Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan) if (s_settingsState.confirm_repair_wallet) { if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::WarningVec4()); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg")); ImGui::Spacing(); ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_repair_wallet_safe")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool doCancel = false, doConfirm = false; material::DialogConfirmFooter(TrId("cancel", "repair_wallet_cancel").c_str(), TrId("repair_wallet", "repair_wallet_confirm").c_str(), false, doCancel, doConfirm); if (doCancel) { s_settingsState.confirm_repair_wallet = false; } if (doConfirm) { app->repairWallet(); s_settingsState.confirm_repair_wallet = false; } EndOverlayDialog(); } } // Confirm: reinstall the bundled daemon binary (stop → overwrite → restart) if (s_settingsState.confirm_reinstall_daemon) { if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::WarningVec4()); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg")); ImGui::Spacing(); ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_reinstall_daemon_safe")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool doCancel = false, doConfirm = false; material::DialogConfirmFooter(TrId("cancel", "reinstall_daemon_cancel").c_str(), TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), false, doCancel, doConfirm); if (doCancel) { s_settingsState.confirm_reinstall_daemon = false; } if (doConfirm) { app->reinstallBundledDaemon(); s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap s_settingsState.confirm_reinstall_daemon = false; } EndOverlayDialog(); } } // Confirm: restart daemon (briefly drops the connection to apply changed options) if (s_settingsState.confirm_restart_daemon) { if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::WarningVec4()); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool doCancel = false, doConfirm = false; material::DialogConfirmFooter(TrId("cancel", "restartd_cancel").c_str(), TrId("settings_restart_daemon", "restartd_confirm").c_str(), false, doCancel, doConfirm); if (doCancel) { s_settingsState.confirm_restart_daemon = false; } if (doConfirm) { s_settingsState.debug_cats_dirty = false; app->restartDaemon(); s_settingsState.confirm_restart_daemon = false; } EndOverlayDialog(); } } // Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe) if (s_settingsState.confirm_lite_redownload) { if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::WarningVec4()); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg")); ImGui::Spacing(); ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_lite_redownload_safe")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); bool doCancel = false, doConfirm = false; material::DialogConfirmFooter(TrId("cancel", "lite_redl_cancel").c_str(), TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), false, doCancel, doConfirm); if (doCancel) { s_settingsState.confirm_lite_redownload = false; } if (doConfirm) { if (auto* lite = app->liteWallet()) lite->startRescan(); s_settingsState.confirm_lite_redownload = false; } EndOverlayDialog(); } } } void SweepOpenDebugGate(bool open) { s_settingsState.debug_gate_open = open; if (open) { s_settingsState.debug_gate_buf[0] = '\0'; s_settingsState.debug_gate_err.clear(); s_settingsState.debug_gate_verifying = false; } } } // namespace ui } // namespace dragonx