// 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/console_tab.h" #include "../../util/i18n.h" #include "../../util/platform.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/type.h" #include "../material/colors.h" #include "../windows/validate_address_dialog.h" #include "../windows/address_book_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 "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; // 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; bool effects_expanded = false; bool tools_expanded = false; bool rpc_expanded = false; // Node & Security: reveal the RPC connection fields 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; // Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input. static int liteSeedWordCount(const char* s) { int words = 0; bool inWord = false; for (; s && *s; ++s) { const bool space = std::isspace(static_cast(*s)) != 0; if (space) inWord = false; else if (!inWord) { inWord = true; ++words; } } return words; } 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; input.request.restoreRequest.seedPhrase = 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}; // 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 (words != 24) { s_settingsState.lite_lifecycle_status = "Enter all 24 seed words to restore (got " + std::to_string(words) + ")"; 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 = "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() ? "Could not start the operation" : lite->status().message; Notifications::instance().warning(s_settingsState.lite_lifecycle_status); } return; } const auto result = wallet::executeLiteWalletLifecycleUiRequest(*app->settings(), input); s_settingsState.lite_lifecycle_summary = result.requestSummaryRedacted; if (result.ok) { s_settingsState.lite_lifecycle_status = "Ready"; } else { s_settingsState.lite_lifecycle_status = result.error.empty() ? wallet::liteWalletLifecycleUiExecutionStatusName(result.status) : result.error; 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 s_settingsState.blur_amount = settings->getBlurMultiplier(); 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; s_settingsState.rpc_plaintext_remote = rpc::Connection::usesPlaintextRemote(rpc::Connection::autoDetectConfig()); // 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(); } // ============================================================================ // Settings Page Renderer // ============================================================================ 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 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; } else if (s_settingsState.low_spec_snapshot.valid) { // Hotkey turned low-spec OFF — restore snapshot 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; s_settingsState.low_spec_snapshot.valid = 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; 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); // Scrollable content area — NoBackground matches other tabs ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 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; ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); ImFont* sub1 = Type().subtitle1(); // ==================================================================== // THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit // which breaks BeginCombo popup rendering in some ImGui versions) // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size; float compactBP = S.drawElement("components.settings-page", "compact-breakpoint").size; bool wideLayout = availWidth >= compactBP; 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; } } // --- 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 between wide and narrow paths) --- 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(); } } } }; if (wideLayout) { // ============================================================ // Wide: 3 combos on one row + compact 3-column effects grid // ============================================================ // --- Combo row: Theme | Layout | Language [Refresh] --- { ImGui::PushFont(body2); float lblGap = Layout::spacingXs(); float lblThemeW = ImGui::CalcTextSize(TR("theme")).x + lblGap; float lblLayoutW = ImGui::CalcTextSize(TR("balance_layout")).x + lblGap; float lblLangW = ImGui::CalcTextSize(TR("language")).x + lblGap; float totalFixed = lblThemeW + lblLayoutW + lblLangW + comboGap * 2 + Layout::spacingSm() + refreshBtnW; float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("theme")); ImGui::SameLine(0, lblGap); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##Theme", active_preview.c_str())) { renderThemeComboPopup(); ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_hotkey")); ImGui::SameLine(0, comboGap); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("balance_layout")); ImGui::SameLine(0, lblGap); ImGui::SetNextItemWidth(comboW); 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(s_settingsState.balance_layout); app->settings()->save(); } } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_layout_hotkey")); ImGui::SameLine(0, comboGap); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("language")); ImGui::SameLine(0, lblGap); ImGui::SetNextItemWidth(comboW); 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")); ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); Notifications::instance().info("Theme list refreshed"); } if (ImGui::IsItemHovered()) { material::Tooltip(TR("tt_scan_themes"), schema::SkinManager::getUserSkinsDirectory().c_str()); } ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Font Scale slider (always visible) --- { ImGui::PushFont(body2); ImGui::TextUnformatted(TR("font_scale")); float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW); 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::spacingSm())); // --- Collapsible: Advanced Effects... --- { const char* arrow = s_settingsState.effects_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("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) { s_settingsState.effects_expanded = !s_settingsState.effects_expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects")); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = body2; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); } if (s_settingsState.effects_expanded) { ImGui::PushFont(body2); // Checkbox row: Low-spec | Console scanline | Theme effects | Gradient background 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) { 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; effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); effects::ImGuiAcrylic::SetUIOpacity(1.0f); effects::ThemeEffects::instance().setEnabled(false); ConsoleTab::s_scanline_enabled = false; } else if (s_settingsState.low_spec_snapshot.valid) { 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; 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; } saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec")); ImGui::SameLine(0, Layout::spacingLg()); 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")); ImGui::SameLine(0, Layout::spacingLg()); 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); ImGui::SameLine(0, Layout::spacingLg()); 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")); ImGui::SameLine(0, Layout::spacingLg()); 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")); // 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::max(effCtrlMinW, halfW); 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 * 25.0f); if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, 4.0f, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < 0.15f) 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 } else { // ============================================================ // Narrow: stacked combos + 2-column effects (original layout) // ============================================================ // --- Theme row --- { ImGui::PushFont(body2); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("theme")); ImGui::SameLine(labelW); float themeComboW = std::max(S.drawElement("components.settings-page", "theme-combo-min-width").size, availWidth - pad * 2 - labelW - refreshBtnW - Layout::spacingSm()); ImGui::SetNextItemWidth(themeComboW); if (ImGui::BeginCombo("##Theme", active_preview.c_str())) { renderThemeComboPopup(); ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_hotkey")); if (active_is_custom) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*"); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_theme")); } ImGui::SameLine(); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); Notifications::instance().info("Theme list refreshed"); } if (ImGui::IsItemHovered()) { material::Tooltip(TR("tt_scan_themes"), schema::SkinManager::getUserSkinsDirectory().c_str()); } ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // --- Balance Layout row --- { ImGui::PushFont(body2); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("balance_layout")); ImGui::SameLine(labelW); ImGui::SetNextItemWidth(std::max(180.0f, inputW)); 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(s_settingsState.balance_layout); app->settings()->save(); } } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_layout_hotkey")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // --- Language row --- { ImGui::PushFont(body2); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("language")); ImGui::SameLine(labelW); ImGui::SetNextItemWidth(inputW); 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")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Font Scale slider (always visible) --- { ImGui::PushFont(body2); ImGui::TextUnformatted(TR("font_scale")); float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, availWidth - pad * 2); 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::spacingSm())); // --- Collapsible: Advanced Effects... --- { const char* arrow = s_settingsState.effects_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)); { float narrowContentW = availWidth - pad * 2; ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) { s_settingsState.effects_expanded = !s_settingsState.effects_expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects")); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = body2; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); } if (s_settingsState.effects_expanded) { ImGui::PushFont(body2); 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) { 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; effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); effects::ImGuiAcrylic::SetUIOpacity(1.0f); effects::ThemeEffects::instance().setEnabled(false); ConsoleTab::s_scanline_enabled = false; } else if (s_settingsState.low_spec_snapshot.valid) { 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; 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; } saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec")); if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_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_alt")); 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); 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")); ImGui::SameLine(0, Layout::spacingLg()); if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").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")); float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, availWidth - pad * 2.0f); ImGui::TextUnformatted(TR("acrylic")); 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 * 25.0f); if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, 4.0f, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < 0.15f) 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")); ImGui::TextUnformatted(TR("noise")); ImGui::SetNextItemWidth(ctrlW); { char noise_fmt[16]; if (s_settingsState.noise_opacity < 0.01f) snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off")); else snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_settingsState.noise_opacity * 100.0f); if (ImGui::SliderFloat("##NoiseOpacity", &s_settingsState.noise_opacity, 0.0f, 1.0f, noise_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise")); ImGui::TextUnformatted(TR("ui_opacity")); ImGui::SetNextItemWidth(ctrlW); { char uiop_fmt[16]; snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", s_settingsState.ui_opacity * 100.0f); if (ImGui::SliderFloat("##UIOpacity", &s_settingsState.ui_opacity, 0.3f, 1.0f, uiop_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity")); ImGui::TextUnformatted(TR("window_opacity")); ImGui::SetNextItemWidth(ctrlW); { char winop_fmt[16]; snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", s_settingsState.window_opacity * 100.0f); if (ImGui::SliderFloat("##WindowOpacity", &s_settingsState.window_opacity, 0.3f, 1.0f, winop_fmt, ImGuiSliderFlags_AlwaysClamp)) { } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity")); ImGui::EndDisabled(); // low-spec ImGui::PopFont(); } // s_settingsState.effects_expanded } // Bottom padding ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); // Draw glass panel behind content (auto-sized) ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // WALLET — card (privacy/daemon toggles + collapsible tools) // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; // Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit { const bool showDaemonOptions = app->supportsFullNodeLifecycleActions(); float cbSpacing = Layout::spacingMd(); float fh = ImGui::GetFrameHeight(); float inner = ImGui::GetStyle().ItemInnerSpacing.x; auto cbW = [&](const char* label) { return fh + inner + ImGui::CalcTextSize(label).x; }; float totalW = cbW(TR("save_z_transactions")) + cbSpacing + cbW(TR("auto_shield")) + cbSpacing + cbW(TR("use_tor")) + cbSpacing; if (showDaemonOptions) { totalW += cbW(TR("keep_daemon")) + cbSpacing + cbW(TR("stop_external")) + cbSpacing; } totalW += cbW(TR("verbose_logging")); float scale = (totalW > contentW) ? contentW / totalW : 1.0f; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); float sp = cbSpacing * scale; ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &s_settingsState.save_ztxs); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx")); ImGui::SameLine(0, sp); ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &s_settingsState.auto_shield); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield")); ImGui::SameLine(0, sp); ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor")); if (showDaemonOptions) { ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) { saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_keep_daemon")); ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) { saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external")); } ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &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")); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Tools & Actions... --- { const char* arrow = s_settingsState.tools_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("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) { s_settingsState.tools_expanded = !s_settingsState.tools_expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions")); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = body2; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); } if (s_settingsState.tools_expanded) { float btnSpacing = Layout::spacingMd(); int btnsPerRow = (contentW >= 600.0f) ? 3 : 2; float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow; float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f); bw = std::max(minBtnW, bw); if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button"))) AddressBookDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book")); ImGui::SameLine(0, btnSpacing); if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button"))) ValidateAddressDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_validate")); if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button"))) RequestPaymentDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_request_payment")); if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); } if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button"))) ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining")); ImGui::SameLine(0, btnSpacing); if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button"))) ShieldDialog::show(ShieldDialog::Mode::MergeToAddress); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge")); if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) { s_settingsState.confirm_clear_ztx = true; } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clear_ztx")); } // --- Backup & Data (merged in from its former standalone card) --- ImGui::Dummy(ImVec2(0, Layout::spacingMd())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f); const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")}; const char* t1[] = { TR("tt_import_key"), TR("tt_export_key"), TR("tt_export_all"), TR("tt_backup"), TR("tt_export_csv") }; const bool showFullNodeLifecycleActions = app->supportsFullNodeLifecycleActions(); const char* wizLabel = TR("setup_wizard"); const char* bsLabel = TR("download_bootstrap"); float sp = Layout::spacingSm(); ImFont* btnFont = S.resolveFont("button"); float btnPadX = btnPad * 2; float naturalW = 0; for (int i = 0; i < 5; i++) naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX; float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f; float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f; float totalW = naturalW + sp * 5; if (showFullNodeLifecycleActions) totalW += wizW + bsW + sp * 2; float scale = (totalW > contentW) ? contentW / totalW : 1.0f; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); float scaledSp = sp * scale; if (TactileButton(r1[0], ImVec2(0, 0), btnFont)) app->showImportKeyDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[1], ImVec2(0, 0), btnFont)) app->showExportKeyDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[1]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[2], ImVec2(0, 0), btnFont)) ExportAllKeysDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[2]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[3], ImVec2(0, 0), btnFont)) app->showBackupDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[3]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[4], ImVec2(0, 0), btnFont)) ExportTransactionsDialog::show(); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[4]); if (showFullNodeLifecycleActions) { // Right-align Setup Wizard + Download Bootstrap. float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f; float curX = ImGui::GetCursorScreenPos().x; float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2; float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2; float rightEdge = cardMin.x + availWidth - pad; float rightGroupW = bsBtnW + scaledSp + wizBtnW; float groupX = rightEdge - rightGroupW; if (groupX > curX) { ImGui::SameLine(0, 0); ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y)); } else { ImGui::SameLine(0, scaledSp); } if (TactileButton(bsLabel, ImVec2(0, 0), btnFont)) BootstrapDownloadDialog::show(app); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_download_bootstrap")); ImGui::SameLine(0, scaledSp); if (TactileButton(wizLabel, ImVec2(0, 0), btnFont)) app->restartWizard(); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wizard")); } if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // NODE & SECURITY — card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + 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) { // 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. 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; } } if (includeRpcEncrypt) ImGui::SameLine(0, Layout::spacingLg()); else ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock")); ImGui::SameLine(0, Layout::spacingXs()); ImGui::PushItemWidth(comboW); 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")); ImGui::PopItemWidth(); // 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::SameLine(0, Layout::spacingMd()); 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::SameLine(0, Layout::spacingMd()); 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(), "Lite servers are managed in the Network tab."); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) { s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded; } if (s_settingsState.lite_lifecycle_expanded) { const char* lifecycleLabels[] = {"Create", "Open", "Restore"}; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("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 (s_settingsState.lite_lifecycle_operation == 1 || s_settingsState.lite_lifecycle_operation == 2) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Wallet"); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path, sizeof(s_settingsState.lite_wallet_path)); } if (s_settingsState.lite_lifecycle_operation == 2) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Seed"); 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); // 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), "%d / 24 words", 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("Birthday"); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.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(), "Block height to start scanning from. Leave 0 if unknown (slower full scan)."); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Account"); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.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("Overwrite##LiteRestoreOverwrite", &s_settingsState.lite_restore_overwrite); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Passphrase"); 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); // 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 = "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("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) { evaluateLiteLifecycleRequestFromPageState(app); } 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 (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) { 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().text(TypeStyle::Body2, "Backup & keys"); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) { 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 = "Seed phrase — the ONLY way to restore your wallet. " "Write it down, store it offline, never share it."; 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 } ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) { 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 = "Private keys — anyone with these can spend your funds"; s_settingsState.lite_backup_status.clear(); } else { s_settingsState.lite_export_label.clear(); s_settingsState.lite_backup_status = r.error; } wallet::secureWipeLiteSecret(r.privateKeysJson); } // 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), "Birthday: %llu (back this up too)", 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("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) { app->copySecretToClipboard(s_settingsState.lite_export_secret); } 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("Save to file##LiteExportSave", 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 ? ("Saved (plaintext, owner-only) to " + path) : ("Could not write " + path); } if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton("Hide & wipe##LiteExportHide", 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; } } // Import a spending/viewing key (history appears after the next sync). ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Import key"); 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); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key); sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); s_settingsState.lite_backup_status = r.ok ? "Key imported — run a sync to scan its history" : r.error; } 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().text(TypeStyle::Body2, "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("Passphrase"); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Encrypt wallet##LiteEncrypt", 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 ? "Wallet encrypted" : r.error; } } else { if (wstate.isLocked()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Unlock"); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Unlock##LiteUnlock", 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 ? "Wallet unlocked" : "Unlock failed"; } } else { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) { s_settingsState.lite_encryption_status = app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed"; } } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Passphrase"); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Remove encryption##LiteDecrypt", 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 ? "Encryption removed" : r.error; } } 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().text(TypeStyle::Body2, TR("lite_maintenance")); const bool scanning = app->liteWallet()->scanInProgress(); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::BeginDisabled(scanning); if (TactileButton(TR("lite_redownload_blocks"), ImVec2(0, 0), S.resolveFont("button"))) { 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)); } else { // ========================= FULL NODE ========================= // One full-width column; each sub-section's controls flow left-to-right. ImGui::PushFont(body2Info); const float spMd = Layout::spacingMd(); // -------------------- NODE / DATA -------------------- Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { std::string dirPath = util::Platform::getDragonXDataDir(); std::string walletPath = dirPath + "wallet.dat"; uint64_t wallet_size = util::Platform::getFileSize(walletPath); std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); // Right-aligned "Open data folder" button trailing the row; the path // gets whatever width is left after the data-dir label, wallet-size // label+value, and the trailing button. ImFont* nodeBtnFont = S.resolveFont("button"); if (!nodeBtnFont) nodeBtnFont = Type().button(); // Measure with the BUTTON font (not the current font) so the width matches // the real TactileButton size — the old estimate mis-measured and the button // overflowed the card's right edge, worse at high DPI. float openBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0, TR("settings_open_data_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f; // "Open app folder" (the ObsidianDragon config folder), left of the data button. float appBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0, TR("settings_open_app_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f; float openGroupW = appBtnW + spMd + openBtnW; float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x; float sizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x; float sizeValueW = ImGui::CalcTextSize(size_str.c_str()).x; // Space reserved to the right of the path for the size block + both buttons. float trailW = sizeLabelW + Layout::spacingXs() + sizeValueW + spMd + openGroupW; float availForPath = contentW - dataDirLabelW - Layout::spacingXs() - spMd - trailW; if (availForPath < 60.0f) availForPath = contentW - dataDirLabelW - Layout::spacingXs(); ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary()); ImVec4 linkHoverCol = linkCol; linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f); float rowTopY = ImGui::GetCursorScreenPos().y; // top of the data-dir row ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("settings_data_dir")); ImGui::SameLine(0, Layout::spacingXs()); ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str()); bool hovered = false; ImGui::AlignTextToFramePadding(); if (pathSize.x > availForPath && availForPath > 0) { float scale = availForPath / pathSize.x; float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f); ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize); ImGui::TextColored(linkCol, "%s", dirPath.c_str()); hovered = ImGui::IsItemHovered(); if (hovered) { ImVec2 tMin = ImGui::GetItemRectMin(); ImVec2 tMax = ImGui::GetItemRectMax(); dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol)); } ImGui::SetWindowFontScale(1.0f); } else { ImGui::TextColored(linkCol, "%s", dirPath.c_str()); hovered = ImGui::IsItemHovered(); if (hovered) { ImVec2 tMin = ImGui::GetItemRectMin(); ImVec2 tMax = ImGui::GetItemRectMax(); dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol)); } } if (hovered) { material::Tooltip("%s", TR("tt_open_dir")); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath); // Wallet Size label + value, trailing the path on the same row. ImGui::SameLine(0, spMd); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("settings_wallet_size_label")); ImGui::SameLine(0, Layout::spacingXs()); ImGui::AlignTextToFramePadding(); if (wallet_size > 0) ImGui::TextUnformatted(size_str.c_str()); else ImGui::TextDisabled("%s", TR("settings_not_found")); // Folder buttons: [Open app folder] [Open data folder], right-aligned as a // group on the data-dir row (rowTopY), each an EXPLICIT width so the group's // right edge lands exactly on the card edge (no overflow at any DPI). float rowY = rowTopY; float rightEdge = sectionOrigin.x + contentW; float groupX = rightEdge - openGroupW; float afterSizeX = ImGui::GetItemRectMax().x + spMd; ImGui::SameLine(0, spMd); if (groupX > afterSizeX) ImGui::SetCursorScreenPos(ImVec2(groupX, rowY)); // App (ObsidianDragon config) folder — opens ~/.config/ObsidianDragon etc. if (TactileButton(TR("settings_open_app_dir"), ImVec2(appBtnW, 0), nodeBtnFont)) { util::Platform::openFolder(util::Platform::getObsidianDragonDir()); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_app_dir")); ImGui::SameLine(0, spMd); if (TactileButton(TR("settings_open_data_dir"), ImVec2(openBtnW, 0), nodeBtnFont)) { 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); 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"); float labelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(portLbl).x + ImGui::CalcTextSize(userLbl).x + ImGui::CalcTextSize(passLbl).x; const bool fourAcross = contentW >= 700.0f; auto field = [&](const char* label, const char* id, char* buf, size_t bufSz, float inputW, bool password) { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(label); ImGui::SameLine(0, Layout::spacingXs()); ImGui::SetNextItemWidth(inputW); ImGui::InputText(id, buf, bufSz, password ? ImGuiInputTextFlags_Password : 0); }; if (fourAcross) { // fieldW = (contentW - labels - per-field label gaps - 3 inter-field gaps) / 4 float inputTotal = contentW - labelsW - Layout::spacingXs() * 4 - spMd * 3; float inputW = std::max(60.0f, std::floor(inputTotal / 4.0f)); field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host")); ImGui::SameLine(0, spMd); field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port")); ImGui::SameLine(0, spMd); field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); ImGui::SameLine(0, spMd); field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass")); } else { // 2x2: two fields per row. float halfLabelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(userLbl).x; float inputW = std::max(60.0f, std::floor( (contentW - halfLabelsW - Layout::spacingXs() * 2 - spMd) / 2.0f)); field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host")); ImGui::SameLine(0, spMd); field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port")); ImGui::SameLine(0, spMd); field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, 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("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS."); 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); // -------------------- DAEMON BINARY -------------------- if (app->supportsFullNodeLifecycleActions()) { ImFont* dbBtnFont = S.resolveFont("button"); 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); }; const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()); ImGui::Dummy(ImVec2(0, Layout::spacingLg())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // Inline single line: Installed ( · ) · Bundled () · ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("daemon_installed")); ImGui::SameLine(0, Layout::spacingXs()); if (inst.exists) { ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str()); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(dbDim, "(%s · %s)", util::Platform::formatFileSize(inst.size).c_str(), fmtDate(inst.modifiedEpoch).c_str()); } else { ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed")); } ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(dbDim, "·"); ImGui::SameLine(0, Layout::spacingXs()); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("daemon_bundled")); ImGui::SameLine(0, Layout::spacingXs()); if (bun.available) { ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str()); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(dbDim, "(%s)", util::Platform::formatFileSize(bun.size).c_str()); } else { ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled")); } if (bun.available) { const bool sameSize = inst.exists && inst.size == bun.size; ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(dbDim, "·"); ImGui::SameLine(0, Layout::spacingXs()); if (!inst.exists) ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing")); else if (sameSize) ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match")); else ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ")); } // In-app node update: download + verify the latest dragonxd from the project Gitea. // Refresh the cached daemon info once an install has completed. if (ui::DaemonUpdateDialog::consumeInstalled()) s_settingsState.daemon_info_loaded = false; // Action row: Check for updates | Refresh | Test connection on the left, // with the four maintenance actions right-aligned on the same row. ImGui::Dummy(ImVec2(0, Layout::spacingXs())); float daemonBtnRowY = ImGui::GetCursorScreenPos().y; if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) { ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check")); ImGui::SameLine(0, spMd); if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) { s_settingsState.daemon_info_loaded = false; } ImGui::SameLine(0, spMd); ImGui::BeginDisabled(!app->isConnected()); if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) { 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("RPC connection OK"); }; } catch (const std::exception& e) { std::string err = e.what(); return [err]() { Notifications::instance().error("RPC error: " + err); }; } }); } else { Notifications::instance().warning("Not connected to daemon"); } } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); ImGui::EndDisabled(); // The four maintenance actions (Install bundled / Rescan / Delete blockchain // / Repair wallet), right-aligned on the SAME row as Test connection — no // separate "Advanced" toggle. Every BeginDisabled/EndDisabled predicate is // preserved exactly. Wraps to a right-aligned next row when too narrow. auto renderDaemonMaintenanceButtons = [&]() { ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) { s_settingsState.confirm_reinstall_daemon = true; } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); ImGui::EndDisabled(); ImGui::SameLine(0, spMd); ImGui::BeginDisabled(!app->isConnected()); if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) { s_settingsState.confirm_rescan = true; // Re-probe the available block range each time the dialog is opened. 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::SameLine(0, spMd); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) { s_settingsState.confirm_delete_blockchain = true; } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); ImGui::SameLine(0, spMd); if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) { s_settingsState.confirm_repair_wallet = true; } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); ImGui::EndDisabled(); }; { // Group width (button font) to right-align the four maintenance actions. const char* mLabels[] = { TR("daemon_install_bundled"), TR("rescan"), TR("delete_blockchain"), TR("repair_wallet") }; float groupW = spMd * 3.0f; for (const char* l : mLabels) groupW += dbBtnFont->CalcTextSizeA(dbBtnFont->LegacySize, FLT_MAX, 0, l).x + ImGui::GetStyle().FramePadding.x * 2.0f; float afterTestX = ImGui::GetItemRectMax().x + spMd; // right edge of Test connection float rightEdge = sectionOrigin.x + contentW; float groupX = rightEdge - groupW; if (groupX >= afterTestX) { // Fits on the Test-connection row, right-aligned. ImGui::SameLine(0, 0); ImGui::SetCursorScreenPos(ImVec2(groupX, daemonBtnRowY)); } else { // Too narrow: wrap to the next row, right-aligned (clamped to left edge). ImGui::Dummy(ImVec2(0, Layout::spacingXs())); float wrapX = std::max(sectionOrigin.x, rightEdge - groupW); ImGui::SetCursorScreenPos(ImVec2(wrapX, ImGui::GetCursorScreenPos().y)); } renderDaemonMaintenanceButtons(); } } ImGui::PopFont(); // Advance to the true bottom of the single column. ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y)); } } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // EXPLORER & OPTIONS — full-width card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_section")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; ImGui::PushFont(body2); // Row 1: Transaction URL | Address URL (side-by-side) float halfW = (contentW - Layout::spacingLg()) * 0.5f; float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs(); float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs(); float inputTxW = std::max(80.0f, halfW - lblTxW); float inputAddrW = std::max(80.0f, halfW - lblAddrW); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("transaction_url")); ImGui::SameLine(0, Layout::spacingXs()); ImGui::SetNextItemWidth(inputTxW); ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer)); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url")); ImGui::SameLine(pad + halfW + Layout::spacingLg()); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("address_url")); ImGui::SameLine(0, Layout::spacingXs()); ImGui::SetNextItemWidth(inputAddrW); 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, Layout::spacingXs())); // Row 2: Checkboxes + Block Explorer button (on one line) 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")); ImGui::SameLine(0, Layout::spacingLg()); 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::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // ABOUT — card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("about")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); // Logo on the left side of the about card. Deferred: reserve horizontal space // now, but draw the image after the card's final height is known so it scales to // the full card height (no empty space below it). ImTextureID logoTex = app->getLogoTexture(); float logoAreaW = 0; ImVec2 logoPos = ImGui::GetCursorScreenPos(); float logoAspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f; float logoReserveH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(150.0f); if (logoTex != 0) { logoAreaW = logoReserveH * logoAspect + Layout::spacingLg(); ImGui::Indent(logoAreaW); } float contentW = availWidth - pad * 2 - logoAreaW; // App name + version on same line ImGui::PushFont(sub1); ImGui::TextUnformatted(DRAGONX_APP_NAME); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingLg()); ImGui::PushFont(body2); snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION); ImGui::TextUnformatted(buf); ImGui::SameLine(0, Layout::spacingLg()); snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION); ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf); ImGui::PopFont(); // Daemon version { const auto& st = app->state(); if (st.daemon_version > 0) { int dmaj = st.daemon_version / 1000000; int dmin = (st.daemon_version / 10000) % 100; int dpat = (st.daemon_version / 100) % 100; ImGui::PushFont(body2); snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf); ImGui::PopFont(); } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushFont(body2); ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW); ImGui::TextUnformatted(TR("settings_about_text")); ImGui::PopTextWrapPos(); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushFont(capFont); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_copyright")); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // Top of the (full-width) buttons row — the deferred logo is clamped to end above // this Y so the tall left-column logo never overlaps the buttons. float aboutButtonsTopY = ImGui::GetCursorScreenPos().y; // Buttons — consistent equal-width row (full card width) if (logoAreaW > 0) { ImGui::Unindent(logoAreaW); } { float fullContentW = availWidth - pad * 2; float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f; if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://dragonx.is"); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_website")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues"); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { saveSettingsPageState(app->settings()); Notifications::instance().success("Settings saved"); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { if (app->settings()) { loadSettingsPageState(app->settings()); Notifications::instance().info("Settings reloaded from disk"); } } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings")); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); // Draw the logo now that the card height is known — aspect-preserved, capped to the // reserved width, and clamped to end just above the buttons row so it never overlaps // the text or the buttons. Still on the content channel (1), above the glass. if (logoTex != 0) { float reserveW = logoReserveH * logoAspect; // Height available above the buttons row (the fix for the logo/buttons overlap). float logoBottomLimit = aboutButtonsTopY - Layout::spacingSm(); float logoH = std::max(16.0f, logoBottomLimit - logoPos.y); float logoW = logoH * logoAspect; if (logoW > reserveW) { logoW = reserveW; logoH = (logoAspect > 0.0f) ? logoW / logoAspect : logoH; } dl->AddImage(logoTex, logoPos, ImVec2(logoPos.x + logoW, logoPos.y + logoH)); } dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // DEBUG LOGGING — collapsible card (full-node only: these are dragonxd // daemon debug= categories written to DRAGONX.conf; lite has no daemon) // ==================================================================== if (app->supportsFullNodeLifecycleActions()) { // 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()))) { s_settingsState.debug_expanded = !s_settingsState.debug_expanded; } 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())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); 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")); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } } // --- 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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning")); 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(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) { s_settingsState.confirm_clear_ztx = false; } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); if (ImGui::Button(TrId("clear_anyway", "clear_ztx_btn").c_str(), ImVec2(btnW, 40))) { std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; if (util::Platform::deleteFile(ztx_file)) { Notifications::instance().success("Z-transaction history cleared"); } else { Notifications::instance().info("No history file found"); } s_settingsState.confirm_clear_ztx = false; } ImGui::PopStyleColor(2); 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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning")); 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(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) { s_settingsState.confirm_delete_blockchain = false; } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) { if (app->supportsFullNodeLifecycleActions()) app->deleteBlockchainData(); s_settingsState.confirm_delete_blockchain = false; } ImGui::PopStyleColor(2); 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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); 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 (ImGui::Button(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) { s_settingsState.confirm_rescan = false; } ImGui::SameLine(); ImGui::BeginDisabled(detecting); if (ImGui::Button(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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); 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(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TrId("cancel", "repair_wallet_cancel").c_str(), ImVec2(btnW, 40))) { s_settingsState.confirm_repair_wallet = false; } ImGui::SameLine(); if (ImGui::Button(TrId("repair_wallet", "repair_wallet_confirm").c_str(), ImVec2(btnW, 40))) { 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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); 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(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) { s_settingsState.confirm_reinstall_daemon = false; } ImGui::SameLine(); if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) { 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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TrId("cancel", "restartd_cancel").c_str(), ImVec2(btnW, 40))) { s_settingsState.confirm_restart_daemon = false; } ImGui::SameLine(); if (ImGui::Button(TrId("settings_restart_daemon", "restartd_confirm").c_str(), ImVec2(btnW, 40))) { 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)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); 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(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TrId("cancel", "lite_redl_cancel").c_str(), ImVec2(btnW, 40))) { s_settingsState.confirm_lite_redownload = false; } ImGui::SameLine(); if (ImGui::Button(TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), ImVec2(btnW, 40))) { if (auto* lite = app->liteWallet()) lite->startRescan(); s_settingsState.confirm_lite_redownload = false; } EndOverlayDialog(); } } } } // namespace ui } // namespace dragonx