// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Modal dialog to download / update the DRG-XMRig miner from the project Gitea, driven by // util::XmrigUpdater. States follow the updater: Checking -> UpToDate/UpdateAvailable -> // Downloading/Verifying/Extracting -> Done / Failed. Header-only (static inline state), mirroring // BootstrapDownloadDialog. The installed release tag is persisted to settings on success. #pragma once #include #include #include #include "../../app.h" #include "../../config/settings.h" #include "../../resources/embedded_resources.h" // resources::getDaemonDirectory() #include "../../util/i18n.h" #include "../../util/xmrig_updater.h" #include "../material/draw_helpers.h" #include "../material/type.h" #include "../material/colors.h" #include "../theme.h" #include "release_list_view.h" #include "imgui.h" namespace dragonx { namespace ui { class XmrigDownloadDialog { public: static void show(App* app) { if (!app) return; s_app = app; s_open = true; s_persisted = false; s_installed_tag = app->settings() ? app->settings()->getXmrigVersion() : std::string(); s_rows.clear(); s_releases.clear(); s_updater = std::make_unique(); s_updater->startCheck(s_installed_tag); } static bool isOpen() { return s_open; } 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("xmrig_update_title"), &s_open, 480.0f, 0.94f)) { const auto p = s_updater->getProgress(); using St = util::XmrigUpdater::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::XmrigUpdater::Progress; using St = util::XmrigUpdater::State; static float fullW() { return ImGui::GetContentRegionAvail().x; } // Install button that refuses (with a note) while the miner is running, re-checked live so a // dialog opened before mining started can't replace a now-running binary (TOCTOU guard). static void installAction(const char* label) { using namespace material; if (s_app->isPoolMinerRunning()) { Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("xmrig_stop_mining_first")); return; } 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("xmrig_checking")); } static void renderUnavailable(float, const Progress& p) { using namespace material; Type().text(TypeStyle::Subtitle2, TR("xmrig_unavailable_title")); ImGui::Spacing(); ImGui::TextWrapped("%s", p.status_text.empty() ? TR("xmrig_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() ? "none" : p.installed_tag; if (p.state == St::UpdateAvailable) { Type().text(TypeStyle::Subtitle2, TR("xmrig_update_available")); ImGui::Spacing(); ImGui::Text("%s %s", TR("xmrig_latest"), p.latest_tag.c_str()); ImGui::Text("%s %s", TR("xmrig_installed"), installed.c_str()); ImGui::Spacing(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("xmrig_verify_note")); ImGui::Spacing(); installAction(TR("xmrig_download_install")); } else { Type().text(TypeStyle::Subtitle2, TR("xmrig_up_to_date")); ImGui::Spacing(); ImGui::Text("%s %s", TR("xmrig_installed"), p.latest_tag.c_str()); ImGui::Spacing(); installAction(TR("xmrig_reinstall")); } ImGui::Spacing(); // Browse all releases (pre-releases / older versions). if (TactileButton(TR("xmrig_browse_releases"), 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("xmrig_loading_releases")); } static void renderReleaseList(float dp, const Progress&) { using namespace material; // Snapshot + build rows once per listing (not every frame). Cleared on browse / show. if (s_rows.empty()) { s_releases = s_updater->getReleases(); const std::string token = util::currentXmrigPlatformToken(); 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::selectXmrigAsset(r, token) >= 0; row.installed = !s_installed_tag.empty() && r.tag == s_installed_tag; s_rows.push_back(std::move(row)); } } // Can't replace a running miner binary — disable installs while mining (TOCTOU guard). const char* disabledNote = s_app->isPoolMinerRunning() ? TR("xmrig_stop_mining_first") : nullptr; bool back = false; const int idx = RenderReleaseList(s_rows, dp, &back, disabledNote); if (back) { s_rows.clear(); s_updater->startCheck(s_installed_tag); return; } if (idx >= 0 && !s_app->isPoolMinerRunning() && idx < static_cast(s_releases.size())) { const util::XmrigRelease 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("xmrig_downloading") : p.state == St::Verifying ? TR("xmrig_verifying") : TR("xmrig_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; if (!s_persisted) { if (s_app->settings() && !p.latest_tag.empty()) { s_app->settings()->setXmrigVersion(p.latest_tag); s_app->settings()->save(); } s_persisted = true; } Type().textColored(TypeStyle::Subtitle2, Success(), TR("xmrig_installed_ok")); ImGui::Spacing(); ImGui::Text("%s %s", TR("xmrig_version"), p.latest_tag.c_str()); ImGui::Spacing(); 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("xmrig_update_failed")); ImGui::Spacing(); ImGui::TextWrapped("%s", p.error.empty() ? TR("xmrig_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_persisted = false; static inline std::string s_installed_tag; 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