A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant) to manage lightwalletd servers, replacing the basic selector that was in Settings. - Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an Official/Custom pill. Official DragonX servers get a glowing outline. - Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode); selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the controller against the new server and resyncs — its dtor detaches the uninterruptible sync thread, so this doesn't block). - Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle). - Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button. Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added to lite_connection_service. The Settings page lite-server selector + its plumbing are removed (single source of truth = the tab). Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested (liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is: online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI smoke-launched without crash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2488 lines
132 KiB
C++
2488 lines
132 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;
|
|
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;
|
|
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
|
|
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
|
|
// import. lite_import_key is the import input buffer (wiped right after submission).
|
|
std::string lite_export_secret;
|
|
std::string lite_export_label;
|
|
char lite_import_key[512] = "";
|
|
std::string lite_backup_status;
|
|
// Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is
|
|
// reused for Encrypt (unencrypted wallet) and Unlock (locked wallet); lite_dec_pass for Decrypt.
|
|
char lite_enc_pass[128] = "";
|
|
char lite_dec_pass[128] = "";
|
|
std::string lite_encryption_status;
|
|
bool mine_when_idle = false;
|
|
int mine_idle_delay = 120;
|
|
bool idle_thread_scaling = false;
|
|
int idle_threads_active = 0;
|
|
int idle_threads_idle = 0;
|
|
bool verbose_logging = false;
|
|
std::set<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 wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
|
|
switch (s_settingsState.lite_lifecycle_operation) {
|
|
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
|
|
case 2: return wallet::LiteWalletLifecycleOperation::RestoreFromSeed;
|
|
default: return wallet::LiteWalletLifecycleOperation::CreateNew;
|
|
}
|
|
}
|
|
|
|
|
|
static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
|
if (!app || !app->settings()) return;
|
|
|
|
wallet::LiteWalletLifecycleUiExecutionInput input;
|
|
input.capabilities = app->walletCapabilities();
|
|
input.settingsLoaded = true;
|
|
input.requirePersistedServerSelectionIntent = true;
|
|
input.ui.selectedServerDisplayReady = true;
|
|
input.ui.lifecycleUiOwnerReady = true;
|
|
input.ui.operationConfirmed = true;
|
|
input.ui.privateDataRedactionReady = true;
|
|
input.ui.syncPlannerFeedReady = true;
|
|
input.request.requestProvided = true;
|
|
input.request.operation = liteLifecycleOperationFromPageState();
|
|
|
|
switch (input.request.operation) {
|
|
case wallet::LiteWalletLifecycleOperation::CreateNew:
|
|
input.request.createRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
|
break;
|
|
case wallet::LiteWalletLifecycleOperation::OpenExisting:
|
|
input.request.openRequest.walletPath = s_settingsState.lite_wallet_path;
|
|
input.request.openRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
|
break;
|
|
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
|
|
input.request.restoreRequest.walletPath = s_settingsState.lite_wallet_path;
|
|
input.request.restoreRequest.seedPhrase = s_settingsState.lite_restore_seed;
|
|
input.request.restoreRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
|
input.request.restoreRequest.birthday = static_cast<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();
|
|
// Lite-server selection is managed entirely by the Network tab (not the Settings page).
|
|
s_settingsState.mine_when_idle = settings->getMineWhenIdle();
|
|
s_settingsState.mine_idle_delay = settings->getMineIdleDelay();
|
|
s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling();
|
|
s_settingsState.idle_threads_active = settings->getIdleThreadsActive();
|
|
s_settingsState.idle_threads_idle = settings->getIdleThreadsIdle();
|
|
s_settingsState.verbose_logging = settings->getVerboseLogging();
|
|
s_settingsState.debug_categories = settings->getDebugCategories();
|
|
s_settingsState.debug_cats_dirty = false;
|
|
s_settingsState.rpc_plaintext_remote = rpc::Connection::usesPlaintextRemote(rpc::Connection::autoDetectConfig());
|
|
|
|
// Apply loaded visual effects settings
|
|
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
|
|
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
|
|
effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity);
|
|
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
|
|
|
s_settingsState.initialized = true;
|
|
}
|
|
|
|
static void saveSettingsPageState(config::Settings* settings) {
|
|
if (!settings) return;
|
|
|
|
settings->setTheme(settings->getSkinId());
|
|
settings->setSaveZtxs(s_settingsState.save_ztxs);
|
|
settings->setAllowCustomFees(s_settingsState.allow_custom_fees);
|
|
settings->setAutoShield(s_settingsState.auto_shield);
|
|
settings->setFetchPrices(s_settingsState.fetch_prices);
|
|
settings->setUseTor(s_settingsState.use_tor);
|
|
settings->setTxExplorerUrl(s_settingsState.tx_explorer);
|
|
settings->setAddressExplorerUrl(s_settingsState.addr_explorer);
|
|
|
|
auto& i18n = util::I18n::instance();
|
|
const auto& languages = i18n.getAvailableLanguages();
|
|
auto it = languages.begin();
|
|
std::advance(it, s_settingsState.language_index);
|
|
if (it != languages.end()) {
|
|
settings->setLanguage(it->first);
|
|
}
|
|
|
|
// Visual effects settings
|
|
settings->setAcrylicEnabled(s_settingsState.acrylic_enabled);
|
|
settings->setAcrylicQuality(s_settingsState.blur_amount > 0.001f ? static_cast<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);
|
|
// Lite-server selection is owned by the Network tab; the Settings page no longer writes it.
|
|
settings->setMineWhenIdle(s_settingsState.mine_when_idle);
|
|
settings->setMineIdleDelay(s_settingsState.mine_idle_delay);
|
|
settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling);
|
|
settings->setIdleThreadsActive(s_settingsState.idle_threads_active);
|
|
settings->setIdleThreadsIdle(s_settingsState.idle_threads_idle);
|
|
settings->setVerboseLogging(s_settingsState.verbose_logging);
|
|
settings->setDebugCategories(s_settingsState.debug_categories);
|
|
|
|
settings->save();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Settings Page Renderer
|
|
// ============================================================================
|
|
|
|
void RenderSettingsPage(App* app) {
|
|
// Load settings state on first render
|
|
if (!s_settingsState.initialized && app->settings()) {
|
|
loadSettingsPageState(app->settings());
|
|
}
|
|
|
|
// Sync low-spec / theme-effects state from runtime each frame
|
|
// so that hotkey toggles are reflected in the checkboxes.
|
|
{
|
|
bool runtimeLowSpec = effects::isLowSpecMode();
|
|
if (s_settingsState.low_spec_mode != runtimeLowSpec) {
|
|
if (runtimeLowSpec) {
|
|
// Hotkey turned low-spec ON — save snapshot, override statics
|
|
s_settingsState.low_spec_snapshot.valid = true;
|
|
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
|
|
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
|
|
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
|
|
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
|
|
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
|
|
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
|
|
s_settingsState.acrylic_enabled = false;
|
|
s_settingsState.blur_amount = 0.0f;
|
|
s_settingsState.ui_opacity = 1.0f;
|
|
s_settingsState.window_opacity = 1.0f;
|
|
s_settingsState.theme_effects_enabled = false;
|
|
s_settingsState.scanline_enabled = false;
|
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
|
// Hotkey turned low-spec OFF — restore snapshot
|
|
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
|
|
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
|
|
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
|
|
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
|
|
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
|
|
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
|
|
s_settingsState.low_spec_snapshot.valid = false;
|
|
} else if (app->settings()) {
|
|
// No snapshot — read prefs from settings file
|
|
s_settingsState.blur_amount = app->settings()->getBlurMultiplier();
|
|
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
|
|
s_settingsState.ui_opacity = app->settings()->getUIOpacity();
|
|
s_settingsState.window_opacity = app->settings()->getWindowOpacity();
|
|
s_settingsState.theme_effects_enabled = app->settings()->getThemeEffectsEnabled();
|
|
s_settingsState.scanline_enabled = app->settings()->getScanlineEnabled();
|
|
}
|
|
s_settingsState.low_spec_mode = runtimeLowSpec;
|
|
}
|
|
bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled();
|
|
if (s_settingsState.theme_effects_enabled != runtimeThemeEffects) {
|
|
s_settingsState.theme_effects_enabled = runtimeThemeEffects;
|
|
}
|
|
}
|
|
|
|
auto& S = schema::UI();
|
|
|
|
// Responsive layout — matches other tabs
|
|
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
|
float scrollbarMargin = ImGui::GetStyle().ScrollbarSize + Layout::spacingSm();
|
|
float availWidth = contentAvail.x - scrollbarMargin;
|
|
float hs = Layout::hScale(availWidth);
|
|
float vs = Layout::vScale(contentAvail.y);
|
|
float pad = Layout::cardInnerPadding();
|
|
float bottomPad = std::max(0.0f, pad - ImGui::GetStyle().ItemSpacing.y);
|
|
float gap = Layout::cardGap();
|
|
float glassRound = Layout::glassRounding();
|
|
|
|
char buf[256];
|
|
|
|
// Label column position — adaptive to width
|
|
float labelW = std::max(S.drawElement("components.settings-page", "label-min-width").size, S.drawElement("components.settings-page", "label-width").size * hs);
|
|
// Widen labelW if any translated label is wider (prevents overflow)
|
|
{
|
|
ImFont* mf = Type().body2();
|
|
const char* labelKeys[] = {"theme", "balance_layout", "language", "acrylic", "noise",
|
|
"ui_opacity", "window_opacity", "font_scale",
|
|
"rpc_host", "rpc_port", "rpc_user", "rpc_pass",
|
|
"transaction_url", "address_url"};
|
|
for (const char* k : labelKeys) {
|
|
float tw = mf->CalcTextSizeA(mf->LegacySize, FLT_MAX, 0, TR(k)).x + Layout::spacingMd();
|
|
if (tw > labelW) labelW = tw;
|
|
}
|
|
}
|
|
// Input field width — fill remaining space in card
|
|
float inputW = std::max(S.drawElement("components.settings-page", "input-min-width").size, availWidth - labelW - pad * 2);
|
|
|
|
// Scrollable content area — NoBackground matches other tabs
|
|
|
|
ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false,
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
|
ApplySmoothScroll();
|
|
|
|
// Capture the ACTUAL clip boundaries from inside the child window
|
|
ImVec2 childClipMin = ImGui::GetWindowPos();
|
|
ImVec2 childClipMax(childClipMin.x + ImGui::GetWindowSize().x,
|
|
childClipMin.y + ImGui::GetWindowSize().y);
|
|
const float dp = Layout::dpiScale();
|
|
const float fadeH = schema::UI().drawElement("components.settings-page", "edge-fade-zone").size * dp;
|
|
const float fadeOffTop = schema::UI().drawElement("components.settings-page", "edge-fade-offset-top").size * dp;
|
|
const float fadeOffBot = schema::UI().drawElement("components.settings-page", "edge-fade-offset-bottom").size * dp;
|
|
|
|
// Get draw list AFTER BeginChild so we draw on the child window's list
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
(void)dl; // used by cards below
|
|
|
|
// Capture ForegroundDrawList vertex start — DrawGlassPanel draws
|
|
// theme effects (rainbow border, shimmer, specular glare, edge trace)
|
|
// on the ForegroundDrawList; those bypass the shader fade so we'll
|
|
// apply a vertex-based alpha fade to them after rendering.
|
|
ImDrawList* fgDL = ImGui::GetForegroundDrawList();
|
|
int fgVtxStart = fgDL->VtxBuffer.Size;
|
|
|
|
// --- Shader-based scroll fade: bind custom fragment shader ---
|
|
// The shader multiplies output alpha by a smoothstep gradient based
|
|
// on screen Y, giving a true per-pixel alpha mask at scroll edges.
|
|
float settingsScrollY_pre = ImGui::GetScrollY();
|
|
float settingsScrollMaxY_pre = ImGui::GetScrollMaxY();
|
|
float settingsFadeTopY = childClipMin.y + fadeOffTop;
|
|
float settingsFadeBottomY = childClipMax.y - fadeOffBot;
|
|
float settingsFadeZoneTop = (settingsScrollY_pre > 1.0f) ? fadeH : 0.0f;
|
|
float settingsFadeZoneBot = (settingsScrollMaxY_pre > 0 && settingsScrollY_pre < settingsScrollMaxY_pre - 1.0f) ? fadeH : 0.0f;
|
|
if (fadeH > 0.0f && !s_settingsState.low_spec_mode && s_settingsState.fade_shader.init()) {
|
|
s_settingsState.fade_shader.fadeTopY = settingsFadeTopY;
|
|
s_settingsState.fade_shader.fadeBottomY = settingsFadeBottomY;
|
|
s_settingsState.fade_shader.fadeZoneTop = settingsFadeZoneTop;
|
|
s_settingsState.fade_shader.fadeZoneBottom = settingsFadeZoneBot;
|
|
s_settingsState.fade_shader.addBind(dl);
|
|
}
|
|
|
|
// Top margin from schema
|
|
float topMargin = schema::UI().drawElement("components.settings-page", "top-margin").size;
|
|
if (topMargin > 0.0f)
|
|
ImGui::Dummy(ImVec2(0, topMargin));
|
|
|
|
GlassPanelSpec glassSpec;
|
|
glassSpec.rounding = glassRound;
|
|
ImFont* capFont = Type().caption();
|
|
ImFont* body2 = Type().body2();
|
|
ImFont* sub1 = Type().subtitle1();
|
|
|
|
// ====================================================================
|
|
// THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit
|
|
// which breaks BeginCombo popup rendering in some ImGui versions)
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
dl->ChannelsSplit(2);
|
|
dl->ChannelsSetCurrent(1);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
|
ImGui::Indent(pad);
|
|
|
|
float contentW = availWidth - pad * 2;
|
|
float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size;
|
|
float compactBP = S.drawElement("components.settings-page", "compact-breakpoint").size;
|
|
bool wideLayout = availWidth >= compactBP;
|
|
float refreshBtnW = S.drawElement("components.settings-page", "refresh-btn-width").size;
|
|
|
|
// --- Skin data ---
|
|
auto& skinMgr = schema::SkinManager::instance();
|
|
const auto& skins = skinMgr.available();
|
|
std::string active_preview = "DragonX";
|
|
bool active_is_custom = false;
|
|
for (const auto& skin : skins) {
|
|
if (skin.id == skinMgr.activeSkinId()) {
|
|
active_preview = skin.name;
|
|
active_is_custom = !skin.bundled;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// --- Language data ---
|
|
auto& i18n = util::I18n::instance();
|
|
const auto& languages = i18n.getAvailableLanguages();
|
|
std::vector<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());
|
|
|
|
// Lite-server selection lives in the dedicated Network tab now.
|
|
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
|
"Lite servers are managed in the Network tab.");
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) {
|
|
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
|
|
}
|
|
|
|
if (s_settingsState.lite_lifecycle_expanded) {
|
|
const char* lifecycleLabels[] = {"Create", "Open", "Restore"};
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Action");
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
if (ImGui::BeginCombo("##LiteLifecycleOperation",
|
|
lifecycleLabels[std::max(0, std::min(2, s_settingsState.lite_lifecycle_operation))])) {
|
|
for (int operationIndex = 0; operationIndex < 3; ++operationIndex) {
|
|
const bool selected = s_settingsState.lite_lifecycle_operation == operationIndex;
|
|
if (ImGui::Selectable(lifecycleLabels[operationIndex], selected)) {
|
|
s_settingsState.lite_lifecycle_operation = operationIndex;
|
|
s_settingsState.lite_lifecycle_status.clear();
|
|
s_settingsState.lite_lifecycle_summary.clear();
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
if (s_settingsState.lite_lifecycle_operation == 1 ||
|
|
s_settingsState.lite_lifecycle_operation == 2) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Wallet");
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
|
|
sizeof(s_settingsState.lite_wallet_path));
|
|
}
|
|
|
|
if (s_settingsState.lite_lifecycle_operation == 2) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Seed");
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
|
|
sizeof(s_settingsState.lite_restore_seed),
|
|
ImGuiInputTextFlags_Password);
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
// ---- Backup & keys (open wallet only) ----------------------------------
|
|
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().text(TypeStyle::Body2, "Backup & keys");
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
auto r = app->liteWallet()->exportSeed();
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
if (r.ok) {
|
|
s_settingsState.lite_export_secret = r.seedPhrase;
|
|
s_settingsState.lite_export_label = "Seed phrase — write it down, never share it";
|
|
s_settingsState.lite_backup_status.clear();
|
|
} else {
|
|
s_settingsState.lite_export_label.clear();
|
|
s_settingsState.lite_backup_status = r.error;
|
|
}
|
|
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
auto r = app->liteWallet()->exportPrivateKeys();
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
if (r.ok) {
|
|
s_settingsState.lite_export_secret = r.privateKeysJson;
|
|
s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds";
|
|
s_settingsState.lite_backup_status.clear();
|
|
} else {
|
|
s_settingsState.lite_export_label.clear();
|
|
s_settingsState.lite_backup_status = r.error;
|
|
}
|
|
wallet::secureWipeLiteSecret(r.privateKeysJson);
|
|
}
|
|
|
|
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
|
|
if (!s_settingsState.lite_export_secret.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_export_label.c_str());
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
ImGui::SetClipboardText(s_settingsState.lite_export_secret.c_str());
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
s_settingsState.lite_export_label.clear();
|
|
}
|
|
}
|
|
|
|
// Import a spending/viewing key (history appears after the next sync).
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Import key");
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
|
sizeof(s_settingsState.lite_import_key),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
|
|
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
|
s_settingsState.lite_backup_status =
|
|
r.ok ? "Key imported — run a sync to scan its history" : r.error;
|
|
}
|
|
|
|
if (!s_settingsState.lite_backup_status.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_backup_status.c_str());
|
|
}
|
|
|
|
// ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ----
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().text(TypeStyle::Body2, "Security");
|
|
const auto& wstate = app->getWalletState();
|
|
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
|
|
if (!wstate.isEncrypted()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Passphrase");
|
|
ImGui::SameLine(encLabelX);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
|
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Encrypt wallet##LiteEncrypt", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
|
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
|
s_settingsState.lite_encryption_status = r.ok ? "Wallet encrypted" : r.error;
|
|
}
|
|
} else {
|
|
if (wstate.isLocked()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Unlock");
|
|
ImGui::SameLine(encLabelX);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
|
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Unlock##LiteUnlock", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
|
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
|
s_settingsState.lite_encryption_status = ok ? "Wallet unlocked" : "Unlock failed";
|
|
}
|
|
} else {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
s_settingsState.lite_encryption_status =
|
|
app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed";
|
|
}
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Passphrase");
|
|
ImGui::SameLine(encLabelX);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
|
|
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton("Remove encryption##LiteDecrypt", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
|
|
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
|
s_settingsState.lite_encryption_status = r.ok ? "Encryption removed" : r.error;
|
|
}
|
|
}
|
|
if (!s_settingsState.lite_encryption_status.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_encryption_status.c_str());
|
|
}
|
|
} else if (!s_settingsState.lite_export_secret.empty()) {
|
|
// Wallet closed while a backup/secret was revealed — don't leave it in memory.
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
s_settingsState.lite_export_label.clear();
|
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
|
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
|
}
|
|
} 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
|