// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // app.cpp — Main application: init, shutdown, ImGui render loop, NavPage // dispatch, dialog rendering, and frame-level state management. #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 "wallet/lite_wallet_controller.h" #include "wallet/lite_wallet_server_selection_adapter.h" #include "wallet/lite_rollout_policy.h" #include // std::getenv for the lite kill-switch env var #include // sodium_memzero for the lite unlock-passphrase buffer #include "daemon/daemon_controller.h" #include "daemon/embedded_daemon.h" #include "daemon/lifecycle_adapters.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/network_tab.h" #include "ui/windows/explorer_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/address_label_dialog.h" #include "ui/windows/address_transfer_dialog.h" #include "ui/windows/bootstrap_download_dialog.h" #include "ui/windows/xmrig_download_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 #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include "util/logger.h" #endif namespace dragonx { using json = nlohmann::json; App::App() = default; App::~App() = default; bool App::sendStopCommandSafely(rpc::RPCClient& client, const char* context) { const char* label = context ? context : "App"; try { rpc::RPCClient::TraceScope trace("Daemon lifecycle / Stop command"); client.call("stop"); DEBUG_LOGF("[%s] Stop command sent\n", label); return true; } catch (const std::exception& e) { DEBUG_LOGF("[%s] Stop RPC failed: %s\n", label, e.what()); } catch (...) { DEBUG_LOGF("[%s] Stop RPC failed with unknown exception\n", label); } return false; } class AppDaemonLifecycleRuntime final : public daemon::DaemonController::LifecycleRuntime { public: explicit AppDaemonLifecycleRuntime(App& app) : app_(app) {} void stopDaemonWithPolicy() override { app_.stopEmbeddedDaemon(); } bool startDaemon() override { return app_.startEmbeddedDaemon(); } void resetOutputOffset() override { app_.daemon_output_offset_ = 0; } void requestRpcStopAndDisconnect(const char* context, const char* reason) override { if (app_.rpc_ && app_.rpc_->isConnected()) { app_.sendStopCommandSafely(*app_.rpc_, context); app_.rpc_->disconnect(); } app_.onDisconnected(reason ? reason : "Daemon lifecycle"); } int deleteBlockchainData() override { return daemon::BlockchainDataCleaner::removeBlockchainData(util::Platform::getDragonXDataDir()); } private: App& app_; }; 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(); if (!settings_->load()) { DEBUG_LOGF("Warning: Could not load settings, using defaults\n"); } // On upgrade (version mismatch), re-save to persist new defaults + current version if (settings_->needsUpgradeSave()) { DEBUG_LOGF("[INFO] Wallet upgraded — re-saving settings with new defaults\n"); settings_->save(); settings_->clearUpgradeSave(); } // Lite builds with a linked SDXL backend own a lite wallet controller that drives // real create/open/restore through the bridge. Full-node and unlinked-lite builds // leave lite_wallet_ null (the UI falls back to validation-only). rebuildLiteWallet(); // 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(); // 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 HAS_EMBEDDED_UI_TOML // If on-disk ui.toml exists but differs in size from the embedded // version, a newer wallet binary is running against stale theme // files. Overwrite the on-disk copy so layout matches the binary. if (std::filesystem::exists(schemaPath)) { std::error_code ec; auto diskSize = std::filesystem::file_size(schemaPath, ec); if (!ec && diskSize != static_cast(embedded::ui_toml_size)) { DEBUG_LOGF("[INFO] ui.toml on disk (%ju bytes) differs from embedded (%zu bytes) — updating\n", (uintmax_t)diskSize, embedded::ui_toml_size); std::ofstream ofs(schemaPath, std::ios::binary | std::ios::trunc); if (ofs.is_open()) { ofs.write(reinterpret_cast(embedded::ui_toml_data), embedded::ui_toml_size); ofs.close(); } } } #endif 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(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(); // Initialize background RPC worker thread worker_ = std::make_unique(); worker_->start(); network_refresh_.markDue(services::NetworkRefreshService::Timer::Price); // 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). // The wizard is full-node setup (daemon + blockchain); lite has neither, and isFirstRun() // is always true in lite (no `blocks` dir), so skip it — lite users create/open a wallet in // Settings, guided by the "No wallet open" prompt. if (isFirstRun() && !isLiteBuild()) { 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() is a no-op in lite builds) 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 the per-frame layout cache (reads TOML values once) ui::Layout::beginFrame(); // Refresh balance layout config only when schema changes { static uint32_t s_balanceLayoutGen = 0; uint32_t gen = ui::schema::UISchema::instance().generation(); if (gen != s_balanceLayoutGen) { s_balanceLayoutGen = gen; 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()); } } namespace { // Resolve the lite-wallet rollout / kill-switch decision (see wallet/lite_rollout_policy.h). // Local-only: reads the override + a stable per-install id from settings, an emergency env var, // and a locally-cached manifest file (NO network fetch). Fail-open: a missing/invalid manifest // leaves the wallet enabled. wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& settings) { using namespace dragonx::wallet; LiteRolloutInputs inputs; inputs.appVersion = DRAGONX_VERSION; // Emergency kill-switch env var: any value other than empty/"0"/"false" disables. if (const char* env = std::getenv("DRAGONX_LITE_KILL_SWITCH")) { const std::string v = env; inputs.killSwitchEnv = !v.empty() && v != "0" && v != "false"; } inputs.override = liteRolloutOverrideFromString(settings.getLiteRolloutOverride()); // Stable per-install bucket source — generated once, persisted, never transmitted, no PII. std::string installId = settings.getLiteInstallId(); if (installId.empty()) { if (sodium_init() >= 0) { unsigned char buf[16]; randombytes_buf(buf, sizeof(buf)); static const char kHex[] = "0123456789abcdef"; installId.reserve(sizeof(buf) * 2); for (unsigned char c : buf) { installId.push_back(kHex[c >> 4]); installId.push_back(kHex[c & 0x0F]); } } settings.setLiteInstallId(installId); settings.save(); } inputs.installBucket = liteRolloutBucketFromInstallId(installId); // Local manifest cache next to settings.json — no network fetch (a signed remote fetcher can // populate this later). Absent/unreadable -> fail-open. try { const std::filesystem::path cfgDir = std::filesystem::path(config::Settings::getDefaultPath()).parent_path(); inputs.manifest = loadLiteRolloutManifestFromFile((cfgDir / "lite_rollout.json").string()); } catch (...) { // leave manifest absent -> fail-open } const LiteRolloutDecision decision = evaluateLiteRollout(inputs); if (!decision.allowed) { DEBUG_LOGF("[lite-rollout] lite wallet gated OFF: %s — %s\n", liteRolloutStatusName(decision.status), decision.message.c_str()); } return decision; } } // namespace void App::rebuildLiteWallet(bool force) { if (!supportsLiteBackend() || !settings_) return; // Don't tear down a live session unless forced: if a wallet is already open (and possibly // mid-sync), the new server selection is already persisted to settings and will take effect // the next time the controller is built (next launch, or before another wallet is opened). // Rebuilding would discard the open wallet and its in-flight, uninterruptible sync — which is // exactly what the Network tab's apply-immediately server switch wants (force=true). Replacing // lite_wallet_ destroys the old controller; its destructor detaches the uninterruptible sync // thread (which keeps the shared bridge alive) and only joins the short poll worker, so this // does not block. The app's auto-open loop then reopens the wallet against the new server. if (!force && lite_wallet_ && lite_wallet_->walletOpen()) return; lite_wallet_ = wallet::LiteWalletController::createLinked( walletCapabilities(), wallet::liteConnectionSettingsFromAppSettings(*settings_), resolveLiteRolloutDecision(*settings_)); lite_wallet_->setPersistCallback([this]() { settings_->save(); }); // The new controller starts closed. Re-arm the one-shot auto-open so the next update() // tick reopens the existing wallet against the (possibly changed) server — otherwise a // server switch from the Network tab, or a retry after a failed open, would rebuild the // controller but never reopen, leaving a permanent "disconnected" state. lite_autoopen_done_ = false; lite_open_error_.clear(); } 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 || ImGui::IsAnyItemActive()) { 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(); } // Apply any lite-wallet refresh the controller's background worker produced (main thread). if (lite_wallet_) { // Auto-open an existing wallet once, after the window is up. initialize_existing needs no // passphrase (it just loads the file); a previously-synced wallet then resumes from its // saved height (fast) rather than re-scanning from the checkpoint. if (!lite_autoopen_done_) { lite_autoopen_done_ = true; if (!lite_wallet_->walletOpen() && lite_wallet_->walletExists()) { const auto openResult = lite_wallet_->openWallet(wallet::LiteWalletOpenRequest{}); // Surface why an existing wallet failed to open (e.g. the lightwalletd server // is unreachable) — otherwise the UI just shows a silent "disconnected" spinner. if (!openResult.ok && !openResult.walletReady) { lite_open_error_ = !openResult.error.empty() ? openResult.error : (!openResult.status.message.empty() ? openResult.status.message : "Could not open the wallet"); DEBUG_LOGF("[Lite] auto-open failed: %s\n", lite_open_error_.c_str()); ui::Notifications::instance().error( std::string("Wallet open failed: ") + lite_open_error_, 8.0f); } } } // Lite has no RPC daemon (tryConnect() is a no-op in lite builds), so derive the app's // "online" state — which gates the wallet UI via isConnected() — from the lite wallet. // A wallet is open only after a successful backend init against the lite server, so this // is a non-blocking proxy for "lite backend operational". state_.connected = lite_wallet_->walletOpen(); if (state_.connected) lite_open_error_.clear(); // opened successfully — clear any prior error // Suppress the status bar's full-node connection-detail line in lite ("" and "Connected" // are both hidden); the connected/no-wallet indicator + sync status convey lite state. connection_status_ = state_.connected ? "Connected" : ""; wallet::LiteWalletAppRefreshModel liteModel; if (lite_wallet_->takeRefreshedModel(liteModel)) { wallet::applyLiteRefreshModelToWalletState(liteModel, state_); } // Deliver a completed async send/shield result to the waiting send_tab callback. wallet::LiteBroadcastResult broadcast; if (lite_wallet_->takeBroadcastResult(broadcast)) { if (lite_send_callback_) { lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : broadcast.error); lite_send_callback_ = nullptr; } } // Startup lock screen: once the first refresh reveals the (auto-opened) wallet is // encrypted, prompt to unlock if it's locked. Soft by design — balances stay viewable via // viewing keys while locked; only spending needs the passphrase, so the user may dismiss // and browse read-only. Fires once per session. if (!lite_startup_lock_checked_ && state_.encrypted) { lite_startup_lock_checked_ = true; if (state_.locked) requestLiteUnlock(); } } async_tasks_.reapCompleted(); // Auto-lock check (only when connected + encrypted + unlocked) if (state_.connected && state_.isUnlocked()) { checkAutoLock(); } // Mine-when-idle check (runs every frame, internally rate-limited by idle detection) checkIdleMining(); // P8: Dedup rebuildAddressList — only rebuild once per frame if (address_list_dirty_) { address_list_dirty_ = false; state_.rebuildAddressList(); } using RefreshTimer = services::NetworkRefreshService::Timer; network_refresh_.tick(io.DeltaTime); // Full-node RPC refreshes gate on ACTUAL RPC connectivity, not state_.connected. In lite // builds state_.connected is the lite-wallet "online" proxy (true when a wallet is open, to // enable the wallet UI), but there is no RPC daemon — so RPC polls (mining/balance/peers/txs) // must key off rpc_ being genuinely connected or they'd fire and fail ("Not connected"). const bool rpcConnected = rpc_ && rpc_->isConnected(); // 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 (network_refresh_.consumeDue(RefreshTimer::Fast)) { if (rpcConnected && !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_ && !rescan_status_poll_in_progress_) { auto* rescanRpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); rescan_status_poll_in_progress_ = true; fast_worker_->post([this, rescanRpc]() -> rpc::RPCWorker::MainCb { try { rpc::RPCClient::TraceScope trace("Startup / Rescan monitor"); 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(); try { progress = std::stof(progStr) * 100.0f; } catch (...) {} } return [this, rescanning, progress]() { rescan_status_poll_in_progress_ = false; 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 [this](){ rescan_status_poll_in_progress_ = false; }; } }); } } // 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(memMB * 1024.0 * 1024.0); ps.threads_active = xs.threads_active; ps.pool_hashrate = xs.pool_hashrate; 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; } // Populate solo mining log lines from daemon output if (daemon_controller_ && daemon_controller_->isRunning()) { state_.mining.log_lines = daemon_controller_->recentLines(50); } // Check daemon output for rescan progress (offloaded to worker) if (daemon_controller_ && daemon_controller_->isRunning()) { std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_); if (!newOutput.empty() && fast_worker_) { fast_worker_->post([this, output = std::move(newOutput)]() -> rpc::RPCWorker::MainCb { // Parse on worker thread — pure string work, no shared state access bool foundRescan = false; bool finished = false; float rescanPct = 0.0f; std::string lastStatus; size_t pos = 0; while (pos < output.size()) { size_t eol = output.find('\n', pos); if (eol == std::string::npos) eol = output.size(); std::string line = output.substr(pos, eol - pos); pos = eol + 1; if (line.find("Rescanning from height") != std::string::npos || line.find("Rescanning last") != std::string::npos) { foundRescan = true; lastStatus = line; } auto stillIdx = line.find("Still rescanning"); if (stillIdx != std::string::npos) { foundRescan = true; auto progIdx = line.find("Progress="); if (progIdx != std::string::npos) { size_t numStart = progIdx + 9; 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 (...) {} } } lastStatus = line; } auto rescIdx = line.find("Rescanning..."); if (rescIdx != std::string::npos) { foundRescan = true; auto pctIdx = line.find('%'); if (pctIdx != std::string::npos && pctIdx > 0) { 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 (...) {} } } lastStatus = line; } if (line.find("Done rescanning") != std::string::npos || line.find("Rescan complete") != std::string::npos) { finished = true; } } // Return callback to apply results on main thread return [this, foundRescan, finished, rescanPct, status = std::move(lastStatus)]() { if (finished) { 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(); } else if (foundRescan) { state_.sync.rescanning = true; if (rescanPct > 0.0f) { state_.sync.rescan_progress = rescanPct / 100.0f; } if (!status.empty()) { state_.sync.rescan_status = status; } } }; }); } } else if (!daemon_controller_ || !daemon_controller_->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 (full-node opid flow; lite has none) if (network_refresh_.isDue(RefreshTimer::Opid) && !pending_opids_.empty() && rpcConnected && fast_worker_ && !opid_poll_in_progress_) { network_refresh_.reset(RefreshTimer::Opid); auto opids = pending_opids_; // copy for worker thread opid_poll_in_progress_ = true; fast_worker_->post([this, opids]() -> rpc::RPCWorker::MainCb { auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); if (!rpc) return [this](){ opid_poll_in_progress_ = false; }; json ids = json::array(); for (const auto& id : opids) ids.push_back(id); json result; try { rpc::RPCClient::TraceScope trace("Send tab / Operation status"); result = rpc->call("z_getoperationstatus", {ids}); } catch (...) { return [this](){ opid_poll_in_progress_ = false; }; } auto parsed = services::NetworkRefreshService::parseOperationStatusPoll(result, opids); return [this, parsed = std::move(parsed)]() mutable { opid_poll_in_progress_ = false; // Successes: hand the real txid to any waiting send UI callback. std::unordered_set successfulOpids; for (const auto& [opid, txid] : parsed.successTxidsByOpid) { successfulOpids.insert(opid); markPendingSendTransactionSucceeded(opid, txid); send_txids_.insert(txid); invokeSendResultCallback(opid, true, txid); } // Failures: route to the originating send UI when there is one (it shows // its own error toast); otherwise surface a generic notification (this is // how shield/merge/auto-shield failures become visible). for (const auto& [opid, msg] : parsed.failureByOpid) { if (!invokeSendResultCallback(opid, false, msg)) { ui::Notifications::instance().error(msg); } } std::vector terminalOpids = std::move(parsed.doneOpids); terminalOpids.insert(terminalOpids.end(), parsed.staleOpids.begin(), parsed.staleOpids.end()); for (const auto& id : terminalOpids) { pending_opids_.erase( std::remove(pending_opids_.begin(), pending_opids_.end(), id), pending_opids_.end()); } // Stale opids (no longer reported by the daemon): let any waiting send UI // know the outcome couldn't be confirmed rather than spinning forever. for (const auto& opid : parsed.staleOpids) { invokeSendResultCallback(opid, false, TR("send_status_unconfirmed")); } if (parsed.anySuccess) { std::vector successOpids; std::vector failedOrStaleOpids; for (const auto& opid : terminalOpids) { if (successfulOpids.find(opid) != successfulOpids.end()) successOpids.push_back(opid); else failedOrStaleOpids.push_back(opid); } removePendingSendTransactions(successOpids, false); removePendingSendTransactions(failedOrStaleOpids, true); // Transaction confirmed by daemon — force immediate data refresh transactions_dirty_ = true; addresses_dirty_ = true; last_tx_block_height_ = -1; network_refresh_.markWalletMutationRefresh(); } else { removePendingSendTransactions(terminalOpids, true); maybeFinishTransactionSendProgress(); } }; }); } // Per-category refresh with tab-aware intervals // Skip when wallet is locked — same reason as above. if (rpcConnected && !state_.isLocked()) { const bool walletDataPage = currentPageNeedsWalletDataRefresh(); if (network_refresh_.consumeDue(RefreshTimer::Core)) { refreshCoreData(); } // Skip balance/tx/address refresh during warmup — RPC calls fail with -28 if (!state_.warming_up) { if (network_refresh_.consumeDue(RefreshTimer::Transactions)) { if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) { refreshTransactionData(); } else if (walletDataPage && shouldRefreshRecentTransactions()) { refreshRecentTransactionData(); } } if (network_refresh_.consumeDue(RefreshTimer::Addresses)) { if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) { refreshAddressData(); } } } if (network_refresh_.consumeDue(RefreshTimer::Peers)) { refreshPeerInfo(); } } else if (network_refresh_.consumeDue(RefreshTimer::Core)) { if (!connection_in_progress_ && wizard_phase_ == WizardPhase::None && !bootstrap_downloading_) { tryConnect(); } } // Price refresh every 60 seconds if (network_refresh_.consumeDue(RefreshTimer::Price)) { if (settings_->getFetchPrices()) { refreshPrice(); } } // Keyboard shortcut: Ctrl+, to open Settings page if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) { setCurrentPage(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; } // Handle wizard completion — start daemon and connect if (wizard_phase_ == WizardPhase::Done) { wizard_phase_ = WizardPhase::None; if (!state_.connected) { if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) { startEmbeddedDaemon(); } tryConnect(); } settings_->setWizardCompleted(true); settings_->save(); } // 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 brandOffY = hdrF("offset-y", 0.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 + brandOffY; 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 + brandOffY; 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(state_.peers.size()); sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running; // 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 (pending in mempool, not conflicted/orphaned) { int unconf = 0; for (const auto& tx : state_.transactions) { if (tx.confirmations == 0) ++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::NavPage requestedPage = current_page_; ui::RenderSidebar(requestedPage, sidebarW, sidebarH, sbStatus, sidebar_collapsed_, state_.isLocked()); if (requestedPage != current_page_) { setCurrentPage(requestedPage); } } 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() || (settings_ && settings_->getReduceMotion())) ? 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 on tabs that truly need wallet data. // Tabs that work without full daemon connection: // Console, Peers, Settings, Market, Mining // Tabs that need balance/transaction data (show overlay): // Overview, Send, Receive, History // --------------------------------------------------------------- bool pageNeedsWalletData = wallet::uiSurfaceNeedsWalletData(ui::NavPageSurface(current_page_)); bool daemonReady = state_.connected && !state_.warming_up; // Don't show lock screen while pool mining — xmrig runs independently // of the wallet and locking would block the mining UI needlessly. bool poolMiningActive = xmrig_manager_ && xmrig_manager_->isRunning(); if (state_.isLocked() && !poolMiningActive) { // Lock screen — covers tab content just like the loading overlay renderLockScreen(); } else if (pageNeedsWalletData && state_.warming_up) { // Daemon is reachable but still initializing — show warmup overlay // only on wallet-data tabs. Other tabs render normally. lock_screen_was_visible_ = false; renderLoadingOverlay(contentH); } else if (pageNeedsWalletData && (!daemonReady || (state_.connected && !state_.encryption_state_known))) { // Track how long we've been waiting for encryption state if (state_.connected && !state_.encryption_state_known) { encryption_check_timer_ += ImGui::GetIO().DeltaTime; if (encryption_check_timer_ >= 15.0f) { DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n"); state_.encryption_state_known = true; state_.encrypted = false; encryption_check_timer_ = 0.0f; } } else { encryption_check_timer_ = 0.0f; } // 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::LiteNetwork: ui::RenderLiteNetworkTab(this); break; case ui::NavPage::Explorer: ui::RenderExplorerTab(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. // Fall back to main rpc/worker if fast-lane hasn't connected yet. console_tab_.render(daemon_controller_ ? daemon_controller_->daemon() : nullptr, (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(), (fast_rpc_ && fast_rpc_->isConnected() && 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(a * alpha); v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } } }; // Apply to foreground panel effects (rainbow borders, 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(a * page_alpha_); v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast(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_); } // Lite first-run welcome: prompt to create/restore when no wallet file exists yet. renderLiteFirstRunPrompt(); // Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet). renderLiteUnlockPrompt(); 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); // Address label/icon editor ui::AddressLabelDialog::render(); // Address-to-address transfer confirmation ui::AddressTransferDialog::render(); // Bootstrap download from settings ui::BootstrapDownloadDialog::render(); ui::XmrigDownloadDialog::render(); // 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_.warming_up) { ImGui::PushFont(ui::material::Type().iconSmall()); ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, dotOpacity), ICON_MD_CIRCLE); ImGui::PopFont(); ImGui::SameLine(0, sbIconTextGap); // Show truncated warmup status (e.g. "Activating best chain... (Block 12345)") const char* warmupText = state_.warmup_status.empty() ? TR("sb_warming_up") : state_.warmup_status.c_str(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", warmupText); } else 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("%s", TR("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); // Lite has no daemon connection; "disconnected" here means no wallet is open. ImGui::Text("%s", TR(isLiteBuild() ? "lite_no_wallet_short" : "disconnected")); } // Block height ImGui::SameLine(0, sbSectionGap); ImGui::TextDisabled("|"); ImGui::SameLine(0, sbSeparatorGap); ImGui::Text(TR("sb_block"), 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), TR("sb_rescanning_pct"), 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), "%s%s", TR("sb_rescanning"), dotStr); } } else if (state_.sync.syncing) { int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers; int blocksLeft = chainTip - state_.sync.blocks; if (blocksLeft < 0) blocksLeft = 0; // Calculate sync speed (blocks/sec) using a smoothed rolling average static int s_prev_blocks = 0; static double s_prev_time = 0.0; static double s_blocks_per_sec = 0.0; double now = ImGui::GetTime(); if (s_prev_time == 0.0) { // First sample — seed baseline, don't compute rate yet s_prev_blocks = state_.sync.blocks; s_prev_time = now; } else if (state_.sync.blocks > s_prev_blocks) { double dt = now - s_prev_time; if (dt > 0.5) { double raw = (state_.sync.blocks - s_prev_blocks) / dt; // Exponential smoothing (alpha ~0.3 per update) s_blocks_per_sec = s_blocks_per_sec > 0.0 ? s_blocks_per_sec * 0.7 + raw * 0.3 : raw; s_prev_blocks = state_.sync.blocks; s_prev_time = now; } } else if (now - s_prev_time > 10.0) { // No new blocks for 10s — decay the displayed rate s_blocks_per_sec *= 0.5; s_prev_time = now; } if (s_blocks_per_sec > 0.1) { int eta_sec = (int)(blocksLeft / s_blocks_per_sec); char eta[32]; if (eta_sec >= 3600) snprintf(eta, sizeof(eta), "%dh %dm", eta_sec / 3600, (eta_sec % 3600) / 60); else if (eta_sec >= 60) snprintf(eta, sizeof(eta), "%dm %ds", eta_sec / 60, eta_sec % 60); else snprintf(eta, sizeof(eta), "%ds", eta_sec); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_eta"), state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec, eta); } else { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_basic"), state_.sync.verification_progress * 100.0, blocksLeft); } } else if (state_.connected && !isLiteBuild()) { // Lite has no P2P peers (the lite server isn't a peer set); skip the peer count. ImGui::Text(TR("sb_peers"), 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(TR("sb_net_ghs"), state_.mining.networkHashrate / 1e9); } else if (state_.mining.networkHashrate >= 1e6) { ImGui::Text(TR("sb_net_mhs"), state_.mining.networkHashrate / 1e6); } else if (state_.mining.networkHashrate >= 1e3) { ImGui::Text(TR("sb_net_khs"), state_.mining.networkHashrate / 1e3); } else { ImGui::Text(TR("sb_net_hs"), state_.mining.networkHashrate); } } // Mining indicator (if mining — solo or pool) const bool anyMining = state_.mining.generate || state_.pool_mining.xmrig_running; if (anyMining) { 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); double displayHashrate = state_.pool_mining.xmrig_running ? state_.pool_mining.hashrate_10s : state_.mining.localHashrate; ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), TR("sb_mining_hs"), displayHashrate); } // Transaction submission/operation progress if (hasTransactionSendProgress()) { 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_SEND); ImGui::PopFont(); ImGui::SameLine(0, sbIconTextGap); int dots = (int)(ImGui::GetTime() * 2.0f) % 4; const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : ""; std::string status = transactionSendProgressText(); ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", status.c_str(), dotStr); } // Decrypt-import background task indicator if (wallet_security_workflow_.importActive()) { 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), "%s%s", TR("sb_importing_keys"), 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 = TR("sb_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::renderLiteFirstRunPrompt() { // Lite-only: a brief welcome shown when no wallet file exists yet (full-node uses the wizard, // which is skipped in lite). Routes to the existing create/restore + Backup flows in Settings. if (!lite_wallet_ || lite_firstrun_dismissed_) return; if (lite_wallet_->walletOpen() || lite_wallet_->walletExists()) return; if (!ImGui::IsPopupOpen("##LiteFirstRun")) ImGui::OpenPopup("##LiteFirstRun"); ImGuiViewport* vp = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { ImGui::PushFont(ui::material::Type().subtitle1()); ImGui::TextUnformatted(TR("lite_welcome_title")); ImGui::PopFont(); ImGui::Spacing(); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f); ImGui::TextUnformatted(TR("lite_welcome_msg")); ImGui::PopTextWrapPos(); ImGui::Spacing(); ImGui::Spacing(); const float btnW = 170.0f; if (ImGui::Button(TR("lite_welcome_create"), ImVec2(btnW, 0))) { // One-click create (unencrypted by default); the user backs up the seed next via the // Settings → Backup & keys flow, where the seed can be revealed and copied. const auto result = lite_wallet_->createWallet(wallet::LiteWalletCreateRequest{}); if (result.walletReady) { ui::Notifications::instance().success(TR("lite_welcome_created"), 8.0f); setCurrentPage(ui::NavPage::Settings); } else { ui::Notifications::instance().warning(TR("lite_welcome_create_failed")); } lite_firstrun_dismissed_ = true; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button(TR("lite_welcome_restore"), ImVec2(btnW, 0))) { ui::Notifications::instance().info(TR("lite_welcome_restore_hint"), 8.0f); setCurrentPage(ui::NavPage::Settings); lite_firstrun_dismissed_ = true; ImGui::CloseCurrentPopup(); } ImGui::Spacing(); if (ImGui::Button(TR("lite_welcome_later"), ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) { lite_firstrun_dismissed_ = true; ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } } void App::renderLiteUnlockPrompt() { if (!lite_wallet_) return; static char pass[128] = ""; if (lite_unlock_prompt_ && !ImGui::IsPopupOpen("##LiteUnlock")) { ImGui::OpenPopup("##LiteUnlock"); } ImGuiViewport* vp = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("##LiteUnlock", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { ImGui::PushFont(ui::material::Type().subtitle1()); ImGui::TextUnformatted(TR("lite_unlock_title")); ImGui::PopFont(); ImGui::Spacing(); ImGui::TextUnformatted(TR("lite_unlock_msg")); ImGui::Spacing(); ImGui::SetNextItemWidth(280.0f); const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue); ImGui::Spacing(); const float btnW = 130.0f; bool doUnlock = ImGui::Button(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered; if (doUnlock) { const bool ok = lite_wallet_->unlockWallet(pass); sodium_memzero(pass, sizeof(pass)); if (ok) ui::Notifications::instance().success(TR("lite_unlock_ok"), 5.0f); else ui::Notifications::instance().error(TR("lite_unlock_failed")); lite_unlock_prompt_ = false; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) { sodium_memzero(pass, sizeof(pass)); lite_unlock_prompt_ = false; ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } } 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; }; int btnFont = (int)dlgF("button-font", 1); float btnW = dlgF("button-width", 120.0f); 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_)); // Paste & Clear buttons if (ui::material::StyledButton(TR("paste"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) { snprintf(import_key_input_, sizeof(import_key_input_), "%s", clipboard); // Trim whitespace std::string trimmed(import_key_input_); while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' || trimmed.front() == '\n' || trimmed.front() == '\r')) trimmed.erase(trimmed.begin()); while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' || trimmed.back() == '\n' || trimmed.back() == '\r')) trimmed.pop_back(); snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str()); } } ImGui::SameLine(); if (ui::material::StyledButton(TR("clear"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { memset(import_key_input_, 0, sizeof(import_key_input_)); } // Key validation indicator if (import_key_input_[0] != '\0') { std::string k(import_key_input_); bool isZKey = (k.substr(0, 20) == "secret-extended-key-") || (k.length() >= 2 && k[0] == 'S' && k[1] == 'K'); bool isTKey = (k.length() >= 51 && k.length() <= 52 && (k[0] == '5' || k[0] == 'K' || k[0] == 'L' || k[0] == 'U')); if (isZKey || isTKey) { ImGui::PushFont(ui::material::Type().iconSmall()); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CHECK_CIRCLE); ImGui::PopFont(); ImGui::SameLine(0, 4.0f); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", isZKey ? "Shielded spending key" : "Transparent private key"); } else { ImGui::PushFont(ui::material::Type().iconSmall()); ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP); ImGui::PopFont(); ImGui::SameLine(0, 4.0f); ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Unrecognized key format"); } } 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(); 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 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() { // Trigger immediate refresh on all categories network_refresh_.markImmediateRefresh(); transactions_dirty_ = true; // Force transaction list update addresses_dirty_ = true; // Force address/balance update last_tx_block_height_ = -1; // Reset tx cache invalidateShieldedHistoryScanProgress(true); } 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 setCurrentPage(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::Explorer, // 8 = Explorer ui::NavPage::Settings, // 9 = Settings }; if (tab >= 0 && tab < static_cast(sizeof(kTabMap)/sizeof(kTabMap[0]))) setCurrentPage(kTabMap[tab]); } bool App::startEmbeddedDaemon() { if (!supportsEmbeddedDaemon()) { DEBUG_LOGF("Embedded daemon support unavailable in this build, not starting\n"); return false; } if (!isUsingEmbeddedDaemon()) { 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_ = TR("sb_extracting_sapling"); resources::extractEmbeddedResources(); // Check again after extraction if (!rpc::Connection::verifySaplingParams()) { daemon_status_ = TR("sb_sapling_failed"); 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 { // Fallback: check for params bundled alongside the executable // (zip distributions bundle sapling-*.params next to the binary) namespace fs = std::filesystem; std::string exe_dir = util::Platform::getExecutableDirectory(); std::string daemon_dir = resources::getDaemonDirectory(); const char* paramFiles[] = { "sapling-spend.params", "sapling-output.params", "asmap.dat" }; bool copied = false; if (!exe_dir.empty()) { std::error_code ec; fs::create_directories(daemon_dir, ec); // On macOS .app bundles, params are in Contents/Resources/ // while the executable is in Contents/MacOS/ std::vector searchDirs = { exe_dir }; #ifdef __APPLE__ { fs::path resourcesDir = fs::path(exe_dir).parent_path() / "Resources"; if (fs::is_directory(resourcesDir)) searchDirs.insert(searchDirs.begin(), resourcesDir.string()); } #endif for (const char* name : paramFiles) { fs::path dst = fs::path(daemon_dir) / name; if (fs::exists(dst)) continue; for (const auto& dir : searchDirs) { fs::path src = fs::path(dir) / name; if (fs::exists(src)) { DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str()); fs::copy_file(src, dst, ec); if (!ec) copied = true; break; } } } } if (copied && rpc::Connection::verifySaplingParams()) { DEBUG_LOGF("Sapling params copied from exe directory successfully\n"); } else { daemon_status_ = TR("sb_sapling_not_found"); 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; } } } // Ensure asmap.dat and daemon binaries are copied from the bundle // to the daemon directory even if sapling params already existed. { namespace fs = std::filesystem; std::string exe_dir = util::Platform::getExecutableDirectory(); std::string daemon_dir = resources::getDaemonDirectory(); if (!exe_dir.empty()) { std::error_code ec; fs::create_directories(daemon_dir, ec); std::vector searchDirs = { exe_dir }; #ifdef __APPLE__ { fs::path resourcesDir = fs::path(exe_dir).parent_path() / "Resources"; if (fs::is_directory(resourcesDir)) searchDirs.insert(searchDirs.begin(), resourcesDir.string()); } #endif const char* extraFiles[] = { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" }; for (const char* name : extraFiles) { fs::path dst = fs::path(daemon_dir) / name; if (fs::exists(dst)) continue; for (const auto& dir : searchDirs) { fs::path src = fs::path(dir) / name; if (fs::exists(src)) { DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str()); fs::copy_file(src, dst, ec); break; } } } } } // Create daemon controller if needed if (!daemon_controller_) { daemon_controller_ = std::make_unique(); // Set up state callback daemon_controller_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) { switch (state) { case daemon::EmbeddedDaemon::State::Starting: daemon_status_ = TR("sb_starting_daemon"); break; case daemon::EmbeddedDaemon::State::Running: daemon_status_ = TR("sb_dragonxd_running"); break; case daemon::EmbeddedDaemon::State::Stopping: daemon_status_ = TR("sb_dragonxd_stopping"); break; case daemon::EmbeddedDaemon::State::Stopped: daemon_status_ = TR("sb_dragonxd_stopped"); break; case daemon::EmbeddedDaemon::State::Error: daemon_status_ = "Error: " + msg; break; } }); } return daemon_controller_->start(settings_.get()); } void App::stopEmbeddedDaemon() { if (!daemon_controller_) return; auto shutdownDecision = daemon_controller_->shutdownDecision( false, settings_ && settings_->getStopExternalDaemon()); if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) { DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason); 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(); // 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, config.use_tls)) { DEBUG_LOGF("Temporary RPC connected, sending stop...\n"); if (sendStopCommandSafely(*tmp_rpc, "Temporary daemon stop")) { stop_sent = true; } 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 flush block index and shut down...\n"); shutdown_status_ = "Waiting for daemon to flush block index..."; // Give the daemon time to flush LevelDB to disk before we // escalate to SIGTERM. On macOS/APFS, LevelDB compaction + // fsync can take 15-20s on a large chain. The stop() method // will wait this long for a *natural* exit (via the RPC stop // we already sent) before falling back to SIGTERM. } shutdown_status_ = "Waiting for dragonxd process to exit..."; // 20s grace period for the RPC "stop" to complete (LevelDB flush). // Only after that does stop() escalate to SIGTERM, then SIGKILL. daemon_controller_->stop(20000); } bool App::isEmbeddedDaemonRunning() const { return daemon_controller_ && daemon_controller_->isRunning(); } void App::rescanBlockchain() { if (!supportsFullNodeLifecycleActions()) { ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); return; } auto decision = daemon::DaemonController::evaluateLifecycleOperation( daemon::DaemonController::LifecycleOperation::Rescan, isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning()); if (!decision.allowed) { ui::Notifications::instance().warning(decision.warning); 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 = decision.status; transactions_dirty_ = true; last_tx_block_height_ = -1; invalidateShieldedHistoryScanProgress(true); // Set rescan flag BEFORE stopping so it's ready when we restart daemon_controller_->prepareLifecycleOperation(decision, settings_.get()); DEBUG_LOGF("[App] Rescan flag set, rescanOnNextStart=%d\n", daemon_controller_->rescanOnNextStart() ? 1 : 0); // Stop daemon, then restart async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) { DEBUG_LOGF("[App] Stopping daemon for rescan...\n"); AppDaemonLifecycleRuntime runtime(*this); daemon::AsyncLifecycleTaskContext context(token, shutting_down_); daemon_controller_->executeLifecycleOperation(decision, runtime, context); }); } void App::deleteBlockchainData() { if (!supportsFullNodeLifecycleActions()) { ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); return; } auto decision = daemon::DaemonController::evaluateLifecycleOperation( daemon::DaemonController::LifecycleOperation::DeleteBlockchainData, isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning()); if (!decision.allowed) { ui::Notifications::instance().warning(decision.warning); return; } DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n"); ui::Notifications::instance().info("Stopping daemon and deleting blockchain data..."); daemon_controller_->prepareLifecycleOperation(decision, settings_.get()); async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) { DEBUG_LOGF("[App] Stopping daemon for blockchain deletion...\n"); AppDaemonLifecycleRuntime runtime(*this); daemon::AsyncLifecycleTaskContext context(token, shutting_down_); auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context); DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems); }); } bool App::stopDaemonForBootstrap() { if (!supportsFullNodeLifecycleActions()) { return false; } auto decision = daemon::DaemonController::evaluateLifecycleOperation( daemon::DaemonController::LifecycleOperation::BootstrapStop, isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning()); if (decision.wasRunning) { DEBUG_LOGF("[App] Stopping embedded daemon for bootstrap download...\n"); daemon_controller_->prepareLifecycleOperation(decision, settings_.get()); AppDaemonLifecycleRuntime runtime(*this); daemon::ImmediateLifecycleTaskContext context; daemon_controller_->executeLifecycleOperation(decision, runtime, context); } return decision.wasRunning; } 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 (daemon_controller_ && daemon_controller_->isRunning()) { double mb = daemon_controller_->memoryUsageMB(); 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(); async_tasks_.cancelAll(); // Signal the RPC worker to stop accepting new tasks (non-blocking), and abort any call // already in flight so the later join() doesn't wait out a request timeout. // The actual thread join + rpc disconnect happen in shutdown() after // the render loop exits, so the UI stays responsive. if (rpc_) rpc_->requestAbort(); if (fast_rpc_) fast_rpc_->requestAbort(); 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 (daemon_controller_=%s)\n", daemon_controller_ ? "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 (!daemon_controller_) { 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(); } auto shutdownDecision = daemon_controller_->shutdownDecision( settings_ && settings_->getKeepDaemonRunning(), settings_ && settings_->getStopExternalDaemon()); if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) { DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason); shutdown_status_ = shutdownDecision.status; shutdown_complete_ = true; return; } shutdown_status_ = shutdownDecision.status; 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); ImGuiWindowFlags shutdownFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings; // Allow input after 10s so Force Quit button is clickable if (shutdown_timer_ < 10.0f) shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags); 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(); } // ------------------------------------------------------------------- // 4b. Force Quit button — appears after 10 seconds // ------------------------------------------------------------------- if (shutdown_timer_ >= 10.0f) { ImGui::Spacing(); ImGui::Spacing(); const char* forceLabel = TR("force_quit"); ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0); ImGui::SetCursorPosX(cx - btnSize.x * 0.5f); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); if (ImGui::Button(forceLabel, btnSize)) { force_quit_confirm_ = true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", TR("force_quit_warning")); } ImGui::PopStyleColor(3); } // Force Quit confirmation popup if (force_quit_confirm_) { ImGui::OpenPopup("##ForceQuitConfirm"); force_quit_confirm_ = false; } ImVec2 popupCenter(wp.x + vp_size.x * 0.5f, wp.y + vp_size.y * 0.5f); ImGui::SetNextWindowPos(popupCenter, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("##ForceQuitConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { ImGui::PushFont(Type().subtitle1()); ImGui::TextUnformatted(TR("force_quit_confirm_title")); ImGui::PopFont(); ImGui::Spacing(); ImGui::TextUnformatted(TR("force_quit_confirm_msg")); ImGui::Spacing(); ImGui::Spacing(); float btnW = 120.0f; float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f); if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) { ImGui::CloseCurrentPopup(); } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) { DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_); shutdown_complete_ = true; ImGui::CloseCurrentPopup(); } ImGui::PopStyleColor(3); ImGui::EndPopup(); } 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 (daemon_controller_) { auto lines = daemon_controller_->recentLines(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; } // ------------------------------------------------------------------- // 2b. Warmup description (subtitle explaining what's happening) // ------------------------------------------------------------------- if (state_.warming_up && !state_.warmup_description.empty()) { const char* descText = state_.warmup_description.c_str(); ImFont* capFont = Type().caption(); if (!capFont) capFont = ImGui::GetFont(); ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, descText); dl->AddText(capFont, capFont->LegacySize, ImVec2(wp.x + cx - ts.x * 0.5f, curY), IM_COL32(160, 160, 160, 200), descText); curY += ts.y + gap; } // ------------------------------------------------------------------- // 3. Sync progress bar (if connected and syncing) // ------------------------------------------------------------------- if (state_.connected && state_.sync.syncing) { float progress = static_cast(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 (wallet_security_.hasDeferredEncryption()) { 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 (daemon_controller_ && daemon_controller_->state() == 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 = daemon_controller_->lastError(); 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 (daemon_controller_->crashCount() >= 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 (daemon_controller_) { auto lines = daemon_controller_->recentLines(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"); async_tasks_.cancelAll(); async_tasks_.joinAll(); if (rpc_) rpc_->requestAbort(); // unblock any in-flight curl before joining if (fast_rpc_) fast_rpc_->requestAbort(); if (worker_) { worker_->stop(); } if (fast_worker_) { fast_worker_->stop(); } if (settings_) { settings_->save(); } if (daemon_controller_) { 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(); } async_tasks_.joinAll(); // 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()); } std::string App::transactionSendProgressText() const { using Job = services::NetworkRefreshService::Job; if (send_submissions_in_flight_ > 0) return TR("tx_progress_submitting"); if (!pending_opids_.empty()) { char buf[128]; snprintf(buf, sizeof(buf), TR("tx_progress_waiting_ops"), (int)pending_opids_.size()); return buf; } if (addresses_dirty_ || network_refresh_.jobInProgress(Job::Addresses)) { return TR("tx_progress_balances"); } if (transactions_dirty_ || network_refresh_.jobInProgress(Job::Transactions)) { return TR("tx_progress_history"); } return TR("tx_progress_finalizing"); } std::string App::transactionRefreshProgressText() const { using Job = services::NetworkRefreshService::Job; bool running = network_refresh_.jobInProgress(Job::Transactions); bool canRefresh = state_.connected && !state_.warming_up && !state_.isLocked(); if (!running && !(canRefresh && transactions_dirty_)) return {}; if (!running && transactions_dirty_) return TR("tx_loading_queued"); char buf[128]; if (!send_txids_.empty()) { snprintf(buf, sizeof(buf), TR("tx_loading_enriching_sends"), (int)send_txids_.size()); return buf; } if (!state_.transactions.empty()) { snprintf(buf, sizeof(buf), TR("tx_loading_refreshing_cached"), (int)state_.transactions.size()); return buf; } if (!state_.z_addresses.empty()) { snprintf(buf, sizeof(buf), TR("tx_loading_scanning_shielded"), (int)state_.z_addresses.size()); return buf; } return TR("tx_loading_fetching_transparent"); } void App::maybeFinishTransactionSendProgress() { using Job = services::NetworkRefreshService::Job; if (!send_progress_active_) return; if (send_submissions_in_flight_ > 0 || !pending_opids_.empty()) return; if (addresses_dirty_ || transactions_dirty_) return; if (network_refresh_.jobInProgress(Job::Addresses) || network_refresh_.jobInProgress(Job::Transactions)) return; send_progress_active_ = false; } void App::restartDaemon() { if (!supportsFullNodeLifecycleActions()) { ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); return; } auto decision = daemon::DaemonController::evaluateLifecycleOperation( daemon::DaemonController::LifecycleOperation::ManualRestart, isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load()); if (!decision.allowed) return; daemon_restarting_ = true; daemon_controller_->prepareLifecycleOperation(decision, settings_.get()); DEBUG_LOGF("[App] Restarting embedded daemon...\n"); connection_status_ = TR("sb_restarting_daemon"); // Disconnect RPC so the loading overlay appears if (decision.disconnectRpc && rpc_ && rpc_->isConnected()) { rpc_->disconnect(); } onDisconnected("Daemon restart"); // 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. async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) { AppDaemonLifecycleRuntime runtime(*this); daemon::AsyncLifecycleTaskContext context(token, shutting_down_); daemon_controller_->executeLifecycleOperation(decision, runtime, context); daemon_restarting_ = false; DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n"); }); } } // namespace dragonx