daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup

- 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
This commit is contained in:
dan_s
2026-03-17 14:57:12 -05:00
parent f0c87e4092
commit 4a841fd032
27 changed files with 897 additions and 2050 deletions

View File

@@ -116,6 +116,9 @@ 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
@@ -125,6 +128,7 @@ 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)
@@ -181,6 +185,9 @@ static void loadSettingsPageState(config::Settings* settings) {
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;
@@ -230,6 +237,9 @@ static void saveSettingsPageState(config::Settings* settings) {
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);
@@ -1485,6 +1495,15 @@ void RenderSettingsPage(App* app) {
}
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();
@@ -1737,6 +1756,20 @@ void RenderSettingsPage(App* app) {
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);
@@ -2009,6 +2042,39 @@ void RenderSettingsPage(App* app) {
}
}
// 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

View File

@@ -1,848 +0,0 @@
// 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

View File

@@ -43,6 +43,11 @@ std::string SkinManager::getBundledSkinsDirectory() {
fs::path themes_dir = exe_dir / "res" / "themes";
if (fs::exists(themes_dir)) {
// Update any stale overlay themes from embedded versions
int updated = resources::updateBundledThemes(themes_dir.string());
if (updated > 0)
DEBUG_LOGF("[SkinManager] Updated %d stale theme(s) in %s\n",
updated, themes_dir.string().c_str());
return themes_dir.string();
}

View File

@@ -142,6 +142,7 @@ static constexpr int s_legacyLayoutCount = 10;
static std::vector<BalanceLayoutEntry> s_balanceLayouts;
static std::string s_defaultLayoutId = "classic";
static bool s_layoutConfigLoaded = false;
static bool s_generating_z_address = false;
static void LoadBalanceLayoutConfig()
{
@@ -803,8 +804,16 @@ static void RenderBalanceClassic(App* app)
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(addrSyncing);
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}
@@ -1420,9 +1429,18 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(sharedAddrSyncing);
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}

View File

@@ -147,7 +147,14 @@ void ImportKeyDialog::render(App* app)
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1);
std::string trimmed(clipboard);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
}
}
@@ -156,6 +163,43 @@ void ImportKeyDialog::render(App* app)
s_key_input[0] = '\0';
}
// Key validation indicator
if (s_key_input[0] != '\0') {
auto keys = splitKeys(s_key_input);
int zCount = 0, tCount = 0, unknownCount = 0;
for (const auto& key : keys) {
std::string kt = detectKeyType(key);
if (kt == "z-spending") zCount++;
else if (kt == "t-privkey") tCount++;
else unknownCount++;
}
if (zCount > 0 || tCount > 0) {
ImGui::PushFont(material::Type().iconSmall());
material::Type().textColored(material::TypeStyle::Caption, material::Success(), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 2.0f);
char validBuf[128];
if (zCount > 0 && tCount > 0)
snprintf(validBuf, sizeof(validBuf), "%d shielded, %d transparent key(s)", zCount, tCount);
else if (zCount > 0)
snprintf(validBuf, sizeof(validBuf), "%d shielded key(s)", zCount);
else
snprintf(validBuf, sizeof(validBuf), "%d transparent key(s)", tCount);
material::Type().textColored(material::TypeStyle::Caption, material::Success(), validBuf);
if (unknownCount > 0) {
ImGui::SameLine();
snprintf(validBuf, sizeof(validBuf), "(%d unrecognized)", unknownCount);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), validBuf);
}
} else if (unknownCount > 0) {
ImGui::PushFont(material::Type().iconSmall());
material::Type().textColored(material::TypeStyle::Caption, material::Error(), ICON_MD_ERROR);
ImGui::PopFont();
ImGui::SameLine(0, 2.0f);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
}
}
ImGui::Spacing();
// Rescan options

View File

@@ -156,7 +156,9 @@ void RenderMiningTab(App* app)
s_pool_state_loaded = true;
}
// Default pool worker to user's first shielded address once addresses are available
// Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
// is prompted to generate one before mining.
{
static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker);
@@ -169,18 +171,14 @@ void RenderMiningTab(App* app)
break;
}
}
if (defaultAddr.empty()) {
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
} else {
// No z-address yet — clear the placeholder "x" so field shows empty
s_pool_worker[0] = '\0';
s_pool_settings_dirty = true;
}
s_pool_worker_defaulted = true;
}
@@ -536,7 +534,12 @@ void RenderMiningTab(App* app)
s_pool_settings_dirty = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
std::string currentWorkerStr(s_pool_worker);
if (currentWorkerStr.empty()) {
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
} else {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
}
}
// --- Worker: Dropdown arrow button ---
@@ -739,7 +742,8 @@ void RenderMiningTab(App* app)
if (btnClk) {
strncpy(s_pool_url, "pool.dragonx.is", sizeof(s_pool_url) - 1);
// Default to user's first shielded address for pool payouts
// Default to user's first shielded (z) address for pool payouts.
// Leave blank if no z-address exists yet.
std::string defaultAddr;
for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) {
@@ -747,15 +751,6 @@ void RenderMiningTab(App* app)
break;
}
}
if (defaultAddr.empty()) {
// Fallback to transparent if no shielded available
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
@@ -840,6 +835,7 @@ void RenderMiningTab(App* app)
float idleRightEdge = cardMax.x - pad;
{
bool idleOn = app->settings()->getMineWhenIdle();
bool threadScaling = app->settings()->getIdleThreadScaling();
ImFont* icoFont = Type().iconSmall();
const char* idleIcon = ICON_MD_SCHEDULE;
float icoH = icoFont->LegacySize;
@@ -875,8 +871,40 @@ void RenderMiningTab(App* app)
idleRightEdge = btnX - 4.0f * dp;
// Idle delay combo (to the left of the icon when enabled)
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
if (idleOn) {
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
float sBtnX = idleRightEdge - btnSz;
float sBtnY = btnY;
if (threadScaling) {
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
sIcoCol, scaleIcon);
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setIdleThreadScaling(!threadScaling);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", threadScaling
? TR("mining_idle_scale_on_tooltip")
: TR("mining_idle_scale_off_tooltip"));
}
idleRightEdge = sBtnX - 4.0f * dp;
}
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
if (idleOn && !threadScaling) {
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
@@ -907,6 +935,111 @@ void RenderMiningTab(App* app)
idleRightEdge = comboX - 4.0f * dp;
}
// Thread scaling controls: idle delay + active threads / idle threads combos
if (idleOn && threadScaling) {
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
// Idle delay combo
{
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
// Idle threads combo (threads when system is idle)
{
int curVal = app->settings()->getIdleThreadsIdle();
if (curVal <= 0) curVal = hwThreads;
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsIdle(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
// Separator arrow icon
{
const char* arrowIcon = ICON_MD_ARROW_BACK;
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
float arrX = idleRightEdge - arrSz.x;
float arrY = curY + (headerH - arrSz.y) * 0.5f;
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
idleRightEdge = arrX - 4.0f * dp;
}
// Active threads combo (threads when user is active)
{
int curVal = app->settings()->getIdleThreadsActive();
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsActive(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
}
ImGui::SetCursorScreenPos(savedCur);
}

View File

@@ -65,6 +65,7 @@ static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static std::string s_pending_select_address;
static bool s_generating_address = false;
// ============================================================================
// Helpers
@@ -302,10 +303,18 @@ static void RenderAddressDropdown(App* app, float width) {
// New address button on same line
ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
if (s_generating_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
s_generating_address = true;
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_shielded"));
else {
@@ -315,6 +324,7 @@ static void RenderAddressDropdown(App* app, float width) {
});
} else {
app->createNewTAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_transparent"));
else {

View File

@@ -1,934 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Layout G: QR-Centered Hero
// - QR code dominates center as hero element
// - Address info wraps around the QR
// - Payment request section below QR
// - Horizontal address strip at bottom for fast switching
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../version.h"
#include "../../wallet_state.h"
#include "../../ui/widgets/qr_code.h"
#include "../sidebar.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../notifications.h"
#include "imgui.h"
#include <string>
#include <algorithm>
#include <cmath>
#include <map>
namespace dragonx {
namespace ui {
using namespace material;
// ============================================================================
// State
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[256] = "";
static std::string s_cached_qr_data;
static uintptr_t s_qr_texture = 0;
static bool s_payment_request_open = false;
// Track newly created addresses for NEW badge
static std::map<std::string, double> s_new_address_timestamps;
static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static char s_label_edit_buf[64] = "";
// Address type filter
static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T
// ============================================================================
// Helpers
// ============================================================================
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 35) {
if (addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
static void OpenExplorerURL(const std::string& address) {
std::string url = "https://explorer.dragonx.com/address/" + address;
#ifdef _WIN32
std::string cmd = "start \"\" \"" + url + "\"";
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
#else
std::string cmd = "xdg-open \"" + url + "\"";
#endif
system(cmd.c_str());
}
// ============================================================================
// Sync banner
// ============================================================================
static void RenderSyncBanner(const WalletState& state) {
if (!state.sync.syncing || state.sync.isSynced()) return;
float syncPct = (state.sync.headers > 0)
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf),
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.4f, 0.0f, 0.15f));
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, 28),
false, ImGuiWindowFlags_NoScrollbar);
ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), 6));
Type().textColored(TypeStyle::Caption, Warning(), syncBuf);
ImGui::EndChild();
ImGui::PopStyleColor();
}
// ============================================================================
// Track new addresses (detect creations)
// ============================================================================
static void TrackNewAddresses(const WalletState& state) {
if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) {
for (const auto& a : state.addresses) {
if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) {
s_new_address_timestamps[a.address] = ImGui::GetTime();
}
}
} else if (s_prev_address_count == 0) {
for (const auto& a : state.addresses) {
s_new_address_timestamps[a.address] = 0.0;
}
}
s_prev_address_count = state.addresses.size();
}
// ============================================================================
// Build sorted address groups
// ============================================================================
struct AddressGroups {
std::vector<int> shielded;
std::vector<int> transparent;
};
static AddressGroups BuildSortedAddressGroups(const WalletState& state) {
AddressGroups groups;
for (int i = 0; i < (int)state.addresses.size(); i++) {
if (state.addresses[i].type == "shielded")
groups.shielded.push_back(i);
else
groups.transparent.push_back(i);
}
std::sort(groups.shielded.begin(), groups.shielded.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
std::sort(groups.transparent.begin(), groups.transparent.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
return groups;
}
// ============================================================================
// QR Hero — the centerpiece of Layout G
// ============================================================================
static void RenderQRHero(App* app, ImDrawList* dl, const AddressInfo& addr,
float width, float qrSize,
const std::string& qr_data,
const GlassPanelSpec& glassSpec,
const WalletState& state,
ImFont* sub1, ImFont* /*body2*/, ImFont* capFont) {
char buf[128];
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
const char* typeBadge = isZ ? "Shielded" : "Transparent";
float qrPadding = Layout::spacingLg();
float totalQrSize = qrSize + qrPadding * 2;
float heroH = totalQrSize + 80.0f; // QR + info below
ImVec2 heroMin = ImGui::GetCursorScreenPos();
ImVec2 heroMax(heroMin.x + width, heroMin.y + heroH);
GlassPanelSpec heroGlass = glassSpec;
heroGlass.fillAlpha = 16;
heroGlass.borderAlpha = 35;
DrawGlassPanel(dl, heroMin, heroMax, heroGlass);
// --- Address info bar above QR ---
float infoBarH = 32.0f;
float cx = heroMin.x + Layout::spacingLg();
float cy = heroMin.y + Layout::spacingSm();
// Type badge circle + label
dl->AddCircleFilled(ImVec2(cx + 8, cy + 10), 8.0f, IM_COL32(255, 255, 255, 20));
const char* typeChar = isZ ? "Z" : "T";
ImVec2 tcSz = sub1->CalcTextSizeA(sub1->LegacySize, 100, 0, typeChar);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(cx + 8 - tcSz.x * 0.5f, cy + 10 - tcSz.y * 0.5f),
typeCol, typeChar);
// Education tooltip on badge
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::InvisibleButton("##TypeBadgeHero", ImVec2(22, 22));
if (ImGui::IsItemHovered()) {
if (isZ) {
ImGui::SetTooltip(
"Shielded Address (Z)\n"
"- Full transaction privacy\n"
"- Encrypted sender, receiver, amount\n"
"- Supports encrypted memos\n"
"- Recommended for privacy");
} else {
ImGui::SetTooltip(
"Transparent Address (T)\n"
"- Publicly visible on blockchain\n"
"- Similar to Bitcoin addresses\n"
"- No memo support\n"
"- Use Z addresses for privacy");
}
}
// Type label text
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 24, cy + 4), typeCol, typeBadge);
// Balance right-aligned
snprintf(buf, sizeof(buf), "%.8f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
float balX = heroMax.x - balSz.x - Layout::spacingLg();
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(balX, cy + 2), typeCol, buf);
// USD value
if (state.market.price_usd > 0 && addr.balance > 0) {
double usd = addr.balance * state.market.price_usd;
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(heroMax.x - usdSz.x - Layout::spacingLg(), cy + sub1->LegacySize + 4),
OnSurfaceDisabled(), buf);
}
// --- QR Code centered ---
float qrOffset = (width - totalQrSize) * 0.5f;
ImVec2 qrPanelMin(heroMin.x + qrOffset, heroMin.y + infoBarH + Layout::spacingSm());
ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize);
// Subtle inner panel for QR
GlassPanelSpec qrGlass;
qrGlass.rounding = glassSpec.rounding * 0.75f;
qrGlass.fillAlpha = 12;
qrGlass.borderAlpha = 25;
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
if (s_qr_texture) {
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - 50,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), "QR unavailable");
}
// Click QR to copy
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy %s",
s_request_amount > 0 ? "payment URI" : "address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? "Payment URI copied to clipboard"
: "Address copied to clipboard");
}
// --- Address strip below QR ---
float addrStripY = qrPanelMax.y + Layout::spacingMd();
float addrStripX = heroMin.x + Layout::spacingLg();
float addrStripW = width - Layout::spacingXxl();
// Full address (word-wrapped)
ImVec2 fullAddrPos(addrStripX, addrStripY);
float wrapWidth = addrStripW;
ImVec2 addrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
wrapWidth, addr.address.c_str());
dl->AddText(capFont, capFont->LegacySize, fullAddrPos,
OnSurface(), addr.address.c_str(), nullptr, wrapWidth);
// Address click-to-copy overlay
ImGui::SetCursorScreenPos(fullAddrPos);
ImGui::InvisibleButton("##addrCopyHero", ImVec2(wrapWidth, addrSz.y));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Action buttons row
float btnRowY = addrStripY + addrSz.y + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(addrStripX, btnRowY));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
{
// Copy — primary (uses global glass style)
if (TactileSmallButton("Copy Address##hero", schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
ImGui::SameLine();
// Explorer
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileSmallButton("Explorer##hero", schema::UI().resolveFont("button"))) {
OpenExplorerURL(addr.address);
}
ImGui::PopStyleColor(3);
// Send From
if (addr.balance > 0) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
if (TactileSmallButton("Send \xe2\x86\x97##hero", schema::UI().resolveFont("button"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::PopStyleColor(3);
}
// Label editor (inline)
ImGui::SameLine(0, Layout::spacingXl());
auto lblIt = s_address_labels.find(addr.address);
std::string currentLabel = (lblIt != s_address_labels.end()) ? lblIt->second : "";
snprintf(s_label_edit_buf, sizeof(s_label_edit_buf), "%s", currentLabel.c_str());
ImGui::SetNextItemWidth(std::min(200.0f, addrStripW * 0.3f));
if (ImGui::InputTextWithHint("##LabelHero", "Add label...",
s_label_edit_buf, sizeof(s_label_edit_buf))) {
s_address_labels[addr.address] = std::string(s_label_edit_buf);
}
}
ImGui::PopStyleVar();
// Update hero height based on actual content
float actualBottom = btnRowY + 24;
heroH = actualBottom - heroMin.y + Layout::spacingMd();
heroMax.y = heroMin.y + heroH;
ImGui::SetCursorScreenPos(ImVec2(heroMin.x, heroMax.y));
ImGui::Dummy(ImVec2(width, 0));
}
// ============================================================================
// Payment request section (below QR hero)
// ============================================================================
static void RenderPaymentRequest(ImDrawList* dl, const AddressInfo& addr,
float innerW, const GlassPanelSpec& glassSpec,
const char* suffix) {
auto& S = schema::UI();
const float kLabelPos = S.label("tabs.receive", "label-column").position;
bool hasMemo = (addr.type == "shielded");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Compute card height
float prCardH = 16.0f + 24.0f + 8.0f + 12.0f;
if (hasMemo) prCardH += 24.0f;
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImFont* capF = Type().caption();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
innerW - 24, s_cached_qr_data.c_str());
prCardH += uriSz.y + 8.0f;
}
if (s_request_amount > 0) prCardH += 32.0f;
if (s_request_amount > 0 || s_request_memo[0]) prCardH += 4.0f;
ImVec2 prMin = ImGui::GetCursorScreenPos();
ImVec2 prMax(prMin.x + innerW, prMin.y + prCardH);
DrawGlassPanel(dl, prMin, prMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(prMin.x + Layout::spacingLg(), prMin.y + Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, 0));
ImGui::Text("Amount:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(std::max(S.input("tabs.receive", "amount-input").width, innerW * 0.4f));
char amtId[32];
snprintf(amtId, sizeof(amtId), "##RequestAmount%s", suffix);
ImGui::InputDouble(amtId, &s_request_amount, 0.01, 1.0, "%.8f");
ImGui::SameLine();
ImGui::Text("%s", DRAGONX_TICKER);
if (hasMemo) {
ImGui::Text("Memo:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(innerW - kLabelPos - Layout::spacingXxl());
char memoId[32];
snprintf(memoId, sizeof(memoId), "##RequestMemo%s", suffix);
ImGui::InputText(memoId, s_request_memo, sizeof(s_request_memo));
}
// Live URI preview
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImGui::Spacing();
ImFont* capF = Type().caption();
ImVec2 uriPos = ImGui::GetCursorScreenPos();
float uriWrapW = innerW - Layout::spacingXxl();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
uriWrapW, s_cached_qr_data.c_str());
dl->AddText(capF, capF->LegacySize, uriPos,
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
ImGui::Dummy(ImVec2(uriWrapW, uriSz.y + Layout::spacingSm()));
}
ImGui::Spacing();
if (s_request_amount > 0) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
char copyUriId[64];
snprintf(copyUriId, sizeof(copyUriId), "Copy Payment URI%s", suffix);
if (TactileButton(copyUriId, ImVec2(innerW - Layout::spacingXxl(), 0), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_cached_qr_data.c_str());
Notifications::instance().info("Payment URI copied to clipboard");
}
ImGui::PopStyleVar();
// Share as text
char shareId[32];
snprintf(shareId, sizeof(shareId), "Share as Text%s", suffix);
if (TactileSmallButton(shareId, S.resolveFont("button"))) {
char shareBuf[1024];
snprintf(shareBuf, sizeof(shareBuf),
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
s_request_amount, DRAGONX_TICKER,
addr.address.c_str(), s_cached_qr_data.c_str());
ImGui::SetClipboardText(shareBuf);
Notifications::instance().info("Payment request copied to clipboard");
}
}
if (s_request_amount > 0 || s_request_memo[0]) {
ImGui::SameLine();
char clearId[32];
snprintf(clearId, sizeof(clearId), "Clear%s", suffix);
if (TactileSmallButton(clearId, S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_memo[0] = '\0';
}
}
ImGui::SetCursorScreenPos(ImVec2(prMin.x, prMax.y));
ImGui::Dummy(ImVec2(innerW, 0));
}
// ============================================================================
// Recent received transactions for selected address
// ============================================================================
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& addr,
const WalletState& state, float width,
ImFont* capFont) {
char buf[128];
int recvCount = 0;
for (const auto& tx : state.transactions) {
if (tx.address == addr.address && tx.type == "receive") recvCount++;
}
if (recvCount == 0) return;
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
snprintf(buf, sizeof(buf), "RECENT RECEIVED (%d)", std::min(recvCount, 3));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
int shown = 0;
for (const auto& tx : state.transactions) {
if (tx.address != addr.address || tx.type != "receive") continue;
if (shown >= 3) break;
ImVec2 rMin = ImGui::GetCursorScreenPos();
float rH = 22.0f;
ImVec2 rMax(rMin.x + width, rMin.y + rH);
GlassPanelSpec rsGlass;
rsGlass.rounding = Layout::glassRounding() * 0.5f;
rsGlass.fillAlpha = 8;
DrawGlassPanel(dl, rMin, rMax, rsGlass);
float rx = rMin.x + Layout::spacingMd();
float ry = rMin.y + (rH - capFont->LegacySize) * 0.5f;
// Arrow indicator
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx, ry),
Success(), "\xe2\x86\x90");
snprintf(buf, sizeof(buf), "+%.8f %s %s %s",
tx.amount, DRAGONX_TICKER,
tx.getTimeString().c_str(),
tx.confirmations < 1 ? "(unconfirmed)" : "");
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + 16, ry),
tx.confirmations >= 1 ? Success() : Warning(), buf);
ImGui::Dummy(ImVec2(width, rH));
ImGui::Dummy(ImVec2(0, 2));
shown++;
}
}
// ============================================================================
// Horizontal Address Strip — bottom switching bar (Layout G signature)
// ============================================================================
static void RenderAddressStrip(App* app, ImDrawList* dl, const WalletState& state,
float width, float hs,
ImFont* /*sub1*/, ImFont* capFont) {
char buf[128];
// Header row with filter and + New button
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR ADDRESSES");
float btnW = std::max(70.0f, 85.0f * hs);
float comboW = std::max(48.0f, 58.0f * hs);
ImGui::SameLine(width - btnW - comboW - Layout::spacingMd());
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
const char* types[] = { "All", "Z", "T" };
ImGui::SetNextItemWidth(comboW);
ImGui::Combo("##AddrTypeStrip", &s_addr_type_filter, types, 3);
ImGui::SameLine();
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("+ New##strip", ImVec2(btnW, 0), schema::UI().resolveFont("button"))) {
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new shielded address");
else
Notifications::instance().success("New shielded address created");
});
} else {
app->createNewTAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new transparent address");
else
Notifications::instance().success("New transparent address created");
});
}
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (!app->isConnected()) {
Type().textColored(TypeStyle::Caption, Warning(), "Waiting for connection...");
return;
}
if (state.addresses.empty()) {
// Loading skeleton
ImVec2 skelPos = ImGui::GetCursorScreenPos();
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
for (int sk = 0; sk < 3; sk++) {
dl->AddRectFilled(
ImVec2(skelPos.x + sk * (130 + 8), skelPos.y),
ImVec2(skelPos.x + sk * (130 + 8) + 120, skelPos.y + 56),
skelCol, 6.0f);
}
ImGui::Dummy(ImVec2(width, 60));
return;
}
TrackNewAddresses(state);
AddressGroups groups = BuildSortedAddressGroups(state);
// Build filtered list
std::vector<int> filteredIdxs;
if (s_addr_type_filter != 2)
for (int idx : groups.shielded) filteredIdxs.push_back(idx);
if (s_addr_type_filter != 1)
for (int idx : groups.transparent) filteredIdxs.push_back(idx);
// Horizontal scrolling strip
float cardW = std::max(140.0f, std::min(200.0f, width * 0.22f));
float cardH = std::max(52.0f, 64.0f * hs);
float stripH = cardH + 8;
ImGui::BeginChild("##AddrStrip", ImVec2(width, stripH), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoBackground);
ImDrawList* sdl = ImGui::GetWindowDrawList();
for (size_t fi = 0; fi < filteredIdxs.size(); fi++) {
int i = filteredIdxs[fi];
const auto& addr = state.addresses[i];
bool isCurrent = (i == s_selected_address_idx);
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
bool hasBalance = addr.balance > 0;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
// Card background
GlassPanelSpec cardGlass;
cardGlass.rounding = Layout::glassRounding() * 0.75f;
cardGlass.fillAlpha = isCurrent ? 28 : 14;
cardGlass.borderAlpha = isCurrent ? 50 : 25;
DrawGlassPanel(sdl, cardMin, cardMax, cardGlass);
// Selected indicator — top accent bar
if (isCurrent) {
sdl->AddRectFilled(cardMin, ImVec2(cardMax.x, cardMin.y + 3), Primary(),
cardGlass.rounding);
}
float ix = cardMin.x + Layout::spacingMd();
float iy = cardMin.y + Layout::spacingSm() + (isCurrent ? 4 : 0);
// Type dot
sdl->AddCircleFilled(ImVec2(ix + 4, iy + 6), 3.5f, typeCol);
// Address label or truncated address
auto lblIt = s_address_labels.find(addr.address);
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
size_t addrTruncLen = static_cast<size_t>(std::max(8.0f, (cardW - 30) / 9.0f));
if (hasLabel) {
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? PrimaryLight() : OnSurfaceMedium(),
lblIt->second.c_str());
std::string shortAddr = TruncateAddress(addr.address, std::max((size_t)6, addrTruncLen / 2));
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy + capFont->LegacySize + 2),
OnSurfaceDisabled(), shortAddr.c_str());
} else {
std::string dispAddr = TruncateAddress(addr.address, addrTruncLen);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? OnSurface() : OnSurfaceDisabled(),
dispAddr.c_str());
}
// Balance
snprintf(buf, sizeof(buf), "%.4f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float balY = cardMax.y - balSz.y - Layout::spacingSm();
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, balY),
hasBalance ? typeCol : OnSurfaceDisabled(), buf);
// NEW badge
double now = ImGui::GetTime();
auto newIt = s_new_address_timestamps.find(addr.address);
if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) {
double age = now - newIt->second;
if (age < 10.0) {
float alpha = (float)std::max(0.0, 1.0 - age / 10.0);
int a = (int)(alpha * 220);
ImVec2 badgePos(cardMax.x - 32, cardMin.y + 4);
sdl->AddRectFilled(badgePos, ImVec2(badgePos.x + 28, badgePos.y + 14),
IM_COL32(77, 204, 255, a / 4), 3.0f);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(badgePos.x + 4, badgePos.y + 1),
IM_COL32(77, 204, 255, a), "NEW");
}
}
// Click interaction
ImGui::SetCursorScreenPos(cardMin);
ImGui::PushID(i);
ImGui::InvisibleButton("##addrCard", ImVec2(cardW, cardH));
if (ImGui::IsItemHovered()) {
if (!isCurrent)
sdl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 10),
cardGlass.rounding);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s\nBalance: %.8f %s%s\nDouble-click to copy | Right-click for options",
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? " (selected)" : "");
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
s_selected_address_idx = i;
s_cached_qr_data.clear();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Context menu
if (ImGui::BeginPopupContextItem("##addrStripCtx")) {
if (ImGui::MenuItem("Copy Address")) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
if (ImGui::MenuItem("View on Explorer")) {
OpenExplorerURL(addr.address);
}
if (addr.balance > 0) {
if (ImGui::MenuItem("Send From This Address")) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
}
ImGui::EndPopup();
}
ImGui::PopID();
ImGui::SameLine(0, Layout::spacingSm());
}
// Total balance at end of strip
{
double totalBal = 0;
for (const auto& a : state.addresses) totalBal += a.balance;
ImVec2 totPos = ImGui::GetCursorScreenPos();
float totCardW = std::max(100.0f, cardW * 0.6f);
ImVec2 totMax(totPos.x + totCardW, totPos.y + cardH);
GlassPanelSpec totGlass;
totGlass.rounding = Layout::glassRounding() * 0.75f;
totGlass.fillAlpha = 8;
totGlass.borderAlpha = 15;
DrawGlassPanel(sdl, totPos, totMax, totGlass);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(), totPos.y + Layout::spacingSm()),
OnSurfaceMedium(), "TOTAL");
snprintf(buf, sizeof(buf), "%.8f", totalBal);
ImVec2 totSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm()),
OnSurface(), buf);
snprintf(buf, sizeof(buf), "%s", DRAGONX_TICKER);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm() - capFont->LegacySize - 2),
OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(totCardW, cardH));
}
// Keyboard navigation
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
int next = s_selected_address_idx + 1;
if (next < (int)state.addresses.size()) {
s_selected_address_idx = next;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
int prev = s_selected_address_idx - 1;
if (prev >= 0) {
s_selected_address_idx = prev;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
}
}
ImGui::EndChild(); // ##AddrStrip
}
// ============================================================================
// MAIN: RenderReceiveTab — Layout G: QR-Centered Hero
// ============================================================================
void RenderReceiveTab(App* app)
{
const auto& state = app->getWalletState();
RenderSyncBanner(state);
ImVec2 recvAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##ReceiveScroll", recvAvail, false, ImGuiWindowFlags_NoBackground);
float hs = Layout::hScale(recvAvail.x);
float vScale = Layout::vScale(recvAvail.y);
float glassRound = Layout::glassRounding();
float availWidth = ImGui::GetContentRegionAvail().x;
float contentWidth = std::min(availWidth * 0.92f, 1200.0f * hs);
float offsetX = (availWidth - contentWidth) * 0.5f;
if (offsetX > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
float sectionGap = Layout::spacingXl() * vScale;
ImGui::BeginGroup();
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2();
// Auto-select first address
if (!state.addresses.empty() &&
(s_selected_address_idx < 0 ||
s_selected_address_idx >= (int)state.addresses.size())) {
s_selected_address_idx = 0;
}
const AddressInfo* selected = nullptr;
if (s_selected_address_idx >= 0 &&
s_selected_address_idx < (int)state.addresses.size()) {
selected = &state.addresses[s_selected_address_idx];
}
// Generate QR data
std::string qr_data;
if (selected) {
qr_data = selected->address;
if (s_request_amount > 0) {
qr_data = std::string("dragonx:") + selected->address +
"?amount=" + std::to_string(s_request_amount);
if (s_request_memo[0] && selected->type == "shielded") {
qr_data += "&memo=" + std::string(s_request_memo);
}
}
if (qr_data != s_cached_qr_data) {
if (s_qr_texture) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
int w, h;
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
s_cached_qr_data = qr_data;
}
}
// ================================================================
// Not connected / empty state
// ================================================================
if (!app->isConnected()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 120.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
OnSurfaceDisabled(), "Waiting for daemon connection...");
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + 8),
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
if (state.addresses.empty()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 100.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
ImVec2(emptyMin.x + contentWidth * 0.6f, emptyMin.y + Layout::spacingLg() + 16),
skelCol, 4.0f);
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + 24),
ImVec2(emptyMin.x + contentWidth * 0.4f, emptyMin.y + Layout::spacingLg() + 36),
skelCol, 4.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - 24),
OnSurfaceDisabled(), "Loading addresses...");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
// ================================================================
// QR HERO — dominates center (Layout G signature)
// ================================================================
if (selected) {
// Calculate QR size based on available space
float maxQrForWidth = std::min(contentWidth * 0.6f, 400.0f);
float maxQrForHeight = std::min(recvAvail.y * 0.45f, 400.0f);
float qrSize = std::max(140.0f, std::min(maxQrForWidth, maxQrForHeight));
// Center the hero horizontally
float heroW = std::min(contentWidth, 700.0f * hs);
float heroOffsetX = (contentWidth - heroW) * 0.5f;
if (heroOffsetX > 4) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + heroOffsetX);
}
RenderQRHero(app, dl, *selected, heroW, qrSize, qr_data,
glassSpec, state, sub1, body2, capFont);
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- PAYMENT REQUEST (collapsible on narrow) ----
constexpr float kTwoColumnThreshold = 800.0f;
bool isNarrow = contentWidth < kTwoColumnThreshold;
if (isNarrow) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1, 1, 1, 0.05f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1, 1, 1, 0.08f));
ImGui::PushFont(Type().overline());
s_payment_request_open = ImGui::CollapsingHeader(
"PAYMENT REQUEST (OPTIONAL)",
s_payment_request_open ? ImGuiTreeNodeFlags_DefaultOpen : 0);
ImGui::PopFont();
ImGui::PopStyleColor(3);
if (s_payment_request_open) {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
} else {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- RECENT RECEIVED ----
{
float rcvW = std::min(contentWidth, 600.0f * hs);
float rcvOffX = (contentWidth - rcvW) * 0.5f;
if (rcvOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rcvOffX);
RenderRecentReceived(dl, *selected, state, rcvW, capFont);
}
ImGui::Dummy(ImVec2(0, sectionGap));
}
// ================================================================
// ADDRESS STRIP — horizontal switching bar at bottom
// ================================================================
RenderAddressStrip(app, dl, state, contentWidth, hs, sub1, capFont);
ImGui::EndGroup();
ImGui::EndChild(); // ##ReceiveScroll
}
} // namespace ui
} // namespace dragonx