Files
ObsidianDragon/src/ui/pages/settings_page.cpp
DanS 4b9d6f7db5 fix(lite): rebuild controller on lite-server change (stale-settings audit HIGH)
The LiteWalletController was constructed once at App::init() with the lite
connection settings known at startup; changing the lite server in Settings
persisted to disk but never reached the live controller, so the new server had
no effect until the next launch.

Factor the construction into App::rebuildLiteWallet() and call it after a
successful server-selection save. The rebuild deliberately preserves a live
session: if a wallet is already open (and possibly mid-sync), it no-ops and the
new selection applies on the next controller build, rather than discarding the
open wallet and its uninterruptible in-flight sync.

Closes the last remaining HIGH from the session audit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:32:54 -05:00

2487 lines
129 KiB
C++

// 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 <sodium.h>
#include "../../util/logger.h"
#include "../windows/balance_tab.h"
#include "../windows/console_tab.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#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 "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <vector>
#include <set>
#include <filesystem>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstring>
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;
int lite_server_mode = 0;
char lite_server_url[256] = "https://lite.dragonx.is";
int lite_random_seed = 0;
bool lite_persist_selected_server = true;
std::vector<wallet::LiteServerEndpoint> lite_servers;
std::string lite_server_status;
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;
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<std::string> debug_categories;
bool debug_cats_dirty = false;
bool debug_expanded = false;
bool effects_expanded = false;
bool tools_expanded = false;
bool confirm_clear_ztx = false;
bool confirm_delete_blockchain = false;
effects::ScrollFadeShader fade_shader;
};
static SettingsPageState s_settingsState;
static void copyToSettingsBuffer(char* dest, std::size_t destSize, const std::string& value) {
if (!dest || destSize == 0) return;
std::strncpy(dest, value.c_str(), destSize - 1);
dest[destSize - 1] = '\0';
}
static wallet::LiteConnectionSettings liteConnectionSettingsFromPageState(config::Settings* settings) {
wallet::LiteConnectionSettings connectionSettings = settings
? wallet::liteConnectionSettingsFromAppSettings(*settings)
: wallet::defaultLiteConnectionSettings();
if (!s_settingsState.lite_servers.empty()) {
connectionSettings.servers = s_settingsState.lite_servers;
}
connectionSettings.selectionMode = s_settingsState.lite_server_mode == 1
? wallet::LiteServerSelectionMode::Random
: wallet::LiteServerSelectionMode::Sticky;
connectionSettings.stickyServerUrl = s_settingsState.lite_server_url;
connectionSettings.chainName = wallet::kDragonXLiteChainName;
connectionSettings.randomSelectionSeed = static_cast<std::size_t>(std::max(0, s_settingsState.lite_random_seed));
return connectionSettings;
}
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 saveLiteServerSelectionFromPageState(App* app) {
if (!app || !app->settings()) return;
const auto connectionSettings = liteConnectionSettingsFromPageState(app->settings());
wallet::LiteWalletServerSelectionUiExecutionInput input;
input.capabilities = app->walletCapabilities();
input.intent.selectedServerIntentProvided = true;
input.intent.selectionMode = connectionSettings.selectionMode;
input.intent.selectedServerUrl = connectionSettings.stickyServerUrl;
input.intent.randomSelectionSeed = connectionSettings.randomSelectionSeed;
input.intent.chainName = connectionSettings.chainName;
input.intent.replaceServers = true;
input.intent.servers = connectionSettings.servers;
input.persistence.settingsLoaded = true;
input.persistence.havePersistedSelectionIntent = true;
input.persistence.persistSelectedServer = s_settingsState.lite_persist_selected_server;
input.persistence.persistenceOwnerReady = true;
input.persistence.writeSettings = true;
input.ui.selectedServerDisplayReady = true;
input.ui.lifecycleUiOwnerReady = true;
input.ui.operationConfirmed = true;
input.ui.privateDataRedactionReady = true;
input.ui.syncPlannerFeedReady = true;
input.requireLifecycleReadiness = false;
const auto result = wallet::executeLiteWalletServerSelectionUi(*app->settings(), input);
if (result.settingsWritten) {
s_settingsState.lite_server_status = "Saved";
// Rebuild the lite controller so the newly-saved server actually takes effect (it is
// otherwise captured once at startup). No-op if a wallet is already open mid-session.
app->rebuildLiteWallet();
} else if (!result.error.empty()) {
s_settingsState.lite_server_status = result.error;
Notifications::instance().warning(result.error);
}
}
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<unsigned long long>(std::max(0, s_settingsState.lite_restore_birthday));
input.request.restoreRequest.account = static_cast<unsigned long long>(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};
// When a linked lite backend is present, execute the operation for real through the
// App-owned controller. Otherwise fall back to the validation-only adapter.
if (auto* lite = app->liteWallet()) {
wallet::LiteWalletLifecycleResult result;
switch (input.request.operation) {
case wallet::LiteWalletLifecycleOperation::CreateNew:
result = lite->createWallet(input.request.createRequest);
break;
case wallet::LiteWalletLifecycleOperation::OpenExisting:
result = lite->openWallet(input.request.openRequest);
break;
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
result = lite->restoreWallet(input.request.restoreRequest);
break;
}
// (Secret wiping is handled unconditionally by liteSecretScrubber at function exit.)
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);
}
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();
{
const auto liteSettings = wallet::liteConnectionSettingsFromAppSettings(*settings);
s_settingsState.lite_server_mode = liteSettings.selectionMode == wallet::LiteServerSelectionMode::Random ? 1 : 0;
copyToSettingsBuffer(s_settingsState.lite_server_url,
sizeof(s_settingsState.lite_server_url),
liteSettings.stickyServerUrl);
s_settingsState.lite_random_seed = static_cast<int>(liteSettings.randomSelectionSeed);
s_settingsState.lite_persist_selected_server = settings->getLitePersistSelectedServer();
s_settingsState.lite_servers = liteSettings.servers;
s_settingsState.lite_server_status.clear();
}
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<int>(effects::AcrylicQuality::Low) : static_cast<int>(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);
{
auto liteSettings = liteConnectionSettingsFromPageState(settings);
wallet::applyLiteConnectionSettingsToAppSettings(*settings, liteSettings);
settings->setLitePersistSelectedServer(s_settingsState.lite_persist_selected_server);
}
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<const char*> 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))
ImGui::SetTooltip("%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())
ImGui::SetTooltip("%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())
ImGui::SetTooltip("%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<int>(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()) ImGui::SetTooltip("%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()) {
ImGui::SetTooltip(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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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())
ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) {
ImGui::SetTooltip(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())
ImGui::SetTooltip("%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<int>(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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%s", TR("tt_auto_shield"));
ImGui::SameLine(0, sp);
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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())
ImGui::SetTooltip("%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())
ImGui::SetTooltip("%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())
ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
}
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));
// ====================================================================
// BACKUP & DATA — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data"));
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 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()) ImGui::SetTooltip("%s", t1[0]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
app->showExportKeyDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[1]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
ExportAllKeysDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[2]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
app->showBackupDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[3]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
ExportTransactionsDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%s", TR("tt_download_bootstrap"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
app->restartWizard();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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<const char*> 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 (left) + SECURITY (right) side by side ---
{
float colGap = Layout::spacingLg();
float leftColW = (contentW - colGap) * 0.6f;
float rightColW = (contentW - colGap) * 0.4f;
ImVec2 sectionOrigin = ImGui::GetCursorScreenPos();
float leftX = sectionOrigin.x;
float rightX = sectionOrigin.x + leftColW + colGap;
// ---- LEFT COLUMN: NODE ----
ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y));
ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y),
ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
ImFont* body2Info = S.resolveFont("body2");
if (!body2Info) body2Info = Type().body2();
ImGui::PushFont(body2Info);
if (app->isLiteBuild()) {
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
const char* modeLabels[] = {"Sticky", "Random"};
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Mode");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteServerMode", modeLabels[s_settingsState.lite_server_mode == 1 ? 1 : 0])) {
for (int modeIndex = 0; modeIndex < 2; ++modeIndex) {
const bool selected = s_settingsState.lite_server_mode == modeIndex;
if (ImGui::Selectable(modeLabels[modeIndex], selected)) {
s_settingsState.lite_server_mode = modeIndex;
saveLiteServerSelectionFromPageState(app);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Preset");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
std::string presetPreview = s_settingsState.lite_server_url[0] != '\0'
? std::string(s_settingsState.lite_server_url)
: std::string("Select");
for (const auto& server : s_settingsState.lite_servers) {
if (server.url == s_settingsState.lite_server_url && !server.label.empty()) {
presetPreview = server.label;
break;
}
}
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteServerPreset", presetPreview.c_str())) {
for (const auto& server : s_settingsState.lite_servers) {
if (!server.enabled) continue;
const std::string label = server.label.empty() ? server.url : server.label;
const bool selected = server.url == s_settingsState.lite_server_url;
if (ImGui::Selectable(label.c_str(), selected)) {
copyToSettingsBuffer(s_settingsState.lite_server_url,
sizeof(s_settingsState.lite_server_url),
server.url);
s_settingsState.lite_server_mode = 0;
saveLiteServerSelectionFromPageState(app);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Server");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteServerUrl", s_settingsState.lite_server_url,
sizeof(s_settingsState.lite_server_url));
if (ImGui::IsItemDeactivatedAfterEdit()) {
s_settingsState.lite_server_mode = 0;
saveLiteServerSelectionFromPageState(app);
}
if (s_settingsState.lite_server_mode == 1) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Seed");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
if (ImGui::InputInt("##LiteRandomSeed", &s_settingsState.lite_random_seed)) {
if (s_settingsState.lite_random_seed < 0) s_settingsState.lite_random_seed = 0;
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveLiteServerSelectionFromPageState(app);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (ImGui::Checkbox("Persist selected server##LitePersistServer",
&s_settingsState.lite_persist_selected_server)) {
saveLiteServerSelectionFromPageState(app);
}
if (!s_settingsState.lite_server_status.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_server_status.c_str());
}
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);
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));
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);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) {
evaluateLiteLifecycleRequestFromPageState(app);
}
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());
}
}
} else {
float rpcLblW = std::max(
S.drawElement("components.settings-page", "rpc-label-min-width").size,
std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs));
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
// Data Dir + Wallet Size — same line with "Label: value" format
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
{
// Measure the wallet size string first
std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found");
std::string dirPath = util::Platform::getDragonXDataDir();
// Calculate space taken by "Wallet Size: <size>" on the right
float walletSizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x + Layout::spacingXs();
float walletSizeValW = ImGui::CalcTextSize(size_str.c_str()).x;
float walletSizeTotalW = walletSizeLabelW + walletSizeValW + Layout::spacingLg();
// Available space for "Data Dir: <path>"
float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x + Layout::spacingXs();
float availForPath = leftColW - walletSizeTotalW - dataDirLabelW;
ImGui::TextUnformatted(TR("settings_data_dir"));
ImGui::SameLine(0, Layout::spacingXs());
ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str());
ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary());
ImVec4 linkHoverCol = linkCol;
linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f);
bool hovered = false;
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::AlignTextToFramePadding();
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) {
ImGui::SetTooltip("%s", TR("tt_open_dir"));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
if (ImGui::IsItemClicked())
util::Platform::openFolder(dirPath);
ImGui::SameLine(0, Layout::spacingLg());
ImGui::TextUnformatted(TR("settings_wallet_size_label"));
ImGui::SameLine(0, Layout::spacingXs());
if (wallet_size > 0) {
ImGui::TextUnformatted(size_str.c_str());
} else {
ImGui::TextDisabled("%s", TR("settings_not_found"));
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// RPC connection — two columns: (Host | Username) and (Port | Password)
float rpcHalfW = (leftColW - Layout::spacingMd()) * 0.5f;
float rpcHalfLblW = std::min(rpcLblW, rpcHalfW * 0.4f);
float rpcHalfInputW = std::max(40.0f, rpcHalfW - rpcHalfLblW);
float rpcRightColX = leftX + rpcHalfW + Layout::spacingMd();
// Row 1: RPC Host + Username
float row1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(leftX, row1Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_host"));
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_host"));
float afterRow1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_user"));
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_user"));
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
// Row 2: Port + Password
float row2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(leftX, row2Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_port"));
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_port"));
float afterRow2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_pass"));
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
TR("settings_auto_detected"));
if (s_settingsState.rpc_plaintext_remote) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::PushTextWrapPos(leftX + leftColW);
ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.");
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Node maintenance buttons (full-node build only)
if (app->supportsFullNodeLifecycleActions()) {
ImFont* btnFont = S.resolveFont("button");
float nodeBtnW;
{
if (btnFont) ImGui::PushFont(btnFont);
nodeBtnW = rowBtnW({TR("test_connection"), TR("rescan")});
if (btnFont) ImGui::PopFont(/* btnFont */);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("test_connection"), ImVec2(nodeBtnW, 0), btnFont)) {
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)) ImGui::SetTooltip("%s", TR("tt_test_conn"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) {
app->rescanBlockchain();
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
// Delete blockchain button (always available when using embedded daemon)
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
s_settingsState.confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
ImGui::EndDisabled();
}
}
ImGui::PopFont();
}
float leftBottom = ImGui::GetCursorScreenPos().y;
ImGui::PopClipRect();
// ---- RIGHT COLUMN: SECURITY ----
ImGui::SetCursorScreenPos(ImVec2(rightX, sectionOrigin.y));
ImGui::PushClipRect(ImVec2(rightX, sectionOrigin.y),
ImVec2(rightX + rightColW, sectionOrigin.y + 9999), true);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
// Encrypt / Change passphrase / Lock buttons
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")}), (rightColW - Layout::spacingMd()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
if (!isEncrypted) {
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showEncryptDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showDecryptDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_encrypt"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Auto-Lock timeout
{
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; }
}
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock"));
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
ImGui::PushItemWidth(comboW);
if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) {
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
app->settings()->save();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_lock"));
ImGui::PopItemWidth();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// PIN unlock section
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")}), (rightColW - Layout::spacingSm()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
if (!hasPIN) {
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinSetupDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin"));
}
}
float rightBottom = ImGui::GetCursorScreenPos().y;
ImGui::PopClipRect();
// Advance cursor past both columns
float maxBottom = std::max(leftBottom, rightBottom);
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom));
}
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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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
ImTextureID logoTex = app->getLogoTexture();
float logoAreaW = 0;
if (logoTex != 0) {
float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f);
float logoH = logoMaxH;
float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
float logoW = logoH * aspect;
ImVec2 logoPos = ImGui::GetCursorScreenPos();
dl->AddImage(logoTex,
ImVec2(logoPos.x, logoPos.y),
ImVec2(logoPos.x + logoW, logoPos.y + logoH));
logoAreaW = logoW + 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()));
// 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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%s", TR("tt_reset_settings"));
}
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));
// ====================================================================
// DEBUG LOGGING — collapsible card with glass styling
// ====================================================================
{
// 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()) ImGui::SetTooltip("%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()) ImGui::SetTooltip("%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.debug_cats_dirty = false;
app->restartDaemon();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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<int>(a * alpha);
v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(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();
}
}
}
} // namespace ui
} // namespace dragonx