// 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 namespace dragonx { namespace ui { using namespace material; // Top of the Acrylic slider's range (the blur-radius multiplier at 100%). The panels frost at full // alpha now, so this only controls how BLURRY the max is — kept gentle (the old max of 4.0 was way // too strong). The slider still reads 0–100%; this is just what 100% maps to. static constexpr float kAcrylicMaxBlur = 1.25f; // 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; 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 lite_lifecycle_expanded = false; int lite_lifecycle_operation = 0; char lite_wallet_path[256] = ""; char lite_lifecycle_passphrase[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. char lite_enc_pass[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 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; 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); 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); 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())); } 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_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; } } // 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(config::Settings* settings) { 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.noise_opacity = settings->getNoiseOpacity(); s_settingsState.gradient_background = settings->getGradientBackground(); s_settingsState.balance_layout = settings->getBalanceLayout(); s_settingsState.scanline_enabled = settings->getScanlineEnabled(); ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled; s_settingsState.theme_effects_enabled = settings->getThemeEffectsEnabled(); s_settingsState.low_spec_mode = settings->getLowSpecMode(); effects::setLowSpecMode(s_settingsState.low_spec_mode); 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(); // 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 loaded visual effects settings effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount); effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity); effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled); 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 settings->setAcrylicEnabled(s_settingsState.acrylic_enabled); settings->setAcrylicQuality(s_settingsState.blur_amount > 0.001f ? static_cast(effects::AcrylicQuality::Low) : static_cast(effects::AcrylicQuality::Off)); settings->setBlurMultiplier(s_settingsState.blur_amount); settings->setUIOpacity(s_settingsState.ui_opacity); settings->setWindowOpacity(s_settingsState.window_opacity); settings->setNoiseOpacity(s_settingsState.noise_opacity); settings->setGradientBackground(s_settingsState.gradient_background); settings->setScanlineEnabled(s_settingsState.scanline_enabled); settings->setThemeEffectsEnabled(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); // 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; }; static const T tabs[] = { {TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"}, {TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"}, {TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"}, {TAB_ABOUT, "About", "##stabT"}, }; 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->settings()); } // 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, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::BeginDisabled(true); std::string lbl = skin.name + " (invalid)"; 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, 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())); // --- 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); 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); 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); 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)) { 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; }; CB(TrId("save_z_transactions", "save_ztx"), &s_settingsState.save_ztxs); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx")); CB(TrId("auto_shield", "auto_shld"), &s_settingsState.auto_shield); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield")); // O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox // above only governs the wallet's own fallback shielder (which defers to the node). Nothing // renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged. if (app && app->daemonAutoShieldProbed()) { if (app->daemonAutoShieldActive()) { ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node")); if (!app->daemonAutoShieldAddress().empty()) ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str()); } else if (!app->daemonAutoShieldDisabledReason().empty()) { ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str()); } } CB(TrId("use_tor", "tor"), &s_settingsState.use_tor); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor")); 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")); } 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")); 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")); 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; 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(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%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(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%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(); 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)); if (!hasPIN) { if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinSetupDialog(); if (ImGui::IsItemHovered()) 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 { if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinChangeDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pin")); ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinRemoveDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_pin")); ImGui::SameLine(0, Layout::spacingMd()); ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%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, 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, 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")); // 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)); const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending; if (liteLifecycleBusy) 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 (liteLifecycleBusy) 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)); 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 = "Enter a private key to import."; } else { const auto r = app->liteWallet()->importKey(liteKey); sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); s_settingsState.lite_backup_status = r.ok ? TR("lite_key_imported") : r.error; } } if (ImGui::IsItemHovered()) material::Tooltip("%s", 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")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error; } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt")); } else { 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"))) { const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed"); } 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)); if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass); sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error; } if (ImGui::IsItemHovered()) material::Tooltip("%s", 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)); ImGui::BeginDisabled(scanning); 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 if (!s_settingsState.lite_export_secret.empty()) { // Wallet closed while a backup/secret was revealed — don't leave it in memory. wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); 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()); } // 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()) { if (!s_settingsState.daemon_info_loaded) { s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo(); s_settingsState.daemon_info_loaded = true; } 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; // --- 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; } 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); ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer)); 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::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; ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees")); ImGui::SameLine(0, Layout::spacingLg()); ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices); 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")); } 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")); af.next(material::ActionButtonWidth(TR("reset_to_defaults"), ICON_MD_RESTORE)); if (material::ActionButton("##aboutreset", TR("reset_to_defaults"), ICON_MD_RESTORE, AT::Tertiary)) { if (app->settings()) { loadSettingsPageState(app->settings()); 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)); // ==================================================================== // 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") : TR("tt_debug_expand")); ImGui::PopStyleColor(3); // Draw overline label + arrow on top of the invisible button { ImFont* ovFont = Type().overline(); float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(headerPos.x, textY), OnSurfaceMedium(), TR("debug_logging")); // 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); } if (s_settingsState.debug_expanded) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); // 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("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())); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select")); ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note")); ImGui::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" }; static const char* debugTips[] = { "Peer address tracking and management", "Alert system messages", "Benchmark timings for operations", "Coin database read/write operations", "Berkeley DB operations", "Fee estimation algorithm", "HTTP RPC server activity", "Libevent networking library", "Lock contention debugging", "Transaction memory pool activity", "Network connections and messages", "Payment disclosure protocol", "Proof-of-work mining activity", "SOCKS5 proxy connections", "Block pruning operations", "Random number generation", "Blockchain reindexing progress", "RPC command processing", "Coin selection for transactions", "Tor integration and circuit info", "ZeroMQ notification system", "Shielded (z-addr) RPC operations" }; 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())); // "Restart daemon" button — only active when categories changed if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255)); ImFont* iconFont = Type().iconSmall(); if (iconFont) { ImGui::PushFont(iconFont); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(ICON_MD_INFO); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); } ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("settings_debug_changed")); ImGui::PopStyleColor(); ImGui::SameLine(); if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) { s_settingsState.confirm_restart_daemon = true; } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon")); } } // ---- 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 -= ImGui::GetIO().DeltaTime; 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"), ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg")); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%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; app->detectLowestAvailableBlockHeight([](bool ok, int lowest, bool fullHistory) { s_settingsState.rescan_height_detecting = false; s_settingsState.rescan_height_detected = true; 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"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); 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); ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height); if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0; } else { ImGui::TextWrapped("%s", TR("confirm_rescan_msg")); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%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))) { s_settingsState.confirm_rescan = false; } ImGui::SameLine(); ImGui::BeginDisabled(detecting); if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) { 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"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg")); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%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"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg")); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%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"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); 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"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg")); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%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