// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Modal dialog to download / update the dragonxd full node from the project Gitea, driven by // util::DaemonUpdater. Sibling of XmrigDownloadDialog (the miner updater). States follow the // updater: Checking -> UpToDate/UpdateAvailable -> Downloading/Verifying/Extracting -> Done / // Failed. Header-only (static inline state). The new binary is installed into the daemon directory // without touching the running node; on Done the user is offered a daemon restart to apply it. #pragma once #include #include #include #include "../../app.h" #include "../../resources/embedded_resources.h" // resources::getDaemonDirectory() #include "../../util/daemon_updater.h" #include "../../util/i18n.h" #include "../material/colors.h" #include "../material/draw_helpers.h" #include "../material/type.h" #include "../theme.h" #include "release_list_view.h" #include "imgui.h" namespace dragonx { namespace ui { class DaemonUpdateDialog { public: // `installedVersion` is the version scanned from the installed dragonxd (may be empty/unknown), // used to tell "up to date" from "update available". static void show(App* app, const std::string& installedVersion) { if (!app) return; s_app = app; s_open = true; s_notified = false; s_installed_flag = false; // start each session clean (don't leak a prior install's flag) s_installed_version = installedVersion; s_rows.clear(); s_releases.clear(); s_updater = std::make_unique(); s_updater->startCheck(installedVersion); } static bool isOpen() { return s_open; } // True (once) after an install succeeded, so the Settings panel can refresh its cached daemon // info. Clears on read. static bool consumeInstalled() { const bool v = s_installed_flag; s_installed_flag = false; return v; } static void render() { if (!s_open || !s_app || !s_updater) { if (!s_open) s_updater.reset(); // closed: drop the updater (dtor joins the worker) return; } using namespace material; const float dp = Layout::dpiScale(); if (BeginOverlayDialog(TR("daemon_update_title"), &s_open, 480.0f, 0.94f)) { const auto p = s_updater->getProgress(); using St = util::DaemonUpdater::State; switch (p.state) { case St::Checking: renderChecking(dp, p); break; case St::UpToDate: case St::UpdateAvailable: renderPrompt(dp, p); break; case St::Unavailable: renderUnavailable(dp, p); break; case St::Listing: renderListing(dp, p); break; case St::ReleaseList: renderReleaseList(dp, p); break; case St::Downloading: case St::Verifying: case St::Extracting: renderProgress(dp, p); break; case St::Done: renderDone(dp, p); break; case St::Failed: renderFailed(dp, p); break; default: break; } EndOverlayDialog(); } if (!s_open) s_updater.reset(); } private: using Progress = util::DaemonUpdater::Progress; using St = util::DaemonUpdater::State; static float fullW() { return ImGui::GetContentRegionAvail().x; } static void installAction(const char* label) { using namespace material; if (TactileButton(label, ImVec2(fullW(), 0))) s_updater->startInstall(resources::getDaemonDirectory()); } static void renderChecking(float, const Progress&) { using namespace material; Type().text(TypeStyle::Body2, TR("daemon_update_checking")); } static void renderUnavailable(float, const Progress& p) { using namespace material; Type().text(TypeStyle::Subtitle2, TR("daemon_update_unavailable_title")); ImGui::Spacing(); ImGui::TextWrapped("%s", p.status_text.empty() ? TR("daemon_update_unavailable_body") : p.status_text.c_str()); ImGui::Spacing(); if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false; } static void renderPrompt(float, const Progress& p) { using namespace material; const std::string installed = p.installed_tag.empty() ? TR("xmrig_none") : p.installed_tag; if (p.state == St::UpdateAvailable) { Type().text(TypeStyle::Subtitle2, TR("daemon_update_available")); ImGui::Spacing(); ImGui::Text("%s %s", TR("daemon_update_latest"), p.latest_tag.c_str()); ImGui::Text("%s %s", TR("daemon_update_installed"), installed.c_str()); ImGui::Spacing(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_verify_note")); ImGui::Spacing(); installAction(TR("daemon_update_download_install")); } else { Type().text(TypeStyle::Subtitle2, TR("daemon_update_up_to_date")); ImGui::Spacing(); ImGui::Text("%s %s", TR("daemon_update_installed"), p.latest_tag.c_str()); ImGui::Spacing(); installAction(TR("daemon_update_reinstall")); } ImGui::Spacing(); // Browse all releases (pre-releases / older versions). if (TactileButton(TR("daemon_update_browse"), ImVec2(fullW(), 0))) { s_rows.clear(); s_updater->startListReleases(); } ImGui::Spacing(); if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false; } static void renderListing(float, const Progress&) { using namespace material; Type().text(TypeStyle::Body2, TR("daemon_update_loading")); } static void renderReleaseList(float dp, const Progress&) { using namespace material; // Snapshot the worker's list + build the rows once per listing (not every frame — the // release bodies are large). Caches are cleared when a new listing starts (browse / show). if (s_rows.empty()) { s_releases = s_updater->getReleases(); const std::string token = util::currentDaemonPlatformToken(); const std::string instCore = util::daemonVersionCore(s_installed_version); s_rows.reserve(s_releases.size()); for (const auto& r : s_releases) { ReleaseRow row; row.tag = r.tag; row.title = r.name; row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt; row.prerelease = r.prerelease; row.hasAsset = util::selectDaemonAsset(r, token) >= 0; row.installed = !instCore.empty() && util::daemonVersionCore(r.tag) == instCore; s_rows.push_back(std::move(row)); } } // Downgrade caution: an older node binary may not match the current chain data. Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_downgrade_note")); ImGui::Spacing(); bool back = false; const int idx = RenderReleaseList(s_rows, dp, &back); if (back) { s_rows.clear(); s_updater->startCheck(s_installed_version); return; } if (idx >= 0 && idx < static_cast(s_releases.size())) { const util::DaemonRelease rel = s_releases[idx]; s_rows.clear(); s_updater->startInstallRelease(resources::getDaemonDirectory(), rel); } } static void renderProgress(float dp, const Progress& p) { using namespace material; const char* title = p.state == St::Downloading ? TR("daemon_update_downloading") : p.state == St::Verifying ? TR("daemon_update_verifying") : TR("daemon_update_installing"); Type().text(TypeStyle::Subtitle2, title); ImGui::Spacing(); const float barH = 8.0f * dp; const float barW = fullW(); const ImVec2 bMin = ImGui::GetCursorScreenPos(); const ImVec2 bMax(bMin.x + barW, bMin.y + barH); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp); const float fillW = barW * (p.percent / 100.0f); if (fillW > 0) dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp); ImGui::Dummy(ImVec2(0, barH)); ImGui::Spacing(); ImGui::Text("%s", p.status_text.c_str()); ImGui::Spacing(); // Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort). if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) { s_updater->cancel(); s_open = false; } } static void renderDone(float, const Progress& p) { using namespace material; // Tell the Settings panel to refresh its cached daemon info — once, not every frame (a // re-scan reads the whole daemon binary). if (!s_notified) { s_installed_flag = true; s_notified = true; } Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok")); ImGui::Spacing(); ImGui::Text("%s %s", TR("daemon_update_version"), p.latest_tag.c_str()); ImGui::Spacing(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_restart_note")); ImGui::Spacing(); if (s_app->isUsingEmbeddedDaemon()) { if (TactileButton(TR("daemon_update_restart_now"), ImVec2(fullW(), 0))) { s_app->restartDaemon(); s_open = false; } ImGui::Spacing(); if (TactileButton(TR("daemon_update_later"), ImVec2(fullW(), 0))) s_open = false; } else { if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false; } } static void renderFailed(float, const Progress& p) { using namespace material; Type().textColored(TypeStyle::Subtitle2, Error(), TR("daemon_update_failed")); ImGui::Spacing(); ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str()); ImGui::Spacing(); installAction(TR("retry")); ImGui::Spacing(); if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false; } static inline bool s_open = false; static inline bool s_installed_flag = false; static inline bool s_notified = false; static inline std::string s_installed_version; static inline std::vector s_rows; // built once per listing (UI rows) static inline std::vector s_releases; // matching releases (install by index) static inline App* s_app = nullptr; static inline std::unique_ptr s_updater; }; } // namespace ui } // namespace dragonx