ObsidianDragon - DragonX ImGui Wallet

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)
This commit is contained in:
2026-02-26 02:31:52 -06:00
commit 3aee55b49c
306 changed files with 177789 additions and 0 deletions

View File

@@ -0,0 +1,567 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "settings_window.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../rpc/rpc_client.h"
#include "../theme.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 "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <vector>
#include <filesystem>
// Icon text for settings UI
#define ICON_CUSTOM_THEME ICON_MD_TUNE
#define ICON_REFRESH_THEMES ICON_MD_REFRESH
namespace dragonx {
namespace ui {
// Settings state - these get loaded from Settings on window open
static bool s_initialized = false;
static int s_language_index = 0;
static bool s_save_ztxs = true;
static bool s_allow_custom_fees = false;
static bool s_auto_shield = false;
static bool s_fetch_prices = true;
static bool s_use_tor = false;
static char s_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
static char s_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
static char s_rpc_user[64] = "";
static char s_rpc_password[64] = "";
static char s_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
static char s_addr_explorer[256] = "https://explorer.dragonx.is/address/";
// Acrylic settings
static bool s_acrylic_enabled = true;
static float s_blur_amount = 1.5f; // 0.0=Off, continuous blur multiplier
static float s_noise_opacity = 0.5f;
static bool s_reduced_transparency = false; // Accessibility option
static bool s_gradient_background = false; // Gradient background mode
// Saved skin ID for cancel/revert
static std::string s_saved_skin_id;
// Load current settings into UI state
static void loadSettingsToUI(config::Settings* settings) {
if (!settings) return;
s_saved_skin_id = settings->getSkinId();
s_save_ztxs = settings->getSaveZtxs();
s_allow_custom_fees = settings->getAllowCustomFees();
s_auto_shield = settings->getAutoShield();
s_fetch_prices = settings->getFetchPrices();
s_use_tor = settings->getUseTor();
strncpy(s_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_tx_explorer) - 1);
strncpy(s_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_addr_explorer) - 1);
// Set language index
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::string current_lang = settings->getLanguage();
if (current_lang.empty()) current_lang = "en";
s_language_index = 0;
int idx = 0;
for (const auto& lang : languages) {
if (lang.first == current_lang) {
s_language_index = idx;
break;
}
idx++;
}
s_initialized = true;
}
// Save UI state to settings
static void saveSettingsFromUI(config::Settings* settings) {
if (!settings) return;
settings->setTheme(settings->getSkinId()); // Theme now synced with skin
settings->setSaveZtxs(s_save_ztxs);
settings->setAllowCustomFees(s_allow_custom_fees);
settings->setAutoShield(s_auto_shield);
settings->setFetchPrices(s_fetch_prices);
settings->setUseTor(s_use_tor);
settings->setTxExplorerUrl(s_tx_explorer);
settings->setAddressExplorerUrl(s_addr_explorer);
// Save language
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
auto it = languages.begin();
std::advance(it, s_language_index);
if (it != languages.end()) {
settings->setLanguage(it->first);
}
// Save acrylic / visual effects settings
settings->setAcrylicEnabled(s_acrylic_enabled);
settings->setAcrylicQuality(s_blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
settings->setBlurMultiplier(s_blur_amount);
settings->setReducedTransparency(s_reduced_transparency);
settings->setNoiseOpacity(s_noise_opacity);
settings->setGradientBackground(s_gradient_background);
settings->save();
}
void RenderSettingsWindow(App* app, bool* p_open)
{
// Load settings on first open
if (!s_initialized && app->settings()) {
loadSettingsToUI(app->settings());
// Initialize acrylic settings from current state
s_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
s_blur_amount = effects::ImGuiAcrylic::GetBlurMultiplier();
s_noise_opacity = effects::ImGuiAcrylic::GetNoiseOpacity();
s_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
s_gradient_background = schema::SkinManager::instance().isGradientMode();
}
auto& S = schema::UI();
auto win = S.window("dialogs.settings");
auto lbl = S.label("dialogs.settings", "label");
auto cmb = S.combo("dialogs.settings", "combo");
auto connLbl = S.label("dialogs.settings", "connection-label");
auto portInput = S.input("dialogs.settings", "port-input");
auto walletBtn = S.button("dialogs.settings", "wallet-button");
auto saveBtn = S.button("dialogs.settings", "save-button");
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
// Use acrylic modal popup
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Settings");
if (!effects::ImGuiAcrylic::BeginAcrylicPopupModal("Settings", p_open, ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
effects::ImGuiAcrylic::EndAcrylicPopup();
return;
}
if (ImGui::BeginTabBar("SettingsTabs")) {
// General settings tab
if (ImGui::BeginTabItem("General")) {
ImGui::Spacing();
// Skin/theme selection
ImGui::Text("Theme:");
ImGui::SameLine(lbl.position);
// Active skin combo (populated from SkinManager)
auto& skinMgr = schema::SkinManager::instance();
const auto& skins = skinMgr.available();
// Find active skin for preview text
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;
}
}
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
// Bundled themes header
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();
}
}
// Custom themes (if any)
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 label = skin.name + " (invalid)";
ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("%s", skin.validationError.c_str());
}
} else {
std::string label = skin.name;
if (!skin.author.empty()) {
label += " (" + skin.author + ")";
}
if (ImGui::Selectable(label.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 (ImGui::IsItemHovered())
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
// Show indicator if custom theme is active
if (active_is_custom) {
ImGui::SameLine();
ImGui::PushFont(material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Custom theme active");
}
}
ImGui::SameLine();
ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh();
Notifications::instance().info("Theme list refreshed");
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::Spacing();
// Language selection
ImGui::Text("Language:");
ImGui::SameLine(lbl.position);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
// Build language display names array
std::vector<const char*> lang_names;
lang_names.reserve(languages.size());
for (const auto& lang : languages) {
lang_names.push_back(lang.second.c_str()); // Display name
}
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::Combo("##Language", &s_language_index, lang_names.data(), static_cast<int>(lang_names.size()))) {
// Get locale code from index
auto it = languages.begin();
std::advance(it, s_language_index);
i18n.loadLanguage(it->first);
}
ImGui::TextDisabled(" Note: Some text requires restart to update");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Acrylic Effects settings
ImGui::Text("Visual Effects");
ImGui::Spacing();
ImGui::Text("Acrylic Level:");
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
char blur_fmt[16];
if (s_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &s_blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
if (s_blur_amount > 0.0f && s_blur_amount < 0.15f) s_blur_amount = 0.0f;
s_acrylic_enabled = (s_blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
}
}
ImGui::TextDisabled(" Blur amount (0%% = off, 100%% = maximum)");
ImGui::Spacing();
ImGui::Text("Noise Opacity:");
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
char noise_fmt[16];
if (s_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &s_noise_opacity, 0.0f, 1.0f, noise_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
}
}
ImGui::TextDisabled(" Grain texture intensity (0%% = off, 100%% = maximum)");
ImGui::Spacing();
// Accessibility: Reduced transparency
if (ImGui::Checkbox("Reduce transparency", &s_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
}
ImGui::TextDisabled(" Use solid colors instead of blur effects (accessibility)");
ImGui::Spacing();
if (ImGui::Checkbox("Simple background", &s_gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_gradient_background);
}
ImGui::TextDisabled(" Replace textured backgrounds with smooth gradients");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Privacy settings
ImGui::Text("Privacy");
ImGui::Spacing();
ImGui::Checkbox("Save shielded transaction history locally", &s_save_ztxs);
ImGui::TextDisabled(" Stores z-addr transactions in a local file for viewing");
ImGui::Spacing();
ImGui::Checkbox("Auto-shield transparent funds", &s_auto_shield);
ImGui::TextDisabled(" Automatically move transparent funds to shielded addresses");
ImGui::Spacing();
ImGui::Checkbox("Use Tor for network connections", &s_use_tor);
ImGui::TextDisabled(" Route all connections through Tor for enhanced privacy");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Other settings
ImGui::Text("Other");
ImGui::Spacing();
ImGui::Checkbox("Allow custom transaction fees", &s_allow_custom_fees);
ImGui::Checkbox("Fetch price data from CoinGecko", &s_fetch_prices);
ImGui::EndTabItem();
}
// Connection settings tab
if (ImGui::BeginTabItem("Connection")) {
ImGui::Spacing();
ImGui::Text("RPC Connection");
ImGui::TextDisabled("Configure connection to dragonxd daemon");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Host:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
ImGui::Text("Port:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(portInput.width);
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
ImGui::Spacing();
ImGui::Text("Username:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
ImGui::Text("Password:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
ImGuiInputTextFlags_Password);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("Note: Connection settings are usually auto-detected from DRAGONX.conf");
ImGui::Spacing();
if (material::StyledButton("Test Connection", ImVec2(0,0), S.resolveFont("button"))) {
if (app->rpc()) {
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");
std::string msg = "Connection successful!\ndragonxd version: " + version;
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Connection failed: " + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
}
}
ImGui::EndTabItem();
}
// Wallet tab
if (ImGui::BeginTabItem("Wallet")) {
ImGui::Spacing();
ImGui::Text("Wallet Maintenance");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (material::StyledButton("Rescan Blockchain", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (app->rpc()) {
// Start rescan from block 0
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
int start = result.value("start_height", 0);
int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) +
" to " + std::to_string(end);
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Rescan failed: " + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
}
}
ImGui::TextDisabled(" Rescan blockchain for missing transactions");
ImGui::Spacing();
if (material::StyledButton("Clear Saved Z-Transaction History", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
// Clear z-transaction history file
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::TextDisabled(" Delete locally stored shielded transaction data");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Wallet Info");
ImGui::Spacing();
// Get actual wallet size
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::Text("Wallet file size: %s", size_str.c_str());
} else {
ImGui::TextDisabled("Wallet file not found");
}
ImGui::Text("Wallet location: %s", wallet_path.c_str());
ImGui::EndTabItem();
}
// Explorer tab
if (ImGui::BeginTabItem("Explorer")) {
ImGui::Spacing();
ImGui::Text("Block Explorer URLs");
ImGui::TextDisabled("Configure external block explorer links");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Transaction URL:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
ImGui::Text("Address URL:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
ImGui::Spacing();
ImGui::TextDisabled("URLs should include a trailing slash. The txid/address will be appended.");
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Save/Cancel buttons
if (material::StyledButton("Save", ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
*p_open = false;
}
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
// Reload settings to revert changes
loadSettingsToUI(app->settings());
// Revert skin to what was active when settings opened
if (!s_saved_skin_id.empty()) {
schema::SkinManager::instance().setActiveSkin(s_saved_skin_id);
if (app->settings()) {
app->settings()->setSkinId(s_saved_skin_id);
app->settings()->save();
}
}
*p_open = false;
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx