// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #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 "../notifications.h" #include "bootstrap_completion.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_app = app; // If a download is already in flight, RE-ATTACH to it (just reopen showing progress) instead of // resetting state and orphaning the running worker / restarting an unverified download. if (app->isBootstrapDownloading()) { s_open = true; if (s_state == State::Confirm) s_state = State::Downloading; return; } s_open = true; s_state = State::Confirm; s_bootstrap.reset(); s_errorMsg.clear(); s_wasDaemonRunning = false; } static bool isOpen() { return s_open; } // Close the dialog (used by the UI sweep teardown). static void hide() { s_open = false; s_state = State::Confirm; } // Quit, from beginShutdown(): cancel a running Settings bootstrap without waiting for it. static void cancelForShutdown() { if (s_bootstrap) s_bootstrap->cancel(); } // Quit: true while a cancelled Settings bootstrap is still replacing the chain data. Its abort removes the // partial chain; the shutdown screen stays up until that is done, so main()'s exit watchdog never cuts it. static bool quitPending() { return s_bootstrap && !s_bootstrap->safeToAbandon(); } // Quit, from App::shutdown(): cancel a running Settings bootstrap and wait for its worker until `deadline` // (main() ends in _Exit, and its 15 s watchdog is already running). A worker still running then is left to // the process exit and never destroyed (~Bootstrap would join). static void abortForShutdown(std::chrono::steady_clock::time_point deadline) { if (!s_bootstrap) return; if (s_bootstrap->cancelAndWait(deadline)) s_bootstrap.reset(); else (void)s_bootstrap.release(); } // Process a finished worker. Idempotent and safe every tick: App::update() calls it so completion never // depends on the dialog being drawn (clicked away, window minimized); renderProgress() calls it too (the // UI sweep returns from update() early). With the dialog open it only advances the screen (Done/Failed own // the gate: Close / Restart daemon clear it, so the Done screen behaves as before). With the dialog // closed nobody can press those, so release the connect/reconcile gate here and toast the outcome. static void pump() { if (!s_app || !s_bootstrap || s_app->isShuttingDown()) return; // quit: abortForShutdown() owns it if (!s_bootstrap->isDone()) return; const auto finalProg = s_bootstrap->getProgress(); const auto c = resolveBootstrapCompletion(true, finalProg.state, s_open, s_cancelling); if (c.markRescan) s_app->markPostBootstrapRescanPending(); if (c.next == BootstrapCompletion::Next::Done) { s_state = State::Done; } else { s_errorMsg = finalProg.error; if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed"); s_state = State::Failed; } s_bootstrap.reset(); s_cancelling = false; if (c.releaseGate) s_app->setBootstrapDownloading(false); if (c.toast) { if (c.next == BootstrapCompletion::Next::Done) Notifications::instance().success(TR("bootstrap_success")); else Notifications::instance().error(std::string(TR("bootstrap_failed")) + ": " + s_errorMsg); } } 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(); // The download runs on its own thread: keep frames coming for the progress bar. if (s_state == State::Downloading) RequestProgressFrame(); OverlayDialogSpec ov; ov.title = TR("download_bootstrap"); ov.p_open = &s_open; ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 500.0f; ov.cardBottomViewportRatio = 0.94f; if (BeginOverlayDialog(ov)) { 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(); // Care header (reference style: ⚠ + Warning color, wraps if long). DialogWarningHeader(TR("bootstrap_warning")); 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 — centered as a group. float btnW = 140.0f * dp; float btnSm = 90.0f * dp; const float rowTotal = btnW * 2.0f + btnSm + ImGui::GetStyle().ItemSpacing.x * 2.0f; const float rowOff = (ImGui::GetContentRegionAvail().x - rowTotal) * 0.5f; if (rowOff > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rowOff); 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()) { material::Tooltip("%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 = TR("grpc_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 material::ProgressBar(ImGui::GetContentRegionAvail().x, 8.0f * dp, prog.percent / 100.0f); 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 — once clicked, disable it so the request is visibly // acknowledged (and can't be re-fired) until the worker actually stops. It is also disabled while // the old chain data is being replaced (cancelling then would only leave no chain at all). float btnW = 100.0f * dp; ImGui::BeginDisabled(s_cancelling || !prog.cancellable); if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) { // Refused (no-op) if the destructive window started after this frame read prog.cancellable. s_cancelling = s_bootstrap->requestCancel(); } ImGui::EndDisabled(); // Check completion (shared with App::update() so a closed dialog can't strand a finished bootstrap) pump(); } // ---- 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; // It stops the node first: not while it is busy or running a key import / rescan (as the wizard's bootstrap). if (s_app->refuseIfNodeBusy()) 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(); s_cancelling = false; } 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 bool s_cancelling = false; // cancel requested; disables the Cancel button until the worker stops static inline std::string s_errorMsg; }; } // namespace ui } // namespace dragonx