Add bootstrap download dialog and fix 100 missing translation keys

- New BootstrapDownloadDialog accessible from Settings page
  - Stops daemon before download, prevents auto-restart during bootstrap
  - Confirm/Downloading/Done/Failed states with progress display
  - Mirror support (bootstrap2.dragonx.is)
- Add bootstrap_downloading_ flag to prevent tryConnect() auto-reconnect
- Right-align Download Bootstrap + Setup Wizard buttons in settings
- Add 100 missing i18n keys to all 8 language files (de/es/fr/ja/ko/pt/ru/zh)
  - Includes bootstrap, explorer, mining benchmark, transfer, delete blockchain,
    force quit, address label, and settings section translations
- Update add_missing_translations.py with new translation batch
This commit is contained in:
dan_s
2026-04-12 18:19:01 -05:00
parent 9f23b2781c
commit 077f9a7403
22 changed files with 32135 additions and 25512 deletions
+20 -1
View File
@@ -37,6 +37,7 @@
#include "ui/windows/export_transactions_dialog.h"
#include "ui/windows/address_label_dialog.h"
#include "ui/windows/address_transfer_dialog.h"
#include "ui/windows/bootstrap_download_dialog.h"
#include "ui/windows/console_tab.h"
#include "ui/pages/settings_page.h"
#include "ui/theme.h"
@@ -630,7 +631,8 @@ void App::update()
} else if (core_timer_ >= active_core_interval_) {
core_timer_ = 0.0f;
if (!connection_in_progress_ &&
wizard_phase_ == WizardPhase::None) {
wizard_phase_ == WizardPhase::None &&
!bootstrap_downloading_) {
tryConnect();
}
}
@@ -1356,6 +1358,9 @@ void App::render()
// Address-to-address transfer confirmation
ui::AddressTransferDialog::render();
// Bootstrap download from settings
ui::BootstrapDownloadDialog::render();
// Windows Defender antivirus help dialog
renderAntivirusHelpDialog();
@@ -2357,6 +2362,20 @@ void App::deleteBlockchainData()
}).detach();
}
bool App::stopDaemonForBootstrap()
{
bool wasRunning = isEmbeddedDaemonRunning();
if (wasRunning) {
DEBUG_LOGF("[App] Stopping embedded daemon for bootstrap download...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
return wasRunning;
}
double App::getDaemonMemoryUsageMB() const
{
// If we have an embedded daemon with a tracked process handle, use it
+4
View File
@@ -273,6 +273,9 @@ public:
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
@@ -542,6 +545,7 @@ private:
// First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_;
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
std::string wizard_pending_passphrase_; // held until daemon connects
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
+3467 -2894
View File
File diff suppressed because it is too large Load Diff
+3689 -3115
View File
File diff suppressed because it is too large Load Diff
+3832 -3240
View File
File diff suppressed because it is too large Load Diff
+4045 -3375
View File
File diff suppressed because it is too large Load Diff
+3539 -2970
View File
File diff suppressed because it is too large Load Diff
+3652 -3088
View File
File diff suppressed because it is too large Load Diff
+4802 -4023
View File
File diff suppressed because it is too large Load Diff
+3305 -2799
View File
File diff suppressed because it is too large Load Diff
+16 -7
View File
@@ -32,6 +32,7 @@
#include "../windows/block_info_dialog.h"
#include "../windows/export_all_keys_dialog.h"
#include "../windows/export_transactions_dialog.h"
#include "../windows/bootstrap_download_dialog.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
@@ -1236,6 +1237,7 @@ void RenderSettingsPage(App* app) {
TR("tt_export_csv")
};
const char* wizLabel = TR("setup_wizard");
const char* bsLabel = TR("download_bootstrap");
float sp = Layout::spacingSm();
ImFont* btnFont = S.resolveFont("button");
@@ -1244,7 +1246,8 @@ void RenderSettingsPage(App* app) {
for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
float totalW = naturalW + wizW + sp * 6;
float bsW = ImGui::CalcTextSize(bsLabel).x + btnPadX;
float totalW = naturalW + wizW + bsW + sp * 7;
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
@@ -1271,18 +1274,24 @@ void RenderSettingsPage(App* app) {
ExportTransactionsDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
// Right-align Setup Wizard
// Right-align Setup Wizard + Download Bootstrap
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
float curX = ImGui::GetCursorScreenPos().x;
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
if (scale < 1.0f) wizBtnW *= scale;
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2;
float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2;
float rightEdge = cardMin.x + availWidth - pad;
float wizX = rightEdge - wizBtnW;
if (wizX > curX) {
float rightGroupW = bsBtnW + scaledSp + wizBtnW;
float groupX = rightEdge - rightGroupW;
if (groupX > curX) {
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(wizX, ImGui::GetCursorScreenPos().y));
ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y));
} else {
ImGui::SameLine(0, scaledSp);
}
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
BootstrapDownloadDialog::show(app);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%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"));
+309
View File
@@ -0,0 +1,309 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <memory>
#include "../../app.h"
#include "../../util/bootstrap.h"
#include "../../util/platform.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
/**
* @brief Modal dialog for downloading blockchain bootstrap from Settings.
*
* Reuses the existing Bootstrap class for download/verify/extract.
* States: Confirm → Downloading → Done / Failed
*/
class BootstrapDownloadDialog {
public:
static void show(App* app) {
s_open = true;
s_app = app;
s_state = State::Confirm;
s_bootstrap.reset();
s_errorMsg.clear();
s_wasDaemonRunning = false;
}
static bool isOpen() { return s_open; }
static void render() {
if (!s_app) return;
if (!s_open) {
// Dialog was closed — ensure flag is cleared
if (s_app->isBootstrapDownloading() && s_state != State::Downloading) {
s_app->setBootstrapDownloading(false);
}
return;
}
using namespace material;
const float dp = Layout::dpiScale();
if (BeginOverlayDialog(TR("download_bootstrap"), &s_open, 500.0f, 0.94f)) {
if (s_state == State::Confirm) {
renderConfirm(dp);
} else if (s_state == State::Downloading) {
renderProgress(dp);
} else if (s_state == State::Done) {
renderDone(dp);
} else if (s_state == State::Failed) {
renderFailed(dp);
}
EndOverlayDialog();
}
}
private:
enum class State { Confirm, Downloading, Done, Failed };
// ---- Confirm screen ----
static void renderConfirm(float dp) {
using namespace material;
ImGui::Spacing();
// Description
ImGui::TextWrapped("%s", TR("bootstrap_desc"));
ImGui::Spacing();
// Warning card
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.9f, 0.6f, 0.0f, 0.08f));
ImGui::BeginChild("##bsWarn", ImVec2(0, 0),
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
ImGuiWindowFlags_NoScrollbar);
{
ImFont* iconFont = Type().iconSmall();
ImGui::PushFont(iconFont);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Warning()), "%s", ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextWrapped("%s", TR("bootstrap_warning"));
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::Spacing();
// Trust warning
{
ImFont* iconFont = Type().iconSmall();
ImGui::PushFont(iconFont);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", ICON_MD_VERIFIED_USER);
ImGui::PopFont();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("bootstrap_trust_warning"));
ImGui::PopStyleColor();
}
ImGui::Spacing();
ImGui::Spacing();
// Buttons: Download | Mirror | Cancel
float btnW = 140.0f * dp;
float btnSm = 90.0f * dp;
if (TactileButton(TR("download"), ImVec2(btnW, 0))) {
startDownload("");
}
ImGui::SameLine();
if (TactileButton(TR("bootstrap_mirror"), ImVec2(btnW, 0))) {
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
startDownload(mirrorUrl);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("bootstrap_mirror_tooltip"));
}
ImGui::SameLine();
if (TactileButton(TR("cancel"), ImVec2(btnSm, 0))) {
s_open = false;
}
}
// ---- Progress screen ----
static void renderProgress(float dp) {
using namespace material;
if (!s_bootstrap) {
s_state = State::Failed;
s_errorMsg = "Bootstrap not initialized";
return;
}
auto prog = s_bootstrap->getProgress();
// Status title
const char* statusTitle;
if (prog.state == util::Bootstrap::State::Downloading)
statusTitle = TR("bootstrap_downloading");
else if (prog.state == util::Bootstrap::State::Verifying)
statusTitle = TR("bootstrap_verifying");
else
statusTitle = TR("bootstrap_extracting");
Type().text(TypeStyle::Subtitle2, statusTitle);
ImGui::Spacing();
// Progress bar
float barH = 8.0f * dp;
float barW = ImGui::GetContentRegionAvail().x;
ImVec2 barMin = ImGui::GetCursorScreenPos();
ImVec2 barMax(barMin.x + barW, barMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(barMin, barMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
float fillW = barW * (prog.percent / 100.0f);
if (fillW > 0) {
dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y),
Primary(), 4.0f * dp);
}
ImGui::Dummy(ImVec2(0, barH));
ImGui::Spacing();
// Percent + status text
{
char pctBuf[32];
snprintf(pctBuf, sizeof(pctBuf), "%.1f%%", prog.percent);
float pctW = ImGui::CalcTextSize(pctBuf).x;
ImGui::Text("%s", prog.status_text.c_str());
ImGui::SameLine(ImGui::GetContentRegionAvail().x - pctW + ImGui::GetCursorPosX());
ImGui::Text("%s", pctBuf);
}
// wallet.dat protection notice during extraction
if (prog.state == util::Bootstrap::State::Extracting) {
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("bootstrap_wallet_protected"));
}
ImGui::Spacing();
// Daemon status indicator
{
bool daemonUp = s_app->isEmbeddedDaemonRunning();
const std::string& dStatus = s_app->getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
: IM_COL32(120, 120, 120, 160);
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? TR("bootstrap_daemon_stopping")
: TR("bootstrap_daemon_running"))
: TR("bootstrap_daemon_stopped");
ImDrawList* ddl = ImGui::GetWindowDrawList();
float dotR = 3.5f * dp;
ImVec2 cp = ImGui::GetCursorScreenPos();
ddl->AddCircleFilled(ImVec2(cp.x + dotR, cp.y + ImGui::GetTextLineHeight() * 0.5f),
dotR, dotCol);
ImGui::Indent(dotR * 2.0f + 6.0f * dp);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), label);
ImGui::Unindent(dotR * 2.0f + 6.0f * dp);
}
ImGui::Spacing();
ImGui::Spacing();
// Cancel button
float btnW = 100.0f * dp;
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
s_bootstrap->cancel();
}
// Check completion
if (s_bootstrap->isDone()) {
auto finalProg = s_bootstrap->getProgress();
if (finalProg.state == util::Bootstrap::State::Completed) {
s_state = State::Done;
} else {
s_errorMsg = finalProg.error;
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
s_state = State::Failed;
}
s_bootstrap.reset();
}
}
// ---- Done screen ----
static void renderDone(float dp) {
using namespace material;
ImGui::Spacing();
Type().textColored(TypeStyle::H6, Success(), TR("bootstrap_success"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("bootstrap_success_desc"));
ImGui::Spacing();
ImGui::Spacing();
float btnW = 140.0f * dp;
if (s_wasDaemonRunning) {
if (TactileButton(TR("bootstrap_restart_daemon"), ImVec2(btnW, 0))) {
s_app->setBootstrapDownloading(false);
s_app->startEmbeddedDaemon();
s_open = false;
}
ImGui::SameLine();
}
if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) {
s_app->setBootstrapDownloading(false);
s_open = false;
}
}
// ---- Failed screen ----
static void renderFailed(float dp) {
using namespace material;
ImGui::Spacing();
Type().textColored(TypeStyle::H6, Error(), TR("bootstrap_failed"));
ImGui::Spacing();
ImGui::TextWrapped("%s", s_errorMsg.c_str());
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f * dp;
if (TactileButton(TR("retry"), ImVec2(btnW, 0))) {
startDownload("");
}
ImGui::SameLine();
if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) {
s_app->setBootstrapDownloading(false);
s_open = false;
}
}
// ---- Shared: kick off download ----
static void startDownload(const std::string& url) {
s_wasDaemonRunning = s_app->stopDaemonForBootstrap();
s_app->setBootstrapDownloading(true);
s_bootstrap = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
if (url.empty())
s_bootstrap->start(dataDir);
else
s_bootstrap->start(dataDir, url);
s_state = State::Downloading;
s_errorMsg.clear();
}
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline State s_state = State::Confirm;
static inline std::unique_ptr<util::Bootstrap> s_bootstrap;
static inline bool s_wasDaemonRunning = false;
static inline std::string s_errorMsg;
};
} // namespace ui
} // namespace dragonx
+20
View File
@@ -334,6 +334,26 @@ void I18n::loadBuiltinEnglish()
strings_["tt_mine_idle"] = "Automatically start mining when the\nsystem is idle (no keyboard/mouse input)";
strings_["tt_idle_delay"] = "How long to wait before starting mining";
strings_["tt_wizard"] = "Re-run the initial setup wizard\nDaemon will be restarted";
strings_["tt_download_bootstrap"] = "Download blockchain bootstrap to speed up sync\nExisting block data will be replaced";
strings_["download_bootstrap"] = "Download Bootstrap";
strings_["download"] = "Download";
strings_["retry"] = "Retry";
strings_["bootstrap_desc"] = "Download a blockchain bootstrap to dramatically speed up initial sync. This downloads a snapshot of the blockchain and extracts it into your data directory.";
strings_["bootstrap_warning"] = "Existing block data (blocks, chainstate, notarizations) will be deleted and replaced. Your wallet.dat will NOT be modified or deleted.";
strings_["bootstrap_trust_warning"] = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
strings_["bootstrap_mirror"] = "Mirror";
strings_["bootstrap_mirror_tooltip"] = "Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.";
strings_["bootstrap_downloading"] = "Downloading bootstrap...";
strings_["bootstrap_verifying"] = "Verifying checksums...";
strings_["bootstrap_extracting"] = "Extracting blockchain data...";
strings_["bootstrap_wallet_protected"] = "(wallet.dat is protected)";
strings_["bootstrap_daemon_stopping"] = "Daemon stopping...";
strings_["bootstrap_daemon_running"] = "Daemon running";
strings_["bootstrap_daemon_stopped"] = "Daemon stopped";
strings_["bootstrap_success"] = "Bootstrap Complete";
strings_["bootstrap_success_desc"] = "Blockchain data has been extracted successfully. Start the daemon to begin syncing from the bootstrap point.";
strings_["bootstrap_restart_daemon"] = "Restart Daemon";
strings_["bootstrap_failed"] = "Bootstrap Failed";
strings_["tt_open_dir"] = "Click to open in file explorer";
strings_["tt_rpc_host"] = "Hostname of the DragonX daemon";
strings_["tt_rpc_user"] = "RPC authentication username";