// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include "../../app.h" #include "../../util/bootstrap.h" #include "../../util/platform.h" #include "../../util/i18n.h" #include "../material/draw_helpers.h" #include "../material/type.h" #include "../material/colors.h" #include "../theme.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" namespace dragonx { namespace ui { /** * @brief Modal dialog for downloading blockchain bootstrap from Settings. * * Reuses the existing Bootstrap class for download/verify/extract. * States: Confirm → Downloading → Done / Failed */ class BootstrapDownloadDialog { public: static void show(App* app) { if (!app || !app->supportsFullNodeLifecycleActions()) return; s_open = true; s_app = app; s_state = State::Confirm; s_bootstrap.reset(); s_errorMsg.clear(); s_wasDaemonRunning = false; } static bool isOpen() { return s_open; } static void render() { if (!s_app) return; if (!s_open) { // Dialog was closed — ensure flag is cleared if (s_app->isBootstrapDownloading() && s_state != State::Downloading) { s_app->setBootstrapDownloading(false); } return; } using namespace material; const float dp = Layout::dpiScale(); if (BeginOverlayDialog(TR("download_bootstrap"), &s_open, 500.0f, 0.94f)) { if (s_state == State::Confirm) { renderConfirm(dp); } else if (s_state == State::Downloading) { renderProgress(dp); } else if (s_state == State::Done) { renderDone(dp); } else if (s_state == State::Failed) { renderFailed(dp); } EndOverlayDialog(); } } private: enum class State { Confirm, Downloading, Done, Failed }; // ---- Confirm screen ---- static void renderConfirm(float dp) { using namespace material; ImGui::Spacing(); // Description ImGui::TextWrapped("%s", TR("bootstrap_desc")); ImGui::Spacing(); // Warning card ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.9f, 0.6f, 0.0f, 0.08f)); ImGui::BeginChild("##bsWarn", ImVec2(0, 0), ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_NoScrollbar); { ImFont* iconFont = Type().iconSmall(); ImGui::PushFont(iconFont); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Warning()), "%s", ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextWrapped("%s", TR("bootstrap_warning")); } ImGui::EndChild(); ImGui::PopStyleColor(); ImGui::Spacing(); // Trust warning { ImFont* iconFont = Type().iconSmall(); ImGui::PushFont(iconFont); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", ICON_MD_VERIFIED_USER); ImGui::PopFont(); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium())); ImGui::TextWrapped("%s", TR("bootstrap_trust_warning")); ImGui::PopStyleColor(); } ImGui::Spacing(); ImGui::Spacing(); // Buttons: Download | Mirror | Cancel float btnW = 140.0f * dp; float btnSm = 90.0f * dp; if (TactileButton(TR("download"), ImVec2(btnW, 0))) { startDownload(""); } ImGui::SameLine(); if (TactileButton(TR("bootstrap_mirror"), ImVec2(btnW, 0))) { std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName; startDownload(mirrorUrl); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", TR("bootstrap_mirror_tooltip")); } ImGui::SameLine(); if (TactileButton(TR("cancel"), ImVec2(btnSm, 0))) { s_open = false; } } // ---- Progress screen ---- static void renderProgress(float dp) { using namespace material; if (!s_bootstrap) { s_state = State::Failed; s_errorMsg = "Bootstrap not initialized"; return; } auto prog = s_bootstrap->getProgress(); // Status title const char* statusTitle; if (prog.state == util::Bootstrap::State::Downloading) statusTitle = TR("bootstrap_downloading"); else if (prog.state == util::Bootstrap::State::Verifying) statusTitle = TR("bootstrap_verifying"); else statusTitle = TR("bootstrap_extracting"); Type().text(TypeStyle::Subtitle2, statusTitle); ImGui::Spacing(); // Progress bar float barH = 8.0f * dp; float barW = ImGui::GetContentRegionAvail().x; ImVec2 barMin = ImGui::GetCursorScreenPos(); ImVec2 barMax(barMin.x + barW, barMin.y + barH); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(barMin, barMax, IM_COL32(255, 255, 255, 30), 4.0f * dp); float fillW = barW * (prog.percent / 100.0f); if (fillW > 0) { dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), Primary(), 4.0f * dp); } ImGui::Dummy(ImVec2(0, barH)); ImGui::Spacing(); // Percent + status text { char pctBuf[32]; snprintf(pctBuf, sizeof(pctBuf), "%.1f%%", prog.percent); float pctW = ImGui::CalcTextSize(pctBuf).x; ImGui::Text("%s", prog.status_text.c_str()); ImGui::SameLine(ImGui::GetContentRegionAvail().x - pctW + ImGui::GetCursorPosX()); ImGui::Text("%s", pctBuf); } // wallet.dat protection notice during extraction if (prog.state == util::Bootstrap::State::Extracting) { Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("bootstrap_wallet_protected")); } ImGui::Spacing(); // Daemon status indicator { bool daemonUp = s_app->isEmbeddedDaemonRunning(); const std::string& dStatus = s_app->getDaemonStatus(); ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) : IM_COL32(120, 120, 120, 160); if (dStatus.find("Stopping") != std::string::npos) dotCol = IM_COL32(255, 167, 38, 200); const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos ? TR("bootstrap_daemon_stopping") : TR("bootstrap_daemon_running")) : TR("bootstrap_daemon_stopped"); ImDrawList* ddl = ImGui::GetWindowDrawList(); float dotR = 3.5f * dp; ImVec2 cp = ImGui::GetCursorScreenPos(); ddl->AddCircleFilled(ImVec2(cp.x + dotR, cp.y + ImGui::GetTextLineHeight() * 0.5f), dotR, dotCol); ImGui::Indent(dotR * 2.0f + 6.0f * dp); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), label); ImGui::Unindent(dotR * 2.0f + 6.0f * dp); } ImGui::Spacing(); ImGui::Spacing(); // Cancel button float btnW = 100.0f * dp; if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) { s_bootstrap->cancel(); } // Check completion if (s_bootstrap->isDone()) { auto finalProg = s_bootstrap->getProgress(); if (finalProg.state == util::Bootstrap::State::Completed) { s_state = State::Done; } else { s_errorMsg = finalProg.error; if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed"; s_state = State::Failed; } s_bootstrap.reset(); } } // ---- Done screen ---- static void renderDone(float dp) { using namespace material; ImGui::Spacing(); Type().textColored(TypeStyle::H6, Success(), TR("bootstrap_success")); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("bootstrap_success_desc")); ImGui::Spacing(); ImGui::Spacing(); float btnW = 140.0f * dp; if (s_wasDaemonRunning) { if (TactileButton(TR("bootstrap_restart_daemon"), ImVec2(btnW, 0))) { s_app->setBootstrapDownloading(false); s_app->startEmbeddedDaemon(); s_open = false; } ImGui::SameLine(); } if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) { s_app->setBootstrapDownloading(false); s_open = false; } } // ---- Failed screen ---- static void renderFailed(float dp) { using namespace material; ImGui::Spacing(); Type().textColored(TypeStyle::H6, Error(), TR("bootstrap_failed")); ImGui::Spacing(); ImGui::TextWrapped("%s", s_errorMsg.c_str()); ImGui::Spacing(); ImGui::Spacing(); float btnW = 120.0f * dp; if (TactileButton(TR("retry"), ImVec2(btnW, 0))) { startDownload(""); } ImGui::SameLine(); if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) { s_app->setBootstrapDownloading(false); s_open = false; } } // ---- Shared: kick off download ---- static void startDownload(const std::string& url) { if (!s_app || !s_app->supportsFullNodeLifecycleActions()) return; s_wasDaemonRunning = s_app->stopDaemonForBootstrap(); s_app->setBootstrapDownloading(true); s_bootstrap = std::make_unique(); std::string dataDir = util::Platform::getDragonXDataDir(); if (url.empty()) s_bootstrap->start(dataDir); else s_bootstrap->start(dataDir, url); s_state = State::Downloading; s_errorMsg.clear(); } static inline bool s_open = false; static inline App* s_app = nullptr; static inline State s_state = State::Confirm; static inline std::unique_ptr s_bootstrap; static inline bool s_wasDaemonRunning = false; static inline std::string s_errorMsg; }; } // namespace ui } // namespace dragonx