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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user