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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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"));

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

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";