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:
309
src/ui/windows/bootstrap_download_dialog.h
Normal file
309
src/ui/windows/bootstrap_download_dialog.h
Normal 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
|
||||
Reference in New Issue
Block a user