// 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. Two-pane layout (mirrors the node updater / Manage-Portfolio editor): the list // of miner versions on the LEFT, the selected version's detail + install action on the RIGHT. // States: Listing -> ReleaseList -> Downloading/Verifying/Extracting -> Done / Failed. Header-only. // The installed release tag is persisted to settings on success. Installs are refused while the // miner is running (you can't replace a running binary). #pragma once #include #include #include #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/colors.h" #include "../material/draw_helpers.h" #include "../material/markdown.h" #include "../material/type.h" #include "../theme.h" #include "release_list_view.h" #include "imgui.h" namespace dragonx { namespace ui { class XmrigDownloadDialog { public: using Progress = util::XmrigUpdater::Progress; using St = util::XmrigUpdater::State; static void show(App* app) { if (!app) return; s_app = app; s_open = true; s_persisted = false; s_sel = 0; s_sweep = false; s_installed_tag = app->settings() ? app->settings()->getXmrigVersion() : std::string(); s_rows.clear(); s_releases.clear(); s_updater = std::make_unique(); s_updater->startListReleases(); // fetch every version up front for the two-pane picker } static bool isOpen() { return s_open; } // Sweep-only: seed fake releases + a forced state so the offline UI sweep can render the modal. static void sweepSeed(App* app, std::vector releases, St state, const std::string& installedTag) { s_app = app; s_open = true; s_persisted = false; s_installed_tag = installedTag; s_sel = 0; s_sweep = true; s_releases = std::move(releases); s_rows.clear(); s_updater.reset(); s_sweepProgress = Progress{}; s_sweepProgress.state = state; if (!s_releases.empty()) s_sweepProgress.latest_tag = s_releases.front().tag; s_sweepProgress.installed_tag = installedTag; s_sweepProgress.percent = 62.0f; s_sweepProgress.status_text = "drg-xmrig-linux-x64.zip"; } static void sweepClose() { s_open = false; s_sweep = false; s_releases.clear(); s_rows.clear(); } static void render() { if (!s_open || !s_app) { if (!s_open) s_updater.reset(); return; } using namespace material; const float dp = Layout::dpiScale(); const Progress p = s_sweep ? s_sweepProgress : (s_updater ? s_updater->getProgress() : Progress{}); const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting); OverlayDialogSpec ov; ov.title = TR("xmrig_update_title"); ov.p_open = active ? nullptr : &s_open; ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 880.0f; ov.cardHeight = std::min(760.0f, ImGui::GetMainViewport()->Size.y * 0.84f / dp); ov.idSuffix = "xmrigupd"; if (BeginOverlayDialog(ov)) { switch (p.state) { case St::Idle: case St::Checking: case St::UpToDate: case St::UpdateAvailable: case St::Listing: renderCentered(TR("xmrig_loading_releases")); break; case St::Unavailable: renderUnavailable(p); break; case St::Failed: if (s_releases.empty()) { renderListFailed(p); break; } renderTwoPane(dp, p, active); break; default: renderTwoPane(dp, p, active); break; } EndOverlayDialog(); } if (!s_open) { s_updater.reset(); s_sweep = false; } } private: static float fullW() { return ImGui::GetContentRegionAvail().x; } static void renderCentered(const char* msg) { using namespace material; ImVec2 av = ImGui::GetContentRegionAvail(); ImFont* f = Type().body2(); ImVec2 ts = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, msg); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + std::max(0.0f, (av.x - ts.x) * 0.5f), ImGui::GetCursorPosY() + av.y * 0.45f)); Type().text(TypeStyle::Body2, msg); } static void renderUnavailable(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(std::min(fullW(), 200.0f * Layout::dpiScale()), 0))) s_open = false; } static void renderListFailed(const Progress& p) { using namespace material; const float bw = std::min(fullW(), 220.0f * Layout::dpiScale()); 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(); if (TactileButton(TR("retry"), ImVec2(bw, 0)) && s_updater) { s_releases.clear(); s_rows.clear(); s_updater->startListReleases(); } ImGui::Spacing(); if (TactileButton(TR("close"), ImVec2(bw, 0))) s_open = false; } static void buildRows() { if (!s_rows.empty() || s_releases.empty()) return; 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)); } } static std::string cleanNotes(const std::string& body) { std::istringstream ss(body); std::string line, out; while (std::getline(ss, line)) { size_t a = line.find_first_not_of(" \t"); std::string lt = (a == std::string::npos) ? std::string() : line.substr(a); std::string low = lt; std::transform(low.begin(), low.end(), low.begin(), [](unsigned char c) { return (char)std::tolower(c); }); if (!lt.empty() && lt[0] == '|') break; if (low.rfind("## checksum", 0) == 0 || low.rfind("#### checksum", 0) == 0 || low.rfind("checksums", 0) == 0 || low.rfind("### sha", 0) == 0) break; out += line; out += '\n'; } while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ' || out.back() == '\t')) out.pop_back(); return out; } static void startInstallSelected() { if (!s_updater || s_app->isPoolMinerRunning()) return; // can't replace a running miner if (s_sel < 0 || s_sel >= (int)s_releases.size()) return; const util::XmrigRelease rel = s_releases[s_sel]; s_updater->startInstallRelease(resources::getDaemonDirectory(), rel); } // ================= Two-pane: version list (left) + detail/progress (right) ================= static void renderTwoPane(float dp, const Progress& p, bool active) { using namespace material; if (!s_sweep && s_releases.empty() && s_updater) s_releases = s_updater->getReleases(); buildRows(); if (s_sel >= (int)s_rows.size()) s_sel = s_rows.empty() ? 0 : (int)s_rows.size() - 1; ImFont* bodyF = Type().body2(); ImFont* capF = Type().caption(); const bool showFooter = (p.state == St::ReleaseList); const float footerH = showFooter ? (ImGui::GetFrameHeight() * 1.25f + ImGui::GetStyle().ItemSpacing.y * 2.0f + Layout::spacingSm()) : 0.0f; const float bodyH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - footerH); const float contentW = ImGui::GetContentRegionAvail().x; const float gap = Layout::spacingLg(); const float masterW = std::min(std::max(contentW * 0.34f, 240.0f * dp), 340.0f * dp); const float detailW = contentW - masterW - gap; // ---- LEFT: borderless list of tactile version cards ---- ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::BeginChild("##xmrigVersions", ImVec2(masterW, bodyH), false); { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, Layout::spacingSm())); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0)); ImDrawList* mdl = ImGui::GetWindowDrawList(); ImFont* icoF = Type().iconSmall(); const float rowH = Layout::spacingMd() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize; const float rowW = ImGui::GetContentRegionAvail().x; const float round = 8.0f * dp; for (int i = 0; i < (int)s_rows.size(); ++i) { const ReleaseRow& r = s_rows[i]; ImGui::PushID(i); bool sel = (s_sel == i); ImVec2 rmn = ImGui::GetCursorScreenPos(); if (ImGui::Selectable("##ver", sel, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) { if (!active) s_sel = i; } bool hov = ImGui::IsItemHovered(); bool held = ImGui::IsItemActive(); ImVec2 rmx(rmn.x + rowW, rmn.y + rowH); ImU32 fill = sel ? WithAlpha(Primary(), held ? 55 : 42) : held ? WithAlpha(OnSurface(), 34) : hov ? WithAlpha(OnSurface(), 24) : WithAlpha(OnSurface(), 12); mdl->AddRectFilled(rmn, rmx, fill, round); if (sel) mdl->AddRect(rmn, rmx, WithAlpha(Primary(), 150), round, 0, 1.6f * dp); if (hov || held) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); const char* ico = nullptr; ImU32 statusC = 0; const char* tt = nullptr; const char* lbl = nullptr; if (r.installed) { ico = ICON_MD_CHECK_CIRCLE; statusC = Success(); tt = TR("xmrig_active_tt"); lbl = TR("upd_installed_badge"); } else if (r.prerelease) { ico = ICON_MD_SCIENCE; statusC = Warning(); tt = TR("xmrig_prerelease_tt"); lbl = TR("upd_prerelease"); } else if (i == 0) { ico = ICON_MD_NEW_RELEASES; statusC = Primary(); tt = TR("xmrig_latest_tt"); lbl = TR("xmrig_latest_badge"); } float rightLimit = rmx.x - Layout::spacingMd(); if (ico) { float cs = bodyF->LegacySize * 1.3f; ImVec2 cp(rmx.x - Layout::spacingMd() - cs, (rmn.y + rmx.y) * 0.5f - cs * 0.5f); mdl->AddText(icoF, cs, cp, statusC, ico); if (tt && hov && ImGui::IsMouseHoveringRect(cp, ImVec2(cp.x + cs, cp.y + cs))) Tooltip("%s", tt); rightLimit = cp.x - Layout::spacingSm(); } float x = rmn.x + Layout::spacingMd(); float tagY = rmn.y + Layout::spacingMd(); mdl->AddText(bodyF, bodyF->LegacySize, ImVec2(x, tagY), r.hasAsset ? OnSurface() : OnSurfaceDisabled(), r.tag.c_str()); if (lbl) { float bx = x + bodyF->CalcTextSizeA(bodyF->LegacySize, FLT_MAX, 0, r.tag.c_str()).x + Layout::spacingSm(); float by = tagY + (bodyF->LegacySize - capF->LegacySize) * 0.5f; if (bx + capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, lbl).x <= rightLimit) mdl->AddText(capF, capF->LegacySize, ImVec2(bx, by), statusC, lbl); } if (!r.date.empty()) mdl->AddText(capF, capF->LegacySize, ImVec2(x, rmx.y - Layout::spacingMd() - capF->LegacySize), OnSurfaceMedium(), r.date.c_str()); ImGui::PopID(); } ImGui::PopStyleColor(3); ImGui::PopStyleVar(); } ImGui::EndChild(); ImGui::PopStyleVar(); ImGui::SameLine(0.0f, gap); // ---- RIGHT: detail / progress / done / failed for the selected version ---- ImGui::BeginChild("##xmrigDetail", ImVec2(detailW, bodyH), false); switch (p.state) { case St::ReleaseList: renderDetail(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; } ImGui::EndChild(); if (showFooter) { ImGui::Spacing(); const float bw = 170.0f * dp, bh = ImGui::GetFrameHeight() * 1.25f; ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - bw)); if (TactileButton(TR("close"), ImVec2(bw, bh))) s_open = false; } } static void renderDetail(float dp, const Progress&) { using namespace material; if (s_sel < 0 || s_sel >= (int)s_releases.size() || s_sel >= (int)s_rows.size()) return; const util::XmrigRelease& rel = s_releases[s_sel]; const ReleaseRow& row = s_rows[s_sel]; const bool haveInstalled = !s_installed_tag.empty(); const bool downgrade = !row.installed && s_sel != 0 && haveInstalled; const bool mining = s_app->isPoolMinerRunning(); const char* noteStr = mining ? TR("xmrig_stop_mining_first") : !row.hasAsset ? TR("upd_no_build_platform") : downgrade ? TR("xmrig_downgrade_note") : TR("xmrig_verify_note"); const char* label = row.installed ? TR("xmrig_reinstall") : (s_sel == 0) ? TR("xmrig_download_install") : TR("xmrig_install_this"); const float btnH = ImGui::GetFrameHeight() * 1.15f; const float noteH = ImGui::CalcTextSize(noteStr, nullptr, false, ImGui::GetContentRegionAvail().x).y; const float reserve = noteH + btnH + Layout::spacingMd() * 3.0f; const float infoH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - reserve); // ---- Info card: ONE rounded Material surface (version header + release notes markdown) ---- ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 22)); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingLg())); ImGui::BeginChild("##xmrigInfo", ImVec2(0, infoH), ImGuiChildFlags_AlwaysUseWindowPadding); { Type().text(TypeStyle::H6, rel.tag.c_str()); if (row.installed) Type().textColored(TypeStyle::Subtitle2, Success(), TR("xmrig_status_current")); else if (s_sel == 0) Type().textColored(TypeStyle::Subtitle2, Primary(), haveInstalled ? TR("xmrig_update_available") : TR("xmrig_status_latest")); else if (haveInstalled) Type().textColored(TypeStyle::Subtitle2, Warning(), TR("xmrig_status_older")); else Type().text(TypeStyle::Subtitle2, rel.name.empty() ? rel.tag.c_str() : rel.name.c_str()); const std::string installed = s_installed_tag.empty() ? std::string(TR("xmrig_none")) : s_installed_tag; ImGui::Spacing(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), (std::string(TR("xmrig_installed")) + " " + installed).c_str()); if (!row.date.empty()) Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), (std::string(TR("xmrig_released")) + " " + row.date).c_str()); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); const std::string notes = cleanNotes(rel.body); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); ImGui::BeginChild("##xmrigNotes", ImVec2(0, std::max(0.0f, ImGui::GetContentRegionAvail().y)), false); if (notes.empty()) Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("xmrig_no_notes")); else RenderMarkdown(notes); ImGui::EndChild(); ImGui::PopStyleColor(); } ImGui::EndChild(); ImGui::PopStyleVar(2); ImGui::PopStyleColor(); // ---- Below the info card: verify / stop-mining note + install button (centered, text-fit) ---- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); Type().textColored(TypeStyle::Caption, (mining || downgrade) ? Warning() : OnSurfaceMedium(), noteStr); ImGui::Spacing(); const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f)); ImGui::BeginDisabled(!row.hasAsset || mining); if (TactileButton(label, ImVec2(bw, btnH))) startInstallSelected(); ImGui::EndDisabled(); } 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(); material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f); ImGui::Spacing(); ImGui::TextWrapped("%s", p.status_text.c_str()); ImGui::Spacing(); if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) { if (s_updater) 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(); ImGui::BeginDisabled(s_app->isPoolMinerRunning()); if (TactileButton(TR("retry"), ImVec2(fullW(), 0))) startInstallSelected(); ImGui::EndDisabled(); 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 bool s_sweep = false; static inline int s_sel = 0; 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 Progress s_sweepProgress; static inline App* s_app = nullptr; static inline std::unique_ptr s_updater; }; } // namespace ui } // namespace dragonx