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
1313 lines
63 KiB
C++
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
|