Files
ObsidianDragon/src/app_wizard.cpp
dan_s 9e94952e0a v1.1.0: explorer tab, bootstrap fixes, full theme overlay merge
Explorer tab:
- New block explorer tab with search, chain stats, mempool info,
  recent blocks table, block detail modal with tx expansion
- Sidebar nav entry, i18n strings, ui.toml layout values

Bootstrap fixes:
- Move wizard Done handler into render() — was dead code, preventing
  startEmbeddedDaemon() and tryConnect() from firing post-wizard
- Stop deleting BDB database/ dir during cleanup — caused LSN mismatch
  that salvaged wallet.dat into wallet.{timestamp}.bak
- Add banlist.dat, db.log, .lock to cleanup file list
- Fatal extraction failure for blocks/ and chainstate/ files
- Verification progress: split SHA-256 (0-50%) and MD5 (50-100%)

Theme system:
- Expand overlay merge to apply ALL sections (tabs, dialogs, components,
  screens, flat sections), not just theme+backdrop+effects
- Add screens and security section parsing to UISchema
- Build-time theme expansion via expand_themes.py (CMake + build.sh)

Other:
- Version bump to 1.1.0
- WalletState::clear() resets all fields (sync, daemon info, etc.)
- Sidebar item-height 42 → 36
2026-03-17 18:49:46 -05:00

1313 lines
63 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// app_wizard.cpp — First-run setup wizard
// Split from app.cpp for maintainability.
#include "app.h"
#include "rpc/rpc_client.h"
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "config/settings.h"
#include "daemon/embedded_daemon.h"
#include "ui/notifications.h"
#include "ui/material/color_theme.h"
#include "ui/material/type.h"
#include "ui/material/typography.h"
#include "ui/material/draw_helpers.h"
#include "ui/schema/ui_schema.h"
#include "ui/schema/skin_manager.h"
#include "ui/effects/low_spec.h"
#include "ui/windows/balance_tab.h"
#include "util/platform.h"
#include "util/bootstrap.h"
#include "util/secure_vault.h"
#include "util/i18n.h"
#include "util/perf_log.h"
#include "embedded/IconsMaterialDesign.h"
#include "resources/embedded_resources.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <cstring>
#include <filesystem>
#include <thread>
#include <chrono>
namespace dragonx {
using json = nlohmann::json;
void App::restartWizard()
{
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt
if (embedded_daemon_) {
embedded_daemon_->resetCrashCount();
}
// Disconnect RPC
if (rpc_ && rpc_->isConnected()) {
rpc_->disconnect();
}
onDisconnected("Wizard restart");
// Stop the embedded daemon in a background thread to avoid
// blocking the UI for up to 32 seconds (RPC stop + process wait).
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
std::thread([this]() {
stopEmbeddedDaemon();
}).detach();
}
// Enter wizard — the wizard completion handler already calls
// startEmbeddedDaemon() + tryConnect(), so no extra logic needed.
wizard_phase_ = WizardPhase::Appearance;
}
// ===========================================================================
// First-Run Wizard Rendering
// ===========================================================================
void App::renderFirstRunWizard() {
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::Begin("##FirstRunWizard", nullptr, flags);
ImGui::PopStyleVar();
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
ImVec2 winSize = ImGui::GetWindowSize();
// Background fill
ImU32 bgCol = ui::material::Surface();
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
// --- Determine which of the 3 masonry sections is focused ---
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
int focusIdx = 0;
switch (wizard_phase_) {
case WizardPhase::Appearance: focusIdx = 0; break;
case WizardPhase::BootstrapOffer:
case WizardPhase::BootstrapInProgress:
case WizardPhase::BootstrapFailed:
focusIdx = 1; break;
case WizardPhase::EncryptOffer:
case WizardPhase::EncryptInProgress:
case WizardPhase::PinSetup:
focusIdx = 2; break;
default: focusIdx = 0; break;
}
// Card visual state: 0 = not-reached, 1 = focused, 2 = completed
auto cardState = [&](int idx) -> int {
if (idx < focusIdx) return 2;
if (idx == focusIdx) return 1;
return 0;
};
// --- Fonts & Colors ---
const auto& S = ui::schema::UI();
ImFont* titleFont = S.resolveFont(S.label("screens.first-run", "title").font);
if (!titleFont) titleFont = ui::material::Type().h5();
ImFont* bodyFont = S.resolveFont(S.label("screens.first-run", "subtitle").font);
if (!bodyFont) bodyFont = ui::material::Type().body1();
ImFont* captionFont = S.resolveFont(S.label("screens.first-run", "trust-warning").font);
if (!captionFont) captionFont = ui::material::Type().caption();
ImU32 textCol = ui::material::OnSurface();
ImU32 dimCol = (textCol & 0x00FFFFFF) | (IM_COL32_A_MASK & IM_COL32(0,0,0,180));
ImFont* iconFont = ui::material::Type().iconSmall();
if (!iconFont) iconFont = captionFont;
// DPI scale factor — multiply all pixel constants by dp
const float dp = ui::Layout::dpiScale();
// --- Header: Logo + Welcome ---
float headerCy = winPos.y + 20.0f * dp;
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
if (logo_tex_ != 0) {
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
float logoW = logoSize * aspect;
float logoX = winPos.x + (winSize.x - logoW) * 0.5f;
dl->AddImage(logo_tex_, ImVec2(logoX, headerCy), ImVec2(logoX + logoW, headerCy + logoSize));
}
headerCy += logoSize + 8.0f * dp;
{
const char* welcomeTitle = "Welcome to ObsidianDragon!";
ImVec2 wts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, welcomeTitle);
dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(winPos.x + (winSize.x - wts.x) * 0.5f, headerCy), textCol, welcomeTitle);
headerCy += wts.y + 16.0f * dp;
}
// --- Masonry: 2 columns ---
// Left column: Card 0 (Appearance) on top, Card 2 (Encrypt+PIN) below
// Right column: Card 1 (Bootstrap)
float totalW = std::min(920.0f * dp, winSize.x - 40.0f * dp);
float gap = 16.0f * dp;
float colW = (totalW - gap) * 0.5f;
float areaX = winPos.x + (winSize.x - totalW) * 0.5f;
float leftX = areaX;
float rightX = areaX + colW + gap;
float cardPad = 24.0f * dp;
float cardRound = 12.0f * dp;
float topY = headerCy;
// Step icon helper
auto stepIcon = [](int state) -> const char* {
return (state == 2) ? ICON_MD_CHECK_CIRCLE :
(state == 1) ? ICON_MD_RADIO_BUTTON_CHECKED :
ICON_MD_RADIO_BUTTON_UNCHECKED;
};
// Split draw list: 0 = backgrounds, 1 = content, 2 = overlays/borders
dl->ChannelsSplit(3);
dl->ChannelsSetCurrent(1);
// Helper: finalize card — draw background, accent border or dim overlay
auto finalizeCard = [&](float cardX_, float cardW_, float ytop, float ybot, int state) {
ImVec2 cMin(cardX_, ytop);
ImVec2 cMax(cardX_ + cardW_, ybot);
// Background (channel 0)
dl->ChannelsSetCurrent(0);
if (state == 1) {
// Focused card: subtle drop shadow
float shadowOff = 3.0f * dp;
dl->AddRectFilled(
ImVec2(cMin.x + shadowOff, cMin.y + shadowOff), ImVec2(cMax.x + shadowOff, cMax.y + shadowOff),
IM_COL32(0, 0, 0, 35), cardRound);
}
// Use DrawGlassPanel for proper acrylic/opacity/noise/theme effects
ui::material::GlassPanelSpec glass;
glass.rounding = cardRound;
ui::material::DrawGlassPanel(dl, cMin, cMax, glass);
// Overlays & borders (channel 2)
dl->ChannelsSetCurrent(2);
if (state == 1) {
// Focused: accent border
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 2.0f * dp);
} else if (state == 2) {
// Completed: dim overlay (preserves color)
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 110), cardRound);
} else {
// Not reached: heavy overlay (creates greyscale look)
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 165), cardRound);
}
dl->ChannelsSetCurrent(1);
};
// ======================= CARD 0: Appearance =======================
float card0Top = topY;
float card0Bot;
{
int state = cardState(0);
bool isFocused = (state == 1);
float cx = leftX + cardPad;
float cy = card0Top + cardPad;
float contentW = colW - 2 * cardPad;
// Step indicator
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 1");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Title
{
const char* t = "Appearance";
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
cy += titleFont->LegacySize + 10.0f * dp;
}
// Separator
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 14.0f * dp;
// Statics for appearance settings
static float wiz_blur_amount = 1.5f;
static bool wiz_theme_effects = true;
static float wiz_ui_opacity = 1.0f;
static bool wiz_low_spec = false;
static bool wiz_scanline = true;
static std::string wiz_balance_layout = "classic";
static int wiz_language_index = 0;
static bool wiz_appearance_init = false;
if (!wiz_appearance_init) {
wiz_blur_amount = settings_->getBlurMultiplier();
wiz_theme_effects = settings_->getThemeEffectsEnabled();
wiz_ui_opacity = settings_->getUIOpacity();
wiz_low_spec = settings_->getLowSpecMode();
wiz_scanline = settings_->getScanlineEnabled();
wiz_balance_layout = settings_->getBalanceLayout();
// Find current language index
const auto& wiz_languages = util::I18n::instance().getAvailableLanguages();
std::string wiz_cur_lang = settings_->getLanguage();
if (wiz_cur_lang.empty()) wiz_cur_lang = "en";
int idx = 0;
for (const auto& lang : wiz_languages) {
if (lang.first == wiz_cur_lang) { wiz_language_index = idx; break; }
idx++;
}
// Apply loaded settings to runtime so visuals match slider values
ui::effects::setLowSpecMode(wiz_low_spec);
ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount);
ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity);
ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects);
ui::effects::ThemeEffects::instance().setReducedTransparency(!wiz_theme_effects);
ui::ConsoleTab::s_scanline_enabled = wiz_scanline;
wiz_appearance_init = true;
}
// Render controls always so content is visible under the dim
// overlay when not focused; disable interaction when not active.
ImGui::BeginDisabled(!isFocused);
// --- Theme combo ---
{
auto& skinMgr = ui::schema::SkinManager::instance();
const auto& skins = skinMgr.available();
std::string activePreview = "DragonX";
for (const auto& skin : skins) {
if (skin.id == skinMgr.activeSkinId()) { activePreview = skin.name; break; }
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Theme");
float comboX = cx + 110.0f * dp;
float comboW = contentW - 110.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##wiz_theme", activePreview.c_str())) {
ImGui::TextDisabled("Built-in");
ImGui::Separator();
for (const auto& skin : skins) {
if (!skin.bundled) continue;
bool sel = (skin.id == skinMgr.activeSkinId());
if (ImGui::Selectable(skin.name.c_str(), sel)) {
skinMgr.setActiveSkin(skin.id);
settings_->setSkinId(skin.id);
settings_->save();
}
if (sel) ImGui::SetItemDefaultFocus();
}
bool hasCustom = false;
for (const auto& skin : skins) { if (!skin.bundled) { hasCustom = true; break; } }
if (hasCustom) {
ImGui::Spacing();
ImGui::TextDisabled("Custom");
ImGui::Separator();
for (const auto& skin : skins) {
if (skin.bundled) continue;
bool sel = (skin.id == skinMgr.activeSkinId());
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1));
ImGui::BeginDisabled(true);
ImGui::Selectable((skin.name + " (invalid)").c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
} else {
std::string lbl = skin.name;
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
if (ImGui::Selectable(lbl.c_str(), sel)) {
skinMgr.setActiveSkin(skin.id);
settings_->setSkinId(skin.id);
settings_->save();
}
if (sel) ImGui::SetItemDefaultFocus();
}
}
}
ImGui::EndCombo();
}
ImGui::PopStyleVar();
cy += bodyFont->LegacySize + 16.0f * dp;
}
// --- Balance Layout combo ---
{
const auto& layouts = ui::GetBalanceLayouts();
std::string balPreview = wiz_balance_layout;
for (const auto& l : layouts) {
if (l.id == wiz_balance_layout) { balPreview = l.name; break; }
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Balance Layout");
float comboX = cx + 110.0f * dp;
float comboW = contentW - 110.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##wiz_layout", balPreview.c_str())) {
for (const auto& l : layouts) {
if (!l.enabled) continue;
bool sel = (l.id == wiz_balance_layout);
if (ImGui::Selectable(l.name.c_str(), sel)) {
wiz_balance_layout = l.id;
settings_->setBalanceLayout(wiz_balance_layout);
settings_->save();
}
if (sel) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::PopStyleVar();
cy += bodyFont->LegacySize + 16.0f * dp;
}
// --- Language combo ---
{
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::vector<const char*> langNames;
langNames.reserve(languages.size());
for (const auto& lang : languages) langNames.push_back(lang.second.c_str());
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Language");
float comboX = cx + 110.0f * dp;
float comboW = contentW - 110.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::SetNextItemWidth(comboW);
if (ImGui::Combo("##wiz_lang", &wiz_language_index, langNames.data(),
static_cast<int>(langNames.size()))) {
auto it = languages.begin();
std::advance(it, wiz_language_index);
i18n.loadLanguage(it->first);
}
ImGui::PopStyleVar();
cy += bodyFont->LegacySize + 20.0f * dp;
}
// --- Low-spec mode checkbox ---
// Snapshot for restoring settings when low-spec is turned off
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
if (ImGui::Checkbox("##wiz_lowspec", &wiz_low_spec)) {
ui::effects::setLowSpecMode(wiz_low_spec);
if (wiz_low_spec) {
// Save current effect settings before zeroing
wiz_lsSnap.valid = true;
wiz_lsSnap.blur = wiz_blur_amount;
wiz_lsSnap.uiOp = wiz_ui_opacity;
wiz_lsSnap.fx = wiz_theme_effects;
wiz_lsSnap.scanline = wiz_scanline;
// Disable all heavy effects
wiz_blur_amount = 0.0f;
wiz_ui_opacity = 1.0f;
wiz_theme_effects = false;
wiz_scanline = false;
ui::effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
ui::effects::ImGuiAcrylic::SetUIOpacity(1.0f);
settings_->setWindowOpacity(1.0f);
ui::effects::ThemeEffects::instance().setEnabled(false);
ui::effects::ThemeEffects::instance().setReducedTransparency(true);
ui::ConsoleTab::s_scanline_enabled = false;
} else if (wiz_lsSnap.valid) {
// Restore previous effect settings
wiz_blur_amount = wiz_lsSnap.blur;
wiz_ui_opacity = wiz_lsSnap.uiOp;
wiz_theme_effects = wiz_lsSnap.fx;
wiz_scanline = wiz_lsSnap.scanline;
ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount);
ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity);
ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects);
ui::effects::ThemeEffects::instance().setReducedTransparency(false);
ui::ConsoleTab::s_scanline_enabled = wiz_scanline;
wiz_lsSnap.valid = false;
}
// Persist immediately so effects read correct values
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
? static_cast<int>(ui::effects::AcrylicQuality::Low)
: static_cast<int>(ui::effects::AcrylicQuality::Off));
settings_->setBlurMultiplier(wiz_blur_amount);
settings_->setUIOpacity(wiz_ui_opacity);
settings_->setThemeEffectsEnabled(wiz_theme_effects);
settings_->setScanlineEnabled(wiz_scanline);
settings_->setLowSpecMode(wiz_low_spec);
settings_->save();
}
ImGui::PopStyleVar();
ImGui::SameLine();
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
"Low-spec mode");
cy += bodyFont->LegacySize + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 28.0f * dp, cy), dimCol, "Disable all heavy visual effects");
cy += captionFont->LegacySize + 16.0f * dp;
ImGui::BeginDisabled(wiz_low_spec);
// Acrylic blur slider
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(cx, cy + 2.0f * dp), textCol,
"Acrylic glass effects");
cy += bodyFont->LegacySize + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx, cy), dimCol, "Translucent blur on panels (Off disables)");
cy += captionFont->LegacySize + 10.0f * dp;
{
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 4.0f * dp, cy), textCol, "Level:");
ImGui::SetCursorScreenPos(ImVec2(cx + 72.0f * dp, cy - 2.0f * dp));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
float sliderW = contentW - 72.0f * dp;
ImGui::SetNextItemWidth(std::max(80.0f * dp, sliderW));
{
char blur_fmt[16];
if (wiz_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", wiz_blur_amount * 25.0f);
if (ImGui::SliderFloat("##wiz_blur", &wiz_blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
if (wiz_blur_amount > 0.0f && wiz_blur_amount < 0.15f) wiz_blur_amount = 0.0f;
ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount);
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
? static_cast<int>(ui::effects::AcrylicQuality::Low)
: static_cast<int>(ui::effects::AcrylicQuality::Off));
settings_->setBlurMultiplier(wiz_blur_amount);
settings_->save();
}
ImGui::PopStyleVar();
cy += bodyFont->LegacySize + 16.0f * dp;
}
// Theme effects checkbox
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
if (ImGui::Checkbox("##wiz_fx", &wiz_theme_effects)) {
ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects);
ui::effects::ThemeEffects::instance().setReducedTransparency(!wiz_theme_effects);
settings_->setThemeEffectsEnabled(wiz_theme_effects);
settings_->save();
}
ImGui::PopStyleVar();
ImGui::SameLine();
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
"Theme visual effects");
cy += bodyFont->LegacySize + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 28.0f * dp, cy), dimCol, "Animated borders, color wash");
cy += captionFont->LegacySize + 16.0f * dp;
// UI Opacity slider
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(cx, cy + 2.0f * dp), textCol,
"UI Opacity");
cy += bodyFont->LegacySize + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx, cy), dimCol, "Card & sidebar transparency (1.0 = solid)");
cy += captionFont->LegacySize + 10.0f * dp;
{
ImGui::SetCursorScreenPos(ImVec2(cx, cy - 2.0f * dp));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::SetNextItemWidth(std::max(80.0f * dp, contentW));
if (ImGui::SliderFloat("##wiz_ui_opacity", &wiz_ui_opacity, 0.3f, 1.0f, "%.2f")) {
ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
settings_->setUIOpacity(wiz_ui_opacity);
settings_->save();
}
ImGui::PopStyleVar();
cy += bodyFont->LegacySize + 16.0f * dp;
}
// Console scanline checkbox
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
if (ImGui::Checkbox("##wiz_scanline", &wiz_scanline)) {
ui::ConsoleTab::s_scanline_enabled = wiz_scanline;
settings_->setScanlineEnabled(wiz_scanline);
settings_->save();
}
ImGui::PopStyleVar();
ImGui::SameLine();
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
"Console scanline");
cy += bodyFont->LegacySize + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 28.0f * dp, cy), dimCol, "CRT scanline effect in console");
cy += captionFont->LegacySize + 24.0f * dp;
ImGui::EndDisabled(); // low-spec
ImGui::EndDisabled(); // !isFocused
// Continue button (only when focused)
if (isFocused) {
float btnW = 140.0f * dp;
float btnH = 40.0f * dp;
float btnX = leftX + (colW - btnW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(btnX, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
// Save appearance choices, advance to Bootstrap
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
? static_cast<int>(ui::effects::AcrylicQuality::Low)
: static_cast<int>(ui::effects::AcrylicQuality::Off));
settings_->setBlurMultiplier(wiz_blur_amount);
settings_->setThemeEffectsEnabled(wiz_theme_effects);
settings_->setUIOpacity(wiz_ui_opacity);
settings_->setLowSpecMode(wiz_low_spec);
settings_->setScanlineEnabled(wiz_scanline);
settings_->setBalanceLayout(wiz_balance_layout);
settings_->save();
wizard_phase_ = WizardPhase::BootstrapOffer;
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
cy += btnH;
}
cy += cardPad;
// Lock card height to the tallest content ever seen
static float card0MaxH = 0.0f;
card0MaxH = std::max(card0MaxH, cy - card0Top);
card0Bot = card0Top + card0MaxH;
// Card 0 finalization deferred until after cards 1+2 are sized
}
// ======================= CARD 1: Bootstrap =======================
float card1Top = topY;
float card1Bot;
{
int state = cardState(1);
bool isFocused = (state == 1);
bool isCollapsed = (state == 2 && cardState(2) == 1); // Minimize when step 3 active
float cx = rightX + cardPad;
float cy = card1Top + cardPad;
float contentW = colW - 2 * cardPad;
// Step indicator + title (inline when collapsed)
if (isCollapsed) {
// Compact single-line: icon + "Step 2" + "Bootstrap" + checkmark
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
float labelX = cx + iconW + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, "Step 2");
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, "Step 2").x;
float titleX = labelX + step2W + 12.0f * dp;
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, "Bootstrap");
cy += captionFont->LegacySize + 4.0f * dp;
} else {
// Step indicator
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 2");
cy += captionFont->LegacySize + 4.0f * dp;
}
// Title
{
const char* t = "Bootstrap";
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
cy += titleFont->LegacySize + 6.0f * dp;
}
// Separator
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 10.0f * dp;
}
// --- Content varies by sub-state (only when focused, skip when collapsed) ---
if (isCollapsed) {
// No content — card is minimized
} else if (isFocused && wizard_phase_ == WizardPhase::BootstrapInProgress) {
// ---- Bootstrap download in progress ----
if (!bootstrap_) {
wizard_phase_ = WizardPhase::EncryptOffer;
} else {
auto prog = bootstrap_->getProgress();
const char* statusTitle;
if (prog.state == util::Bootstrap::State::Downloading)
statusTitle = "Downloading bootstrap...";
else if (prog.state == util::Bootstrap::State::Verifying)
statusTitle = "Verifying checksums...";
else
statusTitle = "Extracting blockchain data...";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
cy += bodyFont->LegacySize + 12.0f * dp;
// Progress bar
float barH = 8.0f * dp, barR = 4.0f * dp;
dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + contentW, cy + barH),
IM_COL32(255,255,255,30), barR);
float fillW = contentW * (prog.percent / 100.0f);
if (fillW > 0) {
dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + fillW, cy + barH),
ui::material::Primary(), barR);
}
cy += barH + 8.0f * dp;
// Status text + percent
{
char pctText[64];
snprintf(pctText, sizeof(pctText), "%.1f%%", prog.percent);
ImVec2 pts = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, 0, pctText);
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(cx + contentW - pts.x, cy), textCol, pctText);
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol,
prog.status_text.c_str());
cy += bodyFont->LegacySize + 6.0f * dp;
if (prog.state == util::Bootstrap::State::Extracting) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
dimCol, "(wallet.dat is protected)");
cy += captionFont->LegacySize + 6.0f * dp;
}
cy += 12.0f * dp;
// Cancel button
float cancelW = 100.0f * dp;
float cancelH = 36.0f * dp;
float cancelBX = rightX + (colW - cancelW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
bootstrap_->cancel();
}
ImGui::PopStyleVar();
cy += cancelH;
// Check completion
if (bootstrap_->isDone()) {
auto finalProg = bootstrap_->getProgress();
if (finalProg.state == util::Bootstrap::State::Completed) {
bootstrap_.reset();
wizard_phase_ = WizardPhase::EncryptOffer;
} else {
wizard_phase_ = WizardPhase::BootstrapFailed;
}
}
}
} else if (isFocused && wizard_phase_ == WizardPhase::BootstrapFailed) {
// ---- Bootstrap failed ----
std::string errMsg;
if (bootstrap_) {
errMsg = bootstrap_->getProgress().error;
bootstrap_.reset();
}
if (errMsg.empty()) errMsg = "Bootstrap failed";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "Download Failed");
cy += bodyFont->LegacySize + 8.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
errMsg.c_str());
cy += captionFont->LegacySize + 16.0f * dp;
// Retry / Skip
float btnW2 = 120.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = btnW2 * 2 + 12.0f * dp;
float bx = rightX + (colW - totalBW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap retry");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
cy += btnH2;
} else {
// ---- Bootstrap offer (default view, also for non-focused) ----
// External daemon check (async — avoids blocking UI thread).
// On Windows isRpcPortInUse() creates a TCP socket + connect()
// which can block for seconds when the port is not listening.
bool externalRunning = false;
if (isFocused) {
static std::atomic<bool> s_extCached{false};
static std::atomic<bool> s_checkInFlight{false};
static double s_extLastCheck = -10.0;
double now = ImGui::GetTime();
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
s_extLastCheck = now;
bool embeddedRunning = isEmbeddedDaemonRunning();
s_checkInFlight.store(true);
std::thread([embeddedRunning]() {
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
s_extCached.store(inUse && !embeddedRunning);
s_checkInFlight.store(false);
}).detach();
}
externalRunning = s_extCached.load();
}
if (isFocused && (externalRunning || wizard_stopping_external_)) {
// --- External daemon warning ---
ImU32 warnCol = ui::material::Warning();
{
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, "External daemon running");
}
cy += bodyFont->LegacySize + 4.0f * dp;
{
const char* warnBody = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
ImVec2 ws = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, warnBody);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, warnBody, nullptr, contentW);
cy += ws.y + 12.0f * dp;
}
if (wizard_stopping_external_) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol,
wizard_stop_status_.c_str());
cy += captionFont->LegacySize + 8.0f * dp;
} else {
float stopW = 150.0f * dp;
float skipW2 = 100.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = stopW + 12.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Error()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(
IM_COL32(220, 60, 60, 255)));
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true;
wizard_stop_status_ = "Sending stop command...";
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
wizard_stop_thread_ = std::thread([this]() {
auto config = rpc::Connection::autoDetectConfig();
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
if (tmp_rpc->connect(config.host, config.port,
config.rpcuser, config.rpcpassword)) {
try { tmp_rpc->call("stop"); } catch (...) {}
tmp_rpc->disconnect();
}
}
wizard_stop_status_ = "Waiting for daemon to shut down...";
for (int i = 0; i < 60; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
wizard_stop_status_ = "Daemon stopped.";
wizard_stopping_external_ = false;
return;
}
}
wizard_stop_status_ = "Daemon did not stop — try manually.";
wizard_stopping_external_ = false;
});
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
cy += btnH2;
}
} else {
// --- Normal bootstrap offer ---
{
const char* bsText = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
ImVec2 bsSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, bsText);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, bsText, nullptr, contentW);
cy += bsSize.y + 8.0f * dp;
}
// Trust warning
{
float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity;
if (warnOpacity <= 0) warnOpacity = 0.7f;
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
float twWrap = contentW - iw - 4.0f * dp;
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
cy += twSize.y + 12.0f * dp;
}
// Buttons (only when focused)
if (isFocused) {
float dlBtnW = 150.0f * dp;
float mirrorW = 150.0f * dp;
float skipW2 = 80.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f;
// --- Download button (main / Cloudflare) ---
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Mirror Download button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
cy += btnH2;
}
}
}
cy += cardPad;
// Lock card height to the tallest content ever seen (but not when collapsed)
static float card1MaxH = 0.0f;
if (isCollapsed) {
card1Bot = card1Top + (cy - card1Top);
} else {
card1MaxH = std::max(card1MaxH, cy - card1Top);
card1Bot = card1Top + card1MaxH;
}
finalizeCard(rightX, colW, card1Top, card1Bot, state);
}
// ======================= CARD 2: Encrypt + PIN =======================
float card2Top = card1Bot + gap;
float card2Bot;
{
int state = cardState(2);
bool isFocused = (state == 1);
float cx = rightX + cardPad;
float cy = card2Top + cardPad;
float contentW = colW - 2 * cardPad;
// Pre-start daemon when encrypt card becomes focused so it's ready
// by the time the user finishes typing their passphrase
if (isFocused) {
static bool wiz_daemon_prestarted = false;
if (!wiz_daemon_prestarted) {
wiz_daemon_prestarted = true;
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
startEmbeddedDaemon();
}
if (!state_.connected && !connection_in_progress_) {
tryConnect();
}
}
}
// Step indicator
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 3");
cy += captionFont->LegacySize + 4.0f * dp;
}
// Title (changes for PinSetup sub-state)
{
const char* t = (isFocused && wizard_phase_ == WizardPhase::PinSetup)
? "Quick-Unlock PIN" : "Encryption";
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
cy += titleFont->LegacySize + 6.0f * dp;
}
// Separator
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 10.0f * dp;
// --- Content varies by sub-state ---
if (isFocused && state_.isEncrypted()) {
// ---- Wallet already encrypted ----
{
ImU32 okCol = ui::material::Secondary();
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_VERIFIED_USER).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), okCol, ICON_MD_VERIFIED_USER);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, "Wallet is already encrypted");
cy += bodyFont->LegacySize + 12.0f * dp;
}
{
const char* desc = "Your wallet is protected with a passphrase. No further action is needed.";
ImVec2 ds = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, desc);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, desc, nullptr, contentW);
cy += ds.y + 20.0f * dp;
}
// Continue button — skip to Done
float btnW2 = 140.0f * dp;
float btnH2 = 40.0f * dp;
float bx = rightX + (colW - btnW2) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
cy += btnH2;
} else if (isFocused) {
// ---- Encryption offer + optional PIN (combined) ----
{
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, encDesc, nullptr, contentW);
cy += edSize.y + 6.0f * dp;
}
{
ImU32 warnCol2 = ui::material::Warning();
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol2, ICON_MD_WARNING);
const char* warnLoss = "If you lose your passphrase, you lose access to your funds.";
float wlWrap = contentW - iw - 4.0f * dp;
ImVec2 wlSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, wlWrap, warnLoss);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol2, warnLoss, nullptr, wlWrap);
cy += wlSize.y + 8.0f * dp;
}
// Passphrase input
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Passphrase:");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushItemWidth(contentW);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
ImGui::InputText("##wiz_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_),
ImGuiInputTextFlags_Password);
ImGui::PopStyleVar();
ImGui::PopItemWidth();
cy += 36.0f * dp + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm:");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushItemWidth(contentW);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
ImGui::InputText("##wiz_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_),
ImGuiInputTextFlags_Password);
ImGui::PopStyleVar();
ImGui::PopItemWidth();
cy += 36.0f * dp + 6.0f * dp;
// Strength meter
{
size_t len = strlen(encrypt_pass_buf_);
const char* strengthLabel = "Weak";
ImU32 strengthCol = ui::material::Error();
float strengthPct = 0.25f;
if (len >= 16) {
strengthLabel = "Strong"; strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
} else if (len >= 12) {
strengthLabel = "Good"; strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
} else if (len >= 8) {
strengthLabel = "Fair"; strengthCol = ui::material::Warning(); strengthPct = 0.5f;
}
float sBarH = 4.0f * dp, sBarR = 2.0f * dp;
dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + contentW, cy + sBarH),
IM_COL32(255,255,255,30), sBarR);
if (len > 0) {
dl->AddRectFilled(ImVec2(cx, cy), ImVec2(cx + contentW * strengthPct, cy + sBarH),
strengthCol, sBarR);
}
cy += sBarH + 4.0f * dp;
char slabel[64];
snprintf(slabel, sizeof(slabel), "Strength: %s", strengthLabel);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, slabel);
cy += captionFont->LegacySize + 10.0f * dp;
}
// Feedback on why Encrypt is disabled
{
size_t pLen = strlen(encrypt_pass_buf_);
if (pLen > 0 && pLen < 8) {
char fb[80];
snprintf(fb, sizeof(fb), "Passphrase must be at least 8 characters (%zu/8)", pLen);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), fb);
cy += captionFont->LegacySize + 6.0f * dp;
} else if (pLen >= 8 && strlen(encrypt_confirm_buf_) > 0 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) != 0) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "Passphrases do not match");
cy += captionFont->LegacySize + 6.0f * dp;
}
}
// ---- Optional PIN section ----
cy += 4.0f * dp;
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 8.0f * dp;
{
const char* pinTitle = "Quick-Unlock PIN (optional)";
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, pinTitle);
cy += captionFont->LegacySize + 4.0f * dp;
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "PIN (4-8 digits):");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushItemWidth(contentW);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
ImGui::InputText("##wiz_pin", wizard_pin_buf_, sizeof(wizard_pin_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
ImGui::PopStyleVar();
ImGui::PopItemWidth();
cy += 36.0f * dp + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm PIN:");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushItemWidth(contentW);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
ImGui::InputText("##wiz_pin_confirm", wizard_pin_confirm_buf_, sizeof(wizard_pin_confirm_buf_),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
ImGui::PopStyleVar();
ImGui::PopItemWidth();
cy += 36.0f * dp + 6.0f * dp;
// PIN validation feedback
{
std::string pinStr(wizard_pin_buf_);
if (!pinStr.empty() && !util::SecureVault::isValidPin(pinStr)) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "PIN must be 4-8 digits");
cy += captionFont->LegacySize + 6.0f * dp;
} else if (!pinStr.empty() && strlen(wizard_pin_confirm_buf_) > 0 &&
pinStr != std::string(wizard_pin_confirm_buf_)) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "PINs do not match");
cy += captionFont->LegacySize + 6.0f * dp;
}
}
// Status
if (!encrypt_status_.empty()) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Warning(), encrypt_status_.c_str());
cy += captionFont->LegacySize + 6.0f * dp;
}
// Buttons
{
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
// PIN is optional: if entered, must be valid + confirmed
std::string pinStr(wizard_pin_buf_);
bool pinEntered = !pinStr.empty();
bool pinOk = !pinEntered ||
(util::SecureVault::isValidPin(pinStr) &&
pinStr == std::string(wizard_pin_confirm_buf_));
bool canEncrypt = passValid && pinOk && !encrypt_in_progress_;
float encBtnW = 180.0f * dp;
float skipW2 = 80.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = encBtnW + 12.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(
canEncrypt ? ui::material::Primary() : IM_COL32(128,128,128,128)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canEncrypt);
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
if (pinEntered && pinOk)
deferred_encrypt_pin_ = pinStr;
deferred_encrypt_pending_ = true;
// Clear sensitive buffers
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
memset(wizard_pin_buf_, 0, sizeof(wizard_pin_buf_));
memset(wizard_pin_confirm_buf_, 0, sizeof(wizard_pin_confirm_buf_));
// Start daemon + finish wizard immediately
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
}
tryConnect();
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
ui::Notifications::instance().info("Encryption will complete in the background");
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
}
tryConnect();
}
ImGui::PopStyleVar();
cy += btnH2;
}
} else {
// ---- Not focused: show static description ----
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), dimCol, encDesc, nullptr, contentW);
cy += edSize.y + 6.0f * dp;
}
cy += cardPad;
{
card2Bot = card2Top + (cy - card2Top);
// Stretch card 2 so its bottom sits flush with card 0 (left column)
if (card0Bot > card2Bot)
card2Bot = card0Bot;
}
finalizeCard(rightX, colW, card2Top, card2Bot, state);
}
// --- Deferred Card 0 finalization: match right column total height ---
{
float rightColBot = card2Bot;
if (rightColBot > card0Bot) card0Bot = rightColBot;
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
}
// Merge channels: backgrounds → content → overlays
dl->ChannelsMerge();
ImGui::End();
}
} // namespace dragonx