feat(updater): in-app dragonxd updater + browse-all-releases

Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog)
reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 +
enforced ed25519 signature) and atomically installs the latest dragonxd from
the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all
releases..." picker (release_list_view) to both the miner and daemon updaters
so users can pin older/pre-release builds. Pure no-I/O cores
(daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh
signs release archives offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 21:27:13 -05:00
parent 2e8e214689
commit 4473e7e00a
19 changed files with 1848 additions and 108 deletions

View File

@@ -40,6 +40,7 @@
#include "../windows/export_all_keys_dialog.h"
#include "../windows/export_transactions_dialog.h"
#include "../windows/bootstrap_download_dialog.h"
#include "../windows/daemon_download_dialog.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
@@ -642,7 +643,7 @@ void RenderSettingsPage(App* app) {
ImGui::EndDisabled();
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", skin.validationError.c_str());
material::Tooltip("%s", skin.validationError.c_str());
} else {
std::string lbl = skin.name;
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
@@ -684,7 +685,7 @@ void RenderSettingsPage(App* app) {
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
material::Tooltip("%s", TR("tt_theme_hotkey"));
ImGui::SameLine(0, comboGap);
ImGui::AlignTextToFramePadding();
@@ -707,7 +708,7 @@ void RenderSettingsPage(App* app) {
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
material::Tooltip("%s", TR("tt_layout_hotkey"));
ImGui::SameLine(0, comboGap);
ImGui::AlignTextToFramePadding();
@@ -724,7 +725,7 @@ void RenderSettingsPage(App* app) {
app->settings()->save();
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_language"));
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
@@ -732,7 +733,7 @@ void RenderSettingsPage(App* app) {
Notifications::instance().info("Theme list refreshed");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(TR("tt_scan_themes"),
material::Tooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::PopFont();
@@ -762,7 +763,7 @@ void RenderSettingsPage(App* app) {
Layout::setUserFontScale(s_settingsState.font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
}
@@ -830,20 +831,20 @@ void RenderSettingsPage(App* app) {
}
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_settingsState.gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion"));
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
@@ -853,14 +854,14 @@ void RenderSettingsPage(App* app) {
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
app->settings()->save();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &s_settingsState.theme_effects_enabled)) {
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
@@ -886,7 +887,7 @@ void RenderSettingsPage(App* app) {
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur"));
float afterRow1Y = ImGui::GetCursorScreenPos().y;
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
@@ -906,7 +907,7 @@ void RenderSettingsPage(App* app) {
saveSettingsPageState(app->settings());
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise"));
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
@@ -922,7 +923,7 @@ void RenderSettingsPage(App* app) {
saveSettingsPageState(app->settings());
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity"));
float afterRow2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
@@ -937,7 +938,7 @@ void RenderSettingsPage(App* app) {
saveSettingsPageState(app->settings());
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity"));
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
@@ -964,11 +965,11 @@ void RenderSettingsPage(App* app) {
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
material::Tooltip("%s", TR("tt_theme_hotkey"));
if (active_is_custom) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_theme"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_theme"));
}
ImGui::SameLine();
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
@@ -976,7 +977,7 @@ void RenderSettingsPage(App* app) {
Notifications::instance().info("Theme list refreshed");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(TR("tt_scan_themes"),
material::Tooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::PopFont();
@@ -1007,7 +1008,7 @@ void RenderSettingsPage(App* app) {
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
material::Tooltip("%s", TR("tt_layout_hotkey"));
ImGui::PopFont();
}
@@ -1030,7 +1031,7 @@ void RenderSettingsPage(App* app) {
app->settings()->save();
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_language"));
ImGui::PopFont();
}
@@ -1059,7 +1060,7 @@ void RenderSettingsPage(App* app) {
Layout::setUserFontScale(s_settingsState.font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
}
@@ -1127,18 +1128,18 @@ void RenderSettingsPage(App* app) {
}
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_low_spec"));
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &s_settingsState.gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_simple_bg_alt"));
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reduce_motion"));
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
@@ -1147,14 +1148,14 @@ void RenderSettingsPage(App* app) {
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
app->settings()->save();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_scanline"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &s_settingsState.theme_effects_enabled)) {
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_theme_effects"));
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2.0f);
@@ -1174,7 +1175,7 @@ void RenderSettingsPage(App* app) {
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_blur"));
ImGui::TextUnformatted(TR("noise"));
ImGui::SetNextItemWidth(ctrlW);
@@ -1190,7 +1191,7 @@ void RenderSettingsPage(App* app) {
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_noise"));
ImGui::TextUnformatted(TR("ui_opacity"));
ImGui::SetNextItemWidth(ctrlW);
@@ -1203,7 +1204,7 @@ void RenderSettingsPage(App* app) {
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_ui_opacity"));
ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetNextItemWidth(ctrlW);
@@ -1215,7 +1216,7 @@ void RenderSettingsPage(App* app) {
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_window_opacity"));
ImGui::EndDisabled(); // low-spec
ImGui::PopFont();
@@ -1274,26 +1275,26 @@ void RenderSettingsPage(App* app) {
float sp = cbSpacing * scale;
ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &s_settingsState.save_ztxs);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_ztx"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx"));
ImGui::SameLine(0, sp);
ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &s_settingsState.auto_shield);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_shield"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield"));
ImGui::SameLine(0, sp);
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
if (showDaemonOptions) {
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_keep_daemon"));
material::Tooltip("%s", TR("tt_keep_daemon"));
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_stop_external"));
material::Tooltip("%s", TR("tt_stop_external"));
}
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
@@ -1301,7 +1302,7 @@ void RenderSettingsPage(App* app) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_verbose"));
material::Tooltip("%s", TR("tt_verbose"));
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
}
@@ -1340,28 +1341,28 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
AddressBookDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
ValidateAddressDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_validate"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
RequestPaymentDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_request_payment"));
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
s_settingsState.confirm_clear_ztx = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clear_ztx"));
}
ImGui::Dummy(ImVec2(0, bottomPad));
@@ -1425,23 +1426,23 @@ void RenderSettingsPage(App* app) {
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
app->showImportKeyDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[0]);
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
app->showExportKeyDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[1]);
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[1]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
ExportAllKeysDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[2]);
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[2]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
app->showBackupDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[3]);
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[3]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
ExportTransactionsDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[4]);
if (showFullNodeLifecycleActions) {
// Right-align Setup Wizard + Download Bootstrap
@@ -1460,11 +1461,11 @@ void RenderSettingsPage(App* app) {
}
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
BootstrapDownloadDialog::show(app);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_download_bootstrap"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_download_bootstrap"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
app->restartWizard();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wizard"));
}
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
@@ -1675,7 +1676,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
util::Platform::openFolder(util::Platform::getLiteWalletDataDir());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_open_data_dir"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
// ---- Backup & keys (open wallet only) ----------------------------------
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
@@ -1859,7 +1860,7 @@ void RenderSettingsPage(App* app) {
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", TR("tt_lite_redownload"));
material::Tooltip("%s", TR("tt_lite_redownload"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc"));
@@ -1926,7 +1927,7 @@ void RenderSettingsPage(App* app) {
}
}
if (hovered) {
ImGui::SetTooltip("%s", TR("tt_open_dir"));
material::Tooltip("%s", TR("tt_open_dir"));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
if (ImGui::IsItemClicked())
@@ -1947,7 +1948,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
util::Platform::openFolder(util::Platform::getDragonXDataDir());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_open_data_dir"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
@@ -1965,7 +1966,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_host"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
float afterRow1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
@@ -1974,7 +1975,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_user"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
@@ -1986,7 +1987,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_port"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
float afterRow2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
@@ -1996,7 +1997,7 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_pass"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
@@ -2038,13 +2039,13 @@ void RenderSettingsPage(App* app) {
if (!isEncrypted) {
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showEncryptDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_encrypt"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_encrypt"));
ImGui::SameLine(0, Layout::spacingMd());
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted"));
} else {
if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showChangePassphraseDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pass"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pass"));
ImGui::SameLine(0, Layout::spacingMd());
if (isLocked) {
ImGui::PushFont(Type().iconSmall());
@@ -2055,7 +2056,7 @@ void RenderSettingsPage(App* app) {
} else {
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->lockWallet();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_lock"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock"));
ImGui::SameLine(0, Layout::spacingSm());
ImGui::PushFont(Type().iconSmall());
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
@@ -2067,7 +2068,7 @@ void RenderSettingsPage(App* app) {
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showDecryptDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_encrypt"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
@@ -2091,7 +2092,7 @@ void RenderSettingsPage(App* app) {
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
app->settings()->save();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_lock"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock"));
ImGui::PopItemWidth();
}
@@ -2107,17 +2108,17 @@ void RenderSettingsPage(App* app) {
if (!hasPIN) {
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinSetupDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_set_pin"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_set_pin"));
ImGui::SameLine(0, Layout::spacingMd());
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin"));
} else {
if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinChangeDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pin"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pin"));
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinRemoveDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_pin"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_pin"));
ImGui::SameLine(0, Layout::spacingMd());
ImGui::PushFont(Type().iconSmall());
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
@@ -2210,6 +2211,17 @@ void RenderSettingsPage(App* app) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
}
// In-app node update: download + verify the latest dragonxd from the project Gitea.
// Refresh the cached daemon info once an install has completed.
if (ui::DaemonUpdateDialog::consumeInstalled())
s_settingsState.daemon_info_loaded = false;
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) {
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string());
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// All node actions on one full-width toolbar row: daemon-binary actions first
// (Install bundled | Refresh), then maintenance (Test connection | Rescan |
@@ -2219,7 +2231,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_reinstall_daemon = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_daemon_install_bundled"));
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) {
@@ -2243,7 +2255,7 @@ void RenderSettingsPage(App* app) {
Notifications::instance().warning("Not connected to daemon");
}
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_test_conn"));
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_rescan = true;
@@ -2251,19 +2263,19 @@ void RenderSettingsPage(App* app) {
s_settingsState.rescan_height_detecting = false;
s_settingsState.rescan_height_detected = false;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_repair_wallet = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_repair_wallet"));
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
ImGui::EndDisabled();
}
}
@@ -2310,29 +2322,29 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(0, Layout::spacingXs());
ImGui::SetNextItemWidth(inputTxW);
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tx_url"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url"));
ImGui::SameLine(pad + halfW + Layout::spacingLg());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("address_url"));
ImGui::SameLine(0, Layout::spacingXs());
ImGui::SetNextItemWidth(inputAddrW);
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_addr_url"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url"));
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Row 2: Checkboxes + Block Explorer button (on one line)
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_fees"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_fetch_prices"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
ImGui::SameLine(0, Layout::spacingLg());
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://explorer.dragonx.is");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_block_explorer"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer"));
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
@@ -2433,18 +2445,18 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://dragonx.is");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_website"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_website"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_report_bug"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
saveSettingsPageState(app->settings());
Notifications::instance().success("Settings saved");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_settings"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
if (app->settings()) {
@@ -2452,7 +2464,7 @@ void RenderSettingsPage(App* app) {
Notifications::instance().info("Settings reloaded from disk");
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reset_settings"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings"));
}
ImGui::Dummy(ImVec2(0, bottomPad));
@@ -2482,7 +2494,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) {
s_settingsState.debug_expanded = !s_settingsState.debug_expanded;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
ImGui::PopStyleColor(3);
// Draw overline label + arrow on top of the invisible button
@@ -2562,7 +2574,7 @@ void RenderSettingsPage(App* app) {
s_settingsState.debug_cats_dirty = true;
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", debugTips[i]);
if (ImGui::IsItemHovered()) material::Tooltip("%s", debugTips[i]);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
@@ -2585,7 +2597,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
s_settingsState.confirm_restart_daemon = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon"));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
}
ImGui::Dummy(ImVec2(0, bottomPad));

View File

@@ -0,0 +1,254 @@
// 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 <memory>
#include <string>
#include <vector>
#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<util::DaemonUpdater>();
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<int>(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<ReleaseRow> s_rows; // built once per listing (UI rows)
static inline std::vector<util::DaemonRelease> s_releases; // matching releases (install by index)
static inline App* s_app = nullptr;
static inline std::unique_ptr<util::DaemonUpdater> s_updater;
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,87 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Shared "Browse all releases" picker, used by both the miner updater (XmrigDownloadDialog) and the
// node updater (DaemonUpdateDialog). Renders a scrollable list of releases (tag, title, date, with
// pre-release / installed badges) inside the current overlay dialog; the caller maps its updater's
// release list into ReleaseRow values and acts on the clicked index.
#pragma once
#include <string>
#include <vector>
#include "../../util/i18n.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
struct ReleaseRow {
std::string tag; // version tag, e.g. "v1.0.2"
std::string title; // human title (release "name"), shown dimmed
std::string date; // YYYY-MM-DD (already trimmed)
bool prerelease = false; // show a pre-release badge
bool hasAsset = true; // a build exists for this platform (else the row's Install is disabled)
bool installed = false; // matches the currently-installed version (Install -> Reinstall)
};
// Renders the picker. Returns the row index whose Install button was clicked this frame, or -1.
// Sets *back when the Back button is clicked. If `globalDisabledTooltip` is non-null, every Install
// button is disabled and shows that tooltip (e.g. "stop mining before updating the miner").
inline int RenderReleaseList(const std::vector<ReleaseRow>& rows, float dp, bool* back,
const char* globalDisabledTooltip = nullptr) {
using namespace material;
int clicked = -1;
Type().text(TypeStyle::Subtitle2, TR("upd_select_version"));
ImGui::Spacing();
const float listH = 300.0f * dp;
if (ImGui::BeginChild("##release_list", ImVec2(0, listH), true)) {
const ImVec4 dim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
const ImVec4 warn = ImVec4(1.0f, 0.78f, 0.25f, 1.0f);
const ImVec4 succ = ImGui::ColorConvertU32ToFloat4(Success());
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
const ReleaseRow& r = rows[i];
ImGui::PushID(i);
ImGui::TextUnformatted(r.tag.c_str());
if (r.prerelease) { ImGui::SameLine(); ImGui::TextColored(warn, "[%s]", TR("upd_prerelease")); }
if (r.installed) { ImGui::SameLine(); ImGui::TextColored(succ, "[%s]", TR("upd_installed_badge")); }
if (!r.title.empty() || !r.date.empty()) {
std::string meta = r.title;
if (!r.title.empty() && !r.date.empty()) meta += " · ";
meta += r.date;
ImGui::TextColored(dim, "%s", meta.c_str());
}
const char* lbl = r.installed ? TR("upd_reinstall") : TR("upd_install");
const bool disabled = !r.hasAsset || globalDisabledTooltip != nullptr;
ImGui::BeginDisabled(disabled);
if (TactileButton(lbl, ImVec2(140.0f * dp, 0))) clicked = i;
ImGui::EndDisabled();
if (disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
material::Tooltip("%s", globalDisabledTooltip ? globalDisabledTooltip
: TR("upd_no_build_platform"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::Spacing();
if (TactileButton(TR("upd_back"), ImVec2(ImGui::GetContentRegionAvail().x, 0))) *back = true;
return clicked;
}
} // namespace ui
} // namespace dragonx

View File

@@ -11,6 +11,7 @@
#include <memory>
#include <string>
#include <vector>
#include "../../app.h"
#include "../../config/settings.h"
@@ -21,6 +22,7 @@
#include "../material/type.h"
#include "../material/colors.h"
#include "../theme.h"
#include "release_list_view.h"
#include "imgui.h"
namespace dragonx {
@@ -33,8 +35,11 @@ public:
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<util::XmrigUpdater>();
s_updater->startCheck(app->settings() ? app->settings()->getXmrigVersion() : std::string());
s_updater->startCheck(s_installed_tag);
}
static bool isOpen() { return s_open; }
@@ -54,6 +59,8 @@ public:
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;
@@ -121,9 +128,50 @@ private:
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<int>(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")
@@ -180,6 +228,9 @@ private:
static inline bool s_open = false;
static inline bool s_persisted = false;
static inline std::string s_installed_tag;
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
static inline std::vector<util::XmrigRelease> s_releases; // matching releases (install by index)
static inline App* s_app = nullptr;
static inline std::unique_ptr<util::XmrigUpdater> s_updater;
};