feat(daemon-update): two-pane version picker (versions left, detail right)
Redesign the Update Node dialog to mirror the Manage-Portfolio editor: the list of node versions on the LEFT (with latest / installed / pre-release badges + date), the selected version's detail on the RIGHT — status vs installed, release notes (checksum table + markdown heading markers stripped), the verify/downgrade note, and the install action (Download & install / Install this version / Reinstall). One Close in the footer. The dialog now fetches the full release list up front (startListReleases) so the picker is populated immediately; Downloading/Verifying/Done/Failed render in the right pane with the list still visible. Layout is GetContentRegionAvail-based so it adapts to a viewport-capped card (verified 100% + 150%, dark + light). Adds a sweep seam (sweepSeed/sweepClose) + a modal-daemon-update sweep surface seeded with fake releases for offline visual verification. Adversarially reviewed (3 lenses); fixed the real findings — live list was never populated from getReleases() (only the sweep seeded it), defensive s_rows bounds in the detail pane, notes box dropped when the pane is too short so the install button stays visible, and a badge-overrun guard for long tags. DPI "findings" were the recurring false positives (LegacySize/spacing/GetFrameHeight are all dp-scaled). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/send_tab.h"
|
#include "ui/windows/send_tab.h"
|
||||||
#include "ui/windows/wallets_dialog.h"
|
#include "ui/windows/wallets_dialog.h"
|
||||||
|
#include "ui/windows/daemon_download_dialog.h"
|
||||||
#include "util/platform.h"
|
#include "util/platform.h"
|
||||||
#include "wallet/wallet_capabilities.h"
|
#include "wallet/wallet_capabilities.h"
|
||||||
|
|
||||||
@@ -437,6 +438,35 @@ void App::buildSweepCatalog()
|
|||||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
|
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
|
||||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
|
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
|
||||||
|
|
||||||
|
// Daemon updater — the two-pane version picker (versions left, selected-version detail right).
|
||||||
|
// Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
|
||||||
|
add("modal-daemon-update", ui::NavPage::Settings,
|
||||||
|
[](App& a) {
|
||||||
|
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||||
|
util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||||
|
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||||
|
util::DaemonReleaseAsset as;
|
||||||
|
as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
|
||||||
|
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
|
||||||
|
r.assets.push_back(as);
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
std::vector<util::DaemonRelease> rels;
|
||||||
|
rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
|
||||||
|
"## What's new\n- Faster initial block sync\n- BIP39 seed-phrase wallet support\n"
|
||||||
|
"- Mempool handling fixes and lower memory use\n\n## Checksums\n| File | SHA-256 |\n"
|
||||||
|
"|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
|
||||||
|
rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
|
||||||
|
"## Notes\n- Stability improvements\n- RPC fixes\n", false));
|
||||||
|
rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
|
||||||
|
"- First tagged mainnet build\n", false));
|
||||||
|
rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
|
||||||
|
"Release candidate for the 1.1.0 series. Testing only.\n", true));
|
||||||
|
ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
|
||||||
|
"v1.0.3-dc45e7d90");
|
||||||
|
},
|
||||||
|
[](App&) { ui::DaemonUpdateDialog::sweepClose(); });
|
||||||
|
|
||||||
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
|
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
|
||||||
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
|
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
|
||||||
add("modal-folder-picker", ui::NavPage::Settings,
|
add("modal-folder-picker", ui::NavPage::Settings,
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
//
|
//
|
||||||
// Modal dialog to download / update the dragonxd full node from the project Gitea, driven by
|
// 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
|
// util::DaemonUpdater. Two-pane layout (mirrors the Manage-Portfolio editor): the list of node
|
||||||
// updater: Checking -> UpToDate/UpdateAvailable -> Downloading/Verifying/Extracting -> Done /
|
// versions on the LEFT, the selected version's detail + install action on the RIGHT. States follow
|
||||||
// Failed. Header-only (static inline state). The new binary is installed into the daemon directory
|
// the updater: Listing -> ReleaseList -> Downloading/Verifying/Extracting -> Done / Failed. The new
|
||||||
// without touching the running node; on Done the user is offered a daemon restart to apply it.
|
// binary is installed into the daemon directory without touching the running node; on Done the user
|
||||||
|
// is offered a daemon restart to apply it. Header-only (static inline state).
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -30,8 +34,11 @@ namespace ui {
|
|||||||
|
|
||||||
class DaemonUpdateDialog {
|
class DaemonUpdateDialog {
|
||||||
public:
|
public:
|
||||||
|
using Progress = util::DaemonUpdater::Progress;
|
||||||
|
using St = util::DaemonUpdater::State;
|
||||||
|
|
||||||
// `installedVersion` is the version scanned from the installed dragonxd (may be empty/unknown),
|
// `installedVersion` is the version scanned from the installed dragonxd (may be empty/unknown),
|
||||||
// used to tell "up to date" from "update available".
|
// used to badge the installed release and tell "up to date" from "update available".
|
||||||
static void show(App* app, const std::string& installedVersion) {
|
static void show(App* app, const std::string& installedVersion) {
|
||||||
if (!app) return;
|
if (!app) return;
|
||||||
s_app = app;
|
s_app = app;
|
||||||
@@ -39,10 +46,12 @@ public:
|
|||||||
s_notified = false;
|
s_notified = false;
|
||||||
s_installed_flag = false; // start each session clean (don't leak a prior install's flag)
|
s_installed_flag = false; // start each session clean (don't leak a prior install's flag)
|
||||||
s_installed_version = installedVersion;
|
s_installed_version = installedVersion;
|
||||||
|
s_sel = 0;
|
||||||
|
s_sweep = false;
|
||||||
s_rows.clear();
|
s_rows.clear();
|
||||||
s_releases.clear();
|
s_releases.clear();
|
||||||
s_updater = std::make_unique<util::DaemonUpdater>();
|
s_updater = std::make_unique<util::DaemonUpdater>();
|
||||||
s_updater->startCheck(installedVersion);
|
s_updater->startListReleases(); // fetch every version up front for the two-pane picker
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isOpen() { return s_open; }
|
static bool isOpen() { return s_open; }
|
||||||
@@ -55,133 +64,329 @@ public:
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sweep-only: seed fake releases + a forced state so the offline UI sweep can render the modal
|
||||||
|
// without a network fetch or a live updater.
|
||||||
|
static void sweepSeed(App* app, std::vector<util::DaemonRelease> releases, St state,
|
||||||
|
const std::string& installedVersion) {
|
||||||
|
s_app = app;
|
||||||
|
s_open = true;
|
||||||
|
s_notified = false;
|
||||||
|
s_installed_version = installedVersion;
|
||||||
|
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 = installedVersion;
|
||||||
|
s_sweepProgress.percent = 62.0f;
|
||||||
|
s_sweepProgress.status_text = "dragonx-1.0.3-linux-amd64.zip";
|
||||||
|
}
|
||||||
|
static void sweepClose() { s_open = false; s_sweep = false; s_releases.clear(); s_rows.clear(); }
|
||||||
|
|
||||||
static void render() {
|
static void render() {
|
||||||
if (!s_open || !s_app || !s_updater) {
|
if (!s_open || !s_app) {
|
||||||
if (!s_open) s_updater.reset(); // closed: drop the updater (dtor joins the worker)
|
if (!s_open) s_updater.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
using namespace material;
|
using namespace material;
|
||||||
const float dp = Layout::dpiScale();
|
const float dp = Layout::dpiScale();
|
||||||
const auto p = s_updater->getProgress();
|
const Progress p = s_sweep ? s_sweepProgress
|
||||||
|
: (s_updater ? s_updater->getProgress() : Progress{});
|
||||||
// No window X during an active download/verify/extract — closing would orphan/block on the
|
// No window X during an active download/verify/extract — closing would orphan/block on the
|
||||||
// worker; the in-dialog Cancel is the intended way to abort.
|
// worker; the in-dialog Cancel is the intended way to abort.
|
||||||
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
|
const bool active = (p.state == St::Downloading || p.state == St::Verifying || p.state == St::Extracting);
|
||||||
OverlayDialogSpec ov;
|
OverlayDialogSpec ov;
|
||||||
ov.title = TR("daemon_update_title"); ov.p_open = active ? nullptr : &s_open;
|
ov.title = TR("daemon_update_title");
|
||||||
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
|
ov.p_open = active ? nullptr : &s_open;
|
||||||
|
ov.style = OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 880.0f;
|
||||||
|
ov.cardHeight = 540.0f;
|
||||||
|
ov.idSuffix = "daemonupd";
|
||||||
if (BeginOverlayDialog(ov)) {
|
if (BeginOverlayDialog(ov)) {
|
||||||
switch (p.state) {
|
switch (p.state) {
|
||||||
case St::Checking: renderChecking(dp, p); break;
|
case St::Idle:
|
||||||
|
case St::Checking:
|
||||||
case St::UpToDate:
|
case St::UpToDate:
|
||||||
case St::UpdateAvailable: renderPrompt(dp, p); break;
|
case St::UpdateAvailable:
|
||||||
case St::Unavailable: renderUnavailable(dp, p); break;
|
case St::Listing: renderCentered(TR("daemon_update_loading")); break;
|
||||||
case St::Listing: renderListing(dp, p); break;
|
case St::Unavailable: renderUnavailable(p); break;
|
||||||
case St::ReleaseList: renderReleaseList(dp, p); break;
|
case St::Failed:
|
||||||
case St::Downloading:
|
// A failed release-list fetch has no versions to show — render a plain retry rather
|
||||||
case St::Verifying:
|
// than an empty two-pane. (An install that failed keeps the list, so falls through.)
|
||||||
case St::Extracting: renderProgress(dp, p); break;
|
if (s_releases.empty()) { renderListFailed(p); break; }
|
||||||
case St::Done: renderDone(dp, p); break;
|
renderTwoPane(dp, p, active); break;
|
||||||
case St::Failed: renderFailed(dp, p); break;
|
default: renderTwoPane(dp, p, active); break;
|
||||||
default: break;
|
|
||||||
}
|
}
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
}
|
}
|
||||||
if (!s_open) s_updater.reset();
|
if (!s_open) { s_updater.reset(); s_sweep = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using Progress = util::DaemonUpdater::Progress;
|
|
||||||
using St = util::DaemonUpdater::State;
|
|
||||||
|
|
||||||
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
||||||
|
|
||||||
static void installAction(const char* label) {
|
static void renderCentered(const char* msg) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
if (TactileButton(label, ImVec2(fullW(), 0)))
|
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||||
s_updater->startInstall(resources::getDaemonDirectory());
|
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 renderChecking(float, const Progress&) {
|
// A failed release-list fetch (no versions to show) — a plain centered retry / close.
|
||||||
|
static void renderListFailed(const Progress& p) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
Type().text(TypeStyle::Body2, TR("daemon_update_checking"));
|
const float bw = std::min(fullW(), 220.0f * Layout::dpiScale());
|
||||||
}
|
Type().textColored(TypeStyle::Subtitle2, Error(), TR("daemon_update_failed"));
|
||||||
|
|
||||||
static void renderUnavailable(float, const Progress& p) {
|
|
||||||
using namespace material;
|
|
||||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_unavailable_title"));
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", p.status_text.empty()
|
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str());
|
||||||
? TR("daemon_update_unavailable_body")
|
|
||||||
: p.status_text.c_str());
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
if (TactileButton(TR("retry"), ImVec2(bw, 0)) && s_updater) {
|
||||||
}
|
s_releases.clear();
|
||||||
|
|
||||||
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_rows.clear();
|
||||||
s_updater->startListReleases();
|
s_updater->startListReleases();
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
if (TactileButton(TR("close"), ImVec2(bw, 0))) s_open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void renderListing(float, const Progress&) {
|
static void renderUnavailable(const Progress& p) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
Type().text(TypeStyle::Body2, TR("daemon_update_loading"));
|
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(std::min(fullW(), 200.0f * Layout::dpiScale()), 0)))
|
||||||
|
s_open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void renderReleaseList(float dp, const Progress&) {
|
// Build the display rows from the fetched releases once per listing (release bodies are large).
|
||||||
|
static void buildRows() {
|
||||||
|
if (!s_rows.empty() || s_releases.empty()) return;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release-notes text with the checksum table / signature block stripped (the body is mostly a
|
||||||
|
// machine-readable checksum table). Returns "" when nothing human-readable remains.
|
||||||
|
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; // markdown table (checksums)
|
||||||
|
if (low.rfind("## checksum", 0) == 0 || low.rfind("#### checksum", 0) == 0 ||
|
||||||
|
low.rfind("checksums", 0) == 0 || low.rfind("### sha", 0) == 0) break;
|
||||||
|
// Strip a leading markdown heading marker ("## Foo" -> "Foo") for cleaner plain-text display.
|
||||||
|
std::string disp = lt;
|
||||||
|
if (!disp.empty() && disp[0] == '#') {
|
||||||
|
size_t h = disp.find_first_not_of('#');
|
||||||
|
if (h != std::string::npos) disp = disp.substr(disp.find_first_not_of(" \t", h));
|
||||||
|
else disp.clear();
|
||||||
|
} else {
|
||||||
|
disp = line; // keep original indentation for non-heading lines (bullets etc.)
|
||||||
|
}
|
||||||
|
out += disp;
|
||||||
|
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_sel < 0 || s_sel >= (int)s_releases.size()) return;
|
||||||
|
const util::DaemonRelease 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;
|
using namespace material;
|
||||||
// Snapshot the worker's list + build the rows once per listing (not every frame — the
|
// Snapshot the worker's release list once it's available (mutex-guarded getReleases()); the
|
||||||
// release bodies are large). Caches are cleared when a new listing starts (browse / show).
|
// sweep seeds s_releases directly, so don't clobber it there.
|
||||||
if (s_rows.empty()) {
|
if (!s_sweep && s_releases.empty() && s_updater) s_releases = s_updater->getReleases();
|
||||||
s_releases = s_updater->getReleases();
|
buildRows();
|
||||||
const std::string token = util::currentDaemonPlatformToken();
|
if (s_sel >= (int)s_rows.size()) s_sel = s_rows.empty() ? 0 : (int)s_rows.size() - 1;
|
||||||
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
|
||||||
s_rows.reserve(s_releases.size());
|
ImFont* bodyF = Type().body2();
|
||||||
for (const auto& r : s_releases) {
|
ImFont* capF = Type().caption();
|
||||||
ReleaseRow row;
|
|
||||||
row.tag = r.tag;
|
const bool showFooter = (p.state == St::ReleaseList);
|
||||||
row.title = r.name;
|
const float footerH = showFooter ? (ImGui::GetFrameHeight() + Layout::spacingSm() * 2.0f + 1.0f) : 0.0f;
|
||||||
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
|
const float bodyH = std::max(120.0f * dp, ImGui::GetContentRegionAvail().y - footerH);
|
||||||
row.prerelease = r.prerelease;
|
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||||
row.hasAsset = util::selectDaemonAsset(r, token) >= 0;
|
const float gap = Layout::spacingLg();
|
||||||
row.installed = !instCore.empty() && util::daemonVersionCore(r.tag) == instCore;
|
const float masterW = std::min(std::max(contentW * 0.34f, 240.0f * dp), 340.0f * dp);
|
||||||
s_rows.push_back(std::move(row));
|
const float detailW = contentW - masterW - gap;
|
||||||
|
|
||||||
|
// ---- LEFT: scrollable version list ----
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingXs(), Layout::spacingXs()));
|
||||||
|
ImGui::BeginChild("##daemonVersions", ImVec2(masterW, bodyH), true);
|
||||||
|
{
|
||||||
|
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||||
|
const float rowH = Layout::spacingSm() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize;
|
||||||
|
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
ImVec2 rmx(rmn.x + rowW, rmn.y + rowH);
|
||||||
|
// Accent bar: installed = green, newest = primary, else a dim rule.
|
||||||
|
ImU32 accent = r.installed ? Success() : (i == 0 ? Primary() : WithAlpha(OnSurface(), 55));
|
||||||
|
mdl->AddRectFilled(ImVec2(rmn.x, rmn.y + 4.0f * dp),
|
||||||
|
ImVec2(rmn.x + 3.0f * dp, rmx.y - 4.0f * dp), accent, 1.5f * dp);
|
||||||
|
float x = rmn.x + Layout::spacingSm() + 4.0f * dp;
|
||||||
|
float tagY = rmn.y + Layout::spacingSm();
|
||||||
|
mdl->AddText(bodyF, bodyF->LegacySize, ImVec2(x, tagY),
|
||||||
|
r.hasAsset ? OnSurface() : OnSurfaceDisabled(), r.tag.c_str());
|
||||||
|
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;
|
||||||
|
auto badge = [&](const char* t, ImU32 c) {
|
||||||
|
float tw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, t).x;
|
||||||
|
if (bx + tw > rmx.x - Layout::spacingXs()) return; // no room: skip (long tag)
|
||||||
|
mdl->AddText(capF, capF->LegacySize, ImVec2(bx, by), c, t);
|
||||||
|
bx += tw + Layout::spacingSm();
|
||||||
|
};
|
||||||
|
if (r.installed) badge(TR("upd_installed_badge"), Success());
|
||||||
|
else if (i == 0) badge(TR("daemon_update_latest_badge"), Primary());
|
||||||
|
if (r.prerelease) badge(TR("upd_prerelease"), Warning());
|
||||||
|
// Date line.
|
||||||
|
if (!r.date.empty())
|
||||||
|
mdl->AddText(capF, capF->LegacySize,
|
||||||
|
ImVec2(x, rmx.y - Layout::spacingSm() - capF->LegacySize),
|
||||||
|
OnSurfaceMedium(), r.date.c_str());
|
||||||
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Downgrade caution: an older node binary may not match the current chain data.
|
ImGui::EndChild();
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_downgrade_note"));
|
ImGui::PopStyleVar();
|
||||||
ImGui::Spacing();
|
|
||||||
bool back = false;
|
ImGui::SameLine(0.0f, gap);
|
||||||
const int idx = RenderReleaseList(s_rows, dp, &back);
|
|
||||||
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_version); return; }
|
// ---- RIGHT: detail / progress / done / failed for the selected version ----
|
||||||
if (idx >= 0 && idx < static_cast<int>(s_releases.size())) {
|
ImGui::BeginChild("##daemonDetail", ImVec2(detailW, bodyH), false);
|
||||||
const util::DaemonRelease rel = s_releases[idx];
|
switch (p.state) {
|
||||||
s_rows.clear();
|
case St::ReleaseList: renderDetail(dp, p); break;
|
||||||
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
|
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();
|
||||||
|
|
||||||
|
// ---- Footer: Close (only while browsing; transient states carry their own actions) ----
|
||||||
|
if (showFooter) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
const float bw = 130.0f * dp;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - bw));
|
||||||
|
if (TactileButton(TR("close"), ImVec2(bw, 0))) s_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right pane for the ReleaseList state: the selected version's detail + install action.
|
||||||
|
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::DaemonRelease& rel = s_releases[s_sel];
|
||||||
|
const ReleaseRow& row = s_rows[s_sel];
|
||||||
|
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
||||||
|
const bool haveInstalled = !instCore.empty();
|
||||||
|
|
||||||
|
// Header: version tag.
|
||||||
|
Type().text(TypeStyle::H6, rel.tag.c_str());
|
||||||
|
|
||||||
|
// Status line — installed / newer / older, colored.
|
||||||
|
if (row.installed) {
|
||||||
|
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_status_current"));
|
||||||
|
} else if (s_sel == 0) {
|
||||||
|
Type().textColored(TypeStyle::Subtitle2, Primary(),
|
||||||
|
haveInstalled ? TR("daemon_update_available") : TR("daemon_update_status_latest"));
|
||||||
|
} else if (haveInstalled) {
|
||||||
|
Type().textColored(TypeStyle::Subtitle2, Warning(), TR("daemon_update_status_older"));
|
||||||
|
} else {
|
||||||
|
Type().text(TypeStyle::Subtitle2, rel.name.empty() ? rel.tag.c_str() : rel.name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta: installed comparison + date.
|
||||||
|
const std::string installed = s_installed_version.empty() ? std::string(TR("xmrig_none"))
|
||||||
|
: s_installed_version;
|
||||||
|
ImGui::Spacing();
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), (std::string(TR("daemon_update_installed")) + " " + installed).c_str());
|
||||||
|
if (!row.date.empty())
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), (std::string(TR("daemon_update_released")) + " " + row.date).c_str());
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// Reserve the bottom for the verify/downgrade note + the install button; notes fill the rest.
|
||||||
|
const float btnH = ImGui::GetFrameHeight();
|
||||||
|
const bool downgrade = !row.installed && s_sel != 0 && haveInstalled;
|
||||||
|
const char* noteStr = !row.hasAsset ? TR("upd_no_build_platform")
|
||||||
|
: downgrade ? TR("daemon_update_downgrade_note")
|
||||||
|
: TR("daemon_update_verify_note");
|
||||||
|
const float noteW = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float noteH = ImGui::CalcTextSize(noteStr, nullptr, false, noteW).y;
|
||||||
|
const float reserve = noteH + btnH + Layout::spacingMd() * 2.0f;
|
||||||
|
// Notes fill whatever's left ABOVE the reserved verify-note + install button. If the pane is
|
||||||
|
// too short to fit a useful notes box, drop it entirely so the install button stays visible
|
||||||
|
// (never let a min-height notes child push the button into a scroll region).
|
||||||
|
const float roomForNotes = ImGui::GetContentRegionAvail().y - reserve;
|
||||||
|
if (roomForNotes >= 44.0f * dp) {
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingSm(), Layout::spacingSm()));
|
||||||
|
ImGui::BeginChild("##daemonNotes", ImVec2(0, roomForNotes), true);
|
||||||
|
{
|
||||||
|
const std::string notes = cleanNotes(rel.body);
|
||||||
|
if (notes.empty())
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("daemon_update_no_notes"));
|
||||||
|
else
|
||||||
|
ImGui::TextWrapped("%s", notes.c_str());
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
Type().textColored(TypeStyle::Caption, downgrade ? Warning() : OnSurfaceMedium(), noteStr);
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// Install button (full detail width).
|
||||||
|
const char* label = row.installed ? TR("daemon_update_reinstall")
|
||||||
|
: (s_sel == 0) ? TR("daemon_update_download_install")
|
||||||
|
: TR("daemon_update_install_this");
|
||||||
|
ImGui::BeginDisabled(!row.hasAsset);
|
||||||
|
if (TactileButton(label, ImVec2(fullW(), 0))) startInstallSelected();
|
||||||
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void renderProgress(float dp, const Progress& p) {
|
static void renderProgress(float dp, const Progress& p) {
|
||||||
@@ -193,19 +398,16 @@ private:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
|
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Text("%s", p.status_text.c_str());
|
ImGui::TextWrapped("%s", p.status_text.c_str());
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
// Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort).
|
|
||||||
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
|
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
|
||||||
s_updater->cancel();
|
if (s_updater) s_updater->cancel();
|
||||||
s_open = false;
|
s_open = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void renderDone(float, const Progress& p) {
|
static void renderDone(float, const Progress& p) {
|
||||||
using namespace material;
|
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; }
|
if (!s_notified) { s_installed_flag = true; s_notified = true; }
|
||||||
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -231,7 +433,7 @@ private:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str());
|
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_unknown_error") : p.error.c_str());
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
installAction(TR("retry"));
|
if (TactileButton(TR("retry"), ImVec2(fullW(), 0))) startInstallSelected();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
}
|
}
|
||||||
@@ -239,9 +441,12 @@ private:
|
|||||||
static inline bool s_open = false;
|
static inline bool s_open = false;
|
||||||
static inline bool s_installed_flag = false;
|
static inline bool s_installed_flag = false;
|
||||||
static inline bool s_notified = false;
|
static inline bool s_notified = false;
|
||||||
|
static inline bool s_sweep = false;
|
||||||
|
static inline int s_sel = 0; // selected version (index into s_releases)
|
||||||
static inline std::string s_installed_version;
|
static inline std::string s_installed_version;
|
||||||
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||||
static inline std::vector<util::DaemonRelease> s_releases; // matching releases (install by index)
|
static inline std::vector<util::DaemonRelease> s_releases; // matching releases (install by index)
|
||||||
|
static inline Progress s_sweepProgress; // forced state for the offline UI sweep
|
||||||
static inline App* s_app = nullptr;
|
static inline App* s_app = nullptr;
|
||||||
static inline std::unique_ptr<util::DaemonUpdater> s_updater;
|
static inline std::unique_ptr<util::DaemonUpdater> s_updater;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1481,6 +1481,13 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["daemon_update_browse"] = "Browse all releases…";
|
strings_["daemon_update_browse"] = "Browse all releases…";
|
||||||
strings_["daemon_update_loading"] = "Loading releases…";
|
strings_["daemon_update_loading"] = "Loading releases…";
|
||||||
strings_["daemon_update_downgrade_note"] = "Older versions may be incompatible with your current chain data. Installing a different version takes effect after a daemon restart.";
|
strings_["daemon_update_downgrade_note"] = "Older versions may be incompatible with your current chain data. Installing a different version takes effect after a daemon restart.";
|
||||||
|
strings_["daemon_update_latest_badge"] = "latest";
|
||||||
|
strings_["daemon_update_status_current"] = "This version is installed";
|
||||||
|
strings_["daemon_update_status_latest"] = "Latest version";
|
||||||
|
strings_["daemon_update_status_older"] = "Older than your installed version";
|
||||||
|
strings_["daemon_update_released"] = "Released:";
|
||||||
|
strings_["daemon_update_no_notes"] = "No release notes for this version.";
|
||||||
|
strings_["daemon_update_install_this"] = "Install this version";
|
||||||
|
|
||||||
// --- Lite Network tab (server browser) ---
|
// --- Lite Network tab (server browser) ---
|
||||||
strings_["lite_net_title"] = "Lite Servers";
|
strings_["lite_net_title"] = "Lite Servers";
|
||||||
|
|||||||
Reference in New Issue
Block a user