Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
849 lines
35 KiB
C++
849 lines
35 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "settings_page.h"
|
|
#include "../../app.h"
|
|
#include "../../version.h"
|
|
#include "../../config/settings.h"
|
|
#include "../../util/i18n.h"
|
|
#include "../../util/platform.h"
|
|
#include "../../rpc/rpc_client.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 "../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 "imgui.h"
|
|
#include <nlohmann/json.hpp>
|
|
#include <vector>
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
using namespace material;
|
|
|
|
// ============================================================================
|
|
// 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 int sp_acrylic_quality = 2;
|
|
static float sp_blur_multiplier = 1.0f;
|
|
static bool sp_reduced_transparency = false;
|
|
|
|
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++;
|
|
}
|
|
|
|
sp_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
|
|
sp_acrylic_quality = static_cast<int>(effects::ImGuiAcrylic::GetQuality());
|
|
sp_blur_multiplier = effects::ImGuiAcrylic::GetBlurMultiplier();
|
|
sp_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
|
|
|
|
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);
|
|
}
|
|
|
|
settings->save();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Settings Page Renderer
|
|
// ============================================================================
|
|
|
|
void RenderSettingsPage(App* app) {
|
|
// Load settings state on first render
|
|
if (!sp_initialized && app->settings()) {
|
|
loadSettingsPageState(app->settings());
|
|
}
|
|
|
|
auto& S = schema::UI();
|
|
|
|
// Responsive layout — matches other tabs
|
|
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
|
float availWidth = contentAvail.x;
|
|
float hs = Layout::hScale(availWidth);
|
|
float vs = Layout::vScale(contentAvail.y);
|
|
float pad = Layout::cardInnerPadding();
|
|
float gap = Layout::cardGap();
|
|
float glassRound = Layout::glassRounding();
|
|
(void)vs;
|
|
|
|
char buf[256];
|
|
|
|
// Label column position — adaptive to width
|
|
float labelW = std::max(100.0f, 120.0f * hs);
|
|
// Input field width — fill remaining space in card
|
|
float inputW = std::max(180.0f, availWidth - labelW - pad * 3);
|
|
|
|
// Scrollable content area — NoBackground matches other tabs
|
|
ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false,
|
|
ImGuiWindowFlags_NoBackground);
|
|
|
|
// Get draw list AFTER BeginChild so we draw on the child window's list
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
GlassPanelSpec glassSpec;
|
|
glassSpec.rounding = glassRound;
|
|
ImFont* ovFont = Type().overline();
|
|
ImFont* capFont = Type().caption();
|
|
ImFont* body2 = Type().body2();
|
|
ImFont* sub1 = Type().subtitle1();
|
|
|
|
// ====================================================================
|
|
// GENERAL — Appearance & Preferences card
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "APPEARANCE");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// Measure content height for card
|
|
// We'll use ImGui cursor-based layout inside the card
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
|
|
// Use a child window inside the glass panel for layout
|
|
// First draw the glass panel, then place content
|
|
// We need to estimate height — use a generous estimate and clip
|
|
float rowH = body2->LegacySize + Layout::spacingSm();
|
|
float sectionGap = Layout::spacingMd();
|
|
float cardH = pad // top pad
|
|
+ rowH // Theme
|
|
+ rowH // Language
|
|
+ sectionGap
|
|
+ rowH * 5 // Visual effects (acrylic + quality + blur + reduce + gap)
|
|
+ pad; // bottom pad
|
|
if (!sp_acrylic_enabled) cardH -= rowH * 2;
|
|
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
// --- Theme row ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Theme");
|
|
ImGui::SameLine(labelW);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
float refreshBtnW = 80.0f;
|
|
ImGui::SetNextItemWidth(inputW - refreshBtnW - Layout::spacingSm());
|
|
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
|
ImGui::TextDisabled("Built-in");
|
|
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("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();
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (active_is_custom) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Custom theme active");
|
|
}
|
|
ImGui::SameLine();
|
|
if (TactileButton("Refresh", ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
|
schema::SkinManager::instance().refresh();
|
|
Notifications::instance().info("Theme list refreshed");
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
|
|
schema::SkinManager::getUserSkinsDirectory().c_str());
|
|
}
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// --- Language row ---
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Language");
|
|
ImGui::SameLine(labelW);
|
|
|
|
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());
|
|
}
|
|
|
|
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);
|
|
}
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// --- Visual Effects subsection ---
|
|
dl->AddText(ovFont, ovFont->LegacySize, ImGui::GetCursorScreenPos(), OnSurfaceMedium(), "VISUAL EFFECTS");
|
|
ImGui::Dummy(ImVec2(0, ovFont->LegacySize + Layout::spacingXs()));
|
|
|
|
{
|
|
// Two-column: left = acrylic toggle + reduce toggle, right = quality + blur
|
|
float colW = (availWidth - pad * 2 - Layout::spacingLg()) * 0.5f;
|
|
|
|
if (ImGui::Checkbox("Acrylic effects", &sp_acrylic_enabled)) {
|
|
effects::ImGuiAcrylic::SetEnabled(sp_acrylic_enabled);
|
|
}
|
|
|
|
if (sp_acrylic_enabled) {
|
|
ImGui::SameLine(labelW + colW + Layout::spacingLg());
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Quality");
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
const char* quality_levels[] = { "Off", "Low", "Medium", "High" };
|
|
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
|
|
if (ImGui::Combo("##AcrylicQuality", &sp_acrylic_quality, quality_levels,
|
|
IM_ARRAYSIZE(quality_levels))) {
|
|
effects::ImGuiAcrylic::SetQuality(
|
|
static_cast<effects::AcrylicQuality>(sp_acrylic_quality));
|
|
}
|
|
}
|
|
|
|
if (ImGui::Checkbox("Reduce transparency", &sp_reduced_transparency)) {
|
|
effects::ImGuiAcrylic::SetReducedTransparency(sp_reduced_transparency);
|
|
}
|
|
|
|
if (sp_acrylic_enabled) {
|
|
ImGui::SameLine(labelW + colW + Layout::spacingLg());
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Blur");
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
|
|
if (ImGui::SliderFloat("##BlurAmount", &sp_blur_multiplier, 0.5f, 2.0f, "%.1fx")) {
|
|
effects::ImGuiAcrylic::SetBlurMultiplier(sp_blur_multiplier);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recalculate actual card bottom from cursor
|
|
ImVec2 cardEnd = ImGui::GetCursorScreenPos();
|
|
float actualH = (cardEnd.y - cardMin.y) + pad;
|
|
if (actualH != cardH) {
|
|
// Redraw glass panel with correct height
|
|
cardMax.y = cardMin.y + actualH;
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// PRIVACY & OPTIONS — Two cards side by side
|
|
// ====================================================================
|
|
{
|
|
float colW = (availWidth - gap) * 0.5f;
|
|
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
|
|
|
|
// --- Privacy card (left) ---
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PRIVACY");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
|
|
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
ImGui::Checkbox("Save shielded tx history", &sp_save_ztxs);
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
ImGui::Checkbox("Auto-shield transparent funds", &sp_auto_shield);
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
ImGui::Checkbox("Use Tor for connections", &sp_use_tor);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(colW, 0));
|
|
}
|
|
|
|
// --- Options card (right) ---
|
|
{
|
|
float rightX = rowOrigin.x + colW + gap;
|
|
// Position cursor at the same Y as privacy label
|
|
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
|
|
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "OPTIONS");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
|
|
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
ImGui::Checkbox("Allow custom transaction fees", &sp_allow_custom_fees);
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
ImGui::Checkbox("Fetch price data from CoinGecko", &sp_fetch_prices);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(colW, 0));
|
|
}
|
|
|
|
// Advance past the side-by-side row
|
|
// Find the maximum bottom
|
|
float rowBottom = ImGui::GetCursorScreenPos().y;
|
|
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// EXPLORER URLS + SAVE — card
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK EXPLORER & SETTINGS");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float rowH = body2->LegacySize + Layout::spacingSm();
|
|
float cardH = pad + rowH * 2 + Layout::spacingSm()
|
|
+ body2->LegacySize + Layout::spacingMd() // save/reset row
|
|
+ pad;
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
// Transaction URL
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Transaction URL");
|
|
ImGui::SameLine(labelW);
|
|
ImGui::SetNextItemWidth(inputW);
|
|
ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer));
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
// Address URL
|
|
{
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Address URL");
|
|
ImGui::SameLine(labelW);
|
|
ImGui::SetNextItemWidth(inputW);
|
|
ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer));
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// Save / Reset — right-aligned
|
|
{
|
|
float saveBtnW = 120.0f;
|
|
float resetBtnW = 140.0f;
|
|
float btnGap = Layout::spacingSm();
|
|
|
|
if (TactileButton("Save Settings", ImVec2(saveBtnW, 0), S.resolveFont("button"))) {
|
|
saveSettingsPageState(app->settings());
|
|
Notifications::instance().success("Settings saved");
|
|
}
|
|
ImGui::SameLine(0, btnGap);
|
|
if (TactileButton("Reset to Defaults", ImVec2(resetBtnW, 0), S.resolveFont("button"))) {
|
|
if (app->settings()) {
|
|
loadSettingsPageState(app->settings());
|
|
Notifications::instance().info("Settings reloaded from disk");
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// KEYS & BACKUP — card with two rows
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "KEYS & BACKUP");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
|
float cardH = pad + btnRowH * 2 + Layout::spacingSm() + pad;
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
// Keys row — spread buttons across width
|
|
{
|
|
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
|
|
if (TactileButton("Import Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
app->showImportKeyDialog();
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Export Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
app->showExportKeyDialog();
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Export All Keys...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
ExportAllKeysDialog::show();
|
|
}
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
// Backup row
|
|
{
|
|
float btnW = (availWidth - pad * 2 - Layout::spacingSm()) / 2.0f;
|
|
if (TactileButton("Backup wallet.dat...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
app->showBackupDialog();
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Export Transactions CSV...", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
ExportTransactionsDialog::show();
|
|
}
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// WALLET — Two cards side by side: Tools | Maintenance
|
|
// ====================================================================
|
|
{
|
|
float colW = (availWidth - gap) * 0.5f;
|
|
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
|
|
float btnH = std::max(28.0f, 34.0f * vs);
|
|
|
|
// --- Wallet Tools card (left) ---
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET TOOLS");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
|
|
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
float innerBtnW = colW - pad * 2;
|
|
if (TactileButton("Address Book...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
|
AddressBookDialog::show();
|
|
}
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
if (TactileButton("Validate Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
|
ValidateAddressDialog::show();
|
|
}
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
if (TactileButton("Request Payment...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
|
RequestPaymentDialog::show();
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(colW, 0));
|
|
}
|
|
|
|
// --- Shielding & Maintenance card (right) ---
|
|
{
|
|
float rightX = rowOrigin.x + colW + gap;
|
|
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
|
|
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SHIELDING & MAINTENANCE");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
|
|
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
float innerBtnW = colW - pad * 2;
|
|
if (TactileButton("Shield Mining Rewards...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
|
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
|
}
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
if (TactileButton("Merge to Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
|
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
|
}
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
ImGui::BeginDisabled(!app->isConnected());
|
|
if (TactileButton("Rescan Blockchain", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
|
|
if (app->rpc() && app->rpc()->isConnected()) {
|
|
app->rpc()->rescanBlockchain(0, [](bool success, const nlohmann::json&) {
|
|
if (success)
|
|
Notifications::instance().success("Blockchain rescan started");
|
|
else
|
|
Notifications::instance().error("Failed to start rescan");
|
|
});
|
|
} else {
|
|
Notifications::instance().warning("Not connected to daemon");
|
|
}
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(colW, 0));
|
|
}
|
|
|
|
// Advance past sidebar row
|
|
float rowBottom = ImGui::GetCursorScreenPos().y;
|
|
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// WALLET INFO — Small card with file path + clear history
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET INFO");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float rowH = body2->LegacySize + Layout::spacingSm();
|
|
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
|
float cardH = pad + rowH * 2 + btnRowH + pad;
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
|
|
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
|
|
|
|
ImGui::PushFont(body2);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Location");
|
|
ImGui::SameLine(labelW);
|
|
ImGui::TextUnformatted(wallet_path.c_str());
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("File size");
|
|
ImGui::SameLine(labelW);
|
|
if (wallet_size > 0) {
|
|
std::string size_str = util::Platform::formatFileSize(wallet_size);
|
|
ImGui::TextUnformatted(size_str.c_str());
|
|
} else {
|
|
ImGui::TextDisabled("Not found");
|
|
}
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
if (TactileButton("Clear Z-Transaction History", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
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");
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// NODE / RPC — card with two-column inputs
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NODE / RPC CONNECTION");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float rowH = body2->LegacySize + Layout::spacingSm();
|
|
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
|
float cardH = pad + rowH * 2 + Layout::spacingSm() + rowH * 2 + Layout::spacingSm()
|
|
+ capFont->LegacySize + Layout::spacingSm()
|
|
+ btnRowH + pad;
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
// Two-column: Host+Port on one line, User+Pass on next
|
|
float halfInput = (availWidth - pad * 2 - labelW * 2 - Layout::spacingLg()) * 0.5f;
|
|
float rpcLabelW = std::max(70.0f, 85.0f * hs);
|
|
|
|
ImGui::PushFont(body2);
|
|
|
|
// Row 1: Host + Port
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Host");
|
|
ImGui::SameLine(rpcLabelW);
|
|
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
|
|
ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host));
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Port");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(std::max(60.0f, halfInput * 0.4f));
|
|
ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port));
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
// Row 2: Username + Password
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Username");
|
|
ImGui::SameLine(rpcLabelW);
|
|
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
|
|
ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user));
|
|
ImGui::SameLine(0, Layout::spacingLg());
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Password");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(halfInput);
|
|
ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password),
|
|
ImGuiInputTextFlags_Password);
|
|
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
"Connection settings are usually auto-detected from DRAGONX.conf");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
if (TactileButton("Test Connection", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
if (app->rpc() && app->rpc()->isConnected()) {
|
|
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
|
(void)result;
|
|
if (error.empty())
|
|
Notifications::instance().success("RPC connection OK");
|
|
else
|
|
Notifications::instance().error("RPC error: " + error);
|
|
});
|
|
} else {
|
|
Notifications::instance().warning("Not connected to daemon");
|
|
}
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Block Info...", ImVec2(0, 0), S.resolveFont("button"))) {
|
|
BlockInfoDialog::show(app->getBlockHeight());
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
// ====================================================================
|
|
// ABOUT — card
|
|
// ====================================================================
|
|
{
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ABOUT");
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
float rowH = body2->LegacySize + Layout::spacingXs();
|
|
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
|
|
float cardH = pad + sub1->LegacySize + rowH * 2 + Layout::spacingSm()
|
|
+ body2->LegacySize * 2 + Layout::spacingSm()
|
|
+ capFont->LegacySize * 2 + Layout::spacingMd()
|
|
+ btnRowH + pad;
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
|
|
|
|
// 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();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
ImGui::PushFont(body2);
|
|
ImGui::PushTextWrapPos(cardMax.x - pad);
|
|
ImGui::TextUnformatted(
|
|
"A shielded cryptocurrency wallet for DragonX (DRGX), "
|
|
"built with Dear ImGui for a lightweight, portable experience.");
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
ImGui::PushFont(capFont);
|
|
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License");
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
|
|
|
// Buttons — spread across width
|
|
{
|
|
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
|
|
if (TactileButton("Website", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
util::Platform::openUrl("https://dragonx.is");
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Report Bug", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
util::Platform::openUrl("https://git.hush.is/hush/SilentDragonX/issues");
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingSm());
|
|
if (TactileButton("Block Explorer", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
|
util::Platform::openUrl("https://explorer.dragonx.is");
|
|
}
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
|
|
ImGui::EndChild(); // ##SettingsPageScroll
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|