Mechanical, behavior-preserving consolidations from the audit. Rendering extractions were kept byte-equivalent; sites that couldn't be made identical were left as-is (noted below). Shared helpers: - util::truncateMiddle(s,maxLen) / (s,front,back) in text_format.h — replaces 6 file-local middle-ellipsis truncators + several inline substr sites (send/receive/transactions/balance_recent_tx/explorer + app.cpp + 3 dialogs). Carries the maxLen<=3 guard, fixing the latent unsigned-underflow copies. - material::LoadingDots() in draw_helpers.h — one animated-ellipsis source for 8 copy-pasted spinner sites (identical GetTime()*3 phase preserved). - Reuse the existing FormatHashrate() in explorer_tab + peers_tab (dropped two inline hashrate ladders). - material::DrawButtonGlassOverlay() — the glass-fill/rim/tactile overlay block shared by TactileButton / TactileSmallButton / schema TactileButton. - material::CollapsibleHeader() — the invisible-button + label + chevron idiom (3 of 5 settings_page sites; RPC/Debug headers skipped — non-identical). - material::GlassCardScope (RAII) — the ChannelsSplit/Indent/DrawGlassPanel/ ChannelsMerge card scaffold (5 settings_page cards; About/send/receive skipped — different padding / logo interleaving). - material::DialogWarningHeader()/DialogConfirmFooter() — warning header + 50/50 Cancel/danger footer across the confirm dialogs; button height moved from a hardcoded 40px into ui.toml (components.overlay-dialog.confirm-btn-height). - File-local enterLowSpec()/exitLowSpec() collapse the 3 low-spec snapshot copies. i18n: wrapped hardcoded English in the shared balance render paths and the whole Lite lifecycle/security section in TR(), with English defaults added to loadBuiltinEnglish() (res/lang/*.json left for the translation tooling — TR falls back to English, so output is unchanged). Full-node + Lite build clean; ctest 1/1; hygiene clean. These are rendering changes — the Settings page (cards/headers/confirm dialogs), all tactile buttons, and the Lite settings section warrant a screenshot check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2821 lines
156 KiB
C++
2821 lines
156 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 "../../resources/embedded_resources.h"
|
|
#include <ctime>
|
|
#include "../../rpc/rpc_client.h"
|
|
#include "../../rpc/connection.h"
|
|
#include "../../rpc/rpc_worker.h"
|
|
#include "../theme.h"
|
|
#include "../layout.h"
|
|
#include "../schema/ui_schema.h"
|
|
#include "../schema/skin_manager.h"
|
|
#include "../notifications.h"
|
|
#include "../effects/imgui_acrylic.h"
|
|
#include "../effects/theme_effects.h"
|
|
#include "../effects/low_spec.h"
|
|
#include "../effects/scroll_fade_shader.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "../material/type.h"
|
|
#include "../material/colors.h"
|
|
#include "../windows/validate_address_dialog.h"
|
|
#include "../windows/address_book_dialog.h"
|
|
#include "../windows/shield_dialog.h"
|
|
#include "../windows/request_payment_dialog.h"
|
|
#include "../windows/block_info_dialog.h"
|
|
#include "../windows/export_all_keys_dialog.h"
|
|
#include "../windows/export_transactions_dialog.h"
|
|
#include "../windows/bootstrap_download_dialog.h"
|
|
#include "../windows/daemon_download_dialog.h"
|
|
#include "../../embedded/IconsMaterialDesign.h"
|
|
#include "imgui.h"
|
|
#include <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;
|
|
// True while an async create/open/restore is in flight (Settings drives the controller's
|
|
// async lifecycle path so a flaky server never freezes the UI; the result is polled each frame).
|
|
bool lite_lifecycle_pending = false;
|
|
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
|
|
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
|
|
// import. lite_import_key is the import input buffer (wiped right after submission).
|
|
std::string lite_export_secret;
|
|
std::string lite_export_label;
|
|
bool lite_export_is_seed = false; // revealed secret is a seed (show birthday + save-to-file)
|
|
unsigned long long lite_export_birthday = 0; // seed birthday (shown with the seed)
|
|
char lite_import_key[512] = "";
|
|
std::string lite_backup_status;
|
|
// Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is
|
|
// reused for Encrypt (unencrypted wallet) and Unlock (locked wallet); lite_dec_pass for Decrypt.
|
|
char lite_enc_pass[128] = "";
|
|
char lite_dec_pass[128] = "";
|
|
std::string lite_encryption_status;
|
|
bool mine_when_idle = false;
|
|
int mine_idle_delay = 120;
|
|
bool idle_thread_scaling = false;
|
|
int idle_threads_active = 0;
|
|
int idle_threads_idle = 0;
|
|
bool verbose_logging = false;
|
|
std::set<std::string> debug_categories;
|
|
bool debug_cats_dirty = false;
|
|
bool debug_expanded = false;
|
|
bool effects_expanded = false;
|
|
bool tools_expanded = false;
|
|
bool rpc_expanded = false; // Node & Security: reveal the RPC connection fields
|
|
bool confirm_clear_ztx = false;
|
|
bool confirm_delete_blockchain = false;
|
|
bool confirm_rescan = false;
|
|
// Rescan dialog: probe the node's available block range so a bootstrapped/pruned node gets a
|
|
// runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis.
|
|
bool rescan_height_detecting = false;
|
|
bool rescan_height_detected = false;
|
|
bool rescan_full_history = true; // genesis present → traditional -rescan restart
|
|
int rescan_start_height = 0; // editable pre-fill for the runtime rescan
|
|
bool confirm_repair_wallet = false;
|
|
bool confirm_reinstall_daemon = false;
|
|
// Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh,
|
|
// since reading the installed binary to scan its version is a one-off disk read).
|
|
bool daemon_info_loaded = false;
|
|
dragonx::resources::DaemonBinaryInfo installed_daemon;
|
|
dragonx::resources::BundledDaemonInfo bundled_daemon;
|
|
bool confirm_restart_daemon = false;
|
|
bool confirm_lite_redownload = false;
|
|
effects::ScrollFadeShader fade_shader;
|
|
};
|
|
|
|
static SettingsPageState s_settingsState;
|
|
|
|
// Low-spec mode enter: snapshot the current effects prefs, then force the
|
|
// low-cost overrides. When applyEffects is true, also push the overrides to
|
|
// the live effects subsystems (the runtime-sync path passes false since it
|
|
// only mirrors an already-applied hotkey toggle into the checkbox state).
|
|
static void enterLowSpec(bool applyEffects) {
|
|
s_settingsState.low_spec_snapshot.valid = true;
|
|
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
|
|
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
|
|
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
|
|
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
|
|
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
|
|
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
|
|
s_settingsState.acrylic_enabled = false;
|
|
s_settingsState.blur_amount = 0.0f;
|
|
s_settingsState.ui_opacity = 1.0f;
|
|
s_settingsState.window_opacity = 1.0f;
|
|
s_settingsState.theme_effects_enabled = false;
|
|
s_settingsState.scanline_enabled = false;
|
|
if (applyEffects) {
|
|
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
|
|
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
|
|
effects::ThemeEffects::instance().setEnabled(false);
|
|
ConsoleTab::s_scanline_enabled = false;
|
|
}
|
|
}
|
|
|
|
// Low-spec mode exit: restore the previously snapshotted effects prefs and
|
|
// clear the snapshot. When applyEffects is true, also push them to the live
|
|
// effects subsystems. Callers guard this on low_spec_snapshot.valid.
|
|
static void exitLowSpec(bool applyEffects) {
|
|
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
|
|
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
|
|
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
|
|
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
|
|
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
|
|
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
|
|
if (applyEffects) {
|
|
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
|
|
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
|
|
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
|
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
|
|
}
|
|
s_settingsState.low_spec_snapshot.valid = false;
|
|
}
|
|
|
|
// Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input.
|
|
static int liteSeedWordCount(const char* s) {
|
|
int words = 0;
|
|
bool inWord = false;
|
|
for (; s && *s; ++s) {
|
|
const bool space = std::isspace(static_cast<unsigned char>(*s)) != 0;
|
|
if (space) inWord = false;
|
|
else if (!inWord) { inWord = true; ++words; }
|
|
}
|
|
return words;
|
|
}
|
|
|
|
static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
|
|
switch (s_settingsState.lite_lifecycle_operation) {
|
|
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
|
|
case 2: return wallet::LiteWalletLifecycleOperation::RestoreFromSeed;
|
|
default: return wallet::LiteWalletLifecycleOperation::CreateNew;
|
|
}
|
|
}
|
|
|
|
|
|
static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
|
if (!app || !app->settings()) return;
|
|
|
|
wallet::LiteWalletLifecycleUiExecutionInput input;
|
|
input.capabilities = app->walletCapabilities();
|
|
input.settingsLoaded = true;
|
|
input.requirePersistedServerSelectionIntent = true;
|
|
input.ui.selectedServerDisplayReady = true;
|
|
input.ui.lifecycleUiOwnerReady = true;
|
|
input.ui.operationConfirmed = true;
|
|
input.ui.privateDataRedactionReady = true;
|
|
input.ui.syncPlannerFeedReady = true;
|
|
input.request.requestProvided = true;
|
|
input.request.operation = liteLifecycleOperationFromPageState();
|
|
|
|
switch (input.request.operation) {
|
|
case wallet::LiteWalletLifecycleOperation::CreateNew:
|
|
input.request.createRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
|
break;
|
|
case wallet::LiteWalletLifecycleOperation::OpenExisting:
|
|
input.request.openRequest.walletPath = s_settingsState.lite_wallet_path;
|
|
input.request.openRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
|
break;
|
|
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
|
|
input.request.restoreRequest.walletPath = s_settingsState.lite_wallet_path;
|
|
input.request.restoreRequest.seedPhrase = s_settingsState.lite_restore_seed;
|
|
input.request.restoreRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
|
input.request.restoreRequest.birthday = static_cast<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};
|
|
|
|
// Restore needs a complete 24-word seed; reject early (the scrubber above still wipes the
|
|
// entered secret on this return path).
|
|
if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) {
|
|
const int words = liteSeedWordCount(s_settingsState.lite_restore_seed);
|
|
if (words != 24) {
|
|
s_settingsState.lite_lifecycle_status =
|
|
"Enter all 24 seed words to restore (got " + std::to_string(words) + ")";
|
|
s_settingsState.lite_lifecycle_summary.clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// When a linked lite backend is present, execute the operation for real through the App-owned
|
|
// controller — ASYNCHRONOUSLY, with server failover, so an unreachable/flaky server never
|
|
// freezes the UI. The request (with its secrets) is moved into the controller; the page polls
|
|
// the outcome each frame in renderLiteLifecyclePending() below. (Secret wiping of the page char
|
|
// buffers + the moved-from request copies is handled by liteSecretScrubber at function exit.)
|
|
if (auto* lite = app->liteWallet()) {
|
|
bool started = false;
|
|
switch (input.request.operation) {
|
|
case wallet::LiteWalletLifecycleOperation::CreateNew:
|
|
started = lite->beginCreateWalletAsync(std::move(input.request.createRequest));
|
|
break;
|
|
case wallet::LiteWalletLifecycleOperation::OpenExisting:
|
|
started = lite->beginOpenWalletAsync(std::move(input.request.openRequest));
|
|
break;
|
|
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
|
|
started = lite->beginRestoreWalletAsync(std::move(input.request.restoreRequest));
|
|
break;
|
|
}
|
|
if (started) {
|
|
s_settingsState.lite_lifecycle_pending = true;
|
|
s_settingsState.lite_lifecycle_status = TR("lite_working");
|
|
s_settingsState.lite_lifecycle_summary.clear();
|
|
} else {
|
|
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
|
|
// usable server). The controller's status carries the reason.
|
|
s_settingsState.lite_lifecycle_status = lite->status().message.empty()
|
|
? "Could not start the operation"
|
|
: lite->status().message;
|
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// No linked lite backend to execute against (full-node build, or the lite backend
|
|
// failed to load / rollout-disabled). The live path above returns when a backend is
|
|
// present, so reaching here means there is nothing to run.
|
|
s_settingsState.lite_lifecycle_summary.clear();
|
|
s_settingsState.lite_lifecycle_status = "Lite wallet backend unavailable";
|
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
|
}
|
|
|
|
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
|
|
|
static void loadSettingsPageState(config::Settings* settings) {
|
|
if (!settings) return;
|
|
|
|
s_settingsState.save_ztxs = settings->getSaveZtxs();
|
|
s_settingsState.allow_custom_fees = settings->getAllowCustomFees();
|
|
s_settingsState.auto_shield = settings->getAutoShield();
|
|
s_settingsState.fetch_prices = settings->getFetchPrices();
|
|
s_settingsState.use_tor = settings->getUseTor();
|
|
|
|
strncpy(s_settingsState.tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_settingsState.tx_explorer) - 1);
|
|
strncpy(s_settingsState.addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_settingsState.addr_explorer) - 1);
|
|
|
|
auto& i18n = util::I18n::instance();
|
|
const auto& languages = i18n.getAvailableLanguages();
|
|
std::string current_lang = settings->getLanguage();
|
|
if (current_lang.empty()) current_lang = "en";
|
|
|
|
s_settingsState.language_index = 0;
|
|
int idx = 0;
|
|
for (const auto& lang : languages) {
|
|
if (lang.first == current_lang) {
|
|
s_settingsState.language_index = idx;
|
|
break;
|
|
}
|
|
idx++;
|
|
}
|
|
|
|
// Load blur amount directly from saved multiplier
|
|
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
|
|
enterLowSpec(false);
|
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
|
// Hotkey turned low-spec OFF — restore snapshot
|
|
exitLowSpec(false);
|
|
} else if (app->settings()) {
|
|
// No snapshot — read prefs from settings file
|
|
s_settingsState.blur_amount = app->settings()->getBlurMultiplier();
|
|
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
|
|
s_settingsState.ui_opacity = app->settings()->getUIOpacity();
|
|
s_settingsState.window_opacity = app->settings()->getWindowOpacity();
|
|
s_settingsState.theme_effects_enabled = app->settings()->getThemeEffectsEnabled();
|
|
s_settingsState.scanline_enabled = app->settings()->getScanlineEnabled();
|
|
}
|
|
s_settingsState.low_spec_mode = runtimeLowSpec;
|
|
}
|
|
bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled();
|
|
if (s_settingsState.theme_effects_enabled != runtimeThemeEffects) {
|
|
s_settingsState.theme_effects_enabled = runtimeThemeEffects;
|
|
}
|
|
}
|
|
|
|
auto& S = schema::UI();
|
|
|
|
// Responsive layout — matches other tabs
|
|
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
|
float scrollbarMargin = ImGui::GetStyle().ScrollbarSize + Layout::spacingSm();
|
|
float availWidth = contentAvail.x - scrollbarMargin;
|
|
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()));
|
|
|
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
|
|
|
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))
|
|
material::Tooltip("%s", skin.validationError.c_str());
|
|
} else {
|
|
std::string lbl = skin.name;
|
|
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
|
|
if (ImGui::Selectable(lbl.c_str(), is_selected)) {
|
|
skinMgr.setActiveSkin(skin.id);
|
|
if (app->settings()) {
|
|
app->settings()->setSkinId(skin.id);
|
|
app->settings()->save();
|
|
}
|
|
}
|
|
if (is_selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (wideLayout) {
|
|
// ============================================================
|
|
// Wide: 3 combos on one row + compact 3-column effects grid
|
|
// ============================================================
|
|
|
|
// --- Combo row: Theme | Layout | Language [Refresh] ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
float lblGap = Layout::spacingXs();
|
|
float lblThemeW = ImGui::CalcTextSize(TR("theme")).x + lblGap;
|
|
float lblLayoutW = ImGui::CalcTextSize(TR("balance_layout")).x + lblGap;
|
|
float lblLangW = ImGui::CalcTextSize(TR("language")).x + lblGap;
|
|
float totalFixed = lblThemeW + lblLayoutW + lblLangW
|
|
+ comboGap * 2 + Layout::spacingSm() + refreshBtnW;
|
|
float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f);
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("theme"));
|
|
ImGui::SameLine(0, lblGap);
|
|
ImGui::SetNextItemWidth(comboW);
|
|
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
|
renderThemeComboPopup();
|
|
ImGui::EndCombo();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_theme_hotkey"));
|
|
|
|
ImGui::SameLine(0, comboGap);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("balance_layout"));
|
|
ImGui::SameLine(0, lblGap);
|
|
ImGui::SetNextItemWidth(comboW);
|
|
if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) {
|
|
for (const auto& l : layouts) {
|
|
if (!l.enabled) continue;
|
|
bool selected = (l.id == s_settingsState.balance_layout);
|
|
if (ImGui::Selectable(l.name.c_str(), selected)) {
|
|
s_settingsState.balance_layout = l.id;
|
|
if (app->settings()) {
|
|
app->settings()->setBalanceLayout(s_settingsState.balance_layout);
|
|
app->settings()->save();
|
|
}
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_layout_hotkey"));
|
|
|
|
ImGui::SameLine(0, comboGap);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("language"));
|
|
ImGui::SameLine(0, lblGap);
|
|
ImGui::SetNextItemWidth(comboW);
|
|
if (ImGui::Combo("##Language", &s_settingsState.language_index, lang_names.data(),
|
|
static_cast<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()) material::Tooltip("%s", TR("tt_language"));
|
|
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
|
schema::SkinManager::instance().refresh();
|
|
Notifications::instance().info("Theme list refreshed");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
material::Tooltip(TR("tt_scan_themes"),
|
|
schema::SkinManager::getUserSkinsDirectory().c_str());
|
|
}
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// --- Font Scale slider (always visible) ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::TextUnformatted(TR("font_scale"));
|
|
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW);
|
|
ImGui::SetNextItemWidth(fontSliderW);
|
|
s_settingsState.font_scale = Layout::userFontScale();
|
|
float prev_font_scale = s_settingsState.font_scale;
|
|
{
|
|
char fs_fmt[16];
|
|
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", s_settingsState.font_scale);
|
|
ImGui::SliderFloat("##FontScale", &s_settingsState.font_scale, 1.0f, 1.5f, fs_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp);
|
|
}
|
|
s_settingsState.font_scale = std::max(1.0f, std::min(1.5f,
|
|
std::round(s_settingsState.font_scale * 20.0f) / 20.0f));
|
|
if (s_settingsState.font_scale != prev_font_scale)
|
|
Layout::setUserFontScaleVisual(s_settingsState.font_scale);
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
|
Layout::setUserFontScale(s_settingsState.font_scale);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale"));
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// --- Collapsible: Advanced Effects... ---
|
|
material::CollapsibleHeader(dl, "##EffectsToggle", TR("advanced_effects"),
|
|
s_settingsState.effects_expanded, contentW,
|
|
body2, OnSurfaceMedium());
|
|
|
|
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) {
|
|
enterLowSpec(true);
|
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
|
exitLowSpec(true);
|
|
}
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec"));
|
|
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_settingsState.gradient_background)) {
|
|
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg"));
|
|
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion"));
|
|
|
|
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
|
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &s_settingsState.scanline_enabled)) {
|
|
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
|
|
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
|
|
app->settings()->save();
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline"));
|
|
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &s_settingsState.theme_effects_enabled)) {
|
|
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
|
|
|
|
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
|
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
|
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
|
float ctrlW = std::max(effCtrlMinW, halfW);
|
|
float baseX = ImGui::GetCursorScreenPos().x;
|
|
float rightX = baseX + ctrlW + Layout::spacingLg();
|
|
|
|
ImGui::TextUnformatted(TR("acrylic"));
|
|
float row1Y = ImGui::GetCursorScreenPos().y;
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char blur_fmt[16];
|
|
if (s_settingsState.blur_amount < 0.01f)
|
|
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
|
|
else
|
|
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_settingsState.blur_amount * 25.0f);
|
|
if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, 4.0f, blur_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < 0.15f) s_settingsState.blur_amount = 0.0f;
|
|
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
|
|
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
|
|
}
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur"));
|
|
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
|
|
|
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
|
|
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH));
|
|
ImGui::TextUnformatted(TR("noise"));
|
|
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y));
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char noise_fmt[16];
|
|
if (s_settingsState.noise_opacity < 0.01f)
|
|
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
|
|
else
|
|
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_settingsState.noise_opacity * 100.0f);
|
|
if (ImGui::SliderFloat("##NoiseOpacity", &s_settingsState.noise_opacity, 0.0f, 1.0f, noise_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise"));
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
|
|
|
|
ImGui::TextUnformatted(TR("ui_opacity"));
|
|
float row2Y = ImGui::GetCursorScreenPos().y;
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char uiop_fmt[16];
|
|
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", s_settingsState.ui_opacity * 100.0f);
|
|
if (ImGui::SliderFloat("##UIOpacity", &s_settingsState.ui_opacity, 0.3f, 1.0f, uiop_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity"));
|
|
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
|
|
ImGui::TextUnformatted(TR("window_opacity"));
|
|
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y));
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char winop_fmt[16];
|
|
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", s_settingsState.window_opacity * 100.0f);
|
|
if (ImGui::SliderFloat("##WindowOpacity", &s_settingsState.window_opacity, 0.3f, 1.0f, winop_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity"));
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
|
|
|
|
ImGui::EndDisabled(); // low-spec
|
|
ImGui::PopFont();
|
|
} // s_settingsState.effects_expanded
|
|
} else {
|
|
// ============================================================
|
|
// Narrow: stacked combos + 2-column effects (original layout)
|
|
// ============================================================
|
|
|
|
// --- Theme row ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("theme"));
|
|
ImGui::SameLine(labelW);
|
|
|
|
float themeComboW = std::max(S.drawElement("components.settings-page", "theme-combo-min-width").size,
|
|
availWidth - pad * 2 - labelW - refreshBtnW - Layout::spacingSm());
|
|
ImGui::SetNextItemWidth(themeComboW);
|
|
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
|
renderThemeComboPopup();
|
|
ImGui::EndCombo();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_theme_hotkey"));
|
|
if (active_is_custom) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_theme"));
|
|
}
|
|
ImGui::SameLine();
|
|
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
|
schema::SkinManager::instance().refresh();
|
|
Notifications::instance().info("Theme list refreshed");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
material::Tooltip(TR("tt_scan_themes"),
|
|
schema::SkinManager::getUserSkinsDirectory().c_str());
|
|
}
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// --- Balance Layout row ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("balance_layout"));
|
|
ImGui::SameLine(labelW);
|
|
ImGui::SetNextItemWidth(std::max(180.0f, inputW));
|
|
if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) {
|
|
for (const auto& l : layouts) {
|
|
if (!l.enabled) continue;
|
|
bool selected = (l.id == s_settingsState.balance_layout);
|
|
if (ImGui::Selectable(l.name.c_str(), selected)) {
|
|
s_settingsState.balance_layout = l.id;
|
|
if (app->settings()) {
|
|
app->settings()->setBalanceLayout(s_settingsState.balance_layout);
|
|
app->settings()->save();
|
|
}
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_layout_hotkey"));
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// --- Language row ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("language"));
|
|
ImGui::SameLine(labelW);
|
|
ImGui::SetNextItemWidth(inputW);
|
|
if (ImGui::Combo("##Language", &s_settingsState.language_index, lang_names.data(),
|
|
static_cast<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()) material::Tooltip("%s", TR("tt_language"));
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// --- Font Scale slider (always visible) ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::TextUnformatted(TR("font_scale"));
|
|
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
|
availWidth - pad * 2);
|
|
ImGui::SetNextItemWidth(fontSliderW);
|
|
s_settingsState.font_scale = Layout::userFontScale();
|
|
float prev_font_scale = s_settingsState.font_scale;
|
|
{
|
|
char fs_fmt[16];
|
|
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", s_settingsState.font_scale);
|
|
ImGui::SliderFloat("##FontScale", &s_settingsState.font_scale, 1.0f, 1.5f, fs_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp);
|
|
}
|
|
s_settingsState.font_scale = std::max(1.0f, std::min(1.5f,
|
|
std::round(s_settingsState.font_scale * 20.0f) / 20.0f));
|
|
if (s_settingsState.font_scale != prev_font_scale)
|
|
Layout::setUserFontScaleVisual(s_settingsState.font_scale);
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
|
Layout::setUserFontScale(s_settingsState.font_scale);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale"));
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// --- Collapsible: Advanced Effects... ---
|
|
{
|
|
float narrowContentW = availWidth - pad * 2;
|
|
material::CollapsibleHeader(dl, "##EffectsToggleN", TR("advanced_effects"),
|
|
s_settingsState.effects_expanded, narrowContentW,
|
|
body2, OnSurfaceMedium());
|
|
}
|
|
|
|
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) {
|
|
enterLowSpec(true);
|
|
} else if (s_settingsState.low_spec_snapshot.valid) {
|
|
exitLowSpec(true);
|
|
}
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec"));
|
|
|
|
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &s_settingsState.gradient_background)) {
|
|
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg_alt"));
|
|
|
|
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion"));
|
|
|
|
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
|
|
|
|
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &s_settingsState.scanline_enabled)) {
|
|
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
|
|
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
|
|
app->settings()->save();
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline"));
|
|
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &s_settingsState.theme_effects_enabled)) {
|
|
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
|
|
|
|
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
|
availWidth - pad * 2.0f);
|
|
ImGui::TextUnformatted(TR("acrylic"));
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char blur_fmt[16];
|
|
if (s_settingsState.blur_amount < 0.01f)
|
|
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
|
|
else
|
|
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_settingsState.blur_amount * 25.0f);
|
|
if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, 4.0f, blur_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < 0.15f) s_settingsState.blur_amount = 0.0f;
|
|
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
|
|
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
|
|
}
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur"));
|
|
|
|
ImGui::TextUnformatted(TR("noise"));
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char noise_fmt[16];
|
|
if (s_settingsState.noise_opacity < 0.01f)
|
|
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
|
|
else
|
|
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_settingsState.noise_opacity * 100.0f);
|
|
if (ImGui::SliderFloat("##NoiseOpacity", &s_settingsState.noise_opacity, 0.0f, 1.0f, noise_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity);
|
|
}
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise"));
|
|
|
|
ImGui::TextUnformatted(TR("ui_opacity"));
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char uiop_fmt[16];
|
|
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", s_settingsState.ui_opacity * 100.0f);
|
|
if (ImGui::SliderFloat("##UIOpacity", &s_settingsState.ui_opacity, 0.3f, 1.0f, uiop_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
|
|
}
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity"));
|
|
|
|
ImGui::TextUnformatted(TR("window_opacity"));
|
|
ImGui::SetNextItemWidth(ctrlW);
|
|
{
|
|
char winop_fmt[16];
|
|
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", s_settingsState.window_opacity * 100.0f);
|
|
if (ImGui::SliderFloat("##WindowOpacity", &s_settingsState.window_opacity, 0.3f, 1.0f, winop_fmt,
|
|
ImGuiSliderFlags_AlwaysClamp)) {
|
|
}
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity"));
|
|
|
|
ImGui::EndDisabled(); // low-spec
|
|
ImGui::PopFont();
|
|
} // s_settingsState.effects_expanded
|
|
}
|
|
}
|
|
|
|
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()));
|
|
|
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
|
const ImVec2& cardMin = card.cardMin;
|
|
|
|
float contentW = availWidth - pad * 2;
|
|
|
|
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
|
|
{
|
|
const bool showDaemonOptions = app->supportsFullNodeLifecycleActions();
|
|
float cbSpacing = Layout::spacingMd();
|
|
float fh = ImGui::GetFrameHeight();
|
|
float inner = ImGui::GetStyle().ItemInnerSpacing.x;
|
|
auto cbW = [&](const char* label) { return fh + inner + ImGui::CalcTextSize(label).x; };
|
|
|
|
float totalW = cbW(TR("save_z_transactions")) + cbSpacing
|
|
+ cbW(TR("auto_shield")) + cbSpacing
|
|
+ cbW(TR("use_tor")) + cbSpacing;
|
|
if (showDaemonOptions) {
|
|
totalW += cbW(TR("keep_daemon")) + cbSpacing
|
|
+ cbW(TR("stop_external")) + cbSpacing;
|
|
}
|
|
totalW += cbW(TR("verbose_logging"));
|
|
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
|
|
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
|
|
float sp = cbSpacing * scale;
|
|
|
|
ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &s_settingsState.save_ztxs);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx"));
|
|
ImGui::SameLine(0, sp);
|
|
ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &s_settingsState.auto_shield);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield"));
|
|
ImGui::SameLine(0, sp);
|
|
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
|
if (showDaemonOptions) {
|
|
ImGui::SameLine(0, sp);
|
|
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_keep_daemon"));
|
|
ImGui::SameLine(0, sp);
|
|
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_stop_external"));
|
|
}
|
|
ImGui::SameLine(0, sp);
|
|
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
|
|
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
material::Tooltip("%s", TR("tt_verbose"));
|
|
|
|
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// --- Collapsible: Tools & Actions... ---
|
|
material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"),
|
|
s_settingsState.tools_expanded, contentW,
|
|
body2, OnSurfaceMedium());
|
|
|
|
if (s_settingsState.tools_expanded) {
|
|
float btnSpacing = Layout::spacingMd();
|
|
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
|
|
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
|
|
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
|
|
bw = std::max(minBtnW, bw);
|
|
|
|
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
|
|
AddressBookDialog::show();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book"));
|
|
ImGui::SameLine(0, btnSpacing);
|
|
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
|
ValidateAddressDialog::show();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_validate"));
|
|
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
|
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
|
|
RequestPaymentDialog::show();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_request_payment"));
|
|
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
|
|
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
|
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining"));
|
|
ImGui::SameLine(0, btnSpacing);
|
|
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
|
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
|
|
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
|
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
|
|
s_settingsState.confirm_clear_ztx = true;
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clear_ztx"));
|
|
}
|
|
|
|
// --- Backup & Data (merged in from its former standalone card) ---
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
{
|
|
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
|
|
const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")};
|
|
const char* t1[] = { TR("tt_import_key"), TR("tt_export_key"), TR("tt_export_all"), TR("tt_backup"), TR("tt_export_csv") };
|
|
const bool showFullNodeLifecycleActions = app->supportsFullNodeLifecycleActions();
|
|
const char* wizLabel = TR("setup_wizard");
|
|
const char* bsLabel = TR("download_bootstrap");
|
|
float sp = Layout::spacingSm();
|
|
ImFont* btnFont = S.resolveFont("button");
|
|
|
|
float btnPadX = btnPad * 2;
|
|
float naturalW = 0;
|
|
for (int i = 0; i < 5; i++)
|
|
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
|
|
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
|
|
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
|
|
float totalW = naturalW + sp * 5;
|
|
if (showFullNodeLifecycleActions) totalW += wizW + bsW + sp * 2;
|
|
|
|
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
|
|
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
|
|
float scaledSp = sp * scale;
|
|
|
|
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
|
|
app->showImportKeyDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]);
|
|
ImGui::SameLine(0, scaledSp);
|
|
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
|
|
app->showExportKeyDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[1]);
|
|
ImGui::SameLine(0, scaledSp);
|
|
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
|
|
ExportAllKeysDialog::show();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[2]);
|
|
ImGui::SameLine(0, scaledSp);
|
|
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
|
|
app->showBackupDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[3]);
|
|
ImGui::SameLine(0, scaledSp);
|
|
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
|
|
ExportTransactionsDialog::show();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[4]);
|
|
|
|
if (showFullNodeLifecycleActions) {
|
|
// Right-align Setup Wizard + Download Bootstrap.
|
|
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
|
|
float curX = ImGui::GetCursorScreenPos().x;
|
|
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2;
|
|
float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2;
|
|
float rightEdge = cardMin.x + availWidth - pad;
|
|
float rightGroupW = bsBtnW + scaledSp + wizBtnW;
|
|
float groupX = rightEdge - rightGroupW;
|
|
if (groupX > curX) {
|
|
ImGui::SameLine(0, 0);
|
|
ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y));
|
|
} else {
|
|
ImGui::SameLine(0, scaledSp);
|
|
}
|
|
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
|
|
BootstrapDownloadDialog::show(app);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_download_bootstrap"));
|
|
ImGui::SameLine(0, scaledSp);
|
|
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
|
app->restartWizard();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wizard"));
|
|
}
|
|
|
|
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
|
}
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
|
|
// ====================================================================
|
|
// NODE & SECURITY — card
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
|
|
|
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 & SECURITY: one full-width card, controls laid out horizontally ---
|
|
// A single vertical flow of sub-sections (Node, RPC, Security, Daemon binary),
|
|
// each with its overline header and its controls flowing left-to-right to fill
|
|
// the card width. No 2x2 grid, no column clips, no masonry placement.
|
|
{
|
|
ImVec2 sectionOrigin = ImGui::GetCursorScreenPos();
|
|
// The lite WALLET block below still positions its rows against a "left"
|
|
// column origin/width; in the single-column layout that is simply the full
|
|
// content column at the section origin.
|
|
const float leftX = sectionOrigin.x;
|
|
const float leftColW = contentW;
|
|
|
|
ImFont* body2Info = S.resolveFont("body2");
|
|
if (!body2Info) body2Info = Type().body2();
|
|
|
|
// Shared SECURITY controls, rendered full-width with horizontal rows. Used by
|
|
// both the lite branch (auto-lock + PIN only) and the full-node branch (which
|
|
// also gets the RPC-backed encrypt/change/lock/remove block, includeRpcEncrypt).
|
|
// `secX` is the row start X (== content-column left); `secColW` its width.
|
|
auto renderSecuritySection = [&](float secX, float secColW, bool includeRpcEncrypt) {
|
|
// Encrypt / Change passphrase / Lock / Remove — RPC-backed, full-node only.
|
|
// In lite the wallet's encryption controls live in the WALLET block above.
|
|
if (includeRpcEncrypt) {
|
|
bool isEncrypted = app->state().isEncrypted();
|
|
bool isLocked = app->state().isLocked();
|
|
|
|
float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (secColW - Layout::spacingMd()) * 0.5f);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
|
|
if (!isEncrypted) {
|
|
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
|
app->showEncryptDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_encrypt"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted"));
|
|
} else {
|
|
if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
|
app->showChangePassphraseDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pass"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (isLocked) {
|
|
ImGui::PushFont(Type().iconSmall());
|
|
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%s", TR("settings_locked"));
|
|
} else {
|
|
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
|
app->lockWallet();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock"));
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
ImGui::PushFont(Type().iconSmall());
|
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked"));
|
|
}
|
|
// Remove Encryption button — trails the row.
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
|
app->showDecryptDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
|
|
}
|
|
}
|
|
|
|
// Auto-Lock timeout + PIN controls — continue on the same row as the encrypt
|
|
// controls (full-node) so the whole SECURITY band fills the width; on lite
|
|
// (no encrypt row) they start a fresh row at the section's left edge.
|
|
{
|
|
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
|
|
|
|
int timeout = app->settings()->getAutoLockTimeout();
|
|
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
|
|
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
|
|
int selTimeout = 0;
|
|
for (int i = 0; i < 6; i++) {
|
|
if (timeoutValues[i] == timeout) { selTimeout = i; break; }
|
|
}
|
|
if (includeRpcEncrypt)
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
else
|
|
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::PushItemWidth(comboW);
|
|
if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) {
|
|
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
|
|
app->settings()->save();
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock"));
|
|
ImGui::PopItemWidth();
|
|
|
|
// PIN unlock controls, trailing the auto-lock combo on the same row.
|
|
bool isEncryptedPIN = app->state().isEncrypted();
|
|
if (isEncryptedPIN) {
|
|
bool hasPIN = app->hasPinVault();
|
|
float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (secColW - Layout::spacingSm()) * 0.5f);
|
|
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (!hasPIN) {
|
|
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
|
app->showPinSetupDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_set_pin"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin"));
|
|
} else {
|
|
if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
|
app->showPinChangeDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pin"));
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
|
app->showPinRemoveDialog();
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_pin"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
ImGui::PushFont(Type().iconSmall());
|
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active"));
|
|
}
|
|
} else {
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin"));
|
|
}
|
|
}
|
|
};
|
|
|
|
if (app->isLiteBuild()) {
|
|
// ============================ LITE ============================
|
|
// Lite has no daemon: this card holds wallet lifecycle/encryption
|
|
// (the "WALLET" block) followed by the shared SECURITY block, both
|
|
// rendered full-width in a single column exactly as before.
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
{
|
|
ImGui::PushFont(body2Info);
|
|
{
|
|
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
|
|
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
|
|
|
|
// Lite-server selection lives in the dedicated Network tab now.
|
|
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
|
TR("lite_servers_network_tab"));
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
if (ImGui::Button(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) {
|
|
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
|
|
}
|
|
|
|
if (s_settingsState.lite_lifecycle_expanded) {
|
|
const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")};
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_action"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
if (ImGui::BeginCombo("##LiteLifecycleOperation",
|
|
lifecycleLabels[std::max(0, std::min(2, s_settingsState.lite_lifecycle_operation))])) {
|
|
for (int operationIndex = 0; operationIndex < 3; ++operationIndex) {
|
|
const bool selected = s_settingsState.lite_lifecycle_operation == operationIndex;
|
|
if (ImGui::Selectable(lifecycleLabels[operationIndex], selected)) {
|
|
s_settingsState.lite_lifecycle_operation = operationIndex;
|
|
s_settingsState.lite_lifecycle_status.clear();
|
|
s_settingsState.lite_lifecycle_summary.clear();
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
if (s_settingsState.lite_lifecycle_operation == 1 ||
|
|
s_settingsState.lite_lifecycle_operation == 2) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_wallet_label"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
|
|
sizeof(s_settingsState.lite_wallet_path));
|
|
}
|
|
|
|
if (s_settingsState.lite_lifecycle_operation == 2) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_seed_label"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
|
|
sizeof(s_settingsState.lite_restore_seed),
|
|
ImGuiInputTextFlags_Password);
|
|
// Live 24-word count so the user knows the seed is complete before restoring.
|
|
{
|
|
const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed);
|
|
char wbuf[32];
|
|
snprintf(wbuf, sizeof(wbuf), TR("lite_word_count"), wc);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf);
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_birthday_label"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
|
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
|
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
TR("lite_birthday_hint"));
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_account_label"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
|
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
|
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(),
|
|
&s_settingsState.lite_restore_overwrite);
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_passphrase_label"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteLifecyclePassphrase",
|
|
s_settingsState.lite_lifecycle_passphrase,
|
|
sizeof(s_settingsState.lite_lifecycle_passphrase),
|
|
ImGuiInputTextFlags_Password);
|
|
|
|
// Poll a completed async create/open/restore (driven by the App-owned
|
|
// controller; finalized on the main thread by App::update's
|
|
// pumpLifecycleResult()). While in flight the button is disabled.
|
|
if (s_settingsState.lite_lifecycle_pending) {
|
|
if (auto* lite = app->liteWallet()) {
|
|
if (!lite->lifecycleRequestInProgress()) {
|
|
s_settingsState.lite_lifecycle_pending = false;
|
|
const auto& result = lite->lastLifecycleResult();
|
|
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
|
|
if (result.walletReady) {
|
|
s_settingsState.lite_lifecycle_status = TR("lite_wallet_ready");
|
|
} else {
|
|
s_settingsState.lite_lifecycle_status = result.error.empty()
|
|
? result.status.message
|
|
: result.error;
|
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
|
}
|
|
}
|
|
} else {
|
|
s_settingsState.lite_lifecycle_pending = false; // backend went away
|
|
}
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
|
|
if (liteLifecycleBusy) ImGui::BeginDisabled();
|
|
if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
evaluateLiteLifecycleRequestFromPageState(app);
|
|
}
|
|
if (liteLifecycleBusy) ImGui::EndDisabled();
|
|
|
|
if (!s_settingsState.lite_lifecycle_status.empty()) {
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_lifecycle_status.c_str());
|
|
}
|
|
if (!s_settingsState.lite_lifecycle_summary.empty()) {
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_lifecycle_summary.c_str());
|
|
}
|
|
}
|
|
|
|
// Open the lite wallet data folder in the OS file manager (always available in
|
|
// lite). The lite backend stores its wallet in its OWN dir (silentdragonxlite),
|
|
// NOT the full-node getDragonXDataDir().
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
util::Platform::openFolder(util::Platform::getLiteWalletDataDir());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
|
|
|
|
// ---- Backup & keys (open wallet only) ----------------------------------
|
|
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().text(TypeStyle::Body2, TR("lite_backup_keys"));
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_show_seed", "LiteExportSeed").c_str(), 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 = TR("lite_seed_warning");
|
|
s_settingsState.lite_export_is_seed = true;
|
|
s_settingsState.lite_export_birthday = r.birthday;
|
|
s_settingsState.lite_backup_status.clear();
|
|
} else {
|
|
s_settingsState.lite_export_label.clear();
|
|
s_settingsState.lite_export_is_seed = false;
|
|
s_settingsState.lite_backup_status = r.error;
|
|
}
|
|
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton(TrId("lite_show_private_keys", "LiteExportKeys").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
auto r = app->liteWallet()->exportPrivateKeys();
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
s_settingsState.lite_export_is_seed = false; // keys, not a seed
|
|
if (r.ok) {
|
|
s_settingsState.lite_export_secret = r.privateKeysJson;
|
|
s_settingsState.lite_export_label = TR("lite_private_keys_warning");
|
|
s_settingsState.lite_backup_status.clear();
|
|
} else {
|
|
s_settingsState.lite_export_label.clear();
|
|
s_settingsState.lite_backup_status = r.error;
|
|
}
|
|
wallet::secureWipeLiteSecret(r.privateKeysJson);
|
|
}
|
|
|
|
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
|
|
if (!s_settingsState.lite_export_secret.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_export_label.c_str());
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
// The seed's birthday is needed to restore quickly — show + back it up too.
|
|
if (s_settingsState.lite_export_is_seed) {
|
|
char bday[64];
|
|
snprintf(bday, sizeof(bday), TR("lite_birthday_backup"),
|
|
s_settingsState.lite_export_birthday);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday);
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
app->copySecretToClipboard(s_settingsState.lite_export_secret);
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
|
|
if (s_settingsState.lite_export_is_seed &&
|
|
TactileButton(TrId("lite_save_to_file", "LiteExportSave").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
std::string path = util::Platform::getConfigDir() + "/lite-seed-backup.txt";
|
|
std::string content = s_settingsState.lite_export_secret + "\nBirthday: " +
|
|
std::to_string(s_settingsState.lite_export_birthday) + "\n";
|
|
const bool ok = util::Platform::writeFileAtomically(
|
|
path, content, /*restrictPermissions=*/true);
|
|
wallet::secureWipeLiteSecret(content);
|
|
s_settingsState.lite_backup_status = ok
|
|
? (std::string(TR("lite_saved_to")) + path)
|
|
: (std::string(TR("lite_could_not_write")) + path);
|
|
}
|
|
if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
s_settingsState.lite_export_label.clear();
|
|
s_settingsState.lite_export_is_seed = false;
|
|
}
|
|
}
|
|
|
|
// Import a spending/viewing key (history appears after the next sync).
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_import_key_label"));
|
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
|
sizeof(s_settingsState.lite_import_key),
|
|
ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), 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 ? TR("lite_key_imported") : 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, TR("lite_security"));
|
|
const auto& wstate = app->getWalletState();
|
|
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
|
|
if (!wstate.isEncrypted()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_passphrase_label"));
|
|
ImGui::SameLine(encLabelX);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
|
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
|
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
|
s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error;
|
|
}
|
|
} else {
|
|
if (wstate.isLocked()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_unlock"));
|
|
ImGui::SameLine(encLabelX);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
|
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
|
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
|
s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed");
|
|
}
|
|
} else {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
s_settingsState.lite_encryption_status =
|
|
app->liteWallet()->lockWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed");
|
|
}
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("lite_passphrase_label"));
|
|
ImGui::SameLine(encLabelX);
|
|
ImGui::SetNextItemWidth(liteInputW);
|
|
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
|
|
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
|
|
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
|
s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error;
|
|
}
|
|
}
|
|
if (!s_settingsState.lite_encryption_status.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
s_settingsState.lite_encryption_status.c_str());
|
|
}
|
|
|
|
// ---- Maintenance: re-download blocks from the lite server ----
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().text(TypeStyle::Body2, TR("lite_maintenance"));
|
|
|
|
const bool scanning = app->liteWallet()->scanInProgress();
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
ImGui::BeginDisabled(scanning);
|
|
if (TactileButton(TR("lite_redownload_blocks"), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
s_settingsState.confirm_lite_redownload = true;
|
|
}
|
|
ImGui::EndDisabled();
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
|
material::Tooltip("%s", TR("tt_lite_redownload"));
|
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc"));
|
|
} else if (!s_settingsState.lite_export_secret.empty()) {
|
|
// Wallet closed while a backup/secret was revealed — don't leave it in memory.
|
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
|
s_settingsState.lite_export_label.clear();
|
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
|
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
|
}
|
|
} // lite WALLET block
|
|
ImGui::PopFont();
|
|
} // lite WALLET push-font scope
|
|
|
|
// ---- SECURITY (lite): auto-lock + PIN only ----
|
|
// The wallet's passphrase encryption controls live in the WALLET block
|
|
// above (lite backend), so this section shows only the shared auto-lock
|
|
// timeout + PIN controls.
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/false);
|
|
|
|
// Advance to the true bottom of the single column.
|
|
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
|
|
|
|
} else {
|
|
// ========================= FULL NODE =========================
|
|
// One full-width column; each sub-section's controls flow left-to-right.
|
|
ImGui::PushFont(body2Info);
|
|
|
|
const float spMd = Layout::spacingMd();
|
|
|
|
// -------------------- NODE / DATA --------------------
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
{
|
|
std::string dirPath = util::Platform::getDragonXDataDir();
|
|
std::string walletPath = dirPath + "wallet.dat";
|
|
uint64_t wallet_size = util::Platform::getFileSize(walletPath);
|
|
std::string size_str = (wallet_size > 0)
|
|
? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found");
|
|
|
|
// Right-aligned "Open data folder" button trailing the row; the path
|
|
// gets whatever width is left after the data-dir label, wallet-size
|
|
// label+value, and the trailing button.
|
|
ImFont* nodeBtnFont = S.resolveFont("button");
|
|
if (!nodeBtnFont) nodeBtnFont = Type().button();
|
|
// Measure with the BUTTON font (not the current font) so the width matches
|
|
// the real TactileButton size — the old estimate mis-measured and the button
|
|
// overflowed the card's right edge, worse at high DPI.
|
|
float openBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0,
|
|
TR("settings_open_data_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f;
|
|
// "Open app folder" (the ObsidianDragon config folder), left of the data button.
|
|
float appBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0,
|
|
TR("settings_open_app_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f;
|
|
float openGroupW = appBtnW + spMd + openBtnW;
|
|
float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x;
|
|
float sizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x;
|
|
float sizeValueW = ImGui::CalcTextSize(size_str.c_str()).x;
|
|
// Space reserved to the right of the path for the size block + both buttons.
|
|
float trailW = sizeLabelW + Layout::spacingXs() + sizeValueW + spMd + openGroupW;
|
|
float availForPath = contentW - dataDirLabelW - Layout::spacingXs() - spMd - trailW;
|
|
if (availForPath < 60.0f) availForPath = contentW - dataDirLabelW - Layout::spacingXs();
|
|
|
|
ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary());
|
|
ImVec4 linkHoverCol = linkCol;
|
|
linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f);
|
|
|
|
float rowTopY = ImGui::GetCursorScreenPos().y; // top of the data-dir row
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("settings_data_dir"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str());
|
|
bool hovered = false;
|
|
ImGui::AlignTextToFramePadding();
|
|
if (pathSize.x > availForPath && availForPath > 0) {
|
|
float scale = availForPath / pathSize.x;
|
|
float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f);
|
|
ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize);
|
|
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
|
|
hovered = ImGui::IsItemHovered();
|
|
if (hovered) {
|
|
ImVec2 tMin = ImGui::GetItemRectMin();
|
|
ImVec2 tMax = ImGui::GetItemRectMax();
|
|
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
|
|
}
|
|
ImGui::SetWindowFontScale(1.0f);
|
|
} else {
|
|
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
|
|
hovered = ImGui::IsItemHovered();
|
|
if (hovered) {
|
|
ImVec2 tMin = ImGui::GetItemRectMin();
|
|
ImVec2 tMax = ImGui::GetItemRectMax();
|
|
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
|
|
}
|
|
}
|
|
if (hovered) {
|
|
material::Tooltip("%s", TR("tt_open_dir"));
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
}
|
|
if (ImGui::IsItemClicked())
|
|
util::Platform::openFolder(dirPath);
|
|
|
|
// Wallet Size label + value, trailing the path on the same row.
|
|
ImGui::SameLine(0, spMd);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("settings_wallet_size_label"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::AlignTextToFramePadding();
|
|
if (wallet_size > 0)
|
|
ImGui::TextUnformatted(size_str.c_str());
|
|
else
|
|
ImGui::TextDisabled("%s", TR("settings_not_found"));
|
|
|
|
// Folder buttons: [Open app folder] [Open data folder], right-aligned as a
|
|
// group on the data-dir row (rowTopY), each an EXPLICIT width so the group's
|
|
// right edge lands exactly on the card edge (no overflow at any DPI).
|
|
float rowY = rowTopY;
|
|
float rightEdge = sectionOrigin.x + contentW;
|
|
float groupX = rightEdge - openGroupW;
|
|
float afterSizeX = ImGui::GetItemRectMax().x + spMd;
|
|
ImGui::SameLine(0, spMd);
|
|
if (groupX > afterSizeX)
|
|
ImGui::SetCursorScreenPos(ImVec2(groupX, rowY));
|
|
// App (ObsidianDragon config) folder — opens ~/.config/ObsidianDragon etc.
|
|
if (TactileButton(TR("settings_open_app_dir"), ImVec2(appBtnW, 0), nodeBtnFont)) {
|
|
util::Platform::openFolder(util::Platform::getObsidianDragonDir());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_app_dir"));
|
|
ImGui::SameLine(0, spMd);
|
|
if (TactileButton(TR("settings_open_data_dir"), ImVec2(openBtnW, 0), nodeBtnFont)) {
|
|
util::Platform::openFolder(dirPath);
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
|
|
}
|
|
|
|
// -------------------- RPC (collapsible, like Tools & Actions) --------------------
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
{
|
|
ImFont* rpcHdrFont = body2; // match the "Tools & Actions..." toggle style
|
|
const char* rpcArrow = s_settingsState.rpc_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
|
ImGui::PushFont(body2);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
|
|
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
|
|
if (ImGui::Button("##RpcToggle", ImVec2(contentW, ImGui::GetFrameHeight())))
|
|
s_settingsState.rpc_expanded = !s_settingsState.rpc_expanded;
|
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
float textY = hdrPos.y + (ImGui::GetFrameHeight() - rpcHdrFont->LegacySize) * 0.5f;
|
|
dl->AddText(rpcHdrFont, rpcHdrFont->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("rpc_connection"));
|
|
ImFont* rpcIconFont = Type().iconSmall();
|
|
if (!rpcIconFont) rpcIconFont = body2;
|
|
float rpcArrowW = rpcIconFont->CalcTextSizeA(rpcIconFont->LegacySize, FLT_MAX, 0, rpcArrow).x;
|
|
dl->AddText(rpcIconFont, rpcIconFont->LegacySize, ImVec2(hdrPos.x + contentW - rpcArrowW, textY), OnSurfaceMedium(), rpcArrow);
|
|
ImGui::PopStyleColor(3);
|
|
ImGui::PopFont();
|
|
}
|
|
if (s_settingsState.rpc_expanded) {
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
// Four fields (Host, Port, Username, Password) across one row when the
|
|
// card is wide enough; otherwise 2x2. Each field is "label [input]".
|
|
const char* hostLbl = TR("rpc_host");
|
|
const char* portLbl = TR("rpc_port");
|
|
const char* userLbl = TR("rpc_user");
|
|
const char* passLbl = TR("rpc_pass");
|
|
float labelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(portLbl).x +
|
|
ImGui::CalcTextSize(userLbl).x + ImGui::CalcTextSize(passLbl).x;
|
|
|
|
const bool fourAcross = contentW >= 700.0f;
|
|
auto field = [&](const char* label, const char* id, char* buf, size_t bufSz,
|
|
float inputW, bool password) {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(label);
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::SetNextItemWidth(inputW);
|
|
ImGui::InputText(id, buf, bufSz,
|
|
password ? ImGuiInputTextFlags_Password : 0);
|
|
};
|
|
|
|
if (fourAcross) {
|
|
// fieldW = (contentW - labels - per-field label gaps - 3 inter-field gaps) / 4
|
|
float inputTotal = contentW - labelsW - Layout::spacingXs() * 4 - spMd * 3;
|
|
float inputW = std::max(60.0f, std::floor(inputTotal / 4.0f));
|
|
field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
|
|
ImGui::SameLine(0, spMd);
|
|
field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
|
|
ImGui::SameLine(0, spMd);
|
|
field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
|
|
ImGui::SameLine(0, spMd);
|
|
field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
|
|
} else {
|
|
// 2x2: two fields per row.
|
|
float halfLabelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(userLbl).x;
|
|
float inputW = std::max(60.0f, std::floor(
|
|
(contentW - halfLabelsW - Layout::spacingXs() * 2 - spMd) / 2.0f));
|
|
field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
|
|
ImGui::SameLine(0, spMd);
|
|
field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
|
|
|
|
field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
|
|
ImGui::SameLine(0, spMd);
|
|
field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
|
|
}
|
|
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("settings_auto_detected"));
|
|
if (s_settingsState.rpc_plaintext_remote) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
|
ImGui::PushTextWrapPos(sectionOrigin.x + contentW);
|
|
ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.");
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
// -------------------- SECURITY --------------------
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/true);
|
|
|
|
// -------------------- DAEMON BINARY --------------------
|
|
if (app->supportsFullNodeLifecycleActions()) {
|
|
ImFont* dbBtnFont = S.resolveFont("button");
|
|
if (!s_settingsState.daemon_info_loaded) {
|
|
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo();
|
|
s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo();
|
|
s_settingsState.daemon_info_loaded = true;
|
|
}
|
|
const auto& inst = s_settingsState.installed_daemon;
|
|
const auto& bun = s_settingsState.bundled_daemon;
|
|
|
|
auto fmtDate = [](std::int64_t epoch) -> std::string {
|
|
if (epoch <= 0) return "—";
|
|
std::time_t t = static_cast<std::time_t>(epoch);
|
|
std::tm tmv{};
|
|
#ifdef _WIN32
|
|
localtime_s(&tmv, &t);
|
|
#else
|
|
localtime_r(&t, &tmv);
|
|
#endif
|
|
char buf[32];
|
|
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv);
|
|
return std::string(buf);
|
|
};
|
|
|
|
const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// Inline single line: Installed <v> (<size> · <date>) · Bundled <v> (<size>) · <status>
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("daemon_installed"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
if (inst.exists) {
|
|
ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str());
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(dbDim, "(%s · %s)",
|
|
util::Platform::formatFileSize(inst.size).c_str(),
|
|
fmtDate(inst.modifiedEpoch).c_str());
|
|
} else {
|
|
ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed"));
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(dbDim, "·");
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("daemon_bundled"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
if (bun.available) {
|
|
ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str());
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(dbDim, "(%s)", util::Platform::formatFileSize(bun.size).c_str());
|
|
} else {
|
|
ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled"));
|
|
}
|
|
if (bun.available) {
|
|
const bool sameSize = inst.exists && inst.size == bun.size;
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::TextColored(dbDim, "·");
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
if (!inst.exists)
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing"));
|
|
else if (sameSize)
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match"));
|
|
else
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
|
|
}
|
|
|
|
// In-app node update: download + verify the latest dragonxd from the project Gitea.
|
|
// Refresh the cached daemon info once an install has completed.
|
|
if (ui::DaemonUpdateDialog::consumeInstalled())
|
|
s_settingsState.daemon_info_loaded = false;
|
|
|
|
// Action row: Check for updates | Refresh | Install bundled on the left,
|
|
// with the four maintenance actions right-aligned on the same row.
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
float daemonBtnRowY = ImGui::GetCursorScreenPos().y;
|
|
if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) {
|
|
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string());
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
|
|
ImGui::SameLine(0, spMd);
|
|
// Unique ImGui ID (label still displays "Refresh") — the theme sections
|
|
// also have "Refresh" buttons, so a bare label collides on this page.
|
|
if (TactileButton((std::string(TR("refresh")) + "##daemonRefresh").c_str(), ImVec2(0, 0), dbBtnFont)) {
|
|
s_settingsState.daemon_info_loaded = false;
|
|
}
|
|
ImGui::SameLine(0, spMd);
|
|
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
|
|
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) {
|
|
s_settingsState.confirm_reinstall_daemon = true;
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
|
|
ImGui::EndDisabled();
|
|
|
|
// The four maintenance actions (Test connection / Rescan / Delete blockchain
|
|
// / Repair wallet), right-aligned on the SAME row as Install bundled — no
|
|
// separate "Advanced" toggle. Every BeginDisabled/EndDisabled predicate is
|
|
// preserved exactly. Wraps to a right-aligned next row when too narrow.
|
|
auto renderDaemonMaintenanceButtons = [&]() {
|
|
ImGui::BeginDisabled(!app->isConnected());
|
|
if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) {
|
|
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
|
|
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
|
try {
|
|
rpc::RPCClient::TraceScope trace("Settings / Test connection");
|
|
rpc->call("getinfo");
|
|
return []() { Notifications::instance().success("RPC connection OK"); };
|
|
} catch (const std::exception& e) {
|
|
std::string err = e.what();
|
|
return [err]() { Notifications::instance().error("RPC error: " + err); };
|
|
}
|
|
});
|
|
} else {
|
|
Notifications::instance().warning("Not connected to daemon");
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn"));
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine(0, spMd);
|
|
ImGui::BeginDisabled(!app->isConnected());
|
|
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
|
|
s_settingsState.confirm_rescan = true;
|
|
// Re-probe the available block range each time the dialog is opened.
|
|
s_settingsState.rescan_height_detecting = false;
|
|
s_settingsState.rescan_height_detected = false;
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine(0, spMd);
|
|
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
|
|
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) {
|
|
s_settingsState.confirm_delete_blockchain = true;
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
|
|
ImGui::SameLine(0, spMd);
|
|
if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) {
|
|
s_settingsState.confirm_repair_wallet = true;
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
|
|
ImGui::EndDisabled();
|
|
};
|
|
{
|
|
// Group width (button font) to right-align the four maintenance actions.
|
|
const char* mLabels[] = { TR("test_connection"), TR("rescan"),
|
|
TR("delete_blockchain"), TR("repair_wallet") };
|
|
float groupW = spMd * 3.0f;
|
|
for (const char* l : mLabels)
|
|
groupW += dbBtnFont->CalcTextSizeA(dbBtnFont->LegacySize, FLT_MAX, 0, l).x
|
|
+ ImGui::GetStyle().FramePadding.x * 2.0f;
|
|
float afterTestX = ImGui::GetItemRectMax().x + spMd; // right edge of Install bundled
|
|
float rightEdge = sectionOrigin.x + contentW;
|
|
float groupX = rightEdge - groupW;
|
|
if (groupX >= afterTestX) {
|
|
// Fits on the Install-bundled row, right-aligned.
|
|
ImGui::SameLine(0, 0);
|
|
ImGui::SetCursorScreenPos(ImVec2(groupX, daemonBtnRowY));
|
|
} else {
|
|
// Too narrow: wrap to the next row, right-aligned (clamped to left edge).
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
float wrapX = std::max(sectionOrigin.x, rightEdge - groupW);
|
|
ImGui::SetCursorScreenPos(ImVec2(wrapX, ImGui::GetCursorScreenPos().y));
|
|
}
|
|
renderDaemonMaintenanceButtons();
|
|
}
|
|
}
|
|
|
|
ImGui::PopFont();
|
|
|
|
// Advance to the true bottom of the single column.
|
|
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// EXPLORER & OPTIONS — full-width card
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_section"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
|
|
|
float contentW = availWidth - pad * 2;
|
|
ImGui::PushFont(body2);
|
|
|
|
// Row 1: Transaction URL | Address URL (side-by-side)
|
|
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
|
float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs();
|
|
float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs();
|
|
float inputTxW = std::max(80.0f, halfW - lblTxW);
|
|
float inputAddrW = std::max(80.0f, halfW - lblAddrW);
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("transaction_url"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::SetNextItemWidth(inputTxW);
|
|
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url"));
|
|
ImGui::SameLine(pad + halfW + Layout::spacingLg());
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("address_url"));
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
ImGui::SetNextItemWidth(inputAddrW);
|
|
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url"));
|
|
|
|
ImGui::PopFont();
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// Row 2: Checkboxes + Block Explorer button (on one line)
|
|
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
util::Platform::openUrl("https://explorer.dragonx.is");
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer"));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// ABOUT — card
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("about"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
dl->ChannelsSplit(2);
|
|
dl->ChannelsSetCurrent(1);
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
|
ImGui::Indent(pad);
|
|
|
|
// Logo on the left side of the about card. Deferred: reserve horizontal space
|
|
// now, but draw the image after the card's final height is known so it scales to
|
|
// the full card height (no empty space below it).
|
|
ImTextureID logoTex = app->getLogoTexture();
|
|
float logoAreaW = 0;
|
|
ImVec2 logoPos = ImGui::GetCursorScreenPos();
|
|
float logoAspect = (app->getLogoHeight() > 0)
|
|
? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
|
|
float logoReserveH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(150.0f);
|
|
if (logoTex != 0) {
|
|
logoAreaW = logoReserveH * logoAspect + Layout::spacingLg();
|
|
ImGui::Indent(logoAreaW);
|
|
}
|
|
|
|
float contentW = availWidth - pad * 2 - logoAreaW;
|
|
|
|
// App name + version on same line
|
|
ImGui::PushFont(sub1);
|
|
ImGui::TextUnformatted(DRAGONX_APP_NAME);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
ImGui::PushFont(body2);
|
|
snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION);
|
|
ImGui::TextUnformatted(buf);
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION);
|
|
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
|
|
ImGui::PopFont();
|
|
|
|
// Daemon version
|
|
{
|
|
const auto& st = app->state();
|
|
if (st.daemon_version > 0) {
|
|
int dmaj = st.daemon_version / 1000000;
|
|
int dmin = (st.daemon_version / 10000) % 100;
|
|
int dpat = (st.daemon_version / 100) % 100;
|
|
ImGui::PushFont(body2);
|
|
snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat);
|
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf);
|
|
ImGui::PopFont();
|
|
}
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
ImGui::PushFont(body2);
|
|
ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW);
|
|
ImGui::TextUnformatted(TR("settings_about_text"));
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
ImGui::PushFont(capFont);
|
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_copyright"));
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
|
|
|
// Top of the (full-width) buttons row — the deferred logo is clamped to end above
|
|
// this Y so the tall left-column logo never overlaps the buttons.
|
|
float aboutButtonsTopY = ImGui::GetCursorScreenPos().y;
|
|
|
|
// Buttons — consistent equal-width row (full card width)
|
|
if (logoAreaW > 0) {
|
|
ImGui::Unindent(logoAreaW);
|
|
}
|
|
{
|
|
float fullContentW = availWidth - pad * 2;
|
|
float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f;
|
|
|
|
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
|
util::Platform::openUrl("https://dragonx.is");
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_website"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
|
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
|
saveSettingsPageState(app->settings());
|
|
Notifications::instance().success("Settings saved");
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings"));
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
|
if (app->settings()) {
|
|
loadSettingsPageState(app->settings());
|
|
Notifications::instance().info("Settings reloaded from disk");
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings"));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, bottomPad));
|
|
ImGui::Unindent(pad);
|
|
|
|
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
|
|
|
// Draw the logo now that the card height is known — aspect-preserved, capped to the
|
|
// reserved width, and clamped to end just above the buttons row so it never overlaps
|
|
// the text or the buttons. Still on the content channel (1), above the glass.
|
|
if (logoTex != 0) {
|
|
float reserveW = logoReserveH * logoAspect;
|
|
// Height available above the buttons row (the fix for the logo/buttons overlap).
|
|
float logoBottomLimit = aboutButtonsTopY - Layout::spacingSm();
|
|
float logoH = std::max(16.0f, logoBottomLimit - logoPos.y);
|
|
float logoW = logoH * logoAspect;
|
|
if (logoW > reserveW) { logoW = reserveW; logoH = (logoAspect > 0.0f) ? logoW / logoAspect : logoH; }
|
|
dl->AddImage(logoTex, logoPos, ImVec2(logoPos.x + logoW, logoPos.y + logoH));
|
|
}
|
|
|
|
dl->ChannelsSetCurrent(0);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
dl->ChannelsMerge();
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// DEBUG LOGGING — collapsible card (full-node only: these are dragonxd
|
|
// daemon debug= categories written to DRAGONX.conf; lite has no daemon)
|
|
// ====================================================================
|
|
if (app->supportsFullNodeLifecycleActions()) {
|
|
// Clickable header row
|
|
ImVec2 headerPos = ImGui::GetCursorScreenPos();
|
|
const char* arrow = s_settingsState.debug_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
|
|
if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) {
|
|
s_settingsState.debug_expanded = !s_settingsState.debug_expanded;
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
|
|
ImGui::PopStyleColor(3);
|
|
|
|
// Draw overline label + arrow on top of the invisible button
|
|
{
|
|
ImFont* ovFont = Type().overline();
|
|
float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f;
|
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(headerPos.x, textY), OnSurfaceMedium(), TR("debug_logging"));
|
|
// Use the icon font for the expand/collapse arrow
|
|
ImFont* iconFont = Type().iconSmall();
|
|
if (!iconFont) iconFont = ovFont;
|
|
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
|
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(headerPos.x + availWidth - arrowW - pad, textY), OnSurfaceMedium(), arrow);
|
|
}
|
|
|
|
if (s_settingsState.debug_expanded) {
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
|
|
|
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select"));
|
|
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// The 22 dragonxd debug categories
|
|
static const char* debugCats[] = {
|
|
"addrman", "alert", "bench", "coindb", "db", "estimatefee",
|
|
"http", "libevent", "lock", "mempool", "net",
|
|
"paymentdisclosure", "pow", "proxy", "prune", "rand",
|
|
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
|
|
};
|
|
static const char* debugTips[] = {
|
|
"Peer address tracking and management",
|
|
"Alert system messages",
|
|
"Benchmark timings for operations",
|
|
"Coin database read/write operations",
|
|
"Berkeley DB operations",
|
|
"Fee estimation algorithm",
|
|
"HTTP RPC server activity",
|
|
"Libevent networking library",
|
|
"Lock contention debugging",
|
|
"Transaction memory pool activity",
|
|
"Network connections and messages",
|
|
"Payment disclosure protocol",
|
|
"Proof-of-work mining activity",
|
|
"SOCKS5 proxy connections",
|
|
"Block pruning operations",
|
|
"Random number generation",
|
|
"Blockchain reindexing progress",
|
|
"RPC command processing",
|
|
"Coin selection for transactions",
|
|
"Tor integration and circuit info",
|
|
"ZeroMQ notification system",
|
|
"Shielded (z-addr) RPC operations"
|
|
};
|
|
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
|
|
|
|
// Render as a 4-column grid of checkboxes
|
|
int columns = 4;
|
|
float dbgContentW = availWidth - pad * 2.0f;
|
|
float colW = dbgContentW / columns;
|
|
|
|
for (int i = 0; i < numCats; i++) {
|
|
if (i > 0 && (i % columns) != 0) {
|
|
ImGui::SameLine(pad + colW * (i % columns));
|
|
}
|
|
bool enabled = s_settingsState.debug_categories.count(debugCats[i]) > 0;
|
|
if (ImGui::Checkbox(debugCats[i], &enabled)) {
|
|
if (enabled) {
|
|
s_settingsState.debug_categories.insert(debugCats[i]);
|
|
} else {
|
|
s_settingsState.debug_categories.erase(debugCats[i]);
|
|
}
|
|
s_settingsState.debug_cats_dirty = true;
|
|
saveSettingsPageState(app->settings());
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", debugTips[i]);
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// "Restart daemon" button — only active when categories changed
|
|
if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255));
|
|
ImFont* iconFont = Type().iconSmall();
|
|
if (iconFont) {
|
|
ImGui::PushFont(iconFont);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(ICON_MD_INFO);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, Layout::spacingXs());
|
|
}
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(TR("settings_debug_changed"));
|
|
ImGui::PopStyleColor();
|
|
ImGui::SameLine();
|
|
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
s_settingsState.confirm_restart_daemon = true;
|
|
}
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
bool doCancel = false, doConfirm = false;
|
|
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_btn").c_str(),
|
|
true, doCancel, doConfirm);
|
|
if (doCancel) {
|
|
s_settingsState.confirm_clear_ztx = false;
|
|
}
|
|
if (doConfirm) {
|
|
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
|
if (util::Platform::deleteFile(ztx_file)) {
|
|
Notifications::instance().success("Z-transaction history cleared");
|
|
} else {
|
|
Notifications::instance().info("No history file found");
|
|
}
|
|
s_settingsState.confirm_clear_ztx = false;
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// Confirmation dialog for deleting blockchain data
|
|
if (s_settingsState.confirm_delete_blockchain) {
|
|
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
bool doCancel = false, doConfirm = false;
|
|
material::DialogConfirmFooter(TR("cancel"), TrId("delete_blockchain_confirm", "del_bc_btn").c_str(),
|
|
true, doCancel, doConfirm);
|
|
if (doCancel) {
|
|
s_settingsState.confirm_delete_blockchain = false;
|
|
}
|
|
if (doConfirm) {
|
|
if (app->supportsFullNodeLifecycleActions())
|
|
app->deleteBlockchainData();
|
|
s_settingsState.confirm_delete_blockchain = false;
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// Confirm: rescan blockchain. On a normal (full-history) node this restarts the daemon with
|
|
// -rescan; on a bootstrapped/pruned node that would fail (pre-snapshot blocks are absent), so we
|
|
// probe the lowest available block height and run a runtime rescanblockchain from a confirmed,
|
|
// editable height instead.
|
|
if (s_settingsState.confirm_rescan) {
|
|
// Kick off the one-shot block-range probe the first frame the dialog is open.
|
|
if (!s_settingsState.rescan_height_detecting && !s_settingsState.rescan_height_detected) {
|
|
s_settingsState.rescan_height_detecting = true;
|
|
app->detectLowestAvailableBlockHeight([](bool ok, int lowest, bool fullHistory) {
|
|
s_settingsState.rescan_height_detecting = false;
|
|
s_settingsState.rescan_height_detected = true;
|
|
s_settingsState.rescan_full_history = (!ok) || fullHistory;
|
|
s_settingsState.rescan_start_height = (ok && !fullHistory) ? lowest : 0;
|
|
});
|
|
}
|
|
|
|
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
|
|
const bool detecting = s_settingsState.rescan_height_detecting;
|
|
const bool bootstrapped = s_settingsState.rescan_height_detected && !s_settingsState.rescan_full_history;
|
|
|
|
if (detecting) {
|
|
ImGui::TextWrapped("%s", TR("rescan_detecting"));
|
|
} else if (bootstrapped) {
|
|
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::Text("%s", TR("rescan_from_height"));
|
|
ImGui::SetNextItemWidth(160.0f);
|
|
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
|
|
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
|
|
} else {
|
|
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
if (ImGui::Button(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
|
|
s_settingsState.confirm_rescan = false;
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(detecting);
|
|
if (ImGui::Button(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
|
if (bootstrapped) {
|
|
app->runtimeRescan(s_settingsState.rescan_start_height);
|
|
} else {
|
|
app->rescanBlockchain();
|
|
}
|
|
s_settingsState.confirm_rescan = false;
|
|
}
|
|
ImGui::EndDisabled();
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
|
|
if (s_settingsState.confirm_repair_wallet) {
|
|
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_repair_wallet_safe"));
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
bool doCancel = false, doConfirm = false;
|
|
material::DialogConfirmFooter(TrId("cancel", "repair_wallet_cancel").c_str(),
|
|
TrId("repair_wallet", "repair_wallet_confirm").c_str(),
|
|
false, doCancel, doConfirm);
|
|
if (doCancel) {
|
|
s_settingsState.confirm_repair_wallet = false;
|
|
}
|
|
if (doConfirm) {
|
|
app->repairWallet();
|
|
s_settingsState.confirm_repair_wallet = false;
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
|
|
if (s_settingsState.confirm_reinstall_daemon) {
|
|
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_reinstall_daemon_safe"));
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
bool doCancel = false, doConfirm = false;
|
|
material::DialogConfirmFooter(TrId("cancel", "reinstall_daemon_cancel").c_str(),
|
|
TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(),
|
|
false, doCancel, doConfirm);
|
|
if (doCancel) {
|
|
s_settingsState.confirm_reinstall_daemon = false;
|
|
}
|
|
if (doConfirm) {
|
|
app->reinstallBundledDaemon();
|
|
s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap
|
|
s_settingsState.confirm_reinstall_daemon = false;
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// Confirm: restart daemon (briefly drops the connection to apply changed options)
|
|
if (s_settingsState.confirm_restart_daemon) {
|
|
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
bool doCancel = false, doConfirm = false;
|
|
material::DialogConfirmFooter(TrId("cancel", "restartd_cancel").c_str(),
|
|
TrId("settings_restart_daemon", "restartd_confirm").c_str(),
|
|
false, doCancel, doConfirm);
|
|
if (doCancel) {
|
|
s_settingsState.confirm_restart_daemon = false;
|
|
}
|
|
if (doConfirm) {
|
|
s_settingsState.debug_cats_dirty = false;
|
|
app->restartDaemon();
|
|
s_settingsState.confirm_restart_daemon = false;
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
// Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe)
|
|
if (s_settingsState.confirm_lite_redownload) {
|
|
if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) {
|
|
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg"));
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_lite_redownload_safe"));
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
bool doCancel = false, doConfirm = false;
|
|
material::DialogConfirmFooter(TrId("cancel", "lite_redl_cancel").c_str(),
|
|
TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(),
|
|
false, doCancel, doConfirm);
|
|
if (doCancel) {
|
|
s_settingsState.confirm_lite_redownload = false;
|
|
}
|
|
if (doConfirm) {
|
|
if (auto* lite = app->liteWallet()) lite->startRescan();
|
|
s_settingsState.confirm_lite_redownload = false;
|
|
}
|
|
EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|