- Add startup binary version checking for dragonxd/xmrig - Display daemon version in UI - Add idle mining thread count adjustment - Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard - Add paste button to import private key dialog with address validation - Add z-address generation UI feedback (loading indicator) - Add option to delete blockchain data while preserving wallet.dat - Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus) - Fix Windows RPC auth: trim \r from config values, add .cookie fallback - Fix connection status message during block index loading - Improve application shutdown to prevent lingering background process
2082 lines
101 KiB
C++
2082 lines
101 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 "../../util/logger.h"
|
||
#include "../windows/balance_tab.h"
|
||
#include "../windows/console_tab.h"
|
||
#include "../../util/i18n.h"
|
||
#include "../../util/platform.h"
|
||
#include "../../rpc/rpc_client.h"
|
||
#include "../../rpc/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 "../../embedded/IconsMaterialDesign.h"
|
||
#include "imgui.h"
|
||
#include <nlohmann/json.hpp>
|
||
#include <vector>
|
||
#include <set>
|
||
#include <filesystem>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
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
|
||
// ============================================================================
|
||
static bool sp_initialized = false;
|
||
static int sp_language_index = 0;
|
||
static bool sp_save_ztxs = true;
|
||
static bool sp_allow_custom_fees = false;
|
||
static bool sp_auto_shield = false;
|
||
static bool sp_fetch_prices = true;
|
||
static bool sp_use_tor = false;
|
||
static char sp_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
|
||
static char sp_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
|
||
static char sp_rpc_user[64] = "";
|
||
static char sp_rpc_password[64] = "";
|
||
static char sp_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
|
||
static char sp_addr_explorer[256] = "https://explorer.dragonx.is/address/";
|
||
|
||
// Acrylic settings
|
||
static bool sp_acrylic_enabled = true;
|
||
static float sp_blur_amount = 1.5f; // 0.0=Off, 0.01–4.0 continuous blur multiplier
|
||
static float sp_noise_opacity = 0.5f; // 0.0–1.0 multiplier
|
||
static float sp_ui_opacity = 1.0f; // 0.3–1.0 card/sidebar opacity
|
||
static float sp_window_opacity = 1.0f; // 0.3–1.0 background alpha
|
||
|
||
// Balance layout (string ID)
|
||
static std::string sp_balance_layout = "classic";
|
||
|
||
// Console scanline
|
||
static bool sp_scanline_enabled = true;
|
||
|
||
// Theme effects
|
||
static bool sp_theme_effects_enabled = true;
|
||
|
||
// Gradient background mode
|
||
static bool sp_gradient_background = false;
|
||
|
||
// Low-spec mode
|
||
static bool sp_low_spec_mode = false;
|
||
|
||
// Font scale (user accessibility, 1.0–3.0)
|
||
static float sp_font_scale = 1.0f;
|
||
|
||
// Snapshot of effect settings saved when low-spec is toggled ON,
|
||
// restored when toggled OFF so user state isn't lost.
|
||
struct LowSpecSnapshot {
|
||
bool valid = false;
|
||
bool acrylic_enabled;
|
||
float blur_amount;
|
||
float ui_opacity;
|
||
float window_opacity;
|
||
bool theme_effects_enabled;
|
||
bool scanline_enabled;
|
||
};
|
||
static LowSpecSnapshot s_lowSpecSnap;
|
||
|
||
// Daemon — keep running on close
|
||
static bool sp_keep_daemon_running = false;
|
||
static bool sp_stop_external_daemon = false;
|
||
|
||
// Mining — mine when idle
|
||
static bool sp_mine_when_idle = false;
|
||
static int sp_mine_idle_delay = 120;
|
||
static bool sp_idle_thread_scaling = false;
|
||
static int sp_idle_threads_active = 0;
|
||
static int sp_idle_threads_idle = 0;
|
||
static bool sp_verbose_logging = false;
|
||
|
||
// Debug logging categories
|
||
static std::set<std::string> sp_debug_categories;
|
||
static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted
|
||
static bool sp_debug_expanded = false; // collapsible card state
|
||
static bool sp_effects_expanded = false; // "Advanced Effects..." toggle
|
||
static bool sp_tools_expanded = false; // "Tools & Actions..." toggle
|
||
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
|
||
static bool sp_confirm_delete_blockchain = false; // confirmation dialog for deleting blockchain data
|
||
|
||
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
||
|
||
// Shader-based scroll-edge fade (per-pixel alpha mask via custom fragment shader)
|
||
static effects::ScrollFadeShader s_fadeShader;
|
||
|
||
|
||
|
||
static void loadSettingsPageState(config::Settings* settings) {
|
||
if (!settings) return;
|
||
|
||
sp_save_ztxs = settings->getSaveZtxs();
|
||
sp_allow_custom_fees = settings->getAllowCustomFees();
|
||
sp_auto_shield = settings->getAutoShield();
|
||
sp_fetch_prices = settings->getFetchPrices();
|
||
sp_use_tor = settings->getUseTor();
|
||
|
||
strncpy(sp_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(sp_tx_explorer) - 1);
|
||
strncpy(sp_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(sp_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";
|
||
|
||
sp_language_index = 0;
|
||
int idx = 0;
|
||
for (const auto& lang : languages) {
|
||
if (lang.first == current_lang) {
|
||
sp_language_index = idx;
|
||
break;
|
||
}
|
||
idx++;
|
||
}
|
||
|
||
// Load blur amount directly from saved multiplier
|
||
sp_blur_amount = settings->getBlurMultiplier();
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = settings->getUIOpacity();
|
||
sp_window_opacity = settings->getWindowOpacity();
|
||
sp_noise_opacity = settings->getNoiseOpacity();
|
||
|
||
sp_gradient_background = settings->getGradientBackground();
|
||
|
||
sp_balance_layout = settings->getBalanceLayout();
|
||
sp_scanline_enabled = settings->getScanlineEnabled();
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
||
sp_low_spec_mode = settings->getLowSpecMode();
|
||
effects::setLowSpecMode(sp_low_spec_mode);
|
||
sp_font_scale = settings->getFontScale();
|
||
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
|
||
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
||
sp_stop_external_daemon = settings->getStopExternalDaemon();
|
||
sp_mine_when_idle = settings->getMineWhenIdle();
|
||
sp_mine_idle_delay = settings->getMineIdleDelay();
|
||
sp_idle_thread_scaling = settings->getIdleThreadScaling();
|
||
sp_idle_threads_active = settings->getIdleThreadsActive();
|
||
sp_idle_threads_idle = settings->getIdleThreadsIdle();
|
||
sp_verbose_logging = settings->getVerboseLogging();
|
||
sp_debug_categories = settings->getDebugCategories();
|
||
sp_debug_cats_dirty = false;
|
||
|
||
// Apply loaded visual effects settings
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity);
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
|
||
sp_initialized = true;
|
||
}
|
||
|
||
static void saveSettingsPageState(config::Settings* settings) {
|
||
if (!settings) return;
|
||
|
||
settings->setTheme(settings->getSkinId());
|
||
settings->setSaveZtxs(sp_save_ztxs);
|
||
settings->setAllowCustomFees(sp_allow_custom_fees);
|
||
settings->setAutoShield(sp_auto_shield);
|
||
settings->setFetchPrices(sp_fetch_prices);
|
||
settings->setUseTor(sp_use_tor);
|
||
settings->setTxExplorerUrl(sp_tx_explorer);
|
||
settings->setAddressExplorerUrl(sp_addr_explorer);
|
||
|
||
auto& i18n = util::I18n::instance();
|
||
const auto& languages = i18n.getAvailableLanguages();
|
||
auto it = languages.begin();
|
||
std::advance(it, sp_language_index);
|
||
if (it != languages.end()) {
|
||
settings->setLanguage(it->first);
|
||
}
|
||
|
||
// Visual effects settings
|
||
settings->setAcrylicEnabled(sp_acrylic_enabled);
|
||
settings->setAcrylicQuality(sp_blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
|
||
settings->setBlurMultiplier(sp_blur_amount);
|
||
settings->setUIOpacity(sp_ui_opacity);
|
||
settings->setWindowOpacity(sp_window_opacity);
|
||
settings->setNoiseOpacity(sp_noise_opacity);
|
||
settings->setGradientBackground(sp_gradient_background);
|
||
settings->setScanlineEnabled(sp_scanline_enabled);
|
||
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
||
settings->setLowSpecMode(sp_low_spec_mode);
|
||
settings->setFontScale(sp_font_scale);
|
||
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
||
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
||
settings->setMineWhenIdle(sp_mine_when_idle);
|
||
settings->setMineIdleDelay(sp_mine_idle_delay);
|
||
settings->setIdleThreadScaling(sp_idle_thread_scaling);
|
||
settings->setIdleThreadsActive(sp_idle_threads_active);
|
||
settings->setIdleThreadsIdle(sp_idle_threads_idle);
|
||
settings->setVerboseLogging(sp_verbose_logging);
|
||
settings->setDebugCategories(sp_debug_categories);
|
||
|
||
settings->save();
|
||
}
|
||
|
||
// ============================================================================
|
||
// Settings Page Renderer
|
||
// ============================================================================
|
||
|
||
void RenderSettingsPage(App* app) {
|
||
// Load settings state on first render
|
||
if (!sp_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 (sp_low_spec_mode != runtimeLowSpec) {
|
||
if (runtimeLowSpec) {
|
||
// Hotkey turned low-spec ON — save snapshot, override statics
|
||
s_lowSpecSnap.valid = true;
|
||
s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled;
|
||
s_lowSpecSnap.blur_amount = sp_blur_amount;
|
||
s_lowSpecSnap.ui_opacity = sp_ui_opacity;
|
||
s_lowSpecSnap.window_opacity = sp_window_opacity;
|
||
s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled;
|
||
s_lowSpecSnap.scanline_enabled = sp_scanline_enabled;
|
||
sp_acrylic_enabled = false;
|
||
sp_blur_amount = 0.0f;
|
||
sp_ui_opacity = 1.0f;
|
||
sp_window_opacity = 1.0f;
|
||
sp_theme_effects_enabled = false;
|
||
sp_scanline_enabled = false;
|
||
} else if (s_lowSpecSnap.valid) {
|
||
// Hotkey turned low-spec OFF — restore snapshot
|
||
sp_blur_amount = s_lowSpecSnap.blur_amount;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = s_lowSpecSnap.ui_opacity;
|
||
sp_window_opacity = s_lowSpecSnap.window_opacity;
|
||
sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled;
|
||
sp_scanline_enabled = s_lowSpecSnap.scanline_enabled;
|
||
s_lowSpecSnap.valid = false;
|
||
} else if (app->settings()) {
|
||
// No snapshot — read prefs from settings file
|
||
sp_blur_amount = app->settings()->getBlurMultiplier();
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = app->settings()->getUIOpacity();
|
||
sp_window_opacity = app->settings()->getWindowOpacity();
|
||
sp_theme_effects_enabled = app->settings()->getThemeEffectsEnabled();
|
||
sp_scanline_enabled = app->settings()->getScanlineEnabled();
|
||
}
|
||
sp_low_spec_mode = runtimeLowSpec;
|
||
}
|
||
bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled();
|
||
if (sp_theme_effects_enabled != runtimeThemeEffects) {
|
||
sp_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 && !sp_low_spec_mode && s_fadeShader.init()) {
|
||
s_fadeShader.fadeTopY = settingsFadeTopY;
|
||
s_fadeShader.fadeBottomY = settingsFadeBottomY;
|
||
s_fadeShader.fadeZoneTop = settingsFadeZoneTop;
|
||
s_fadeShader.fadeZoneBottom = settingsFadeZoneBot;
|
||
s_fadeShader.addBind(dl);
|
||
}
|
||
|
||
// Top margin from schema
|
||
float topMargin = schema::UI().drawElement("components.settings-page", "top-margin").size;
|
||
if (topMargin > 0.0f)
|
||
ImGui::Dummy(ImVec2(0, topMargin));
|
||
|
||
GlassPanelSpec glassSpec;
|
||
glassSpec.rounding = glassRound;
|
||
ImFont* capFont = Type().caption();
|
||
ImFont* body2 = Type().body2();
|
||
ImFont* sub1 = Type().subtitle1();
|
||
|
||
// ====================================================================
|
||
// THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit
|
||
// which breaks BeginCombo popup rendering in some ImGui versions)
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size;
|
||
float compactBP = S.drawElement("components.settings-page", "compact-breakpoint").size;
|
||
bool wideLayout = availWidth >= compactBP;
|
||
float refreshBtnW = S.drawElement("components.settings-page", "refresh-btn-width").size;
|
||
|
||
// --- Skin data ---
|
||
auto& skinMgr = schema::SkinManager::instance();
|
||
const auto& skins = skinMgr.available();
|
||
std::string active_preview = "DragonX";
|
||
bool active_is_custom = false;
|
||
for (const auto& skin : skins) {
|
||
if (skin.id == skinMgr.activeSkinId()) {
|
||
active_preview = skin.name;
|
||
active_is_custom = !skin.bundled;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// --- Language data ---
|
||
auto& i18n = util::I18n::instance();
|
||
const auto& languages = i18n.getAvailableLanguages();
|
||
std::vector<const char*> lang_names;
|
||
lang_names.reserve(languages.size());
|
||
for (const auto& lang : languages) {
|
||
lang_names.push_back(lang.second.c_str());
|
||
}
|
||
|
||
// --- Balance layout data ---
|
||
const auto& layouts = GetBalanceLayouts();
|
||
std::string balPreview = sp_balance_layout;
|
||
for (const auto& l : layouts) {
|
||
if (l.id == sp_balance_layout) { balPreview = l.name; break; }
|
||
}
|
||
|
||
// --- Theme combo popup (shared between wide and narrow paths) ---
|
||
auto renderThemeComboPopup = [&]() {
|
||
ImGui::TextDisabled("%s", TR("settings_builtin"));
|
||
ImGui::Separator();
|
||
for (size_t i = 0; i < skins.size(); i++) {
|
||
const auto& skin = skins[i];
|
||
if (!skin.bundled) continue;
|
||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
|
||
skinMgr.setActiveSkin(skin.id);
|
||
if (app->settings()) {
|
||
app->settings()->setSkinId(skin.id);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (is_selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
bool has_custom = false;
|
||
for (const auto& skin : skins) {
|
||
if (!skin.bundled) { has_custom = true; break; }
|
||
}
|
||
if (has_custom) {
|
||
ImGui::Spacing();
|
||
ImGui::TextDisabled("%s", TR("settings_custom"));
|
||
ImGui::Separator();
|
||
for (size_t i = 0; i < skins.size(); i++) {
|
||
const auto& skin = skins[i];
|
||
if (skin.bundled) continue;
|
||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||
if (!skin.valid) {
|
||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||
ImGui::BeginDisabled(true);
|
||
std::string lbl = skin.name + " (invalid)";
|
||
ImGui::Selectable(lbl.c_str(), false);
|
||
ImGui::EndDisabled();
|
||
ImGui::PopStyleColor();
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||
ImGui::SetTooltip("%s", skin.validationError.c_str());
|
||
} else {
|
||
std::string lbl = skin.name;
|
||
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
|
||
if (ImGui::Selectable(lbl.c_str(), is_selected)) {
|
||
skinMgr.setActiveSkin(skin.id);
|
||
if (app->settings()) {
|
||
app->settings()->setSkinId(skin.id);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (is_selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
if (wideLayout) {
|
||
// ============================================================
|
||
// Wide: 3 combos on one row + compact 3-column effects grid
|
||
// ============================================================
|
||
|
||
// --- Combo row: Theme | Layout | Language [Refresh] ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
float lblGap = Layout::spacingXs();
|
||
float lblThemeW = ImGui::CalcTextSize(TR("theme")).x + lblGap;
|
||
float lblLayoutW = ImGui::CalcTextSize(TR("balance_layout")).x + lblGap;
|
||
float lblLangW = ImGui::CalcTextSize(TR("language")).x + lblGap;
|
||
float totalFixed = lblThemeW + lblLayoutW + lblLangW
|
||
+ comboGap * 2 + Layout::spacingSm() + refreshBtnW;
|
||
float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f);
|
||
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("theme"));
|
||
ImGui::SameLine(0, lblGap);
|
||
ImGui::SetNextItemWidth(comboW);
|
||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||
renderThemeComboPopup();
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
|
||
|
||
ImGui::SameLine(0, comboGap);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("balance_layout"));
|
||
ImGui::SameLine(0, lblGap);
|
||
ImGui::SetNextItemWidth(comboW);
|
||
if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) {
|
||
for (const auto& l : layouts) {
|
||
if (!l.enabled) continue;
|
||
bool selected = (l.id == sp_balance_layout);
|
||
if (ImGui::Selectable(l.name.c_str(), selected)) {
|
||
sp_balance_layout = l.id;
|
||
if (app->settings()) {
|
||
app->settings()->setBalanceLayout(sp_balance_layout);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
|
||
|
||
ImGui::SameLine(0, comboGap);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("language"));
|
||
ImGui::SameLine(0, lblGap);
|
||
ImGui::SetNextItemWidth(comboW);
|
||
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
|
||
static_cast<int>(lang_names.size()))) {
|
||
auto it = languages.begin();
|
||
std::advance(it, sp_language_index);
|
||
i18n.loadLanguage(it->first);
|
||
if (app->settings()) {
|
||
app->settings()->setLanguage(it->first);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
|
||
|
||
ImGui::SameLine(0, Layout::spacingSm());
|
||
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||
schema::SkinManager::instance().refresh();
|
||
Notifications::instance().info("Theme list refreshed");
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip(TR("tt_scan_themes"),
|
||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||
}
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Font Scale slider (always visible) ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::TextUnformatted(TR("font_scale"));
|
||
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW);
|
||
ImGui::SetNextItemWidth(fontSliderW);
|
||
sp_font_scale = Layout::userFontScale();
|
||
float prev_font_scale = sp_font_scale;
|
||
{
|
||
char fs_fmt[16];
|
||
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale);
|
||
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp);
|
||
}
|
||
sp_font_scale = std::max(1.0f, std::min(1.5f,
|
||
std::round(sp_font_scale * 20.0f) / 20.0f));
|
||
if (sp_font_scale != prev_font_scale)
|
||
Layout::setUserFontScaleVisual(sp_font_scale);
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
Layout::setUserFontScale(sp_font_scale);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Collapsible: Advanced Effects... ---
|
||
{
|
||
const char* arrow = sp_effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
||
ImGui::PushFont(body2);
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
|
||
{
|
||
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
|
||
if (ImGui::Button("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
|
||
sp_effects_expanded = !sp_effects_expanded;
|
||
}
|
||
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
|
||
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
|
||
ImFont* iconFont = Type().iconSmall();
|
||
if (!iconFont) iconFont = body2;
|
||
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
|
||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
|
||
}
|
||
ImGui::PopStyleColor(3);
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
if (sp_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(), &sp_low_spec_mode)) {
|
||
effects::setLowSpecMode(sp_low_spec_mode);
|
||
if (sp_low_spec_mode) {
|
||
s_lowSpecSnap.valid = true;
|
||
s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled;
|
||
s_lowSpecSnap.blur_amount = sp_blur_amount;
|
||
s_lowSpecSnap.ui_opacity = sp_ui_opacity;
|
||
s_lowSpecSnap.window_opacity = sp_window_opacity;
|
||
s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled;
|
||
s_lowSpecSnap.scanline_enabled = sp_scanline_enabled;
|
||
sp_acrylic_enabled = false;
|
||
sp_blur_amount = 0.0f;
|
||
sp_ui_opacity = 1.0f;
|
||
sp_window_opacity = 1.0f;
|
||
sp_theme_effects_enabled = false;
|
||
sp_scanline_enabled = false;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
|
||
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
|
||
effects::ThemeEffects::instance().setEnabled(false);
|
||
ConsoleTab::s_scanline_enabled = false;
|
||
} else if (s_lowSpecSnap.valid) {
|
||
sp_blur_amount = s_lowSpecSnap.blur_amount;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = s_lowSpecSnap.ui_opacity;
|
||
sp_window_opacity = s_lowSpecSnap.window_opacity;
|
||
sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled;
|
||
sp_scanline_enabled = s_lowSpecSnap.scanline_enabled;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
s_lowSpecSnap.valid = false;
|
||
}
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &sp_gradient_background)) {
|
||
schema::SkinManager::instance().setGradientMode(sp_gradient_background);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
|
||
|
||
ImGui::BeginDisabled(sp_low_spec_mode);
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
app->settings()->setScanlineEnabled(sp_scanline_enabled);
|
||
app->settings()->save();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &sp_theme_effects_enabled)) {
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
|
||
|
||
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
||
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||
float ctrlW = std::max(effCtrlMinW, halfW);
|
||
float baseX = ImGui::GetCursorScreenPos().x;
|
||
float rightX = baseX + ctrlW + Layout::spacingLg();
|
||
|
||
ImGui::TextUnformatted(TR("acrylic"));
|
||
float row1Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char blur_fmt[16];
|
||
if (sp_blur_amount < 0.01f)
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||
else
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
|
||
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
|
||
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
||
|
||
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH));
|
||
ImGui::TextUnformatted(TR("noise"));
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char noise_fmt[16];
|
||
if (sp_noise_opacity < 0.01f)
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
||
else
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
|
||
|
||
ImGui::TextUnformatted(TR("ui_opacity"));
|
||
float row2Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char uiop_fmt[16];
|
||
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", sp_ui_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##UIOpacity", &sp_ui_opacity, 0.3f, 1.0f, uiop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
|
||
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
|
||
ImGui::TextUnformatted(TR("window_opacity"));
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char winop_fmt[16];
|
||
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", sp_window_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##WindowOpacity", &sp_window_opacity, 0.3f, 1.0f, winop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
|
||
|
||
ImGui::EndDisabled(); // low-spec
|
||
ImGui::PopFont();
|
||
} // sp_effects_expanded
|
||
} else {
|
||
// ============================================================
|
||
// Narrow: stacked combos + 2-column effects (original layout)
|
||
// ============================================================
|
||
|
||
// --- Theme row ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("theme"));
|
||
ImGui::SameLine(labelW);
|
||
|
||
float themeComboW = std::max(S.drawElement("components.settings-page", "theme-combo-min-width").size,
|
||
availWidth - pad * 2 - labelW - refreshBtnW - Layout::spacingSm());
|
||
ImGui::SetNextItemWidth(themeComboW);
|
||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||
renderThemeComboPopup();
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
|
||
if (active_is_custom) {
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_theme"));
|
||
}
|
||
ImGui::SameLine();
|
||
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||
schema::SkinManager::instance().refresh();
|
||
Notifications::instance().info("Theme list refreshed");
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip(TR("tt_scan_themes"),
|
||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||
}
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// --- Balance Layout row ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("balance_layout"));
|
||
ImGui::SameLine(labelW);
|
||
ImGui::SetNextItemWidth(std::max(180.0f, inputW));
|
||
if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) {
|
||
for (const auto& l : layouts) {
|
||
if (!l.enabled) continue;
|
||
bool selected = (l.id == sp_balance_layout);
|
||
if (ImGui::Selectable(l.name.c_str(), selected)) {
|
||
sp_balance_layout = l.id;
|
||
if (app->settings()) {
|
||
app->settings()->setBalanceLayout(sp_balance_layout);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// --- Language row ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("language"));
|
||
ImGui::SameLine(labelW);
|
||
ImGui::SetNextItemWidth(inputW);
|
||
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
|
||
static_cast<int>(lang_names.size()))) {
|
||
auto it = languages.begin();
|
||
std::advance(it, sp_language_index);
|
||
i18n.loadLanguage(it->first);
|
||
if (app->settings()) {
|
||
app->settings()->setLanguage(it->first);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Font Scale slider (always visible) ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::TextUnformatted(TR("font_scale"));
|
||
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||
availWidth - pad * 2);
|
||
ImGui::SetNextItemWidth(fontSliderW);
|
||
sp_font_scale = Layout::userFontScale();
|
||
float prev_font_scale = sp_font_scale;
|
||
{
|
||
char fs_fmt[16];
|
||
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale);
|
||
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp);
|
||
}
|
||
sp_font_scale = std::max(1.0f, std::min(1.5f,
|
||
std::round(sp_font_scale * 20.0f) / 20.0f));
|
||
if (sp_font_scale != prev_font_scale)
|
||
Layout::setUserFontScaleVisual(sp_font_scale);
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
Layout::setUserFontScale(sp_font_scale);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Collapsible: Advanced Effects... ---
|
||
{
|
||
const char* arrow = sp_effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
||
ImGui::PushFont(body2);
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
|
||
{
|
||
float narrowContentW = availWidth - pad * 2;
|
||
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
|
||
if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) {
|
||
sp_effects_expanded = !sp_effects_expanded;
|
||
}
|
||
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
|
||
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
|
||
ImFont* iconFont = Type().iconSmall();
|
||
if (!iconFont) iconFont = body2;
|
||
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
|
||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow);
|
||
}
|
||
ImGui::PopStyleColor(3);
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
if (sp_effects_expanded) {
|
||
ImGui::PushFont(body2);
|
||
|
||
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) {
|
||
effects::setLowSpecMode(sp_low_spec_mode);
|
||
if (sp_low_spec_mode) {
|
||
s_lowSpecSnap.valid = true;
|
||
s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled;
|
||
s_lowSpecSnap.blur_amount = sp_blur_amount;
|
||
s_lowSpecSnap.ui_opacity = sp_ui_opacity;
|
||
s_lowSpecSnap.window_opacity = sp_window_opacity;
|
||
s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled;
|
||
s_lowSpecSnap.scanline_enabled = sp_scanline_enabled;
|
||
sp_acrylic_enabled = false;
|
||
sp_blur_amount = 0.0f;
|
||
sp_ui_opacity = 1.0f;
|
||
sp_window_opacity = 1.0f;
|
||
sp_theme_effects_enabled = false;
|
||
sp_scanline_enabled = false;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
|
||
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
|
||
effects::ThemeEffects::instance().setEnabled(false);
|
||
ConsoleTab::s_scanline_enabled = false;
|
||
} else if (s_lowSpecSnap.valid) {
|
||
sp_blur_amount = s_lowSpecSnap.blur_amount;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = s_lowSpecSnap.ui_opacity;
|
||
sp_window_opacity = s_lowSpecSnap.window_opacity;
|
||
sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled;
|
||
sp_scanline_enabled = s_lowSpecSnap.scanline_enabled;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
s_lowSpecSnap.valid = false;
|
||
}
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
|
||
|
||
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &sp_gradient_background)) {
|
||
schema::SkinManager::instance().setGradientMode(sp_gradient_background);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
|
||
|
||
ImGui::BeginDisabled(sp_low_spec_mode);
|
||
|
||
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
app->settings()->setScanlineEnabled(sp_scanline_enabled);
|
||
app->settings()->save();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &sp_theme_effects_enabled)) {
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
|
||
|
||
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||
availWidth - pad * 2.0f);
|
||
ImGui::TextUnformatted(TR("acrylic"));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char blur_fmt[16];
|
||
if (sp_blur_amount < 0.01f)
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||
else
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
|
||
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
|
||
|
||
ImGui::TextUnformatted(TR("noise"));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char noise_fmt[16];
|
||
if (sp_noise_opacity < 0.01f)
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
||
else
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
|
||
|
||
ImGui::TextUnformatted(TR("ui_opacity"));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char uiop_fmt[16];
|
||
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", sp_ui_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##UIOpacity", &sp_ui_opacity, 0.3f, 1.0f, uiop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
|
||
|
||
ImGui::TextUnformatted(TR("window_opacity"));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char winop_fmt[16];
|
||
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", sp_window_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##WindowOpacity", &sp_window_opacity, 0.3f, 1.0f, winop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
|
||
|
||
ImGui::EndDisabled(); // low-spec
|
||
ImGui::PopFont();
|
||
} // sp_effects_expanded
|
||
}
|
||
|
||
// Bottom padding
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
// Draw glass panel behind content (auto-sized)
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// WALLET — card (privacy/daemon toggles + collapsible tools)
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
|
||
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
|
||
{
|
||
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
|
||
+ cbW(TR("keep_daemon")) + cbSpacing
|
||
+ cbW(TR("stop_external"));
|
||
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(), &sp_save_ztxs);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_ztx"));
|
||
ImGui::SameLine(0, sp);
|
||
ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &sp_auto_shield);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_shield"));
|
||
ImGui::SameLine(0, sp);
|
||
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &sp_use_tor);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
|
||
ImGui::SameLine(0, sp);
|
||
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &sp_keep_daemon_running)) {
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_keep_daemon"));
|
||
ImGui::SameLine(0, sp);
|
||
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &sp_stop_external_daemon)) {
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_stop_external"));
|
||
ImGui::SameLine(0, sp);
|
||
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &sp_verbose_logging)) {
|
||
dragonx::util::Logger::instance().setVerbose(sp_verbose_logging);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("%s", TR("tt_verbose"));
|
||
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Collapsible: Tools & Actions... ---
|
||
{
|
||
const char* arrow = sp_tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
||
ImGui::PushFont(body2);
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
|
||
{
|
||
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
|
||
if (ImGui::Button("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
|
||
sp_tools_expanded = !sp_tools_expanded;
|
||
}
|
||
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
|
||
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions"));
|
||
ImFont* iconFont = Type().iconSmall();
|
||
if (!iconFont) iconFont = body2;
|
||
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
|
||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
|
||
}
|
||
ImGui::PopStyleColor(3);
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
if (sp_tools_expanded) {
|
||
float btnSpacing = Layout::spacingMd();
|
||
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
|
||
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
|
||
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
|
||
bw = std::max(minBtnW, bw);
|
||
|
||
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
|
||
AddressBookDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book"));
|
||
ImGui::SameLine(0, btnSpacing);
|
||
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||
ValidateAddressDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate"));
|
||
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
||
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
|
||
RequestPaymentDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment"));
|
||
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
|
||
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
||
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining"));
|
||
ImGui::SameLine(0, btnSpacing);
|
||
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge"));
|
||
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
||
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
|
||
sp_confirm_clear_ztx = true;
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// BACKUP & DATA — card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
|
||
|
||
{
|
||
const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")};
|
||
const char* t1[] = {
|
||
TR("tt_import_key"),
|
||
TR("tt_export_key"),
|
||
TR("tt_export_all"),
|
||
TR("tt_backup"),
|
||
TR("tt_export_csv")
|
||
};
|
||
const char* wizLabel = TR("setup_wizard");
|
||
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 = ImGui::CalcTextSize(wizLabel).x + btnPadX;
|
||
float totalW = naturalW + wizW + sp * 6;
|
||
|
||
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
|
||
|
||
float scaledSp = sp * scale;
|
||
|
||
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
|
||
app->showImportKeyDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[0]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
|
||
app->showExportKeyDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[1]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
|
||
ExportAllKeysDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[2]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
|
||
app->showBackupDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[3]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
|
||
ExportTransactionsDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
|
||
|
||
// Right-align Setup Wizard
|
||
float curX = ImGui::GetCursorScreenPos().x;
|
||
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
|
||
if (scale < 1.0f) wizBtnW *= scale;
|
||
float rightEdge = cardMin.x + availWidth - pad;
|
||
float wizX = rightEdge - wizBtnW;
|
||
if (wizX > curX) {
|
||
ImGui::SameLine(0, 0);
|
||
ImGui::SetCursorScreenPos(ImVec2(wizX, ImGui::GetCursorScreenPos().y));
|
||
} else {
|
||
ImGui::SameLine(0, scaledSp);
|
||
}
|
||
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
||
app->restartWizard();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
|
||
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// NODE & SECURITY — card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
|
||
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
|
||
auto rowBtnW = [&](std::initializer_list<const char*> labels) -> float {
|
||
float maxTextW = 0;
|
||
for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x);
|
||
return std::max(minBtnW, maxTextW + btnPad * 2);
|
||
};
|
||
|
||
// --- NODE (left) + SECURITY (right) side by side ---
|
||
{
|
||
float colGap = Layout::spacingLg();
|
||
float leftColW = (contentW - colGap) * 0.6f;
|
||
float rightColW = (contentW - colGap) * 0.4f;
|
||
ImVec2 sectionOrigin = ImGui::GetCursorScreenPos();
|
||
float leftX = sectionOrigin.x;
|
||
float rightX = sectionOrigin.x + leftColW + colGap;
|
||
|
||
// ---- LEFT COLUMN: NODE ----
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y));
|
||
ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y),
|
||
ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true);
|
||
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
{
|
||
ImFont* body2Info = S.resolveFont("body2");
|
||
if (!body2Info) body2Info = Type().body2();
|
||
ImGui::PushFont(body2Info);
|
||
|
||
float rpcLblW = std::max(
|
||
S.drawElement("components.settings-page", "rpc-label-min-width").size,
|
||
std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs));
|
||
|
||
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
|
||
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
|
||
|
||
// Data Dir + Wallet Size — same line with "Label: value" format
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::AlignTextToFramePadding();
|
||
{
|
||
// Measure the wallet size string first
|
||
std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found");
|
||
std::string dirPath = util::Platform::getDragonXDataDir();
|
||
|
||
// Calculate space taken by "Wallet Size: <size>" on the right
|
||
float walletSizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x + Layout::spacingXs();
|
||
float walletSizeValW = ImGui::CalcTextSize(size_str.c_str()).x;
|
||
float walletSizeTotalW = walletSizeLabelW + walletSizeValW + Layout::spacingLg();
|
||
|
||
// Available space for "Data Dir: <path>"
|
||
float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x + Layout::spacingXs();
|
||
float availForPath = leftColW - walletSizeTotalW - dataDirLabelW;
|
||
|
||
ImGui::TextUnformatted(TR("settings_data_dir"));
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str());
|
||
ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary());
|
||
ImVec4 linkHoverCol = linkCol;
|
||
linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f);
|
||
bool hovered = false;
|
||
if (pathSize.x > availForPath && availForPath > 0) {
|
||
float scale = availForPath / pathSize.x;
|
||
float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f);
|
||
ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
|
||
hovered = ImGui::IsItemHovered();
|
||
if (hovered) {
|
||
ImVec2 tMin = ImGui::GetItemRectMin();
|
||
ImVec2 tMax = ImGui::GetItemRectMax();
|
||
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
|
||
}
|
||
ImGui::SetWindowFontScale(1.0f);
|
||
} else {
|
||
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
|
||
hovered = ImGui::IsItemHovered();
|
||
if (hovered) {
|
||
ImVec2 tMin = ImGui::GetItemRectMin();
|
||
ImVec2 tMax = ImGui::GetItemRectMax();
|
||
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
|
||
}
|
||
}
|
||
if (hovered) {
|
||
ImGui::SetTooltip("%s", TR("tt_open_dir"));
|
||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||
}
|
||
if (ImGui::IsItemClicked())
|
||
util::Platform::openFolder(dirPath);
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
ImGui::TextUnformatted(TR("settings_wallet_size_label"));
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
if (wallet_size > 0) {
|
||
ImGui::TextUnformatted(size_str.c_str());
|
||
} else {
|
||
ImGui::TextDisabled("%s", TR("settings_not_found"));
|
||
}
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// RPC connection — two columns: (Host | Username) and (Port | Password)
|
||
float rpcHalfW = (leftColW - Layout::spacingMd()) * 0.5f;
|
||
float rpcHalfLblW = std::min(rpcLblW, rpcHalfW * 0.4f);
|
||
float rpcHalfInputW = std::max(40.0f, rpcHalfW - rpcHalfLblW);
|
||
float rpcRightColX = leftX + rpcHalfW + Layout::spacingMd();
|
||
|
||
// Row 1: RPC Host + Username
|
||
float row1Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, row1Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("rpc_host"));
|
||
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_host"));
|
||
|
||
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("rpc_user"));
|
||
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_user"));
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
|
||
|
||
// Row 2: Port + Password
|
||
float row2Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, row2Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("rpc_port"));
|
||
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_port"));
|
||
|
||
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("rpc_pass"));
|
||
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password),
|
||
ImGuiInputTextFlags_Password);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_pass"));
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||
TR("settings_auto_detected"));
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
// Node maintenance buttons
|
||
{
|
||
ImFont* btnFont = S.resolveFont("button");
|
||
float nodeBtnW;
|
||
{
|
||
if (btnFont) ImGui::PushFont(btnFont);
|
||
nodeBtnW = rowBtnW({TR("test_connection"), TR("rescan")});
|
||
if (btnFont) ImGui::PopFont(/* btnFont */);
|
||
}
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::BeginDisabled(!app->isConnected());
|
||
if (TactileButton(TR("test_connection"), ImVec2(nodeBtnW, 0), btnFont)) {
|
||
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
|
||
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||
try {
|
||
rpc->call("getinfo");
|
||
return []() {
|
||
Notifications::instance().success("RPC connection OK");
|
||
};
|
||
} catch (const std::exception& e) {
|
||
std::string err = e.what();
|
||
return [err]() {
|
||
Notifications::instance().error("RPC error: " + err);
|
||
};
|
||
}
|
||
});
|
||
} else {
|
||
Notifications::instance().warning("Not connected to daemon");
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_test_conn"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) {
|
||
app->rescanBlockchain();
|
||
}
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
|
||
ImGui::EndDisabled();
|
||
|
||
// Delete blockchain button (always available when using embedded daemon)
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
|
||
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
|
||
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
|
||
sp_confirm_delete_blockchain = true;
|
||
}
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
|
||
ImGui::EndDisabled();
|
||
}
|
||
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
float leftBottom = ImGui::GetCursorScreenPos().y;
|
||
ImGui::PopClipRect();
|
||
|
||
// ---- RIGHT COLUMN: SECURITY ----
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, sectionOrigin.y));
|
||
ImGui::PushClipRect(ImVec2(rightX, sectionOrigin.y),
|
||
ImVec2(rightX + rightColW, sectionOrigin.y + 9999), true);
|
||
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
{
|
||
// Encrypt / Change passphrase / Lock buttons
|
||
bool isEncrypted = app->state().isEncrypted();
|
||
bool isLocked = app->state().isLocked();
|
||
|
||
float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (rightColW - Layout::spacingMd()) * 0.5f);
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
if (!isEncrypted) {
|
||
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->showEncryptDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_encrypt"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted"));
|
||
} else {
|
||
if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->showChangePassphraseDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pass"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (isLocked) {
|
||
ImGui::PushFont(Type().iconSmall());
|
||
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%s", TR("settings_locked"));
|
||
} else {
|
||
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->lockWallet();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_lock"));
|
||
ImGui::SameLine(0, Layout::spacingSm());
|
||
ImGui::PushFont(Type().iconSmall());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked"));
|
||
}
|
||
// Remove Encryption button
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
|
||
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->showDecryptDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_encrypt"));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// Auto-Lock timeout
|
||
{
|
||
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
|
||
|
||
int timeout = app->settings()->getAutoLockTimeout();
|
||
const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" };
|
||
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
|
||
int selTimeout = 0;
|
||
for (int i = 0; i < 6; i++) {
|
||
if (timeoutValues[i] == timeout) { selTimeout = i; break; }
|
||
}
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock"));
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::PushItemWidth(comboW);
|
||
if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) {
|
||
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
|
||
app->settings()->save();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_lock"));
|
||
ImGui::PopItemWidth();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// PIN unlock section
|
||
bool isEncryptedPIN = app->state().isEncrypted();
|
||
if (isEncryptedPIN) {
|
||
bool hasPIN = app->hasPinVault();
|
||
float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (rightColW - Layout::spacingSm()) * 0.5f);
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
if (!hasPIN) {
|
||
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||
app->showPinSetupDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_set_pin"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin"));
|
||
} else {
|
||
if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||
app->showPinChangeDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pin"));
|
||
ImGui::SameLine(0, Layout::spacingSm());
|
||
if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||
app->showPinRemoveDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_pin"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
ImGui::PushFont(Type().iconSmall());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active"));
|
||
}
|
||
} else {
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin"));
|
||
}
|
||
}
|
||
|
||
float rightBottom = ImGui::GetCursorScreenPos().y;
|
||
ImGui::PopClipRect();
|
||
|
||
// Advance cursor past both columns
|
||
float maxBottom = std::max(leftBottom, rightBottom);
|
||
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// EXPLORER & OPTIONS — full-width card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
ImGui::PushFont(body2);
|
||
|
||
// Row 1: Transaction URL | Address URL (side-by-side)
|
||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||
float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs();
|
||
float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs();
|
||
float inputTxW = std::max(80.0f, halfW - lblTxW);
|
||
float inputAddrW = std::max(80.0f, halfW - lblAddrW);
|
||
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("transaction_url"));
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::SetNextItemWidth(inputTxW);
|
||
ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tx_url"));
|
||
ImGui::SameLine(pad + halfW + Layout::spacingLg());
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(TR("address_url"));
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::SetNextItemWidth(inputAddrW);
|
||
ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_addr_url"));
|
||
|
||
ImGui::PopFont();
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// Row 2: Checkboxes + Block Explorer button (on one line)
|
||
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &sp_allow_custom_fees);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_fees"));
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &sp_fetch_prices);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_fetch_prices"));
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_block_explorer"));
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// ABOUT — card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("about"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
// Logo on the left side of the about card
|
||
ImTextureID logoTex = app->getLogoTexture();
|
||
float logoAreaW = 0;
|
||
if (logoTex != 0) {
|
||
float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f);
|
||
float logoH = logoMaxH;
|
||
float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
|
||
float logoW = logoH * aspect;
|
||
ImVec2 logoPos = ImGui::GetCursorScreenPos();
|
||
dl->AddImage(logoTex,
|
||
ImVec2(logoPos.x, logoPos.y),
|
||
ImVec2(logoPos.x + logoW, logoPos.y + logoH));
|
||
logoAreaW = logoW + Layout::spacingLg();
|
||
ImGui::Indent(logoAreaW);
|
||
}
|
||
|
||
float contentW = availWidth - pad * 2 - logoAreaW;
|
||
|
||
// App name + version on same line
|
||
ImGui::PushFont(sub1);
|
||
ImGui::TextUnformatted(DRAGONX_APP_NAME);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
ImGui::PushFont(body2);
|
||
snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION);
|
||
ImGui::TextUnformatted(buf);
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION);
|
||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
|
||
ImGui::PopFont();
|
||
|
||
// Daemon version
|
||
{
|
||
const auto& st = app->state();
|
||
if (st.daemon_version > 0) {
|
||
int dmaj = st.daemon_version / 1000000;
|
||
int dmin = (st.daemon_version / 10000) % 100;
|
||
int dpat = (st.daemon_version / 100) % 100;
|
||
ImGui::PushFont(body2);
|
||
snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat);
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf);
|
||
ImGui::PopFont();
|
||
}
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
ImGui::PushFont(body2);
|
||
ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW);
|
||
ImGui::TextUnformatted(TR("settings_about_text"));
|
||
ImGui::PopTextWrapPos();
|
||
ImGui::PopFont();
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
ImGui::PushFont(capFont);
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_copyright"));
|
||
ImGui::PopFont();
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||
|
||
// Buttons — consistent equal-width row (full card width)
|
||
if (logoAreaW > 0) {
|
||
ImGui::Unindent(logoAreaW);
|
||
}
|
||
{
|
||
float fullContentW = availWidth - pad * 2;
|
||
float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f;
|
||
|
||
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
util::Platform::openUrl("https://dragonx.is");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_website"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_report_bug"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
saveSettingsPageState(app->settings());
|
||
Notifications::instance().success("Settings saved");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_settings"));
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
if (app->settings()) {
|
||
loadSettingsPageState(app->settings());
|
||
Notifications::instance().info("Settings reloaded from disk");
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reset_settings"));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// DEBUG LOGGING — collapsible card with glass styling
|
||
// ====================================================================
|
||
{
|
||
// Clickable header row
|
||
ImVec2 headerPos = ImGui::GetCursorScreenPos();
|
||
const char* arrow = sp_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()))) {
|
||
sp_debug_expanded = !sp_debug_expanded;
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", sp_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 (sp_debug_expanded) {
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select"));
|
||
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// The 22 dragonxd debug categories
|
||
static const char* debugCats[] = {
|
||
"addrman", "alert", "bench", "coindb", "db", "estimatefee",
|
||
"http", "libevent", "lock", "mempool", "net",
|
||
"paymentdisclosure", "pow", "proxy", "prune", "rand",
|
||
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
|
||
};
|
||
static const char* debugTips[] = {
|
||
"Peer address tracking and management",
|
||
"Alert system messages",
|
||
"Benchmark timings for operations",
|
||
"Coin database read/write operations",
|
||
"Berkeley DB operations",
|
||
"Fee estimation algorithm",
|
||
"HTTP RPC server activity",
|
||
"Libevent networking library",
|
||
"Lock contention debugging",
|
||
"Transaction memory pool activity",
|
||
"Network connections and messages",
|
||
"Payment disclosure protocol",
|
||
"Proof-of-work mining activity",
|
||
"SOCKS5 proxy connections",
|
||
"Block pruning operations",
|
||
"Random number generation",
|
||
"Blockchain reindexing progress",
|
||
"RPC command processing",
|
||
"Coin selection for transactions",
|
||
"Tor integration and circuit info",
|
||
"ZeroMQ notification system",
|
||
"Shielded (z-addr) RPC operations"
|
||
};
|
||
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
|
||
|
||
// Render as a 4-column grid of checkboxes
|
||
int columns = 4;
|
||
float dbgContentW = availWidth - pad * 2.0f;
|
||
float colW = dbgContentW / columns;
|
||
|
||
for (int i = 0; i < numCats; i++) {
|
||
if (i > 0 && (i % columns) != 0) {
|
||
ImGui::SameLine(pad + colW * (i % columns));
|
||
}
|
||
bool enabled = sp_debug_categories.count(debugCats[i]) > 0;
|
||
if (ImGui::Checkbox(debugCats[i], &enabled)) {
|
||
if (enabled) {
|
||
sp_debug_categories.insert(debugCats[i]);
|
||
} else {
|
||
sp_debug_categories.erase(debugCats[i]);
|
||
}
|
||
sp_debug_cats_dirty = true;
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", debugTips[i]);
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// "Restart daemon" button — only active when categories changed
|
||
if (sp_debug_cats_dirty) {
|
||
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"))) {
|
||
sp_debug_cats_dirty = false;
|
||
app->restartDaemon();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon"));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
}
|
||
|
||
// --- Shader-based scroll fade: unbind (restore ImGui's default shader) ---
|
||
if (fadeH > 0.0f && !sp_low_spec_mode && s_fadeShader.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 (sp_confirm_clear_ztx) {
|
||
if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &sp_confirm_clear_ztx, 480.0f, 0.94f)) {
|
||
ImGui::PushFont(Type().iconLarge());
|
||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
|
||
|
||
ImGui::Spacing();
|
||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
||
ImGui::Spacing();
|
||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
|
||
ImGui::Spacing();
|
||
ImGui::Separator();
|
||
ImGui::Spacing();
|
||
|
||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
||
sp_confirm_clear_ztx = false;
|
||
}
|
||
ImGui::SameLine();
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
|
||
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_btn").c_str(), ImVec2(btnW, 40))) {
|
||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||
if (util::Platform::deleteFile(ztx_file)) {
|
||
Notifications::instance().success("Z-transaction history cleared");
|
||
} else {
|
||
Notifications::instance().info("No history file found");
|
||
}
|
||
sp_confirm_clear_ztx = false;
|
||
}
|
||
ImGui::PopStyleColor(2);
|
||
EndOverlayDialog();
|
||
}
|
||
}
|
||
|
||
// Confirmation dialog for deleting blockchain data
|
||
if (sp_confirm_delete_blockchain) {
|
||
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &sp_confirm_delete_blockchain, 500.0f, 0.94f)) {
|
||
ImGui::PushFont(Type().iconLarge());
|
||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning"));
|
||
|
||
ImGui::Spacing();
|
||
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
||
ImGui::Spacing();
|
||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
|
||
ImGui::Spacing();
|
||
ImGui::Separator();
|
||
ImGui::Spacing();
|
||
|
||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
|
||
sp_confirm_delete_blockchain = false;
|
||
}
|
||
ImGui::SameLine();
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
|
||
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
|
||
app->deleteBlockchainData();
|
||
sp_confirm_delete_blockchain = false;
|
||
}
|
||
ImGui::PopStyleColor(2);
|
||
EndOverlayDialog();
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
} // namespace ui
|
||
} // namespace dragonx
|