Diagnostics & logging: - add verbose logging system (VERBOSE_LOGF) with toggle in Settings - forward app-level log messages to Console tab for in-UI visibility - add detailed connection attempt logging (attempt #, daemon state, config paths, auth failures, port owner identification) - detect HTTP 401 auth failures and show actionable error messages - identify port owner process (PID + name) on both Linux and Windows - demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF - persist verbose_logging preference in settings.json - link iphlpapi on Windows for GetExtendedTcpTable Security & encryption: - update local encryption state immediately after encryptwallet RPC so Settings reflects the change before daemon restarts - show notifications for encrypt success/failure and PIN skip - use dedicated RPC client for z_importwallet during decrypt flow to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh) - force full state refresh (addresses, transactions, peers) after successful wallet import Network tab: - redesign peers refresh button as glass-panel with icon + label, matching the mining button style - add spinning arc animation while peer data is loading (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo) - prevent double-click spam during refresh - add refresh-button size to ui.toml Other: - use fast_rpc_ for rescan polling to avoid blocking on main rpc_ - enable DRAGONX_DEBUG in all build configs (was debug-only) - setup.sh: pull latest xmrig-hac when repo already exists
2675 lines
109 KiB
C++
2675 lines
109 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "app.h"
|
|
#include "config/version.h"
|
|
#include "rpc/rpc_client.h"
|
|
#include "rpc/rpc_worker.h"
|
|
#include "rpc/connection.h"
|
|
#include "config/settings.h"
|
|
#include "daemon/embedded_daemon.h"
|
|
#include "daemon/xmrig_manager.h"
|
|
#include "ui/windows/main_window.h"
|
|
#include "ui/windows/balance_tab.h"
|
|
#include "ui/windows/send_tab.h"
|
|
#include "ui/windows/receive_tab.h"
|
|
#include "ui/windows/transactions_tab.h"
|
|
#include "ui/windows/mining_tab.h"
|
|
#include "ui/windows/peers_tab.h"
|
|
#include "ui/windows/market_tab.h"
|
|
#include "ui/windows/settings_window.h"
|
|
#include "ui/windows/about_dialog.h"
|
|
#include "embedded/IconsMaterialDesign.h"
|
|
#include "ui/windows/key_export_dialog.h"
|
|
#include "ui/windows/transaction_details_dialog.h"
|
|
#include "ui/windows/qr_popup_dialog.h"
|
|
#include "ui/windows/validate_address_dialog.h"
|
|
#include "ui/windows/address_book_dialog.h"
|
|
#include "ui/windows/shield_dialog.h"
|
|
#include "ui/windows/request_payment_dialog.h"
|
|
#include "ui/windows/block_info_dialog.h"
|
|
#include "ui/windows/export_all_keys_dialog.h"
|
|
#include "ui/windows/export_transactions_dialog.h"
|
|
#include "ui/windows/console_tab.h"
|
|
#include "ui/pages/settings_page.h"
|
|
#include "ui/theme.h"
|
|
#include "ui/sidebar.h"
|
|
#include "ui/effects/imgui_acrylic.h"
|
|
#include "ui/effects/theme_effects.h"
|
|
#include "ui/effects/low_spec.h"
|
|
#include "ui/material/color_theme.h"
|
|
#include "ui/material/type.h"
|
|
#include "ui/material/typography.h"
|
|
#include "ui/material/draw_helpers.h"
|
|
#include "ui/notifications.h"
|
|
#include "util/i18n.h"
|
|
#include "util/platform.h"
|
|
#include "util/payment_uri.h"
|
|
#include "util/texture_loader.h"
|
|
#include "util/bootstrap.h"
|
|
#include "util/secure_vault.h"
|
|
#include "resources/embedded_resources.h"
|
|
#include "ui/schema/ui_schema.h"
|
|
#include "ui/schema/skin_manager.h"
|
|
#include "util/perf_log.h"
|
|
|
|
// Embedded ui.toml (generated at build time) — fallback when file not on disk
|
|
#if __has_include("ui_toml_embedded.h")
|
|
#include "ui_toml_embedded.h"
|
|
#define HAS_EMBEDDED_UI_TOML 1
|
|
#else
|
|
#define HAS_EMBEDDED_UI_TOML 0
|
|
#endif
|
|
|
|
#include "imgui.h"
|
|
#include <nlohmann/json.hpp>
|
|
#include <curl/curl.h>
|
|
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <set>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
#include <chrono>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#include "util/logger.h"
|
|
#endif
|
|
|
|
namespace dragonx {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
App::App() = default;
|
|
App::~App() = default;
|
|
|
|
bool App::init()
|
|
{
|
|
DEBUG_LOGF("Initializing ObsidianDragon...\n");
|
|
|
|
// Extract embedded resources (Sapling params, asmap.dat) on first run
|
|
if (resources::hasEmbeddedResources() && resources::needsParamsExtraction()) {
|
|
DEBUG_LOGF("First run - extracting bundled resources...\n");
|
|
resources::extractEmbeddedResources();
|
|
}
|
|
|
|
// Initialize settings
|
|
settings_ = std::make_unique<config::Settings>();
|
|
if (!settings_->load()) {
|
|
DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
|
|
}
|
|
|
|
// Apply verbose logging preference from saved settings
|
|
util::Logger::instance().setVerbose(settings_->getVerboseLogging());
|
|
|
|
// Apply saved user font scale so fonts are correct on first reload
|
|
{
|
|
float fs = settings_->getFontScale();
|
|
if (fs > 1.0f) {
|
|
ui::Layout::setUserFontScale(fs);
|
|
// Fonts were loaded at default scale in Init(); rebuild now.
|
|
auto& typo = ui::material::Typography::instance();
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
typo.reload(io, typo.getDpiScale());
|
|
// Consume the flag so App::update() doesn't double-reload
|
|
ui::Layout::consumeUserFontReload();
|
|
DEBUG_LOGF("App: Applied saved font scale %.1fx\n", fs);
|
|
}
|
|
}
|
|
|
|
// Ensure ObsidianDragon config directory and template files exist
|
|
util::Platform::ensureObsidianDragonSetup();
|
|
|
|
// Initialize PIN vault
|
|
vault_ = std::make_unique<util::SecureVault>();
|
|
|
|
// Theme is now applied via SkinManager below after UISchema loads.
|
|
// The old SetThemeById() C++ fallback is no longer needed at startup
|
|
// because SkinManager.setActiveSkin() loads colors from ui.toml directly.
|
|
|
|
// Initialize unified UI schema (loads TOML, drives all layout values)
|
|
{
|
|
std::string schemaPath = util::Platform::getExecutableDirectory() + "/res/themes/ui.toml";
|
|
bool loaded = false;
|
|
if (std::filesystem::exists(schemaPath)) {
|
|
loaded = ui::schema::UISchema::instance().loadFromFile(schemaPath);
|
|
}
|
|
// Fallback: load from build-time embedded data when file not on disk
|
|
// (e.g., single-file Windows distribution without res/ directory)
|
|
if (!loaded) {
|
|
#if HAS_EMBEDDED_UI_TOML
|
|
std::string embedded(reinterpret_cast<const char*>(embedded::ui_toml_data),
|
|
embedded::ui_toml_size);
|
|
ui::schema::UISchema::instance().loadFromString(embedded, "embedded");
|
|
#else
|
|
DEBUG_LOGF("Warning: ui.toml not found at %s and no embedded fallback\n", schemaPath.c_str());
|
|
#endif
|
|
}
|
|
|
|
// Initialize SkinManager and activate saved skin
|
|
auto& skinMgr = ui::schema::SkinManager::instance();
|
|
skinMgr.refresh();
|
|
|
|
// Register image reload callback for skin changes
|
|
skinMgr.setImageReloadCallback([this](const std::string& bgPath, const std::string& logoPath) {
|
|
reloadThemeImages(bgPath, logoPath);
|
|
});
|
|
|
|
std::string skinId = settings_->getSkinId();
|
|
if (skinId.empty()) skinId = "dragonx";
|
|
|
|
// Apply gradient background preference before activating the skin
|
|
// so the initial image resolution uses the correct mode.
|
|
if (settings_->getGradientBackground()) {
|
|
skinMgr.setGradientMode(true);
|
|
}
|
|
|
|
skinMgr.setActiveSkin(skinId);
|
|
}
|
|
|
|
// Apply saved language
|
|
std::string lang = settings_->getLanguage();
|
|
if (!lang.empty()) {
|
|
util::I18n::instance().loadLanguage(lang);
|
|
}
|
|
|
|
// Initialize RPC client
|
|
rpc_ = std::make_unique<rpc::RPCClient>();
|
|
|
|
// Initialize background RPC worker thread
|
|
worker_ = std::make_unique<rpc::RPCWorker>();
|
|
worker_->start();
|
|
|
|
// Forward error/warning notifications to the console tab
|
|
// Use ConsoleTab colors so the Errors filter works correctly
|
|
ui::Notifications::instance().setConsoleCallback(
|
|
[this](const std::string& msg, bool is_error) {
|
|
ImU32 color = is_error ? ui::ConsoleTab::COLOR_ERROR : ui::material::Warning();
|
|
console_tab_.addLine(msg, color);
|
|
});
|
|
|
|
// Forward all app-level log messages (DEBUG_LOGF, LOGF, etc.) to the
|
|
// console tab so they are visible in the UI, not just in the log file.
|
|
util::Logger::instance().setCallback(
|
|
[this](const std::string& msg) {
|
|
// Classify by content: errors in red, warnings in warning color,
|
|
// everything else in the default info color.
|
|
ImU32 color = ui::ConsoleTab::COLOR_INFO;
|
|
if (msg.find("[ERROR]") != std::string::npos ||
|
|
msg.find("error") != std::string::npos ||
|
|
msg.find("Error") != std::string::npos ||
|
|
msg.find("failed") != std::string::npos ||
|
|
msg.find("Failed") != std::string::npos) {
|
|
color = ui::ConsoleTab::COLOR_ERROR;
|
|
} else if (msg.find("[WARN]") != std::string::npos ||
|
|
msg.find("warn") != std::string::npos) {
|
|
color = ui::material::Warning();
|
|
}
|
|
// Strip trailing newline so console tab lines look clean
|
|
std::string trimmed = msg;
|
|
while (!trimmed.empty() && (trimmed.back() == '\n' || trimmed.back() == '\r'))
|
|
trimmed.pop_back();
|
|
if (!trimmed.empty())
|
|
console_tab_.addLine("[app] " + trimmed, color);
|
|
});
|
|
|
|
// Check for first-run wizard — also re-run if blockchain data is missing
|
|
// even when wizard was previously completed (e.g. data dir was deleted)
|
|
if (isFirstRun()) {
|
|
wizard_phase_ = WizardPhase::Appearance;
|
|
DEBUG_LOGF("First run detected — starting wizard\n");
|
|
// Don't start daemon yet — wait for wizard completion
|
|
} else {
|
|
// Normal startup — connect to daemon
|
|
tryConnect();
|
|
}
|
|
|
|
DEBUG_LOGF("Initialization complete\n");
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pre-frame: font atlas rebuilds (must run BEFORE ImGui::NewFrame)
|
|
// ============================================================================
|
|
void App::preFrame()
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Hot-reload unified UI schema
|
|
{
|
|
PERF_SCOPE("PreFrame.SchemaHotReload");
|
|
ui::schema::UISchema::instance().pollForChanges();
|
|
ui::schema::UISchema::instance().applyIfDirty();
|
|
}
|
|
|
|
// Refresh balance layout config after schema reload
|
|
ui::RefreshBalanceLayoutConfig();
|
|
|
|
// If font sizes changed in the TOML, rebuild the font atlas
|
|
if (ui::schema::UISchema::instance().consumeFontsChanged()) {
|
|
auto& typo = ui::material::Typography::instance();
|
|
typo.reload(io, typo.getDpiScale());
|
|
DEBUG_LOGF("App: Font atlas rebuilt after hot-reload\n");
|
|
}
|
|
|
|
// If the user changed font scale in Settings, rebuild the font atlas
|
|
if (ui::Layout::consumeUserFontReload()) {
|
|
auto& typo = ui::material::Typography::instance();
|
|
typo.reload(io, typo.getDpiScale());
|
|
DEBUG_LOGF("App: Font atlas rebuilt after user font-scale change (%.1fx)\n",
|
|
ui::Layout::userFontScale());
|
|
}
|
|
}
|
|
|
|
void App::update()
|
|
{
|
|
PERF_SCOPE("Update.Total");
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Track user interaction for auto-lock
|
|
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
|
io.MouseClicked[0] || io.MouseClicked[1] ||
|
|
io.InputQueueCharacters.Size > 0) {
|
|
last_interaction_ = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
// Drain completed RPC results back onto the main thread
|
|
if (worker_) {
|
|
worker_->drainResults();
|
|
}
|
|
if (fast_worker_) {
|
|
fast_worker_->drainResults();
|
|
}
|
|
|
|
// Auto-lock check (only when connected + encrypted + unlocked)
|
|
if (state_.connected && state_.isUnlocked()) {
|
|
checkAutoLock();
|
|
}
|
|
|
|
// P8: Dedup rebuildAddressList — only rebuild once per frame
|
|
if (address_list_dirty_) {
|
|
address_list_dirty_ = false;
|
|
state_.rebuildAddressList();
|
|
}
|
|
|
|
// Update timers
|
|
refresh_timer_ += io.DeltaTime;
|
|
price_timer_ += io.DeltaTime;
|
|
fast_refresh_timer_ += io.DeltaTime;
|
|
tx_age_timer_ += io.DeltaTime;
|
|
opid_poll_timer_ += io.DeltaTime;
|
|
|
|
// Fast refresh (mining stats + daemon memory) every second
|
|
// Skip when wallet is locked — no need to poll, and queued tasks
|
|
// would delay the PIN unlock worker task.
|
|
if (fast_refresh_timer_ >= FAST_REFRESH_INTERVAL) {
|
|
fast_refresh_timer_ = 0.0f;
|
|
if (state_.connected && !state_.isLocked()) {
|
|
refreshMiningInfo();
|
|
|
|
// Poll getrescaninfo for rescan progress (if rescan flag is set)
|
|
// Use fast_rpc_ when available to avoid blocking on rpc_'s
|
|
// curl_mutex (which may be held by a long-running import).
|
|
if (state_.sync.rescanning && fast_worker_) {
|
|
auto* rescanRpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
|
fast_worker_->post([this, rescanRpc]() -> rpc::RPCWorker::MainCb {
|
|
try {
|
|
auto info = rescanRpc->call("getrescaninfo");
|
|
bool rescanning = info.value("rescanning", false);
|
|
float progress = 0.0f;
|
|
if (info.contains("rescan_progress")) {
|
|
std::string progStr = info["rescan_progress"].get<std::string>();
|
|
try { progress = std::stof(progStr) * 100.0f; } catch (...) {}
|
|
}
|
|
return [this, rescanning, progress]() {
|
|
if (rescanning) {
|
|
state_.sync.rescanning = true;
|
|
if (progress > 0.0f) {
|
|
state_.sync.rescan_progress = progress / 100.0f;
|
|
}
|
|
} else if (state_.sync.rescanning) {
|
|
// Rescan just finished
|
|
ui::Notifications::instance().success("Blockchain rescan complete");
|
|
state_.sync.rescanning = false;
|
|
state_.sync.rescan_progress = 1.0f;
|
|
state_.sync.rescan_status.clear();
|
|
}
|
|
};
|
|
} catch (...) {
|
|
// RPC not available yet or failed
|
|
return [](){};
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Poll xmrig stats every ~2 seconds (use a simple toggle)
|
|
static bool xmrig_poll_tick = false;
|
|
xmrig_poll_tick = !xmrig_poll_tick;
|
|
if (xmrig_poll_tick && xmrig_manager_ && xmrig_manager_->isRunning()) {
|
|
xmrig_manager_->pollStats();
|
|
auto& ps = state_.pool_mining;
|
|
auto& xs = xmrig_manager_->getStats();
|
|
ps.xmrig_running = true;
|
|
ps.hashrate_10s = xs.hashrate_10s;
|
|
ps.hashrate_60s = xs.hashrate_60s;
|
|
ps.hashrate_15m = xs.hashrate_15m;
|
|
ps.accepted = xs.accepted;
|
|
ps.rejected = xs.rejected;
|
|
ps.uptime_sec = xs.uptime_sec;
|
|
ps.pool_diff = xs.pool_diff;
|
|
ps.pool_url = xs.pool_url;
|
|
ps.algo = xs.algo;
|
|
ps.connected = xs.connected;
|
|
// Get memory directly from OS (more reliable than API)
|
|
double memMB = xmrig_manager_->getMemoryUsageMB();
|
|
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
|
|
ps.threads_active = xs.threads_active;
|
|
ps.log_lines = xmrig_manager_->getRecentLines(30);
|
|
|
|
// Record hashrate sample for the chart
|
|
ps.hashrate_history.push_back(ps.hashrate_10s);
|
|
if (ps.hashrate_history.size() > PoolMiningState::MAX_HISTORY) {
|
|
ps.hashrate_history.erase(ps.hashrate_history.begin());
|
|
}
|
|
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
|
state_.pool_mining.xmrig_running = false;
|
|
}
|
|
|
|
// Check daemon output for rescan progress
|
|
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
|
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
|
|
if (!newOutput.empty()) {
|
|
// Look for rescan progress patterns in new output
|
|
// Hush patterns: "Still rescanning. At block X. Progress=Y" or "Rescanning..." with percentage
|
|
bool foundRescan = false;
|
|
float rescanPct = 0.0f;
|
|
|
|
// Search line by line for rescan info
|
|
size_t pos = 0;
|
|
while (pos < newOutput.size()) {
|
|
size_t eol = newOutput.find('\n', pos);
|
|
if (eol == std::string::npos) eol = newOutput.size();
|
|
std::string line = newOutput.substr(pos, eol - pos);
|
|
pos = eol + 1;
|
|
|
|
// Check for "Rescanning from height" (rescan starting)
|
|
if (line.find("Rescanning from height") != std::string::npos ||
|
|
line.find("Rescanning last") != std::string::npos) {
|
|
foundRescan = true;
|
|
state_.sync.rescan_status = line;
|
|
}
|
|
|
|
// Check for "Still rescanning" with progress
|
|
auto stillIdx = line.find("Still rescanning");
|
|
if (stillIdx != std::string::npos) {
|
|
foundRescan = true;
|
|
// Try to extract progress (Progress=0.XXXX)
|
|
auto progIdx = line.find("Progress=");
|
|
if (progIdx != std::string::npos) {
|
|
size_t numStart = progIdx + 9; // strlen("Progress=")
|
|
size_t numEnd = numStart;
|
|
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
|
|
numEnd++;
|
|
}
|
|
if (numEnd > numStart) {
|
|
try {
|
|
rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f;
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
state_.sync.rescan_status = line;
|
|
}
|
|
|
|
// Check for "Rescanning..." with percentage (ShowProgress output)
|
|
auto rescIdx = line.find("Rescanning...");
|
|
if (rescIdx != std::string::npos) {
|
|
foundRescan = true;
|
|
// Try to extract percentage
|
|
auto pctIdx = line.find('%');
|
|
if (pctIdx != std::string::npos && pctIdx > 0) {
|
|
// Walk backwards to find the number
|
|
size_t numEnd = pctIdx;
|
|
size_t numStart = numEnd;
|
|
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
|
|
numStart--;
|
|
}
|
|
if (numStart < numEnd) {
|
|
try {
|
|
rescanPct = std::stof(line.substr(numStart, numEnd - numStart));
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
state_.sync.rescan_status = line;
|
|
}
|
|
|
|
// Check for "Done rescanning" (rescan complete)
|
|
if (line.find("Done rescanning") != std::string::npos ||
|
|
line.find("Rescan complete") != std::string::npos) {
|
|
if (state_.sync.rescanning) {
|
|
ui::Notifications::instance().success("Blockchain rescan complete");
|
|
}
|
|
state_.sync.rescanning = false;
|
|
state_.sync.rescan_progress = 1.0f;
|
|
state_.sync.rescan_status.clear();
|
|
}
|
|
}
|
|
|
|
if (foundRescan) {
|
|
state_.sync.rescanning = true;
|
|
if (rescanPct > 0.0f) {
|
|
state_.sync.rescan_progress = rescanPct / 100.0f;
|
|
}
|
|
}
|
|
}
|
|
} else if (!embedded_daemon_ || !embedded_daemon_->isRunning()) {
|
|
// Clear rescan state if daemon is not running (but preserve during restart)
|
|
if (state_.sync.rescanning && state_.sync.rescan_progress >= 0.99f) {
|
|
state_.sync.rescanning = false;
|
|
state_.sync.rescan_status.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Poll pending z_sendmany operations for completion
|
|
if (opid_poll_timer_ >= OPID_POLL_INTERVAL && !pending_opids_.empty()
|
|
&& state_.connected && fast_worker_) {
|
|
opid_poll_timer_ = 0.0f;
|
|
auto opids = pending_opids_; // copy for worker thread
|
|
fast_worker_->post([this, opids]() -> rpc::RPCWorker::MainCb {
|
|
auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
|
if (!rpc) return [](){};
|
|
json ids = json::array();
|
|
for (const auto& id : opids) ids.push_back(id);
|
|
json result;
|
|
try {
|
|
result = rpc->call("z_getoperationstatus", {ids});
|
|
} catch (...) {
|
|
return [](){};
|
|
}
|
|
// Collect completed/failed opids
|
|
std::vector<std::string> done;
|
|
bool anySuccess = false;
|
|
for (const auto& op : result) {
|
|
std::string status = op.value("status", "");
|
|
std::string opid = op.value("id", "");
|
|
if (status == "success") {
|
|
done.push_back(opid);
|
|
anySuccess = true;
|
|
} else if (status == "failed") {
|
|
done.push_back(opid);
|
|
std::string msg = "Transaction failed";
|
|
if (op.contains("error") && op["error"].contains("message"))
|
|
msg = op["error"]["message"].get<std::string>();
|
|
// Capture for main thread
|
|
return [this, done, msg]() {
|
|
ui::Notifications::instance().error(msg);
|
|
for (const auto& id : done) {
|
|
pending_opids_.erase(
|
|
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
|
pending_opids_.end());
|
|
}
|
|
};
|
|
}
|
|
}
|
|
return [this, done, anySuccess]() {
|
|
for (const auto& id : done) {
|
|
pending_opids_.erase(
|
|
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
|
pending_opids_.end());
|
|
}
|
|
if (anySuccess) {
|
|
// Transaction confirmed by daemon — force immediate data refresh
|
|
transactions_dirty_ = true;
|
|
addresses_dirty_ = true;
|
|
last_tx_block_height_ = -1;
|
|
refresh_timer_ = REFRESH_INTERVAL;
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
// Regular refresh every 5 seconds
|
|
// Skip when wallet is locked — same reason as above.
|
|
if (refresh_timer_ >= REFRESH_INTERVAL) {
|
|
refresh_timer_ = 0.0f;
|
|
if (state_.connected && !state_.isLocked()) {
|
|
refreshData();
|
|
} else if (!connection_in_progress_ &&
|
|
wizard_phase_ == WizardPhase::None) {
|
|
tryConnect();
|
|
}
|
|
}
|
|
|
|
// Price refresh every 60 seconds
|
|
if (price_timer_ >= PRICE_INTERVAL) {
|
|
price_timer_ = 0.0f;
|
|
if (settings_->getFetchPrices()) {
|
|
refreshPrice();
|
|
}
|
|
}
|
|
|
|
// Keyboard shortcut: Ctrl+, to open Settings page
|
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
|
current_page_ = ui::NavPage::Settings;
|
|
}
|
|
|
|
// Keyboard shortcut: Ctrl+Left/Right to cycle themes
|
|
if (io.KeyCtrl && !io.WantTextInput) {
|
|
bool prevTheme = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
|
bool nextTheme = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
|
if (prevTheme || nextTheme) {
|
|
auto& skinMgr = ui::schema::SkinManager::instance();
|
|
const auto& skins = skinMgr.available();
|
|
if (!skins.empty()) {
|
|
int cur = 0;
|
|
for (int i = 0; i < (int)skins.size(); i++) {
|
|
if (skins[i].id == skinMgr.activeSkinId()) { cur = i; break; }
|
|
}
|
|
if (prevTheme)
|
|
cur = (cur - 1 + (int)skins.size()) % (int)skins.size();
|
|
else
|
|
cur = (cur + 1) % (int)skins.size();
|
|
skinMgr.setActiveSkin(skins[cur].id);
|
|
if (settings_) {
|
|
settings_->setSkinId(skins[cur].id);
|
|
settings_->save();
|
|
}
|
|
ui::Notifications::instance().info("Theme: " + skins[cur].name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keyboard shortcut: F5 to refresh
|
|
if (ImGui::IsKeyPressed(ImGuiKey_F5)) {
|
|
refreshNow();
|
|
}
|
|
|
|
// Keyboard shortcut: Ctrl+Shift+Down to toggle low-spec mode
|
|
// (checked BEFORE Ctrl+Down so it doesn't also trigger theme effects)
|
|
if (io.KeyCtrl && io.KeyShift && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
|
bool newLow = !ui::effects::isLowSpecMode();
|
|
ui::effects::setLowSpecMode(newLow);
|
|
if (newLow) {
|
|
// Disable all heavy effects at runtime (don't overwrite saved prefs)
|
|
ui::effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
|
|
ui::effects::ImGuiAcrylic::SetUIOpacity(1.0f);
|
|
ui::effects::ThemeEffects::instance().setEnabled(false);
|
|
ui::effects::ThemeEffects::instance().setReducedTransparency(true);
|
|
if (settings_) {
|
|
settings_->setLowSpecMode(true);
|
|
settings_->setWindowOpacity(1.0f);
|
|
settings_->save();
|
|
}
|
|
} else {
|
|
// Restore effect settings from saved preferences
|
|
if (settings_) {
|
|
settings_->setLowSpecMode(false);
|
|
settings_->save();
|
|
ui::effects::ImGuiAcrylic::ApplyBlurAmount(settings_->getBlurMultiplier());
|
|
ui::effects::ImGuiAcrylic::SetUIOpacity(settings_->getUIOpacity());
|
|
ui::effects::ThemeEffects::instance().setEnabled(settings_->getThemeEffectsEnabled());
|
|
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
|
|
}
|
|
}
|
|
ui::Notifications::instance().info(newLow ? "Low-spec mode enabled" : "Low-spec mode disabled");
|
|
}
|
|
|
|
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
|
|
else if (io.KeyCtrl && !io.KeyShift && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
|
bool newState = !ui::effects::ThemeEffects::instance().isEnabled();
|
|
ui::effects::ThemeEffects::instance().setEnabled(newState);
|
|
ui::effects::ThemeEffects::instance().setReducedTransparency(!newState);
|
|
if (settings_) {
|
|
settings_->setThemeEffectsEnabled(newState);
|
|
settings_->save();
|
|
}
|
|
ui::Notifications::instance().info(newState ? "Theme effects enabled" : "Theme effects disabled");
|
|
}
|
|
|
|
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
|
|
if (io.KeyCtrl && !io.KeyShift && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
|
|
bool newGrad = !settings_->getGradientBackground();
|
|
settings_->setGradientBackground(newGrad);
|
|
ui::schema::SkinManager::instance().setGradientMode(newGrad);
|
|
settings_->save();
|
|
ui::Notifications::instance().info(newGrad ? "Simple background enabled" : "Simple background disabled");
|
|
}
|
|
|
|
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
|
|
constexpr bool ENABLE_WIZARD_HOTKEY = false;
|
|
if constexpr (ENABLE_WIZARD_HOTKEY) {
|
|
if (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_W)) {
|
|
wizard_phase_ = WizardPhase::Appearance;
|
|
DEBUG_LOGF("[Debug] Wizard re-opened via Ctrl+Shift+W\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
void App::render()
|
|
{
|
|
// First-run wizard gate — blocks all normal UI
|
|
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
|
renderFirstRunWizard();
|
|
return;
|
|
}
|
|
|
|
// Process deferred encryption from wizard (runs in background)
|
|
processDeferredEncryption();
|
|
|
|
// Main content area - use full window (no menu bar)
|
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
|
|
|
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
|
ImGuiWindowFlags_NoSavedSettings |
|
|
ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoScrollWithMouse;
|
|
|
|
// When OS backdrop is active, use NoBackground so DWM Mica/Acrylic shows through
|
|
if (ui::material::IsBackdropActive()) {
|
|
window_flags |= ImGuiWindowFlags_NoBackground;
|
|
}
|
|
|
|
// Main window padding from ui.toml schema (DPI-scaled)
|
|
const float hdp = ui::Layout::dpiScale();
|
|
const auto& mwWin = ui::schema::UI().window("components.main-window");
|
|
const float mainPadX = (mwWin.padding[0] > 0.0f ? mwWin.padding[0] : 16.0f) * hdp;
|
|
const float mainPadTop = (mwWin.padding[1] > 0.0f ? mwWin.padding[1] : 42.0f) * hdp;
|
|
const float mainPadBot = ui::schema::UI().drawElement("components.main-window", "padding-bottom").size * hdp;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(mainPadX, mainPadTop));
|
|
ImGui::Begin("MainContent", nullptr, window_flags);
|
|
ImGui::PopStyleVar(); // WindowPadding — applied to the window, safe to pop now
|
|
|
|
// ---- Top-left branding: logo + "ObsidianDragon" title ----
|
|
// Drawn via DrawList in the top padding area — zero layout impact.
|
|
{
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 winPos = ImGui::GetWindowPos();
|
|
const auto& S = ui::schema::UI();
|
|
auto hdrElem = S.drawElement("components.main-window", "header-title");
|
|
auto hdrLabel = S.label("components.main-window", "header-title");
|
|
|
|
// Helper to read extraFloats with fallback (DPI-scaled)
|
|
auto hdrF = [&](const char* key, float fb) -> float {
|
|
auto it = hdrElem.extraFloats.find(key);
|
|
return ((it != hdrElem.extraFloats.end()) ? it->second : fb) * hdp;
|
|
};
|
|
|
|
const float brandPadX = hdrF("pad-x", mainPadX / hdp);
|
|
const float brandPadY = hdrF("pad-y", 8.0f);
|
|
const float logoGap = hdrF("logo-gap", 8.0f);
|
|
const float hdrOpacity = (hdrElem.opacity >= 0.0f) ? hdrElem.opacity : 0.7f;
|
|
|
|
// Logo
|
|
float logoSize = mainPadTop - brandPadY * 2.0f; // fit within header
|
|
if (logoSize < 16.0f * hdp) logoSize = 16.0f * hdp;
|
|
float logoX = winPos.x + brandPadX;
|
|
float logoY = winPos.y + brandPadY;
|
|
if (logo_tex_ != 0) {
|
|
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
|
float logoW = logoSize * aspect;
|
|
dl->AddImage(logo_tex_,
|
|
ImVec2(logoX, logoY),
|
|
ImVec2(logoX + logoW, logoY + logoSize));
|
|
logoX += logoW + logoGap;
|
|
}
|
|
|
|
// Title text — font and size from schema (DPI-scaled)
|
|
ImFont* titleFont = S.resolveFont(hdrLabel.font);
|
|
if (!titleFont) titleFont = ui::material::Type().subtitle1();
|
|
// TOML size is in logical pixels; scale by DPI. Font's LegacySize
|
|
// is already DPI-scaled from the atlas reload.
|
|
float titleFontSize = hdrElem.size >= 0.0f
|
|
? hdrElem.size * hdp
|
|
: titleFont->LegacySize;
|
|
if (titleFont) {
|
|
float textY = winPos.y + (mainPadTop - titleFontSize) * 0.5f;
|
|
ImU32 textCol = ui::material::OnSurface();
|
|
// Apply header text opacity
|
|
int a = (int)((float)((textCol >> 24) & 0xFF) * hdrOpacity);
|
|
textCol = (textCol & 0x00FFFFFF) | ((ImU32)a << 24);
|
|
dl->AddText(titleFont, titleFontSize,
|
|
ImVec2(logoX, textY),
|
|
textCol, "ObsidianDragon");
|
|
}
|
|
}
|
|
|
|
// Sidebar + Content layout
|
|
const float dp = ui::Layout::dpiScale();
|
|
auto sbde = [dp](const char* key, float fb) {
|
|
float v = ui::schema::UI().drawElement("components.sidebar", key).size;
|
|
return (v >= 0 ? v : fb) * dp;
|
|
};
|
|
float statusBarH = ui::schema::UI().window("components.status-bar").height;
|
|
if (statusBarH <= 0.0f) statusBarH = 24.0f; // safety fallback
|
|
// Content area padding from ui.toml schema
|
|
const auto& caWin = ui::schema::UI().window("components.content-area");
|
|
const float caMarginTop = ui::schema::UI().drawElement("components.content-area", "margin-top").size;
|
|
const float caMarginBot = ui::schema::UI().drawElement("components.content-area", "margin-bottom").size;
|
|
const float contentH = ImGui::GetContentRegionAvail().y - statusBarH - ImGui::GetStyle().ItemSpacing.y
|
|
- caMarginTop - caMarginBot;
|
|
|
|
// Auto-collapse when viewport is narrow (skip if user manually toggled)
|
|
float vpW = viewport->WorkSize.x;
|
|
float collapseHysteresis = ui::schema::UI().drawElement("components.main-window", "collapse-hysteresis").sizeOr(60.0f) * dp;
|
|
const float autoCollapseThreshold = sbde("auto-collapse-threshold", 800.0f);
|
|
if (!sidebar_user_toggled_) {
|
|
if (vpW < autoCollapseThreshold && !sidebar_collapsed_) {
|
|
sidebar_collapsed_ = true;
|
|
} else if (vpW >= autoCollapseThreshold + collapseHysteresis && sidebar_collapsed_) {
|
|
sidebar_collapsed_ = false;
|
|
}
|
|
} else {
|
|
// Reset manual override when viewport crosses the threshold significantly,
|
|
// so auto-collapse resumes after the user resizes the window
|
|
if ((!sidebar_collapsed_ && vpW >= autoCollapseThreshold + collapseHysteresis) ||
|
|
(sidebar_collapsed_ && vpW < autoCollapseThreshold)) {
|
|
sidebar_user_toggled_ = false;
|
|
}
|
|
}
|
|
|
|
// Animate sidebar width
|
|
float targetW = sidebar_collapsed_ ? sbde("collapsed-width", 64.0f) : sbde("width", 160.0f);
|
|
// On DPI change, snap instantly instead of animating from stale value
|
|
bool dpiChanged = (prev_dpi_scale_ > 0.0f && std::abs(dp - prev_dpi_scale_) > 0.01f);
|
|
prev_dpi_scale_ = dp;
|
|
if (sidebar_width_anim_ <= 0.0f || dpiChanged) sidebar_width_anim_ = targetW;
|
|
{
|
|
float diff = targetW - sidebar_width_anim_;
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
float t = dt * sbde("collapse-anim-speed", 10.0f);
|
|
if (t > 1.0f) t = 1.0f;
|
|
sidebar_width_anim_ += diff * t;
|
|
}
|
|
const float sidebarW = sidebar_width_anim_;
|
|
|
|
// Build sidebar status for badges + footer
|
|
ui::SidebarStatus sbStatus;
|
|
sbStatus.peerCount = static_cast<int>(state_.peers.size());
|
|
sbStatus.miningActive = state_.mining.generate;
|
|
|
|
// Load logo texture lazily on first frame (or after theme change)
|
|
// Also reload when dark↔light mode changes so the correct variant shows
|
|
{
|
|
bool wantDark = ui::material::IsDarkTheme();
|
|
if (!logo_loaded_ || (wantDark != logo_is_dark_variant_)) {
|
|
logo_loaded_ = true;
|
|
logo_is_dark_variant_ = wantDark;
|
|
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
|
|
|
// 1) Check for theme-override logo from active skin
|
|
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
|
ui::schema::SkinManager::instance().activeSkinId());
|
|
std::string logoPath;
|
|
if (activeSkin && !activeSkin->logoPath.empty()) {
|
|
logoPath = activeSkin->logoPath;
|
|
} else {
|
|
// 2) Read icon filename from ui.toml (dark/light variant)
|
|
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
|
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
|
auto it = iconElem.extraColors.find(iconKey);
|
|
std::string iconFile;
|
|
if (it != iconElem.extraColors.end() && !it->second.empty()) {
|
|
iconFile = it->second;
|
|
} else {
|
|
// Fallback filenames
|
|
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
|
}
|
|
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
|
}
|
|
if (util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
|
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
|
} else {
|
|
// Try embedded data fallback — use actual filename from path
|
|
// so light/dark variants resolve correctly on Windows single-file
|
|
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
|
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
|
if (!logoRes || !logoRes->data || logoRes->size == 0) {
|
|
// Final fallback: try the default dark logo constant
|
|
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
|
}
|
|
if (logoRes && logoRes->data && logoRes->size > 0) {
|
|
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_)) {
|
|
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
|
} else {
|
|
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
|
}
|
|
} else {
|
|
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load coin logo texture lazily (DragonX currency icon for balance tab)
|
|
if (!coin_logo_loaded_) {
|
|
coin_logo_loaded_ = true;
|
|
coin_logo_tex_ = 0; coin_logo_w_ = 0; coin_logo_h_ = 0;
|
|
|
|
// Read coin icon filename from ui.toml
|
|
auto coinElem = ui::schema::UI().drawElement("components.main-window", "coin-icon");
|
|
auto cit = coinElem.extraColors.find("icon");
|
|
std::string coinFile = (cit != coinElem.extraColors.end() && !cit->second.empty())
|
|
? cit->second : "logos/logo_dragonx_128.png";
|
|
std::string coinPath = util::getExecutableDirectory() + "/res/img/" + coinFile;
|
|
if (util::LoadTextureFromFile(coinPath.c_str(), &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
|
DEBUG_LOGF("Loaded coin logo from %s (%dx%d)\n", coinPath.c_str(), coin_logo_w_, coin_logo_h_);
|
|
} else {
|
|
// Try embedded resource fallback (Windows single-file distribution)
|
|
std::string coinBasename = std::filesystem::path(coinFile).filename().string();
|
|
const auto* coinRes = resources::getEmbeddedResource(coinBasename);
|
|
if (coinRes && coinRes->data && coinRes->size > 0) {
|
|
if (util::LoadTextureFromMemory(coinRes->data, coinRes->size, &coin_logo_tex_, &coin_logo_w_, &coin_logo_h_)) {
|
|
DEBUG_LOGF("Loaded coin logo from embedded: %s (%dx%d)\n", coinBasename.c_str(), coin_logo_w_, coin_logo_h_);
|
|
} else {
|
|
DEBUG_LOGF("Note: Failed to decode embedded coin logo\n");
|
|
}
|
|
} else {
|
|
DEBUG_LOGF("Note: Coin logo not found at %s\n", coinPath.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (logo_tex_ != 0) {
|
|
sbStatus.logoTexID = logo_tex_;
|
|
sbStatus.logoW = logo_w_;
|
|
sbStatus.logoH = logo_h_;
|
|
}
|
|
if (gradient_tex_ != 0) {
|
|
sbStatus.gradientTexID = gradient_tex_;
|
|
}
|
|
// Count unconfirmed transactions
|
|
{
|
|
int unconf = 0;
|
|
for (const auto& tx : state_.transactions) {
|
|
if (!tx.isConfirmed()) ++unconf;
|
|
}
|
|
sbStatus.unconfirmedTxCount = unconf;
|
|
}
|
|
|
|
// Sidebar margins from ui.toml schema (DPI-scaled like all sidebar values)
|
|
const float sbMarginTop = sbde("margin-top", 0.0f);
|
|
const float sbMarginBottom = sbde("margin-bottom", 0.0f);
|
|
const float sbMinHeight = sbde("min-height", 360.0f);
|
|
|
|
// Ensure sidebar is tall enough to fit all buttons — shrink margins if needed
|
|
float sidebarH = contentH - sbMarginTop - sbMarginBottom;
|
|
float effectiveMarginTop = sbMarginTop;
|
|
if (sidebarH < sbMinHeight) {
|
|
float available = contentH - sbMinHeight;
|
|
if (available > 0.0f) {
|
|
float ratio = available / (sbMarginTop + sbMarginBottom);
|
|
effectiveMarginTop = sbMarginTop * ratio;
|
|
} else {
|
|
effectiveMarginTop = 0.0f;
|
|
}
|
|
sidebarH = std::max(contentH - effectiveMarginTop, sbMinHeight);
|
|
}
|
|
|
|
// Sidebar navigation
|
|
// Save cursor Y before applying sidebar margin so the content area
|
|
// (placed via SameLine) starts at the original row position, not the
|
|
// margin-shifted one.
|
|
float preSidebarCursorY = ImGui::GetCursorPosY();
|
|
if (effectiveMarginTop > 0.0f)
|
|
ImGui::SetCursorPosY(preSidebarCursorY + effectiveMarginTop);
|
|
bool prevCollapsed = sidebar_collapsed_;
|
|
{
|
|
PERF_SCOPE("Render.Sidebar");
|
|
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_);
|
|
}
|
|
if (sbStatus.exitClicked) {
|
|
requestQuit();
|
|
}
|
|
if (sidebar_collapsed_ != prevCollapsed) {
|
|
sidebar_user_toggled_ = true; // user clicked chevron or pressed [
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
// Restore cursor Y so content area is not shifted by sidebar margin
|
|
ImGui::SetCursorPosY(preSidebarCursorY);
|
|
|
|
// Page transition: detect change, ramp alpha
|
|
if (current_page_ != prev_page_) {
|
|
page_alpha_ = ui::effects::isLowSpecMode() ? 1.0f : 0.0f;
|
|
prev_page_ = current_page_;
|
|
}
|
|
if (page_alpha_ < 1.0f) {
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
float pageFadeSpeed = ui::schema::UI().drawElement("components.main-window", "page-fade-speed").sizeOr(8.0f);
|
|
page_alpha_ += dt * pageFadeSpeed;
|
|
if (page_alpha_ > 1.0f) page_alpha_ = 1.0f;
|
|
}
|
|
|
|
// Content area — fills remaining width, tabs handle their own scrolling
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + caMarginTop);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
|
|
ImGui::GetStyle().Alpha * page_alpha_);
|
|
ImGuiWindowFlags contentFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
|
if (ui::material::IsBackdropActive())
|
|
contentFlags |= ImGuiWindowFlags_NoBackground;
|
|
float caPadX = caWin.padding[0] > 0.0f ? caWin.padding[0] : ImGui::GetStyle().WindowPadding.x;
|
|
float caPadY = caWin.padding[1] > 0.0f ? caWin.padding[1] : ImGui::GetStyle().WindowPadding.y;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(caPadX, caPadY));
|
|
|
|
// Capture content area screen position for edge fade mask
|
|
ImVec2 caScreenPos = ImGui::GetCursorScreenPos();
|
|
|
|
ImGui::BeginChild("##ContentArea", ImVec2(0, contentH), false, contentFlags);
|
|
|
|
// Capture vertex start for edge fade mask
|
|
ImDrawList* caDL = ImGui::GetWindowDrawList();
|
|
int caVtxStart = caDL->VtxBuffer.Size;
|
|
|
|
// Also capture ForegroundDrawList vertex start — DrawGlassPanel draws
|
|
// theme effects (rainbow border, edge trace, shimmer, specular glare)
|
|
// on the ForegroundDrawList, and those must also be edge-faded.
|
|
ImDrawList* fgDL = ImGui::GetForegroundDrawList();
|
|
int fgVtxStart = fgDL->VtxBuffer.Size;
|
|
|
|
// ---------------------------------------------------------------
|
|
// Loading overlay — show only while daemon is not yet connected.
|
|
// Once connected, all tabs render normally; individual tabs show
|
|
// inline sync-progress indicators when the chain is still syncing.
|
|
// Pages that remain accessible even without a connection:
|
|
// Console, Peers, Settings
|
|
// ---------------------------------------------------------------
|
|
bool pageNeedsDaemon = (current_page_ != ui::NavPage::Console &&
|
|
current_page_ != ui::NavPage::Peers &&
|
|
current_page_ != ui::NavPage::Settings);
|
|
bool daemonReady = state_.connected; // don't gate on sync state
|
|
|
|
if (state_.isLocked()) {
|
|
// Lock screen — covers tab content just like the loading overlay
|
|
renderLockScreen();
|
|
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
|
// Reset lock screen focus flag so it auto-focuses next time
|
|
lock_screen_was_visible_ = false;
|
|
// Show loading overlay instead of tab content
|
|
renderLoadingOverlay(contentH);
|
|
} else {
|
|
lock_screen_was_visible_ = false;
|
|
|
|
{
|
|
PERF_SCOPE("Render.ActiveTab");
|
|
switch (current_page_) {
|
|
case ui::NavPage::Overview:
|
|
ui::RenderBalanceTab(this);
|
|
break;
|
|
case ui::NavPage::Send:
|
|
ui::RenderSendTab(this);
|
|
break;
|
|
case ui::NavPage::Receive:
|
|
ui::RenderReceiveTab(this);
|
|
break;
|
|
case ui::NavPage::History:
|
|
ui::RenderTransactionsTab(this);
|
|
break;
|
|
case ui::NavPage::Mining:
|
|
ui::RenderMiningTab(this);
|
|
break;
|
|
case ui::NavPage::Peers:
|
|
ui::RenderPeersTab(this);
|
|
break;
|
|
case ui::NavPage::Market:
|
|
ui::RenderMarketTab(this);
|
|
break;
|
|
case ui::NavPage::Console:
|
|
// Use fast-lane worker for console commands to avoid head-of-line
|
|
// blocking behind the consolidated refreshData() batch.
|
|
console_tab_.render(embedded_daemon_.get(),
|
|
fast_rpc_ ? fast_rpc_.get() : rpc_.get(),
|
|
fast_worker_ ? fast_worker_.get() : worker_.get(),
|
|
xmrig_manager_.get());
|
|
break;
|
|
case ui::NavPage::Settings:
|
|
ui::RenderSettingsPage(this);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} // PERF_SCOPE
|
|
|
|
} // end loading gate
|
|
|
|
// Snapshot ForegroundDrawList vertex count BEFORE viewport-wide effects
|
|
// so the edge fade only applies to per-panel theme effects, not embers/overlay.
|
|
int fgVtxEnd = fgDL->VtxBuffer.Size;
|
|
|
|
// Viewport-wide ambient theme effects (e.g. ember particles for fire themes)
|
|
{
|
|
PERF_SCOPE("Render.ThemeEffects");
|
|
auto& fx = ui::effects::ThemeEffects::instance();
|
|
ImDrawList* fgDl = ImGui::GetForegroundDrawList();
|
|
|
|
if (fx.hasEmberRise()) {
|
|
fx.drawViewportEmbers(fgDl);
|
|
}
|
|
|
|
// Sandstorm particles (wind-driven sand for desert themes)
|
|
if (fx.hasSandstorm()) {
|
|
fx.drawSandstorm(fgDl);
|
|
}
|
|
|
|
// Shader-like post-processing overlay (color wash + vignette)
|
|
if (fx.hasViewportOverlay()) {
|
|
fx.drawViewportOverlay(fgDl);
|
|
}
|
|
}
|
|
|
|
// Apply edge fade mask to content area — soft transparency at top/bottom edges
|
|
// so content doesn't appear sharply clipped at the content area boundary.
|
|
{
|
|
PERF_SCOPE("Render.EdgeFade");
|
|
float caTopEdge = caScreenPos.y;
|
|
float caBottomEdge = caScreenPos.y + contentH;
|
|
// Cache the schema lookup — value only changes on theme reload
|
|
static uint32_t s_fadeGen = 0;
|
|
static float s_fadeZone = 0.0f;
|
|
uint32_t curGen = ui::schema::UI().generation();
|
|
if (curGen != s_fadeGen) {
|
|
s_fadeGen = curGen;
|
|
s_fadeZone = ui::schema::UI().drawElement("components.content-area", "edge-fade-zone").size;
|
|
}
|
|
float fadeZone = s_fadeZone * ui::Layout::dpiScale();
|
|
// fadeZone <= 0 disables the effect entirely
|
|
if (fadeZone > 0.0f) {
|
|
// Pre-compute safe zone — vertices with y fully inside don't need any work
|
|
float safeTop = caTopEdge + fadeZone;
|
|
float safeBot = caBottomEdge - fadeZone;
|
|
|
|
// Lambda to fade a range of vertices in a given draw list
|
|
auto fadeVerts = [&](ImDrawList* dl, int vtxStart, int vtxEnd) {
|
|
for (int vi = vtxStart; vi < vtxEnd; vi++) {
|
|
ImDrawVert& v = dl->VtxBuffer[vi];
|
|
// Skip vertices in the safe zone (the common case)
|
|
if (v.pos.y >= safeTop && v.pos.y <= safeBot)
|
|
continue;
|
|
|
|
float alpha = 1.0f;
|
|
|
|
// Top fade
|
|
float dTop = v.pos.y - caTopEdge;
|
|
if (dTop < fadeZone)
|
|
alpha = std::min(alpha, std::max(0.0f, dTop / fadeZone));
|
|
|
|
// Bottom fade
|
|
float dBot = caBottomEdge - v.pos.y;
|
|
if (dBot < fadeZone)
|
|
alpha = std::min(alpha, std::max(0.0f, dBot / fadeZone));
|
|
|
|
if (alpha < 1.0f) {
|
|
int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF;
|
|
a = static_cast<int>(a * alpha);
|
|
v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Fade content area window draw list (fills, text, borders)
|
|
fadeVerts(caDL, caVtxStart, caDL->VtxBuffer.Size);
|
|
|
|
// Fade ForegroundDrawList panel effects (rainbow border, edge trace,
|
|
// shimmer, specular glare) — but NOT viewport-wide effects (embers,
|
|
// overlay) which were added after fgVtxEnd.
|
|
fadeVerts(fgDL, fgVtxStart, fgVtxEnd);
|
|
}
|
|
}
|
|
|
|
// Page-transition alpha: during page switches page_alpha_ ramps 0→1.
|
|
// ImGuiStyleVar_Alpha only affects widget rendering inside the content
|
|
// child window. ForegroundDrawList effects (per-panel AND viewport-wide)
|
|
// bypass it, so we manually scale their vertex alpha here.
|
|
if (page_alpha_ < 1.0f) {
|
|
int fgEnd = fgDL->VtxBuffer.Size;
|
|
for (int vi = fgVtxStart; vi < fgEnd; vi++) {
|
|
ImDrawVert& v = fgDL->VtxBuffer[vi];
|
|
int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF;
|
|
a = static_cast<int>(a * page_alpha_);
|
|
v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleVar(); // WindowPadding (content area)
|
|
ImGui::PopStyleVar(); // Alpha
|
|
|
|
// Status bar — anchored to the bottom of the main window
|
|
// Use mainPadX/mainPadBot (the actual MainContent window padding) because the
|
|
// style var was already popped and GetStyle().WindowPadding is now wrong.
|
|
{
|
|
PERF_SCOPE("Render.StatusBar");
|
|
float windowBottom = ImGui::GetWindowPos().y + ImGui::GetWindowSize().y;
|
|
float statusY = windowBottom - statusBarH - mainPadBot;
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x + mainPadX, statusY));
|
|
renderStatusBar();
|
|
}
|
|
|
|
// ====================================================================
|
|
// Modal dialogs — rendered INSIDE "MainContent" window scope so that
|
|
// ImGui's modal dim-layer and input blocking covers the sidebar,
|
|
// content area, status bar, and every other widget in this window.
|
|
// ====================================================================
|
|
PERF_BEGIN(_perfDialogs);
|
|
if (show_settings_) {
|
|
ui::RenderSettingsWindow(this, &show_settings_);
|
|
}
|
|
|
|
if (show_about_) {
|
|
ui::RenderAboutDialog(this, &show_about_);
|
|
}
|
|
|
|
if (show_import_key_) {
|
|
renderImportKeyDialog();
|
|
}
|
|
|
|
if (show_export_key_) {
|
|
renderExportKeyDialog();
|
|
}
|
|
|
|
if (show_backup_) {
|
|
renderBackupDialog();
|
|
}
|
|
|
|
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
|
|
// to ensure they appear on top of all other content
|
|
|
|
// Send confirm popup
|
|
ui::RenderSendConfirmPopup(this);
|
|
|
|
// Console RPC Command Reference popup
|
|
console_tab_.renderCommandsPopupModal();
|
|
|
|
// Key export dialog (triggered from balance tab context menu)
|
|
ui::KeyExportDialog::render(this);
|
|
|
|
// Transaction details dialog (triggered from transactions tab)
|
|
ui::TransactionDetailsDialog::render(this);
|
|
|
|
// QR code popup dialog (triggered from balance tab)
|
|
ui::QRPopupDialog::render(this);
|
|
|
|
// Validate address dialog (triggered from Edit menu)
|
|
ui::ValidateAddressDialog::render(this);
|
|
|
|
// Address book dialog (triggered from Edit menu)
|
|
ui::AddressBookDialog::render(this);
|
|
|
|
// Shield/merge dialog (triggered from Wallet menu)
|
|
ui::ShieldDialog::render(this);
|
|
|
|
// Request payment dialog (triggered from Wallet menu)
|
|
ui::RequestPaymentDialog::render(this);
|
|
|
|
// Block info dialog (triggered from View menu)
|
|
ui::BlockInfoDialog::render(this);
|
|
|
|
// Export all keys dialog (triggered from File menu)
|
|
ui::ExportAllKeysDialog::render(this);
|
|
|
|
// Export transactions to CSV dialog (triggered from File menu)
|
|
ui::ExportTransactionsDialog::render(this);
|
|
|
|
// Windows Defender antivirus help dialog
|
|
renderAntivirusHelpDialog();
|
|
|
|
PERF_END("Render.Dialogs", _perfDialogs);
|
|
|
|
ImGui::End();
|
|
|
|
// Debug: ImGui demo window
|
|
if (show_demo_window_) {
|
|
ImGui::ShowDemoWindow(&show_demo_window_);
|
|
}
|
|
|
|
// Security overlay dialogs (must render LAST to be on top of everything)
|
|
renderEncryptWalletDialog();
|
|
renderDecryptWalletDialog();
|
|
renderPinDialogs();
|
|
|
|
// Render notifications (toast messages)
|
|
ui::Notifications::instance().render();
|
|
}
|
|
|
|
void App::renderStatusBar()
|
|
{
|
|
// Status bar layout from unified UI schema
|
|
const auto& S = ui::schema::UI();
|
|
const auto& sbWin = S.window("components.status-bar");
|
|
const float sbHeight = sbWin.height;
|
|
const float sbPadX = sbWin.padding[0];
|
|
const float sbPadY = sbWin.padding[1];
|
|
const float sbIconTextGap = S.drawElement("components.status-bar", "icon-text-gap").size;
|
|
const float sbSectionGap = S.drawElement("components.status-bar", "section-gap").size;
|
|
const float sbSeparatorGap = S.drawElement("components.status-bar", "separator-gap").size;
|
|
|
|
ImGuiWindowFlags childFlags = ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoScrollWithMouse;
|
|
if (ui::material::IsBackdropActive()) {
|
|
childFlags |= ImGuiWindowFlags_NoBackground;
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(sbPadX, sbPadY));
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
|
|
ImGui::BeginChild("##StatusBar", ImVec2(0, sbHeight), false, childFlags);
|
|
|
|
// Use schema-configured font for status bar text (default: body2 = caption+1px)
|
|
auto sbTextStyle = S.label("components.status-bar", "text-style");
|
|
ImFont* sbFont = S.resolveFont(sbTextStyle.font);
|
|
if (!sbFont) sbFont = ui::material::Type().body2();
|
|
ImGui::PushFont(sbFont);
|
|
|
|
// Apply text opacity from schema (default 60%)
|
|
auto sbTextElem = S.drawElement("components.status-bar", "text-style");
|
|
float sbTextOpacity = (sbTextElem.opacity >= 0.0f) ? sbTextElem.opacity : 0.6f;
|
|
ImVec4 baseTextCol = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
|
baseTextCol.w *= sbTextOpacity;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, baseTextCol);
|
|
|
|
// Vertically center text within the status bar below the divider line.
|
|
// sbPadY is already applied by WindowPadding, so the content region starts
|
|
// at wPos.y + sbPadY. We set CursorPosY relative to wPos.y (not content
|
|
// start), so we must account for sbPadY ourselves to avoid placing text
|
|
// above the padded region.
|
|
{
|
|
float fontH = ImGui::GetFont()->LegacySize;
|
|
float topMargin = sbPadY; // match the window padding = space below divider
|
|
float availH = sbHeight - topMargin;
|
|
float centerY = topMargin + (availH - fontH) * 0.5f;
|
|
if (centerY > 0.0f)
|
|
ImGui::SetCursorPosY(centerY);
|
|
}
|
|
{
|
|
ImVec2 wPos = ImGui::GetWindowPos();
|
|
float wWidth = ImGui::GetWindowWidth();
|
|
ImGui::GetWindowDrawList()->AddLine(
|
|
ImVec2(wPos.x, wPos.y),
|
|
ImVec2(wPos.x + wWidth, wPos.y),
|
|
ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 20)), 1.0f);
|
|
}
|
|
|
|
// Connection status
|
|
float dotOpacity = S.drawElement("components.status-bar", "connection-dot").opacity;
|
|
if (dotOpacity < 0.0f) dotOpacity = 1.0f;
|
|
if (state_.connected) {
|
|
ImGui::PushFont(ui::material::Type().iconSmall());
|
|
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, dotOpacity), ICON_MD_CIRCLE);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, sbIconTextGap);
|
|
ImGui::Text("Connected");
|
|
} else {
|
|
ImGui::PushFont(ui::material::Type().iconSmall());
|
|
ImGui::TextColored(ImVec4(0.8f, 0.2f, 0.2f, dotOpacity), ICON_MD_CIRCLE);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, sbIconTextGap);
|
|
ImGui::Text("Disconnected");
|
|
}
|
|
|
|
// Block height
|
|
ImGui::SameLine(0, sbSectionGap);
|
|
ImGui::TextDisabled("|");
|
|
ImGui::SameLine(0, sbSeparatorGap);
|
|
ImGui::Text("Block: %d", state_.sync.blocks);
|
|
|
|
// Sync status or peer count
|
|
ImGui::SameLine(0, sbSectionGap);
|
|
ImGui::TextDisabled("|");
|
|
ImGui::SameLine(0, sbSeparatorGap);
|
|
if (state_.sync.rescanning) {
|
|
// Show rescan progress (takes priority over sync)
|
|
// Use animated dots if progress is unknown (0%)
|
|
if (state_.sync.rescan_progress > 0.01f) {
|
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning %.0f%%",
|
|
state_.sync.rescan_progress * 100.0f);
|
|
} else {
|
|
// Animated "Rescanning..." with pulsing dots
|
|
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
|
|
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
|
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning%s", dotStr);
|
|
}
|
|
} else if (state_.sync.syncing) {
|
|
int blocksLeft = state_.sync.headers - state_.sync.blocks;
|
|
if (blocksLeft < 0) blocksLeft = 0;
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
|
|
state_.sync.verification_progress * 100.0, blocksLeft);
|
|
} else if (state_.connected) {
|
|
ImGui::Text("Peers: %zu", state_.peers.size());
|
|
}
|
|
|
|
// Network hashrate (if connected and have data)
|
|
if (state_.connected && state_.mining.networkHashrate > 0) {
|
|
ImGui::SameLine(0, sbSectionGap);
|
|
ImGui::TextDisabled("|");
|
|
ImGui::SameLine(0, sbSeparatorGap);
|
|
if (state_.mining.networkHashrate >= 1e9) {
|
|
ImGui::Text("Net: %.2f GH/s", state_.mining.networkHashrate / 1e9);
|
|
} else if (state_.mining.networkHashrate >= 1e6) {
|
|
ImGui::Text("Net: %.2f MH/s", state_.mining.networkHashrate / 1e6);
|
|
} else if (state_.mining.networkHashrate >= 1e3) {
|
|
ImGui::Text("Net: %.2f KH/s", state_.mining.networkHashrate / 1e3);
|
|
} else {
|
|
ImGui::Text("Net: %.1f H/s", state_.mining.networkHashrate);
|
|
}
|
|
}
|
|
|
|
// Mining indicator (if mining)
|
|
if (state_.mining.generate) {
|
|
ImGui::SameLine(0, sbSectionGap);
|
|
ImGui::TextDisabled("|");
|
|
ImGui::SameLine(0, sbSeparatorGap);
|
|
ImGui::PushFont(ui::material::Type().iconSmall());
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CONSTRUCTION);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, sbIconTextGap);
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%.1f H/s",
|
|
state_.mining.localHashrate);
|
|
}
|
|
|
|
// Decrypt-import background task indicator
|
|
if (decrypt_import_active_) {
|
|
ImGui::SameLine(0, sbSectionGap);
|
|
ImGui::TextDisabled("|");
|
|
ImGui::SameLine(0, sbSeparatorGap);
|
|
ImGui::PushFont(ui::material::Type().iconSmall());
|
|
float pulse = 0.6f + 0.4f * sinf((float)ImGui::GetTime() * 3.0f);
|
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_LOCK_OPEN);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine(0, sbIconTextGap);
|
|
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
|
|
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
|
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Importing keys%s", dotStr);
|
|
}
|
|
|
|
// Right side: version always at far right, connection status to its left.
|
|
// Compute positions dynamically from actual text widths so they
|
|
// never overlap and always stay within the window at any font scale.
|
|
{
|
|
char versionBuf[32];
|
|
snprintf(versionBuf, sizeof(versionBuf), "v%s", DRAGONX_VERSION);
|
|
float versionW = ImGui::CalcTextSize(versionBuf).x;
|
|
float rightPad = sbPadX; // match the left window padding
|
|
float versionX = ImGui::GetWindowWidth() - versionW - rightPad;
|
|
|
|
// Connection / daemon status sits to the left of the version string
|
|
// with a small gap.
|
|
float gap = sbSectionGap;
|
|
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
|
float statusW = ImGui::CalcTextSize(connection_status_.c_str()).x;
|
|
float statusX = versionX - statusW - gap;
|
|
ImGui::SameLine(statusX);
|
|
ImGui::TextDisabled("%s", connection_status_.c_str());
|
|
} else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) {
|
|
const char* errText = "Daemon not found";
|
|
float statusW = ImGui::CalcTextSize(errText).x;
|
|
float statusX = versionX - statusW - gap;
|
|
ImGui::SameLine(statusX);
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "%s", errText);
|
|
}
|
|
|
|
// Version always at far right
|
|
ImGui::SameLine(versionX);
|
|
ImGui::Text("%s", versionBuf);
|
|
}
|
|
|
|
ImGui::PopStyleColor(1); // Text opacity
|
|
ImGui::PopFont(); // status bar font
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleColor(2); // ChildBg + Border
|
|
ImGui::PopStyleVar(1); // WindowPadding
|
|
}
|
|
|
|
void App::reloadThemeImages(const std::string& bgPath, const std::string& logoPath)
|
|
{
|
|
// Reload background image from the path resolved by the skin system.
|
|
// Each theme specifies its image via [theme] images.background_image in its .toml.
|
|
if (!bgPath.empty()) {
|
|
ImTextureID newTex = 0;
|
|
int w = 0, h = 0;
|
|
bool loaded = false;
|
|
#ifdef _WIN32
|
|
// On Windows, try embedded resource by filename if file load fails
|
|
{
|
|
std::filesystem::path p(bgPath);
|
|
std::string filename = p.filename().string();
|
|
const auto* res = resources::getEmbeddedResource(filename);
|
|
if (res && res->data && res->size > 0) {
|
|
loaded = util::LoadTextureFromMemory(res->data, res->size, &newTex, &w, &h);
|
|
if (loaded)
|
|
DEBUG_LOGF("[App] Loaded theme background from embedded: %s (%dx%d)\n", filename.c_str(), w, h);
|
|
}
|
|
}
|
|
#endif
|
|
if (!loaded) {
|
|
loaded = util::LoadTextureFromFile(bgPath.c_str(), &newTex, &w, &h);
|
|
if (loaded)
|
|
DEBUG_LOGF("[App] Loaded theme background image: %s (%dx%d)\n", bgPath.c_str(), w, h);
|
|
else
|
|
DEBUG_LOGF("[App] Warning: Failed to load theme background: %s\n", bgPath.c_str());
|
|
}
|
|
if (loaded)
|
|
gradient_tex_ = newTex;
|
|
else
|
|
gradient_tex_ = 0; // Clear stale texture when load fails
|
|
} else {
|
|
// No background image specified by theme — clear texture,
|
|
// main loop will fall back to programmatic gradient
|
|
gradient_tex_ = 0;
|
|
}
|
|
|
|
// Reset logo loaded flags — will reload on next render frame
|
|
logo_loaded_ = false;
|
|
logo_tex_ = 0;
|
|
logo_w_ = 0;
|
|
logo_h_ = 0;
|
|
coin_logo_loaded_ = false;
|
|
coin_logo_tex_ = 0;
|
|
coin_logo_w_ = 0;
|
|
coin_logo_h_ = 0;
|
|
}
|
|
|
|
void App::renderAboutDialog()
|
|
{
|
|
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "about");
|
|
auto dlgF = [&](const char* key, float fb) -> float {
|
|
auto it = dlg.extraFloats.find(key);
|
|
return it != dlg.extraFloats.end() ? it->second : fb;
|
|
};
|
|
|
|
if (!ui::material::BeginOverlayDialog("About ObsidianDragon", &show_about_, dlgF("width", 400.0f), 0.94f)) {
|
|
return;
|
|
}
|
|
|
|
ui::material::Type().text(ui::material::TypeStyle::H6, DRAGONX_APP_NAME);
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
|
|
ImGui::Text("Version: %s", DRAGONX_VERSION);
|
|
ImGui::Text("ImGui: %s", IMGUI_VERSION);
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextWrapped("A shielded cryptocurrency wallet for DragonX (DRGX), "
|
|
"built with Dear ImGui for a lightweight, portable experience.");
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Copyright 2024-2026 The Hush Developers");
|
|
ImGui::Text("Released under the GPLv3 License");
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
|
|
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont((int)dlgF("button-font", 1)))) {
|
|
show_about_ = false;
|
|
}
|
|
|
|
ui::material::EndOverlayDialog();
|
|
}
|
|
|
|
void App::renderImportKeyDialog()
|
|
{
|
|
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "import-key");
|
|
auto dlgF = [&](const char* key, float fb) -> float {
|
|
auto it = dlg.extraFloats.find(key);
|
|
return it != dlg.extraFloats.end() ? it->second : fb;
|
|
};
|
|
|
|
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
|
|
return;
|
|
}
|
|
|
|
ui::material::Type().text(ui::material::TypeStyle::H6, "Import Private Key");
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Private Key:");
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (!import_status_.empty()) {
|
|
if (import_success_) {
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", import_status_.c_str());
|
|
} else {
|
|
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
|
|
int btnFont = (int)dlgF("button-font", 1);
|
|
float btnW = dlgF("button-width", 120.0f);
|
|
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
|
std::string key(import_key_input_);
|
|
if (!key.empty()) {
|
|
importPrivateKey(key, [this](bool success, const std::string& msg) {
|
|
import_success_ = success;
|
|
import_status_ = msg;
|
|
if (success) {
|
|
memset(import_key_input_, 0, sizeof(import_key_input_));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
|
show_import_key_ = false;
|
|
import_status_.clear();
|
|
memset(import_key_input_, 0, sizeof(import_key_input_));
|
|
}
|
|
|
|
ui::material::EndOverlayDialog();
|
|
}
|
|
|
|
void App::renderExportKeyDialog()
|
|
{
|
|
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "export-key");
|
|
auto dlgF = [&](const char* key, float fb) -> float {
|
|
auto it = dlg.extraFloats.find(key);
|
|
return it != dlg.extraFloats.end() ? it->second : fb;
|
|
};
|
|
int btnFont = (int)dlgF("button-font", 1);
|
|
|
|
if (!ui::material::BeginOverlayDialog("Export Private Key", &show_export_key_, dlgF("width", 600.0f), 0.94f)) {
|
|
return;
|
|
}
|
|
|
|
ui::material::Type().text(ui::material::TypeStyle::H6, "Export Private Key");
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
|
|
"WARNING: Anyone with this key can spend your coins!");
|
|
ImGui::Spacing();
|
|
|
|
// Address selector
|
|
ImGui::Text("Select Address:");
|
|
std::vector<std::string> all_addrs;
|
|
for (const auto& a : state_.t_addresses) all_addrs.push_back(a.address);
|
|
for (const auto& a : state_.z_addresses) all_addrs.push_back(a.address);
|
|
|
|
int addrFrontLen = (int)dlgF("addr-front-len", 20);
|
|
int addrBackLen = (int)dlgF("addr-back-len", 8);
|
|
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
|
|
for (const auto& addr : all_addrs) {
|
|
bool selected = (export_address_ == addr);
|
|
std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen);
|
|
if (ImGui::Selectable(display.c_str(), selected)) {
|
|
export_address_ = addr;
|
|
export_result_.clear();
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
|
|
if (!export_address_.empty()) {
|
|
exportPrivateKey(export_address_, [this](const std::string& key) {
|
|
export_result_ = key;
|
|
});
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (!export_result_.empty()) {
|
|
ImGui::Text("Private Key:");
|
|
ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(),
|
|
export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly);
|
|
|
|
if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
|
|
ImGui::SetClipboardText(export_result_.c_str());
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
|
|
int closeBtnFont = (int)dlgF("close-button-font", -1);
|
|
if (closeBtnFont < 0) closeBtnFont = btnFont;
|
|
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) {
|
|
show_export_key_ = false;
|
|
export_result_.clear();
|
|
export_address_.clear();
|
|
}
|
|
|
|
ui::material::EndOverlayDialog();
|
|
}
|
|
|
|
void App::renderBackupDialog()
|
|
{
|
|
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "backup");
|
|
auto dlgF = [&](const char* key, float fb) -> float {
|
|
auto it = dlg.extraFloats.find(key);
|
|
return it != dlg.extraFloats.end() ? it->second : fb;
|
|
};
|
|
int btnFont = (int)dlgF("button-font", 1);
|
|
float btnW = dlgF("button-width", 120.0f);
|
|
|
|
if (!ui::material::BeginOverlayDialog("Backup Wallet", &show_backup_, dlgF("width", 500.0f), 0.94f)) {
|
|
return;
|
|
}
|
|
|
|
ui::material::Type().text(ui::material::TypeStyle::H6, "Backup Wallet");
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
ImGui::TextWrapped("Export all private keys to a file. Keep this file secure!");
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Backup File Path:");
|
|
static char backup_path[512] = "dragonx-backup.txt";
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputText("##backuppath", backup_path, sizeof(backup_path));
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (!backup_status_.empty()) {
|
|
if (backup_success_) {
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", backup_status_.c_str());
|
|
} else {
|
|
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", backup_status_.c_str());
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
|
|
if (ui::material::StyledButton("Save Backup", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
|
std::string path(backup_path);
|
|
if (!path.empty()) {
|
|
backupWallet(path, [this](bool success, const std::string& msg) {
|
|
backup_success_ = success;
|
|
backup_status_ = msg;
|
|
});
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
|
show_backup_ = false;
|
|
backup_status_.clear();
|
|
}
|
|
|
|
ui::material::EndOverlayDialog();
|
|
}
|
|
|
|
void App::renderAntivirusHelpDialog()
|
|
{
|
|
#ifdef _WIN32
|
|
if (!pending_antivirus_dialog_) return;
|
|
|
|
if (!ui::material::BeginOverlayDialog("Windows Defender Blocked Miner", &pending_antivirus_dialog_, 560.0f, 0.94f)) {
|
|
return;
|
|
}
|
|
|
|
ui::material::Type().text(ui::material::TypeStyle::H6, "Windows Defender Blocked xmrig");
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
|
|
ImGui::TextWrapped(
|
|
"Mining software is often flagged as potentially unwanted. "
|
|
"Follow these steps to enable pool mining:");
|
|
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 1: Add Exclusion");
|
|
ImGui::BulletText("Open Windows Security > Virus & threat protection");
|
|
ImGui::BulletText("Click Manage settings > Exclusions > Add or remove");
|
|
ImGui::BulletText("Add folder: %%APPDATA%%\\ObsidianDragon\\");
|
|
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 2: Restore from Quarantine (if needed)");
|
|
ImGui::BulletText("Windows Security > Protection history");
|
|
ImGui::BulletText("Find xmrig.exe and click Restore");
|
|
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 3: Restart wallet and try again");
|
|
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
|
ImGui::Separator();
|
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
|
|
|
float btnW = 160.0f;
|
|
if (ui::material::StyledButton("Open Windows Security", ImVec2(btnW, 0))) {
|
|
// Open Windows Security app to the exclusions page
|
|
ShellExecuteA(NULL, "open", "windowsdefender://threat", NULL, NULL, SW_SHOWNORMAL);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ui::material::StyledButton("Close", ImVec2(100.0f, 0))) {
|
|
pending_antivirus_dialog_ = false;
|
|
}
|
|
|
|
ui::material::EndOverlayDialog();
|
|
#endif
|
|
}
|
|
|
|
void App::refreshNow()
|
|
{
|
|
refresh_timer_ = REFRESH_INTERVAL; // Trigger immediate refresh
|
|
transactions_dirty_ = true; // Force transaction list update
|
|
addresses_dirty_ = true; // Force address/balance update
|
|
last_tx_block_height_ = -1; // Reset tx cache
|
|
}
|
|
|
|
void App::handlePaymentURI(const std::string& uri)
|
|
{
|
|
auto payment = util::parsePaymentURI(uri);
|
|
|
|
if (!payment.valid) {
|
|
ui::Notifications::instance().error("Invalid payment URI: " + payment.error);
|
|
return;
|
|
}
|
|
|
|
// Store pending payment
|
|
pending_payment_valid_ = true;
|
|
pending_to_address_ = payment.address;
|
|
pending_amount_ = payment.amount;
|
|
pending_memo_ = payment.memo;
|
|
pending_label_ = payment.label;
|
|
|
|
// Switch to Send page
|
|
current_page_ = ui::NavPage::Send;
|
|
|
|
// Notify user
|
|
std::string msg = "Payment request loaded";
|
|
if (payment.amount > 0) {
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
|
|
msg += buf;
|
|
}
|
|
ui::Notifications::instance().info(msg);
|
|
}
|
|
|
|
void App::setCurrentTab(int tab) {
|
|
// Legacy int-to-NavPage mapping (used by balance_tab context menus)
|
|
static const ui::NavPage kTabMap[] = {
|
|
ui::NavPage::Overview, // 0 = Balance
|
|
ui::NavPage::Send, // 1 = Send
|
|
ui::NavPage::Receive, // 2 = Receive
|
|
ui::NavPage::History, // 3 = Transactions
|
|
ui::NavPage::Mining, // 4 = Mining
|
|
ui::NavPage::Peers, // 5 = Peers
|
|
ui::NavPage::Market, // 6 = Market
|
|
ui::NavPage::Console, // 7 = Console
|
|
ui::NavPage::Settings, // 8 = Settings
|
|
};
|
|
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
|
|
current_page_ = kTabMap[tab];
|
|
}
|
|
|
|
bool App::startEmbeddedDaemon()
|
|
{
|
|
if (!use_embedded_daemon_) {
|
|
DEBUG_LOGF("Embedded daemon disabled, not starting\n");
|
|
return false;
|
|
}
|
|
|
|
// Check if Sapling params exist - try extracting if embedded
|
|
if (!rpc::Connection::verifySaplingParams()) {
|
|
DEBUG_LOGF("Sapling params not found, checking for embedded resources...\n");
|
|
|
|
// Try to extract embedded resources if available
|
|
if (resources::hasEmbeddedResources()) {
|
|
DEBUG_LOGF("Extracting embedded Sapling params...\n");
|
|
daemon_status_ = "Extracting Sapling parameters...";
|
|
resources::extractEmbeddedResources();
|
|
|
|
// Check again after extraction
|
|
if (!rpc::Connection::verifySaplingParams()) {
|
|
daemon_status_ = "Failed to extract Sapling parameters.";
|
|
DEBUG_LOGF("Sapling params still not found after extraction!\n");
|
|
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
|
|
return false;
|
|
}
|
|
DEBUG_LOGF("Sapling params extracted successfully\n");
|
|
} else {
|
|
daemon_status_ = "Sapling parameters not found. They should be in: " + rpc::Connection::getSaplingParamsDir();
|
|
DEBUG_LOGF("Sapling params not found and no embedded resources available!\n");
|
|
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Create daemon manager if needed
|
|
if (!embedded_daemon_) {
|
|
embedded_daemon_ = std::make_unique<daemon::EmbeddedDaemon>();
|
|
|
|
// Set up state callback
|
|
embedded_daemon_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) {
|
|
switch (state) {
|
|
case daemon::EmbeddedDaemon::State::Starting:
|
|
daemon_status_ = "Starting dragonxd...";
|
|
break;
|
|
case daemon::EmbeddedDaemon::State::Running:
|
|
daemon_status_ = "dragonxd running";
|
|
break;
|
|
case daemon::EmbeddedDaemon::State::Stopping:
|
|
daemon_status_ = "Stopping dragonxd...";
|
|
break;
|
|
case daemon::EmbeddedDaemon::State::Stopped:
|
|
daemon_status_ = "dragonxd stopped";
|
|
break;
|
|
case daemon::EmbeddedDaemon::State::Error:
|
|
daemon_status_ = "Error: " + msg;
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Sync debug logging categories from user settings
|
|
if (settings_) {
|
|
embedded_daemon_->setDebugCategories(settings_->getDebugCategories());
|
|
}
|
|
|
|
return embedded_daemon_->start();
|
|
}
|
|
|
|
void App::stopEmbeddedDaemon()
|
|
{
|
|
if (!embedded_daemon_) return;
|
|
|
|
// Never stop an external daemon unless the user explicitly opted in
|
|
// via the "Stop external daemon" checkbox in Settings. This is a
|
|
// defence-in-depth guard — callers should also check, but this
|
|
// ensures no code path accidentally shuts down a daemon we don't own.
|
|
if (embedded_daemon_->externalDaemonDetected() &&
|
|
!(settings_ && settings_->getStopExternalDaemon())) {
|
|
DEBUG_LOGF("stopEmbeddedDaemon: external daemon detected — "
|
|
"skipping (stop_external_daemon setting is off)\n");
|
|
return;
|
|
}
|
|
|
|
// Send RPC "stop" command — this is the graceful path that lets the
|
|
// daemon flush state, save block indexes, close sockets, etc.
|
|
bool stop_sent = false;
|
|
|
|
// Try the existing RPC connection first
|
|
if (rpc_ && rpc_->isConnected()) {
|
|
DEBUG_LOGF("Sending stop command via existing RPC connection...\n");
|
|
try {
|
|
rpc_->stop([](const json&) {
|
|
DEBUG_LOGF("Stop command acknowledged by daemon\n");
|
|
});
|
|
stop_sent = true;
|
|
} catch (...) {
|
|
DEBUG_LOGF("Failed to send stop via existing connection\n");
|
|
}
|
|
}
|
|
|
|
// If the main connection wasn't established (e.g. daemon was still
|
|
// starting up when user closed the window), create a temporary
|
|
// RPC connection just to send the stop command.
|
|
if (!stop_sent) {
|
|
DEBUG_LOGF("Main RPC not connected — creating temporary connection for stop...\n");
|
|
auto config = rpc::Connection::autoDetectConfig();
|
|
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
|
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
|
// Use a short timeout — if daemon isn't listening yet, don't block
|
|
if (tmp_rpc->connect(config.host, config.port,
|
|
config.rpcuser, config.rpcpassword)) {
|
|
DEBUG_LOGF("Temporary RPC connected, sending stop...\n");
|
|
try {
|
|
tmp_rpc->call("stop");
|
|
stop_sent = true;
|
|
DEBUG_LOGF("Stop command sent via temporary connection\n");
|
|
} catch (...) {
|
|
DEBUG_LOGF("Stop RPC failed via temporary connection\n");
|
|
}
|
|
tmp_rpc->disconnect();
|
|
} else {
|
|
DEBUG_LOGF("Could not establish temporary RPC connection\n");
|
|
}
|
|
} else {
|
|
DEBUG_LOGF("No RPC credentials available (DRAGONX.conf missing?)\n");
|
|
}
|
|
}
|
|
|
|
if (stop_sent) {
|
|
DEBUG_LOGF("Waiting for daemon to begin shutdown...\n");
|
|
shutdown_status_ = "Waiting for daemon to begin shutdown...";
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
|
|
}
|
|
|
|
// Wait for process to exit; SIGTERM/TerminateProcess as last resort
|
|
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
|
embedded_daemon_->stop(30000);
|
|
}
|
|
|
|
bool App::isEmbeddedDaemonRunning() const
|
|
{
|
|
return embedded_daemon_ && embedded_daemon_->isRunning();
|
|
}
|
|
|
|
void App::rescanBlockchain()
|
|
{
|
|
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
|
|
ui::Notifications::instance().warning(
|
|
"Rescan requires embedded daemon. Restart your daemon with -rescan manually.");
|
|
return;
|
|
}
|
|
|
|
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
|
|
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
|
|
|
|
// Initialize rescan state for status bar display
|
|
state_.sync.rescanning = true;
|
|
state_.sync.rescan_progress = 0.0f;
|
|
state_.sync.rescan_status = "Starting rescan...";
|
|
|
|
// Set rescan flag BEFORE stopping so it's ready when we restart
|
|
embedded_daemon_->setRescanOnNextStart(true);
|
|
DEBUG_LOGF("[App] Rescan flag set, rescanOnNextStart=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
|
|
|
|
// Stop daemon, then restart
|
|
std::thread([this]() {
|
|
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
|
|
stopEmbeddedDaemon();
|
|
|
|
// Wait for daemon to fully stop
|
|
DEBUG_LOGF("[App] Waiting for daemon to fully stop...\n");
|
|
std::this_thread::sleep_for(std::chrono::seconds(3));
|
|
|
|
// Reset output offset so we parse fresh output for rescan progress
|
|
daemon_output_offset_ = 0;
|
|
|
|
DEBUG_LOGF("[App] Starting daemon with rescan flag=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
|
|
startEmbeddedDaemon();
|
|
}).detach();
|
|
}
|
|
|
|
double App::getDaemonMemoryUsageMB() const
|
|
{
|
|
// If we have an embedded daemon with a tracked process handle, use it
|
|
// directly — more reliable than a process scan since we own the handle.
|
|
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
|
double mb = embedded_daemon_->getMemoryUsageMB();
|
|
daemon_mem_diag_ = "embedded";
|
|
if (mb > 0.0) return mb;
|
|
} else {
|
|
daemon_mem_diag_ = "process scan";
|
|
}
|
|
// Fall back to platform-level process scan (external daemon)
|
|
return util::Platform::getDaemonMemoryUsageMB();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Shutdown
|
|
// ============================================================================
|
|
|
|
void App::requestQuit()
|
|
{
|
|
beginShutdown();
|
|
}
|
|
|
|
void App::beginShutdown()
|
|
{
|
|
// Only start shutdown once
|
|
if (shutting_down_) return;
|
|
shutting_down_ = true;
|
|
quit_requested_ = true;
|
|
shutdown_timer_ = 0.0f;
|
|
shutdown_start_time_ = std::chrono::steady_clock::now();
|
|
|
|
// Signal the RPC worker to stop accepting new tasks (non-blocking).
|
|
// The actual thread join + rpc disconnect happen in shutdown() after
|
|
// the render loop exits, so the UI stays responsive.
|
|
if (worker_) {
|
|
worker_->requestStop();
|
|
}
|
|
if (fast_worker_) {
|
|
fast_worker_->requestStop();
|
|
}
|
|
|
|
// Stop xmrig pool miner before stopping the daemon
|
|
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
|
|
shutdown_status_ = "Stopping pool miner...";
|
|
xmrig_manager_->stop(3000);
|
|
}
|
|
|
|
DEBUG_LOGF("beginShutdown: starting (embedded_daemon_=%s)\n",
|
|
embedded_daemon_ ? "yes" : "no");
|
|
|
|
// If no embedded daemon, just mark done — don't stop
|
|
// an externally-managed daemon that the user started themselves.
|
|
// Worker join + RPC disconnect happen in shutdown().
|
|
if (!embedded_daemon_) {
|
|
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
|
|
shutdown_status_ = "Disconnecting...";
|
|
if (settings_) {
|
|
settings_->save();
|
|
}
|
|
shutdown_complete_ = true;
|
|
return;
|
|
}
|
|
|
|
// Save settings now (safe to do on main thread)
|
|
if (settings_) {
|
|
settings_->save();
|
|
}
|
|
|
|
// If user opted to keep daemon running, just mark done.
|
|
// Also never stop an external daemon the user started themselves,
|
|
// unless they've explicitly enabled the "stop external daemon" setting.
|
|
bool externalDaemon = embedded_daemon_ && embedded_daemon_->externalDaemonDetected();
|
|
if ((settings_ && settings_->getKeepDaemonRunning()) ||
|
|
(externalDaemon && !(settings_ && settings_->getStopExternalDaemon()))) {
|
|
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n",
|
|
externalDaemon ? "external daemon (not ours to stop)"
|
|
: "keep_daemon_running enabled");
|
|
shutdown_status_ = "Disconnecting (daemon stays running)...";
|
|
shutdown_complete_ = true;
|
|
return;
|
|
}
|
|
|
|
shutdown_status_ = "Sending stop command to daemon...";
|
|
DEBUG_LOGF("beginShutdown: spawning shutdown thread for daemon stop\n");
|
|
|
|
// Run the daemon shutdown on a background thread so the UI
|
|
// keeps rendering the shutdown screen (like SilentDragonX's
|
|
// modal "Please wait" dialog).
|
|
shutdown_thread_ = std::thread([this]() {
|
|
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
|
|
shutdown_status_ = "Sending stop command to daemon...";
|
|
// Send RPC stop command
|
|
stopEmbeddedDaemon();
|
|
|
|
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
|
|
shutdown_status_ = "Cleaning up...";
|
|
|
|
DEBUG_LOGF("shutdown thread: complete\n");
|
|
shutdown_status_ = "Shutdown complete";
|
|
shutdown_complete_ = true;
|
|
});
|
|
}
|
|
|
|
void App::renderShutdownScreen()
|
|
{
|
|
using namespace ui::material;
|
|
auto shutElem = [](const char* key, float fb) {
|
|
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
|
|
return v >= 0 ? v : fb;
|
|
};
|
|
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
|
|
|
// Use the main viewport so the overlay covers the primary window
|
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
|
ImVec2 vp_pos = vp->Pos;
|
|
ImVec2 vp_size = vp->Size;
|
|
float cx = vp_size.x * 0.5f; // horizontal centre (local coords)
|
|
|
|
// Semi-transparent dark overlay covering the entire main window
|
|
ImGui::SetNextWindowPos(vp_pos);
|
|
ImGui::SetNextWindowSize(vp_size);
|
|
ImGui::SetNextWindowFocus();
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
ImGui::Begin("##ShutdownOverlay", nullptr,
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoSavedSettings);
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
// Convert local centre to screen coords for draw-list primitives
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
constexpr float kPi = 3.14159265f;
|
|
|
|
// -------------------------------------------------------------------
|
|
// Vertical centering: estimate total content height
|
|
// -------------------------------------------------------------------
|
|
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
|
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
|
|
float spinnerD = shutElem("spinner-radius", 20.0f) * 2.0f + 12.0f;
|
|
float statusH = lineH * 2.0f;
|
|
float sepH = lineH;
|
|
float panelH = shutElem("panel-max-height", 160.0f);
|
|
float totalH = titleH + spinnerD + statusH + sepH + panelH + lineH * 4.0f;
|
|
float y_start = (vp_size.y - totalH) * 0.5f;
|
|
if (y_start < lineH * 2.0f) y_start = lineH * 2.0f;
|
|
ImGui::SetCursorPosY(y_start);
|
|
|
|
// -------------------------------------------------------------------
|
|
// 1. Title — large, gold/amber
|
|
// -------------------------------------------------------------------
|
|
{
|
|
const char* title = "Shutting Down";
|
|
ImGui::PushFont(Type().h5());
|
|
ImVec2 ts = ImGui::CalcTextSize(title);
|
|
ImGui::SetCursorPosX(cx - ts.x * 0.5f);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
|
ImGui::TextUnformatted(title);
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
|
|
// -------------------------------------------------------------------
|
|
// 2. Animated arc spinner
|
|
// -------------------------------------------------------------------
|
|
{
|
|
float r = shutElem("spinner-radius", 20.0f);
|
|
float thick = shutElem("spinner-thickness", 3.0f);
|
|
// Screen-space centre for draw list
|
|
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f);
|
|
|
|
// Background ring (dim)
|
|
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
|
|
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-track)", IM_COL32(255, 255, 255, 25)), 0, thick);
|
|
|
|
// Spinning foreground arc (~270°)
|
|
float angle = shutdown_timer_ * shutElem("spinner-speed", 2.5f);
|
|
float a_min = angle;
|
|
float a_max = angle + kPi * 1.5f;
|
|
dl->PathArcTo(sc, r, a_min, a_max, 36);
|
|
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
|
|
|
|
// Advance cursor past the spinner
|
|
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// -------------------------------------------------------------------
|
|
// 3. Phase status (what the shutdown thread is doing)
|
|
// -------------------------------------------------------------------
|
|
if (!shutdown_status_.empty()) {
|
|
ImVec2 ts = ImGui::CalcTextSize(shutdown_status_.c_str());
|
|
ImGui::SetCursorPosX(cx - ts.x * 0.5f);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.0f));
|
|
ImGui::TextUnformatted(shutdown_status_.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// -------------------------------------------------------------------
|
|
// 4. Elapsed time — small, dim
|
|
// -------------------------------------------------------------------
|
|
{
|
|
char elapsed[64];
|
|
int secs = (int)shutdown_timer_;
|
|
if (secs < 60)
|
|
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs);
|
|
else
|
|
snprintf(elapsed, sizeof(elapsed), "%d min %d sec", secs / 60, secs % 60);
|
|
|
|
ImGui::PushFont(Type().caption());
|
|
ImVec2 ts = ImGui::CalcTextSize(elapsed);
|
|
ImGui::SetCursorPosX(cx - ts.x * 0.5f);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.45f, 1.0f));
|
|
ImGui::TextUnformatted(elapsed);
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
|
|
// -------------------------------------------------------------------
|
|
// 5. Separator line
|
|
// -------------------------------------------------------------------
|
|
{
|
|
float pad = vp_size.x * shutElem("separator-pad-fraction", 0.25f);
|
|
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
|
|
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
|
|
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
|
|
ImGui::Dummy(ImVec2(0, 4.0f));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// -------------------------------------------------------------------
|
|
// 6. Daemon output panel — terminal-style box
|
|
// -------------------------------------------------------------------
|
|
if (embedded_daemon_) {
|
|
auto lines = embedded_daemon_->getRecentLines(8);
|
|
if (!lines.empty()) {
|
|
float panelW = vp_size.x * shutElem("panel-width-fraction", 0.70f);
|
|
float panelX = cx - panelW * 0.5f;
|
|
float panelH = shutElem("panel-max-height", 160.0f);
|
|
|
|
// Panel background (dark, rounded)
|
|
ImVec2 panelMin(wp.x + panelX, wp.y + ImGui::GetCursorPosY());
|
|
ImVec2 panelMax(panelMin.x + panelW, panelMin.y + panelH);
|
|
dl->AddRectFilled(panelMin, panelMax,
|
|
ui::schema::UI().resolveColor("var(--shutdown-panel-bg)", IM_COL32(12, 14, 20, 220)), shutElem("panel-rounding", 6.0f));
|
|
dl->AddRect(panelMin, panelMax,
|
|
ui::schema::UI().resolveColor("var(--shutdown-panel-border)", IM_COL32(255, 255, 255, 18)), shutElem("panel-rounding", 6.0f), 0, 1.0f);
|
|
|
|
// Label above lines
|
|
float panelPad = shutElem("panel-padding", 12.0f);
|
|
ImGui::SetCursorPos(ImVec2(panelX + panelPad,
|
|
ImGui::GetCursorPosY() + panelPad));
|
|
ImGui::PushFont(Type().caption());
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
|
|
ImGui::TextUnformatted("dragonxd output");
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Output lines in green — clip to panel bounds
|
|
float leftPad = panelX + panelPad;
|
|
float maxTextW = panelW - panelPad * 2.0f;
|
|
ImGui::PushClipRect(panelMin, panelMax, true);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.78f, 0.4f, 0.9f));
|
|
ImGui::PushFont(Type().caption());
|
|
for (const auto& line : lines) {
|
|
// Stop rendering if we've reached near the panel bottom
|
|
float curY = wp.y + ImGui::GetCursorPosY();
|
|
if (curY + ImGui::GetTextLineHeight() > panelMax.y - panelPad)
|
|
break;
|
|
|
|
ImGui::SetCursorPosX(leftPad);
|
|
// Truncate if wider than panel
|
|
ImVec2 ts = ImGui::CalcTextSize(line.c_str());
|
|
if (ts.x > maxTextW && line.size() > 10) {
|
|
// Binary search for fit is overkill; just trim chars
|
|
size_t maxChars = (size_t)(line.size() * (maxTextW / ts.x));
|
|
if (maxChars > 3) {
|
|
std::string trunc = line.substr(0, maxChars - 3) + "...";
|
|
ImGui::TextUnformatted(trunc.c_str());
|
|
} else {
|
|
ImGui::TextUnformatted("...");
|
|
}
|
|
} else {
|
|
ImGui::TextUnformatted(line.c_str());
|
|
}
|
|
}
|
|
ImGui::PopFont();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopClipRect();
|
|
|
|
// Advance cursor past the panel
|
|
float panelBottom = panelMax.y - wp.y;
|
|
ImGui::SetCursorPosY(panelBottom + 4.0f);
|
|
ImGui::Dummy(ImVec2(0, 0));
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleVar(2);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Loading Overlay — shown inside content area while daemon is starting/syncing
|
|
// ============================================================================
|
|
|
|
void App::renderLoadingOverlay(float contentH)
|
|
{
|
|
using namespace ui::material;
|
|
constexpr float kPi = 3.14159265f;
|
|
|
|
auto loadElem = [](const char* key, float fb) {
|
|
float v = ui::schema::UI().drawElement("screens.loading", key).size;
|
|
return v >= 0 ? v : fb;
|
|
};
|
|
|
|
loading_timer_ += ImGui::GetIO().DeltaTime;
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
ImVec2 ws = ImGui::GetWindowSize();
|
|
|
|
// Layout constants
|
|
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
|
float spinnerR = loadElem("spinner-radius", 18.0f);
|
|
float gap = loadElem("vertical-gap", 8.0f);
|
|
float barH = loadElem("progress-bar", 6.0f);
|
|
float barW = loadElem("progress-width", 260.0f);
|
|
float cx = ws.x * 0.5f; // centre X (local coords)
|
|
|
|
// Estimate total block height for vertical centering
|
|
float totalH = spinnerR * 2.0f + gap + lineH * 2.0f + gap + barH + gap + lineH;
|
|
float yOff = (ws.y - totalH) * 0.5f;
|
|
if (yOff < lineH * 2.0f) yOff = lineH * 2.0f;
|
|
|
|
// Screen-space Y cursor (absolute)
|
|
float curY = wp.y + yOff;
|
|
|
|
// -------------------------------------------------------------------
|
|
// 1. Animated arc spinner
|
|
// -------------------------------------------------------------------
|
|
{
|
|
float r = spinnerR;
|
|
float thick = loadElem("spinner-thickness", 2.5f);
|
|
ImVec2 sc(wp.x + cx, curY + r + 2.0f);
|
|
|
|
// Background ring (dim)
|
|
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
|
|
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-track)",
|
|
IM_COL32(255, 255, 255, 25)), 0, thick);
|
|
|
|
// Spinning foreground arc (~270°)
|
|
float angle = loading_timer_ * loadElem("spinner-speed", 2.5f);
|
|
dl->PathArcTo(sc, r, angle, angle + kPi * 1.5f, 36);
|
|
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)",
|
|
IM_COL32(255, 218, 0, 200)), 0, thick);
|
|
|
|
curY += r * 2.0f + gap + 4.0f;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// 2. Connection / daemon status text
|
|
// -------------------------------------------------------------------
|
|
{
|
|
const char* statusText = connection_status_.c_str();
|
|
ImFont* font = Type().subtitle1();
|
|
if (!font) font = ImGui::GetFont();
|
|
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0.0f, statusText);
|
|
dl->AddText(font, font->LegacySize,
|
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
|
IM_COL32(220, 220, 220, 255), statusText);
|
|
curY += ts.y + gap;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// 3. Sync progress bar (if connected and syncing)
|
|
// -------------------------------------------------------------------
|
|
if (state_.connected && state_.sync.syncing) {
|
|
float progress = static_cast<float>(state_.sync.verification_progress);
|
|
float barRadius = loadElem("progress-bar", 3.0f);
|
|
|
|
float barX = wp.x + cx - barW * 0.5f;
|
|
ImVec2 barMin(barX, curY);
|
|
ImVec2 barMax(barX + barW, curY + barH);
|
|
|
|
// Track background
|
|
dl->AddRectFilled(barMin, barMax,
|
|
ui::schema::UI().resolveColor("var(--progress-track)",
|
|
IM_COL32(255, 255, 255, 30)), barRadius);
|
|
|
|
// Filled portion
|
|
ImVec2 fillMax(barMin.x + barW * progress, barMax.y);
|
|
if (fillMax.x > barMin.x + 1.0f) {
|
|
dl->AddRectFilled(barMin, fillMax,
|
|
ui::schema::UI().resolveColor("var(--primary)",
|
|
IM_COL32(255, 218, 0, 200)), barRadius);
|
|
}
|
|
|
|
curY += barH + gap;
|
|
|
|
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
|
|
char syncBuf[128];
|
|
snprintf(syncBuf, sizeof(syncBuf), "Syncing %.1f%% — Block %d / %d",
|
|
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
|
|
ImFont* capFont = Type().caption();
|
|
if (!capFont) capFont = ImGui::GetFont();
|
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, syncBuf);
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
|
IM_COL32(150, 150, 150, 255), syncBuf);
|
|
curY += ts.y + gap;
|
|
} else if (!state_.connected && state_.sync.blocks > 0) {
|
|
// Show last known block height while reconnecting
|
|
char blockBuf[64];
|
|
snprintf(blockBuf, sizeof(blockBuf), "Last block: %d", state_.sync.blocks);
|
|
ImFont* capFont = Type().caption();
|
|
if (!capFont) capFont = ImGui::GetFont();
|
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
|
IM_COL32(130, 130, 130, 255), blockBuf);
|
|
curY += ts.y + gap;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// 3b. Deferred encryption status
|
|
// -------------------------------------------------------------------
|
|
if (deferred_encrypt_pending_) {
|
|
curY += gap;
|
|
ImFont* capFont = Type().caption();
|
|
if (!capFont) capFont = ImGui::GetFont();
|
|
|
|
const char* encLabel = encrypt_in_progress_
|
|
? "Encrypting wallet..."
|
|
: "Waiting for daemon to encrypt wallet...";
|
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
|
|
ImU32 encCol = IM_COL32(255, 218, 0, 200);
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY), encCol, encLabel);
|
|
curY += ts.y + gap * 0.5f;
|
|
|
|
// Indeterminate progress bar
|
|
float encBarW = barW * 0.6f;
|
|
float encBarH = 4.0f;
|
|
float encBarX = wp.x + cx - encBarW * 0.5f;
|
|
dl->AddRectFilled(ImVec2(encBarX, curY), ImVec2(encBarX + encBarW, curY + encBarH),
|
|
IM_COL32(255, 255, 255, 20), 2.0f);
|
|
float t = loading_timer_;
|
|
float pulse = 0.5f + 0.5f * sinf(t * 2.0f);
|
|
float segW = encBarW * 0.3f;
|
|
float segX = encBarX + (encBarW - segW) * pulse;
|
|
dl->AddRectFilled(ImVec2(segX, curY), ImVec2(segX + segW, curY + encBarH),
|
|
encCol, 2.0f);
|
|
curY += encBarH + gap;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// 3c. Daemon crash error message
|
|
// -------------------------------------------------------------------
|
|
if (embedded_daemon_ &&
|
|
embedded_daemon_->getState() == daemon::EmbeddedDaemon::State::Error) {
|
|
curY += gap;
|
|
ImFont* capFont = Type().caption();
|
|
if (!capFont) capFont = ImGui::GetFont();
|
|
ImFont* bodyFont2 = Type().body2();
|
|
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
|
|
|
|
// Error title
|
|
const char* errTitle = "Daemon Error";
|
|
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
|
|
dl->AddText(bodyFont2, bodyFont2->LegacySize,
|
|
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
|
IM_COL32(255, 90, 90, 255), errTitle);
|
|
curY += ts.y + gap * 0.5f;
|
|
|
|
// Error details (wrapped) — show full diagnostic info
|
|
const std::string& errDetail = embedded_daemon_->getLastError();
|
|
if (!errDetail.empty()) {
|
|
float wrapW = ws.x * 0.8f;
|
|
if (wrapW > 700.0f) wrapW = 700.0f;
|
|
ImVec2 es = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, wrapW, errDetail.c_str());
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(wp.x + cx - wrapW * 0.5f, curY),
|
|
IM_COL32(255, 180, 180, 220), errDetail.c_str(), nullptr, wrapW);
|
|
curY += es.y + gap;
|
|
}
|
|
|
|
// Crash count hint
|
|
if (embedded_daemon_->getCrashCount() >= 3) {
|
|
const char* hint = "Use Settings > Restart Daemon to try again";
|
|
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
|
|
IM_COL32(200, 200, 200, 180), hint);
|
|
curY += hs2.y + gap;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// 4. Daemon output snippet (last few lines, if embedded)
|
|
// -------------------------------------------------------------------
|
|
if (embedded_daemon_) {
|
|
auto lines = embedded_daemon_->getRecentLines(8);
|
|
if (!lines.empty()) {
|
|
curY += gap;
|
|
|
|
float panelW = ws.x * 0.85f;
|
|
if (panelW > 900.0f) panelW = 900.0f;
|
|
float panelX = wp.x + cx - panelW * 0.5f;
|
|
float panelPad = 8.0f;
|
|
|
|
ImFont* capFont = Type().caption();
|
|
if (!capFont) capFont = ImGui::GetFont();
|
|
float panelLineH = capFont->LegacySize + 4.0f;
|
|
float panelContentH = panelPad * 2.0f + panelLineH * (float)lines.size();
|
|
|
|
ImVec2 panelMin(panelX, curY);
|
|
ImVec2 panelMax(panelX + panelW, curY + panelContentH);
|
|
|
|
dl->AddRectFilled(panelMin, panelMax,
|
|
ui::schema::UI().resolveColor("var(--shutdown-panel-bg)",
|
|
IM_COL32(12, 14, 20, 180)), 6.0f);
|
|
dl->AddRect(panelMin, panelMax,
|
|
ui::schema::UI().resolveColor("var(--shutdown-panel-border)",
|
|
IM_COL32(255, 255, 255, 15)), 6.0f, 0, 1.0f);
|
|
|
|
float textY = curY + panelPad;
|
|
float maxTextW = panelW - panelPad * 2.0f;
|
|
ImU32 textCol = IM_COL32(102, 199, 102, 217); // green tint
|
|
|
|
for (const auto& line : lines) {
|
|
std::string display = line;
|
|
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, display.c_str());
|
|
if (ts.x > maxTextW && display.size() > 10) {
|
|
size_t maxChars = (size_t)(display.size() * (maxTextW / ts.x));
|
|
if (maxChars > 3) display = display.substr(0, maxChars - 3) + "...";
|
|
else display = "...";
|
|
}
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(panelX + panelPad, textY),
|
|
textCol, display.c_str());
|
|
textY += panelLineH;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void App::shutdown()
|
|
{
|
|
// Clean up bootstrap if running
|
|
if (bootstrap_) {
|
|
bootstrap_->cancel();
|
|
bootstrap_.reset();
|
|
}
|
|
|
|
// If beginShutdown() was never called (e.g. direct exit),
|
|
// do synchronous shutdown as fallback.
|
|
if (!shutting_down_) {
|
|
DEBUG_LOGF("Synchronous shutdown fallback...\n");
|
|
if (worker_) {
|
|
worker_->stop();
|
|
}
|
|
if (fast_worker_) {
|
|
fast_worker_->stop();
|
|
}
|
|
if (settings_) {
|
|
settings_->save();
|
|
}
|
|
if (embedded_daemon_) {
|
|
stopEmbeddedDaemon();
|
|
}
|
|
if (rpc_) {
|
|
rpc_->disconnect();
|
|
}
|
|
if (fast_rpc_) {
|
|
fast_rpc_->disconnect();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Wait for the async shutdown thread to finish
|
|
if (shutdown_thread_.joinable()) {
|
|
shutdown_thread_.join();
|
|
}
|
|
// Wait for wizard's external daemon stop thread
|
|
if (wizard_stop_thread_.joinable()) {
|
|
wizard_stop_thread_.join();
|
|
}
|
|
// Wait for daemon restart thread
|
|
if (daemon_restart_thread_.joinable()) {
|
|
daemon_restart_thread_.join();
|
|
}
|
|
// Join the RPC worker thread (was signaled in beginShutdown via requestStop)
|
|
if (worker_) {
|
|
worker_->stop();
|
|
}
|
|
if (fast_worker_) {
|
|
fast_worker_->stop();
|
|
}
|
|
// Disconnect RPC after worker is fully stopped (safe — no curl in flight)
|
|
if (rpc_) {
|
|
rpc_->disconnect();
|
|
}
|
|
if (fast_rpc_) {
|
|
fast_rpc_->disconnect();
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// First-run detection
|
|
// ===========================================================================
|
|
|
|
bool App::isFirstRun() const {
|
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
|
std::error_code ec;
|
|
return !std::filesystem::exists(dataDir, ec) ||
|
|
!std::filesystem::exists(std::filesystem::path(dataDir) / "blocks", ec);
|
|
}
|
|
|
|
bool App::hasPinVault() const {
|
|
return vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled();
|
|
}
|
|
|
|
bool App::hasPendingRPCResults() const {
|
|
return (worker_ && worker_->hasPendingResults())
|
|
|| (fast_worker_ && fast_worker_->hasPendingResults());
|
|
}
|
|
void App::restartDaemon()
|
|
{
|
|
if (!use_embedded_daemon_ || daemon_restarting_.load()) return;
|
|
daemon_restarting_ = true;
|
|
|
|
// Reset crash counter on manual restart
|
|
if (embedded_daemon_) {
|
|
embedded_daemon_->resetCrashCount();
|
|
}
|
|
|
|
DEBUG_LOGF("[App] Restarting embedded daemon...\n");
|
|
connection_status_ = "Restarting daemon...";
|
|
|
|
// Disconnect RPC so the loading overlay appears
|
|
if (rpc_ && rpc_->isConnected()) {
|
|
rpc_->disconnect();
|
|
}
|
|
onDisconnected("Daemon restart");
|
|
|
|
// Sync debug categories from settings to daemon
|
|
if (embedded_daemon_ && settings_) {
|
|
embedded_daemon_->setDebugCategories(settings_->getDebugCategories());
|
|
}
|
|
|
|
// Run stop + start on a background thread to avoid blocking the UI.
|
|
// The 5-second auto-retry in render() will reconnect once the daemon
|
|
// is back up.
|
|
if (daemon_restart_thread_.joinable()) {
|
|
daemon_restart_thread_.join();
|
|
}
|
|
daemon_restart_thread_ = std::thread([this]() {
|
|
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
|
|
stopEmbeddedDaemon();
|
|
}
|
|
// Brief pause to let the port free up
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
startEmbeddedDaemon();
|
|
daemon_restarting_ = false;
|
|
DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n");
|
|
});
|
|
}
|
|
|
|
} // namespace dragonx
|