feat(recovery): redesign the wallet auto-recovery flow
Turns the "Daemon Error + raw log dump" moment into one calm, honest recovery
dialog plus a recovery-aware rescan screen. Presentation + orchestration only —
the file-safety logic in rebuildWalletDatabase()/restoreOriginalWallet() (source
selection, verify-before-swap, copy/rename-never-delete, .bak) is unchanged.
- One authoritative dialog with a phase machine Offer -> Working -> Done/Failed.
The duplicate in-overlay recovery card, the untranslated red "Daemon Error"
heading, and the raw daemon-log dump are gone for the recovery case (they stay
for genuine, unrelated crashes).
- Offer is a choice-cards layout: "Repair automatically" (recommended, accent-
tinted) vs "Restore original", side by side; the rare actions ("Show me the
files", "Decide later") and a plain-language "What happens to my files?" sit in
a quiet footer. When the rebuild helper is missing, it collapses to a single
Restore card — never a dead end.
- Post-repair rescan shows a calm "Finishing your wallet repair" screen with
elapsed time + the growing wallet size, instead of "RPC timeout / taking longer
than expected / restart daemon"; the daemon-crash toast is suppressed and the
detection toast is downgraded from red to info.
- Fixes a confirmed dead-end: if a repair succeeds but the restarted daemon then
crashes for a *different* reason (block index, disk, OOM), the recovery flags
now clear (in tryConnect + onConnected) so it surfaces as a normal daemon
failure instead of freezing forever on a reassuring "don't restart" screen.
- Clickable "Wallet repair available" status-bar chip for re-entry.
The same app.cpp changes HiDPI-harden the surfaces the recovery flow lives on:
the status-bar and loading-overlay hand-drawn geometry are multiplied by dpiScale
(they rendered native-size and clipped at HiDPI / font_scale>1), the loading-
overlay status text wraps instead of running off both edges, and the node-status
banner floors its height to its DPI-baked font so the title can't clip off the top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
608
src/app.cpp
608
src/app.cpp
@@ -1644,8 +1644,10 @@ void App::render()
|
||||
float v = ui::schema::UI().drawElement("components.sidebar", key).size;
|
||||
return (v >= 0 ? v : fb) * dp;
|
||||
};
|
||||
float statusBarH = ui::schema::UI().window("components.status-bar").height;
|
||||
if (statusBarH <= 0.0f) statusBarH = 24.0f; // safety fallback
|
||||
// Scale by dp to match the rendered status-bar child height (renderStatusBar), so the reserved
|
||||
// content strip stays in step with the bar at HiDPI instead of under-reserving.
|
||||
float statusBarH = ui::schema::UI().window("components.status-bar").height * dp;
|
||||
if (statusBarH <= 0.0f) statusBarH = 24.0f * dp; // safety fallback
|
||||
// Content area padding from ui.toml schema
|
||||
const auto& caWin = ui::schema::UI().window("components.content-area");
|
||||
const float caMarginTop = ui::schema::UI().drawElement("components.content-area", "margin-top").size;
|
||||
@@ -2189,9 +2191,18 @@ void App::renderNodeStatusBanner()
|
||||
const auto& S = ui::schema::UI();
|
||||
const float minH = S.drawElement("banners.node-status", "min-height").size;
|
||||
const float baseH = S.drawElement("banners.node-status", "height").size;
|
||||
|
||||
// Fonts up-front so the banner height can never be shorter than the glyph row it vertically
|
||||
// centers — otherwise the title/icon draw above the child's top clip rect and slice off (seen at
|
||||
// HiDPI / large font scale, where the DPI-baked glyphs outgrow the schema height). Metrics scaled.
|
||||
ImFont* icoFont = m::Type().iconSmall();
|
||||
ImFont* txtFont = m::Type().body2();
|
||||
const float glyphH = std::max(icoFont ? icoFont->LegacySize : 0.0f,
|
||||
txtFont ? txtFont->LegacySize : 0.0f);
|
||||
// Both operands must be in scaled px: vScale() already folds in dpiScale(), so the raw min-height
|
||||
// floor needs the same dpiScale() or it under-clamps the banner at HiDPI.
|
||||
const float bannerH = std::max(minH * ui::Layout::dpiScale(), baseH * ui::Layout::vScale());
|
||||
float bannerH = std::max(minH * ui::Layout::dpiScale(), baseH * ui::Layout::vScale());
|
||||
bannerH = std::max(bannerH, glyphH + ui::Layout::spacingSm() * 2.0f); // never shorter than text
|
||||
|
||||
const bool isError = (banner.severity == ui::NodeBannerSeverity::Error);
|
||||
const ImU32 sevCol = isError ? m::Error() : m::Warning();
|
||||
@@ -2222,18 +2233,15 @@ void App::renderNodeStatusBanner()
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
const float winW = ImGui::GetWindowSize().x;
|
||||
|
||||
ImFont* icoFont = m::Type().iconSmall();
|
||||
ImFont* txtFont = m::Type().body2();
|
||||
|
||||
// Icon — centered on its own metrics.
|
||||
ImGui::SetCursorPos(ImVec2(padX, (bannerH - icoFont->LegacySize) * 0.5f));
|
||||
// Icon — centered on its own metrics (clamped so it never draws above the child's top).
|
||||
ImGui::SetCursorPos(ImVec2(padX, std::max(0.0f, (bannerH - icoFont->LegacySize) * 0.5f)));
|
||||
ImGui::PushFont(icoFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, sevCol);
|
||||
ImGui::TextUnformatted(icon);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
|
||||
const float txtCy = (bannerH - txtFont->LegacySize) * 0.5f;
|
||||
const float txtCy = std::max(0.0f, (bannerH - txtFont->LegacySize) * 0.5f);
|
||||
|
||||
// Right-aligned action button geometry (measured first so the detail text can be clipped to
|
||||
// never run underneath it).
|
||||
@@ -2281,7 +2289,7 @@ void App::renderNodeStatusBanner()
|
||||
|
||||
// Action button.
|
||||
if (actionLabel) {
|
||||
ImGui::SetCursorPos(ImVec2(winW - btnW - padX, (bannerH - btnH) * 0.5f));
|
||||
ImGui::SetCursorPos(ImVec2(winW - btnW - padX, std::max(0.0f, (bannerH - btnH) * 0.5f)));
|
||||
if (m::TactileButton(actionLabel, ImVec2(btnW, btnH))) {
|
||||
if (banner.action == ui::NodeBannerAction::RestartNode) restartDaemon();
|
||||
else if (banner.action == ui::NodeBannerAction::Reconnect) tryConnect();
|
||||
@@ -2388,12 +2396,15 @@ void App::renderStatusBar()
|
||||
// Status bar layout from unified UI schema
|
||||
const auto& S = ui::schema::UI();
|
||||
const auto& sbWin = S.window("components.status-bar");
|
||||
const float sbHeight = sbWin.height;
|
||||
const float sbPadX = sbWin.padding[0];
|
||||
const float sbPadY = sbWin.padding[1];
|
||||
const float sbIconTextGap = S.drawElement("components.status-bar", "icon-text-gap").size;
|
||||
const float sbSectionGap = S.drawElement("components.status-bar", "section-gap").size;
|
||||
const float sbSeparatorGap = S.drawElement("components.status-bar", "separator-gap").size;
|
||||
// Schema values are logical px; the fonts/icons drawn inside are DPI-baked, so the box and its
|
||||
// gaps must be scaled by the same factor or they clip the text at HiDPI / font_scale > 1.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
const float sbHeight = sbWin.height * dp;
|
||||
const float sbPadX = sbWin.padding[0] * dp;
|
||||
const float sbPadY = sbWin.padding[1] * dp;
|
||||
const float sbIconTextGap = S.drawElement("components.status-bar", "icon-text-gap").size * dp;
|
||||
const float sbSectionGap = S.drawElement("components.status-bar", "section-gap").size * dp;
|
||||
const float sbSeparatorGap = S.drawElement("components.status-bar", "separator-gap").size * dp;
|
||||
|
||||
ImGuiWindowFlags childFlags = ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse;
|
||||
@@ -2625,6 +2636,8 @@ void App::renderStatusBar()
|
||||
// Compute positions dynamically from actual text widths so they
|
||||
// never overlap and always stay within the window at any font scale.
|
||||
{
|
||||
// Where the left-side chain ended, so the right cluster never SameLine()s backward onto it.
|
||||
const float leftEndX = ImGui::GetCursorPosX();
|
||||
char versionBuf[32];
|
||||
snprintf(versionBuf, sizeof(versionBuf), "v%s", DRAGONX_VERSION);
|
||||
float versionW = ImGui::CalcTextSize(versionBuf).x;
|
||||
@@ -2636,10 +2649,36 @@ void App::renderStatusBar()
|
||||
float gap = sbSectionGap;
|
||||
float occupiedX = versionX; // leftmost X used by the version + connection status so far
|
||||
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
||||
float statusW = ImGui::CalcTextSize(connection_status_.c_str()).x;
|
||||
// During a post-repair rescan the raw status is a scary "RPC request failed: Timeout" — show a
|
||||
// calm line instead (the rescan legitimately can't answer RPC yet).
|
||||
const std::string statusBase = post_recovery_rescan_ ? std::string(TR("sb_finishing_repair"))
|
||||
: connection_status_;
|
||||
// Middle-ellipsize to the space between the left chain and the version so a long status
|
||||
// (e.g. "Wallet needs recovery — see the prompt") can't push off-screen or overprint the
|
||||
// left chain; then clamp its start so it never crosses left of where the chain ended.
|
||||
float availW = versionX - leftEndX - gap * 2.0f;
|
||||
if (availW < 24.0f * dp) availW = 24.0f * dp;
|
||||
ImFont* stFont = ImGui::GetFont();
|
||||
std::string statusShown = ui::material::TruncateToWidth(
|
||||
statusBase, stFont, stFont->LegacySize, availW);
|
||||
float statusW = ImGui::CalcTextSize(statusShown.c_str()).x;
|
||||
float statusX = versionX - statusW - gap;
|
||||
if (statusX < leftEndX + gap) statusX = leftEndX + gap;
|
||||
ImGui::SameLine(statusX);
|
||||
ImGui::TextDisabled("%s", connection_status_.c_str());
|
||||
if (wallet_auto_recovered_ && !post_recovery_rescan_) {
|
||||
// Actionable re-entry: a full-opacity accent chip that reopens the recovery dialog.
|
||||
// Dismiss is non-destructive, so this is the guaranteed way back to the prompt.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Warning());
|
||||
ImGui::TextUnformatted(statusShown.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsItemClicked() && recovery_phase_ != RecoveryPhase::Working) {
|
||||
show_wallet_recovered_dialog_ = true; // reopen the (non-destructively dismissed) prompt
|
||||
recovery_phase_ = RecoveryPhase::Offer;
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", statusShown.c_str());
|
||||
}
|
||||
occupiedX = statusX;
|
||||
} else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) {
|
||||
const char* errText = TR("sb_daemon_not_found");
|
||||
@@ -2667,7 +2706,6 @@ void App::renderStatusBar()
|
||||
// including ones whose toast already faded; an unread dot marks alerts that arrived since
|
||||
// the panel was last opened.
|
||||
{
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
auto& notes = ui::Notifications::instance();
|
||||
ImFont* bellFont = ui::material::Type().iconSmall();
|
||||
const bool anyHist = notes.hasHistory();
|
||||
@@ -3994,9 +4032,16 @@ void App::renderSeedMigrationDialog()
|
||||
ImGui::TextWrapped("%s", TR("mig_already_mnemonic"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
if (ui::material::TactileButton(TR("mig_backup_instead"), ImVec2(200 * dp, 0))) {
|
||||
close();
|
||||
showSeedBackupDialog();
|
||||
{
|
||||
// Size to the label — the fixed 200px clipped the FR/PT/ES/DE/RU translations.
|
||||
ImFont* bkFont = ui::material::Type().button();
|
||||
const float bkW = std::max(200.0f * dp,
|
||||
bkFont->CalcTextSizeA(bkFont->LegacySize, FLT_MAX, 0, TR("mig_backup_instead")).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp);
|
||||
if (ui::material::TactileButton(TR("mig_backup_instead"), ImVec2(bkW, 0))) {
|
||||
close();
|
||||
showSeedBackupDialog();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(120 * dp, 0))) close();
|
||||
@@ -4167,8 +4212,12 @@ void App::renderSeedMigrationDialog()
|
||||
ImGui::Spacing();
|
||||
char cbuf[64]; snprintf(cbuf, sizeof(cbuf), TR("mig_confs"), seed_migration_sweep_confs_);
|
||||
ImGui::TextColored(kMedium, "%s", cbuf);
|
||||
if (!seed_migration_sweep_txid_.empty())
|
||||
ImGui::TextColored(kMedium, "%s%s", TR("mig_txid"), seed_migration_sweep_txid_.c_str());
|
||||
if (!seed_migration_sweep_txid_.empty()) {
|
||||
// A 64-hex txid overflows the fixed 580px card; render it in a wrapping, copyable field
|
||||
// (same widget the import-key sweep result uses) instead of a raw one-line label.
|
||||
ImGui::TextColored(kMedium, "%s", TR("mig_txid"));
|
||||
ui::widgets::AddressCopyField("##migtxid", seed_migration_sweep_txid_);
|
||||
}
|
||||
if (seed_migration_legacy_remaining_ >= 0.0) {
|
||||
char rbuf[96]; snprintf(rbuf, sizeof(rbuf), TR("mig_remaining"), seed_migration_legacy_remaining_);
|
||||
ImGui::TextColored(kMedium, "%s", rbuf);
|
||||
@@ -4292,39 +4341,319 @@ void App::renderWalletRecoveredDialog()
|
||||
|
||||
ui::material::OverlayDialogSpec ov;
|
||||
ov.title = TR("wallet_recovered_title");
|
||||
ov.p_open = &show_wallet_recovered_dialog_;
|
||||
// Dismiss (backdrop / [X]) is disabled during Working — files are mid-swap. A null p_open is safe:
|
||||
// BeginOverlayDialog guards both the close button and backdrop-close on it.
|
||||
ov.p_open = (recovery_phase_ == RecoveryPhase::Working) ? nullptr : &show_wallet_recovered_dialog_;
|
||||
ov.style = ui::material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 560.0f;
|
||||
ov.cardWidth = 620.0f; // wide enough for the two side-by-side choice cards
|
||||
ov.idSuffix = "walletrecovered";
|
||||
if (!ui::material::BeginOverlayDialog(ov)) return;
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
// Size each button to its label (with a modest floor) instead of a magic fixed width, so
|
||||
// translations of these keys — added additively to res/lang later — can't silently clip.
|
||||
ImFont* rbf = ui::material::Type().button();
|
||||
auto fitBtnW = [&](const char* label) {
|
||||
return std::max(96.0f * dp,
|
||||
rbf->CalcTextSizeA(rbf->LegacySize, FLT_MAX, 0, label).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp);
|
||||
};
|
||||
// A full-width action row: the button, then a dim wrapped one-line explanation of what it does.
|
||||
auto actionRow = [&](const char* label, const char* sub) -> bool {
|
||||
const bool clicked = ui::material::TactileButton(label, ImVec2(fitBtnW(label), 0));
|
||||
if (sub && sub[0]) {
|
||||
ImGui::PushFont(ui::material::Type().caption());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextDisabled("%s", sub);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopFont();
|
||||
}
|
||||
return clicked;
|
||||
};
|
||||
// A disclosure that recedes: no filled bar, dim label — so the primary action stays dominant.
|
||||
auto quietHeader = [&](const char* label) -> bool {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ui::material::WithAlpha(ui::material::OnSurface(), 20));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ui::material::WithAlpha(ui::material::OnSurface(), 32));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
const bool open = ImGui::CollapsingHeader(label);
|
||||
ImGui::PopStyleColor(4);
|
||||
return open;
|
||||
};
|
||||
// Opt-in daemon log — the same lines that used to be dumped on screen, now behind a quiet disclosure.
|
||||
auto techDetails = [&]() {
|
||||
if (!daemon_controller_) return;
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
if (quietHeader(TR("wallet_recovery_details_label"))) {
|
||||
ImGui::PushFont(ui::material::Type().caption());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
for (const auto& ln : daemon_controller_->recentLines(8))
|
||||
ImGui::TextDisabled("%s", ln.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopFont();
|
||||
}
|
||||
};
|
||||
// Funds-safety hero: a shield icon + the reassurance, made the visual focal point of the screen.
|
||||
auto safetyHero = [&]() {
|
||||
ImFont* icoF = ui::material::Type().iconLarge();
|
||||
ImFont* txtF = ui::material::Type().subtitle1();
|
||||
const float rowTop = ImGui::GetCursorPosY();
|
||||
ImGui::PushFont(icoF);
|
||||
ImGui::TextColored(ui::material::SuccessVec4(), ICON_MD_HEALTH_AND_SAFETY);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
const float iconH = icoF->LegacySize;
|
||||
const float textH = txtF ? txtF->LegacySize : ImGui::GetFontSize();
|
||||
if (iconH > textH) ImGui::SetCursorPosY(rowTop + (iconH - textH) * 0.5f);
|
||||
ImGui::PushFont(txtF);
|
||||
ImGui::TextColored(ui::material::SuccessVec4(), "%s", TR("wallet_recovered_safety"));
|
||||
ImGui::PopFont();
|
||||
};
|
||||
// Accent-tinted primary action so the recommended button clearly dominates the quiet disclosures
|
||||
// (a translucent Primary tint over the glass button — keeps the label readable on any theme).
|
||||
auto primaryAction = [&](const char* label, const char* sub) -> bool {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ui::material::WithAlpha(ui::material::Primary(), 60));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ui::material::WithAlpha(ui::material::Primary(), 90));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ui::material::WithAlpha(ui::material::Primary(), 115));
|
||||
const bool clicked = actionRow(label, sub);
|
||||
ImGui::PopStyleColor(3);
|
||||
return clicked;
|
||||
};
|
||||
// A choice card: icon + title (+ optional "recommended" chip) + wrapped blurb + a full-width button
|
||||
// pinned to the card bottom. The recommended card gets a gold-tinted fill + border so the choice
|
||||
// reads at a glance. cardH is precomputed by the caller so side-by-side cards stay equal height.
|
||||
auto renderCard = [&](const char* id, const char* glyph, const char* title, const char* desc,
|
||||
const char* btn, bool recommended, float cardW, float cardH) -> bool {
|
||||
const float pad = ui::Layout::spacingMd();
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
||||
recommended ? ui::material::WithAlpha(ui::material::Primary(), 22)
|
||||
: ui::material::WithAlpha(ui::material::OnSurface(), 8));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border,
|
||||
recommended ? ui::material::WithAlpha(ui::material::Primary(), 140)
|
||||
: ui::material::WithAlpha(ui::material::OnSurface(), 28));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(pad, pad));
|
||||
ImGui::BeginChild(id, ImVec2(cardW, cardH), true,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// Zero implicit item spacing so the only vertical gaps are the explicit Dummy()s below — that
|
||||
// keeps the caller's measured cardH exact, so the bottom-pinned button never clips.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ui::material::DialogWarningHeader(TR("wallet_recovered_warn"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("%s", TR("wallet_recovered_body"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
// Preferred fix when available: REBUILD the wallet database. Plain "Restore original" hands the same
|
||||
// BDB-inconsistent file back and the daemon just re-salvages it (the cascade); the rebuild produces a
|
||||
// fresh, consistent copy of every key that the daemon loads cleanly. Copy/rename-only — never deletes.
|
||||
if (walletRebuildAvailable()) {
|
||||
if (ui::material::TactileButton(TR("wallet_recovered_rebuild"), ImVec2(280.0f * dp, 0))) {
|
||||
rebuildWalletDatabase(); // clears show_wallet_recovered_dialog_
|
||||
ImGui::PushFont(ui::material::Type().iconMed());
|
||||
ImGui::TextColored(recommended ? ui::material::PrimaryVec4()
|
||||
: ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()),
|
||||
"%s", glyph);
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingXs()));
|
||||
|
||||
ImGui::PushFont(ui::material::Type().subtitle2());
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::PopFont();
|
||||
if (recommended) {
|
||||
ImGui::PushFont(ui::material::Type().caption());
|
||||
ImGui::TextColored(ui::material::PrimaryVec4(), "%s", TR("wallet_recovery_recommended"));
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingXs()));
|
||||
|
||||
ImGui::PushFont(ui::material::Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextWrapped("%s", desc);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
|
||||
// Pin the button to the card bottom so both cards' buttons line up.
|
||||
const float btnH = ImGui::GetFrameHeight() + 6.0f * dp;
|
||||
const float remaining = ImGui::GetContentRegionAvail().y - btnH;
|
||||
if (remaining > 0.0f) ImGui::Dummy(ImVec2(0, remaining));
|
||||
bool clicked;
|
||||
if (recommended) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ui::material::WithAlpha(ui::material::Primary(), 65));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ui::material::WithAlpha(ui::material::Primary(), 100));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ui::material::WithAlpha(ui::material::Primary(), 125));
|
||||
clicked = ui::material::TactileButton(btn, ImVec2(-FLT_MIN, btnH));
|
||||
ImGui::PopStyleColor(3);
|
||||
} else {
|
||||
clicked = ui::material::TactileButton(btn, ImVec2(-FLT_MIN, btnH));
|
||||
}
|
||||
ImGui::PopStyleVar(); // ItemSpacing
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor(2);
|
||||
return clicked;
|
||||
};
|
||||
// A quiet clickable text link for the footer (Show me the files / Not now).
|
||||
auto linkText = [&](const char* label) -> bool {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::PopStyleColor();
|
||||
const bool clicked = ImGui::IsItemClicked();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const ImVec2 mn = ImGui::GetItemRectMin(), mx = ImGui::GetItemRectMax();
|
||||
ImGui::GetWindowDrawList()->AddLine(ImVec2(mn.x, mx.y), ImVec2(mx.x, mx.y),
|
||||
ui::material::OnSurface());
|
||||
}
|
||||
return clicked;
|
||||
};
|
||||
|
||||
switch (recovery_phase_) {
|
||||
case RecoveryPhase::Offer: {
|
||||
safetyHero();
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("%s", TR("wallet_recovered_warn"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
|
||||
// The two real actions as side-by-side CHOICE CARDS — the recommended one gold-tinted, so the
|
||||
// decision reads at a glance instead of hiding in a menu. Rebuild is the more complete fix; when
|
||||
// its helper is missing, Restore becomes the single recommended card (never a dead end).
|
||||
const bool canRebuild = walletRebuildAvailable();
|
||||
const float cardGap = ui::Layout::spacingMd();
|
||||
const float pad = cardGap;
|
||||
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||
const float cardW = canRebuild ? (contentW - cardGap) * 0.5f
|
||||
: std::min(contentW, 320.0f * dp);
|
||||
// Fix a shared card height off the taller card body so the two cards line up. Measure title AND
|
||||
// desc wraps (titles/descs can be multi-line, esp. after translation), with a little width slack.
|
||||
ImFont* descF = ui::material::Type().caption();
|
||||
ImFont* titleF = ui::material::Type().subtitle2();
|
||||
const float innerW = std::max(1.0f, cardW - 2.0f * pad - 6.0f * dp);
|
||||
auto measureH = [&](ImFont* f, const char* s) {
|
||||
return f->CalcTextSizeA(f->LegacySize, FLT_MAX, innerW, s).y;
|
||||
};
|
||||
const float titleH = std::max(measureH(titleF, TR("wallet_recovery_rebuild_card")),
|
||||
measureH(titleF, TR("wallet_recovery_restore_card")));
|
||||
const float descH = std::max(measureH(descF, TR("wallet_recovery_rebuild_card_desc")),
|
||||
measureH(descF, TR("wallet_recovery_restore_card_desc")));
|
||||
const float cardH = 2.0f * pad
|
||||
+ ui::material::Type().iconMed()->LegacySize + ui::Layout::spacingXs() // icon + gap
|
||||
+ titleH + descF->LegacySize // title + RECOMMENDED chip
|
||||
+ ui::Layout::spacingXs() // gap before desc
|
||||
+ descH + ui::Layout::spacingSm() // desc + gap before button
|
||||
+ ImGui::GetFrameHeight() + 6.0f * dp // button
|
||||
+ ui::Layout::spacingXs(); // small buffer
|
||||
|
||||
if (canRebuild) {
|
||||
const bool r = renderCard("##rcRepair", ICON_MD_AUTO_FIX_HIGH, TR("wallet_recovery_rebuild_card"),
|
||||
TR("wallet_recovery_rebuild_card_desc"), TR("wallet_recovery_repair_go"),
|
||||
true, cardW, cardH);
|
||||
ImGui::SameLine(0, cardGap);
|
||||
const bool s = renderCard("##rcRestore", ICON_MD_SETTINGS_BACKUP_RESTORE, TR("wallet_recovery_restore_card"),
|
||||
TR("wallet_recovery_restore_card_desc"), TR("wallet_recovery_restore_go"),
|
||||
false, cardW, cardH);
|
||||
if (r) rebuildWalletDatabase();
|
||||
if (s) restoreOriginalWallet();
|
||||
} else {
|
||||
const float indent = (contentW - cardW) * 0.5f;
|
||||
if (indent > 0.0f) ImGui::Indent(indent);
|
||||
const bool s = renderCard("##rcRestoreOnly", ICON_MD_SETTINGS_BACKUP_RESTORE, TR("wallet_recovery_restore_card"),
|
||||
TR("wallet_recovery_restore_card_desc"), TR("wallet_recovery_restore_go"),
|
||||
true, cardW, cardH);
|
||||
if (indent > 0.0f) ImGui::Unindent(indent);
|
||||
if (s) restoreOriginalWallet();
|
||||
}
|
||||
|
||||
// Quiet footer: inspect-files link + a clearly-labelled, reversible dismiss (with a tooltip that
|
||||
// spells out the consequence — plain "Not now" was ambiguous).
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
if (linkText(TR("wallet_recovered_open_folder")))
|
||||
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||
ImGui::SameLine(0, ui::Layout::spacingSm());
|
||||
ImGui::TextDisabled("\xC2\xB7"); // middle dot separator
|
||||
ImGui::SameLine(0, ui::Layout::spacingSm());
|
||||
if (linkText(TR("wallet_recovery_decide_later"))) {
|
||||
show_wallet_recovered_dialog_ = false; // non-destructive; reopen from the status bar
|
||||
recovery_phase_ = RecoveryPhase::Offer;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 22.0f);
|
||||
ImGui::TextUnformatted(TR("wallet_recovery_decide_later_tip"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
// Plain-language "what happens to my files" — replaces the unhelpful raw daemon log on this screen
|
||||
// (the log is still available in the Console tab for troubleshooting).
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
if (quietHeader(TR("wallet_recovery_files_label"))) {
|
||||
ImGui::PushFont(ui::material::Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextWrapped("%s", TR("wallet_recovery_files_detail"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RecoveryPhase::Working: {
|
||||
ImGui::TextWrapped("%s", recovery_last_action_rebuild_ ? TR("wallet_rebuild_started")
|
||||
: TR("wallet_restore_started"));
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
ImGui::Text("%s%s", TR("wallet_recovery_working_label"), ui::material::LoadingDots());
|
||||
break;
|
||||
}
|
||||
case RecoveryPhase::Done: {
|
||||
safetyHero();
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("%s", recovery_last_action_rebuild_ ? TR("wallet_recovery_success_body")
|
||||
: TR("wallet_recovery_success_restore"));
|
||||
// A sev-1 warning (e.g. restored but the node didn't relaunch) carries a specific message.
|
||||
if (recovery_outcome_sev_ == 1 && !recovery_outcome_msg_.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingXs()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Warning());
|
||||
ImGui::TextWrapped("%s", recovery_outcome_msg_.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
if (ui::material::TactileButton(TR("wallet_recovery_done"), ImVec2(fitBtnW(TR("wallet_recovery_done")), 0))) {
|
||||
show_wallet_recovered_dialog_ = false;
|
||||
recovery_phase_ = RecoveryPhase::Offer;
|
||||
// Clean success — clear the session flag so a later unrelated disconnect doesn't re-raise the
|
||||
// "repair available" chip/dialog for a wallet that's already fixed. (Warnings keep it set.)
|
||||
if (recovery_outcome_sev_ == 0) wallet_auto_recovered_ = false;
|
||||
}
|
||||
techDetails();
|
||||
break;
|
||||
}
|
||||
case RecoveryPhase::Failed: {
|
||||
ImGui::PushFont(ui::material::Type().subtitle1());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||
ImGui::TextWrapped("%s", TR("wallet_recovery_failure_title"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
ImGui::TextWrapped("%s", TR("wallet_recovery_failure_body"));
|
||||
if (!recovery_outcome_msg_.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingXs()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||
ImGui::TextWrapped("%s", recovery_outcome_msg_.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||
// Offer the UNtried option (Restore needs no helper; Rebuild only if its helper exists).
|
||||
if (recovery_last_action_rebuild_) {
|
||||
if (actionRow(TR("wallet_recovery_try_other"), TR("wallet_recovered_restore_sub")))
|
||||
restoreOriginalWallet();
|
||||
} else if (walletRebuildAvailable()) {
|
||||
if (actionRow(TR("wallet_recovery_try_other"), TR("wallet_recovered_rebuild_sub")))
|
||||
rebuildWalletDatabase();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
if (actionRow(TR("wallet_recovered_open_folder"), TR("wallet_recovered_open_folder_sub")))
|
||||
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(fitBtnW(TR("close")), 0))) {
|
||||
show_wallet_recovered_dialog_ = false;
|
||||
recovery_phase_ = RecoveryPhase::Offer;
|
||||
}
|
||||
techDetails();
|
||||
break;
|
||||
}
|
||||
// Restore of the untouched original (stops the node, swaps the .bak back over the salvaged copy,
|
||||
// clears the stale BDB env, restarts).
|
||||
if (ui::material::TactileButton(TR("wallet_recovered_restore"), ImVec2(260.0f * dp, 0))) {
|
||||
restoreOriginalWallet(); // clears show_wallet_recovered_dialog_
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("wallet_recovered_open_folder"), ImVec2(200.0f * dp, 0))) {
|
||||
util::Platform::openFolder(util::Platform::getDragonXDataDir()); // manual restore instead
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("wallet_recovered_dismiss"), ImVec2(150.0f * dp, 0))) {
|
||||
show_wallet_recovered_dialog_ = false; // acknowledged; keeps the salvaged wallet loaded
|
||||
}
|
||||
|
||||
ui::material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
@@ -5462,6 +5791,10 @@ void App::renderLoadingOverlay(float contentH)
|
||||
using namespace ui::material;
|
||||
constexpr float kPi = 3.14159265f;
|
||||
|
||||
// The wallet-recovery dialog owns the screen while a salvage is pending — suppress the loading
|
||||
// spinner underneath it so "still loading" and "make a decision" are never the same screen.
|
||||
if (show_wallet_recovered_dialog_) return;
|
||||
|
||||
auto loadElem = [](const char* key, float fb) {
|
||||
float v = ui::schema::UI().drawElement("screens.loading", key).size;
|
||||
return v >= 0 ? v : fb;
|
||||
@@ -5473,12 +5806,17 @@ void App::renderLoadingOverlay(float contentH)
|
||||
ImVec2 wp = ImGui::GetWindowPos();
|
||||
ImVec2 ws = ImGui::GetWindowSize();
|
||||
|
||||
// Hand-drawn geometry here is in logical px; the fonts drawn between the elements are DPI-baked,
|
||||
// so every spinner/bar/gap constant must be multiplied by dpiScale or it renders native-tiny and
|
||||
// the spacing/centering drifts at HiDPI / font_scale > 1. (Font metrics are already scaled.)
|
||||
const float dpi = ui::Layout::dpiScale();
|
||||
|
||||
// Layout constants
|
||||
float lineH = ImGui::GetTextLineHeightWithSpacing();
|
||||
float spinnerR = loadElem("spinner-radius", 18.0f);
|
||||
float gap = loadElem("vertical-gap", 8.0f);
|
||||
float barH = loadElem("progress-bar", 6.0f);
|
||||
float barW = loadElem("progress-width", 260.0f);
|
||||
float spinnerR = loadElem("spinner-radius", 18.0f) * dpi;
|
||||
float gap = loadElem("vertical-gap", 8.0f) * dpi;
|
||||
float barH = loadElem("progress-bar", 6.0f) * dpi;
|
||||
float barW = loadElem("progress-width", 260.0f) * dpi;
|
||||
float cx = ws.x * 0.5f; // centre X (local coords)
|
||||
|
||||
// Estimate total block height for vertical centering
|
||||
@@ -5494,8 +5832,8 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// -------------------------------------------------------------------
|
||||
{
|
||||
float r = spinnerR;
|
||||
float thick = loadElem("spinner-thickness", 2.5f);
|
||||
ImVec2 sc(wp.x + cx, curY + r + 2.0f);
|
||||
float thick = loadElem("spinner-thickness", 2.5f) * dpi;
|
||||
ImVec2 sc(wp.x + cx, curY + r + 2.0f * dpi);
|
||||
|
||||
// Background ring (dim)
|
||||
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
|
||||
@@ -5508,7 +5846,63 @@ void App::renderLoadingOverlay(float contentH)
|
||||
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)",
|
||||
IM_COL32(255, 218, 0, 200)), 0, thick);
|
||||
|
||||
curY += r * 2.0f + gap + 4.0f;
|
||||
curY += r * 2.0f + gap + 4.0f * dpi;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Post-repair rescan — a calm, recovery-aware screen (not the generic "daemon stuck / RPC timeout /
|
||||
// restart daemon" text). The daemon was intentionally restarted with a full rescan, which
|
||||
// legitimately takes minutes and won't answer RPC yet; reassure + show elapsed and the growing size.
|
||||
// -------------------------------------------------------------------
|
||||
// Only while the daemon is actually alive-and-rescanning. If it EXITED (crashed for a reason distinct
|
||||
// from the wallet file), fall through to the normal error/stall UI — tryConnect() clears the flag, but
|
||||
// gate here too so a lingering frame never shows "everything's fine" over a dead daemon.
|
||||
if (post_recovery_rescan_ &&
|
||||
!(daemon_controller_ && daemon_controller_->state() == daemon::EmbeddedDaemon::State::Error)) {
|
||||
if (post_recovery_rescan_since_ <= 0.0) post_recovery_rescan_since_ = ImGui::GetTime();
|
||||
ImFont* titleF = Type().subtitle1(); if (!titleF) titleF = ImGui::GetFont();
|
||||
ImFont* capF = Type().caption(); if (!capF) capF = ImGui::GetFont();
|
||||
|
||||
auto centeredLine = [&](ImFont* f, ImU32 col, const char* s, float wrapW) {
|
||||
ImVec2 ts = f->CalcTextSizeA(f->LegacySize, FLT_MAX, wrapW, s);
|
||||
float x = (wrapW > 0.0f) ? (wp.x + cx - wrapW * 0.5f) : (wp.x + cx - ts.x * 0.5f);
|
||||
dl->AddText(f, f->LegacySize, ImVec2(x, curY), col, s, nullptr, wrapW);
|
||||
curY += ts.y + gap;
|
||||
};
|
||||
|
||||
centeredLine(titleF, IM_COL32(230, 210, 90, 235), TR("wallet_recovery_rescan_title"), 0.0f);
|
||||
float wrapW = ws.x * 0.8f; if (wrapW > 620.0f * dpi) wrapW = 620.0f * dpi;
|
||||
centeredLine(capF, IM_COL32(200, 200, 200, 220), TR("wallet_recovery_rescan_body"), wrapW);
|
||||
|
||||
// Elapsed + the growing wallet size — concrete "it's working" feedback while RPC is silent. Recompute
|
||||
// at most once a second (the display granularity) to avoid a stat()+format on every frame.
|
||||
int secs = (int)(ImGui::GetTime() - post_recovery_rescan_since_); if (secs < 0) secs = 0;
|
||||
static int s_lastSec = -1;
|
||||
static std::string s_elapsed, s_size;
|
||||
if (secs != s_lastSec) {
|
||||
s_lastSec = secs;
|
||||
char clock[16]; snprintf(clock, sizeof(clock), "%d:%02d", secs / 60, secs % 60);
|
||||
char ebuf[96]; snprintf(ebuf, sizeof(ebuf), TR("wallet_recovery_rescan_elapsed"), clock);
|
||||
s_elapsed = ebuf;
|
||||
const std::string walletPath = util::Platform::getDragonXDataDir() + "/" +
|
||||
((settings_ && !settings_->getActiveWalletFile().empty())
|
||||
? settings_->getActiveWalletFile() : std::string("wallet.dat"));
|
||||
uint64_t wsz = util::Platform::getFileSize(walletPath);
|
||||
if (wsz > 0) { char sbuf[96]; snprintf(sbuf, sizeof(sbuf), TR("wallet_recovery_rescan_size"),
|
||||
util::Platform::formatFileSize(wsz).c_str()); s_size = sbuf; }
|
||||
else s_size.clear();
|
||||
}
|
||||
curY += gap * 0.5f;
|
||||
centeredLine(capF, IM_COL32(150, 150, 150, 220), s_elapsed.c_str(), 0.0f);
|
||||
if (!s_size.empty()) centeredLine(capF, IM_COL32(130, 130, 130, 210), s_size.c_str(), 0.0f);
|
||||
|
||||
// Backstop for a genuinely slow (but still running) rescan: after several minutes add a gentle
|
||||
// "it's safe to leave running / watch the Console" line — never the scary "restart the node".
|
||||
if (secs > 600) {
|
||||
curY += gap * 0.5f;
|
||||
centeredLine(capF, IM_COL32(150, 150, 150, 200), TR("wallet_recovery_rescan_slow"), wrapW);
|
||||
}
|
||||
return; // skip the generic status / stall / error / daemon-log sections
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -5518,11 +5912,23 @@ void App::renderLoadingOverlay(float contentH)
|
||||
const char* statusText = connection_status_.c_str();
|
||||
ImFont* font = Type().subtitle1();
|
||||
if (!font) font = ImGui::GetFont();
|
||||
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0.0f, statusText);
|
||||
dl->AddText(font, font->LegacySize,
|
||||
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
|
||||
IM_COL32(220, 220, 220, 255), statusText);
|
||||
curY += ts.y + gap;
|
||||
// Short statuses (the common case: "Connected", "Starting daemon") stay centered; long ones
|
||||
// (a full dir_error path, a libcurl connect error) wrap to a clamped box instead of running
|
||||
// off both edges of the overlay — the same clamp the daemon-error blocks below use.
|
||||
float wrapW = ws.x * 0.8f; if (wrapW > 640.0f * dpi) wrapW = 640.0f * dpi;
|
||||
ImVec2 full = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0.0f, statusText);
|
||||
if (full.x <= wrapW) {
|
||||
dl->AddText(font, font->LegacySize,
|
||||
ImVec2(wp.x + cx - full.x * 0.5f, curY),
|
||||
IM_COL32(220, 220, 220, 255), statusText);
|
||||
curY += full.y + gap;
|
||||
} else {
|
||||
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, wrapW, statusText);
|
||||
dl->AddText(font, font->LegacySize,
|
||||
ImVec2(wp.x + cx - wrapW * 0.5f, curY),
|
||||
IM_COL32(220, 220, 220, 255), statusText, nullptr, wrapW);
|
||||
curY += ts.y + gap;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -5546,7 +5952,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
float progress = state_.sync.witness_progress;
|
||||
if (progress < 0.0f) progress = 0.0f;
|
||||
if (progress > 1.0f) progress = 1.0f;
|
||||
float barRadius = loadElem("progress-bar", 3.0f);
|
||||
float barRadius = loadElem("progress-bar", 3.0f) * dpi;
|
||||
float barX = wp.x + cx - barW * 0.5f;
|
||||
ImVec2 barMin(barX, curY);
|
||||
ImVec2 barMax(barX + barW, curY + barH);
|
||||
@@ -5583,7 +5989,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// -------------------------------------------------------------------
|
||||
if (state_.connected && state_.sync.syncing) {
|
||||
float progress = static_cast<float>(state_.sync.verification_progress);
|
||||
float barRadius = loadElem("progress-bar", 3.0f);
|
||||
float barRadius = loadElem("progress-bar", 3.0f) * dpi;
|
||||
|
||||
float barX = wp.x + cx - barW * 0.5f;
|
||||
ImVec2 barMin(barX, curY);
|
||||
@@ -5647,7 +6053,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
|
||||
// Indeterminate progress bar
|
||||
float encBarW = barW * 0.6f;
|
||||
float encBarH = 4.0f;
|
||||
float encBarH = 4.0f * dpi;
|
||||
float encBarX = wp.x + cx - encBarW * 0.5f;
|
||||
dl->AddRectFilled(ImVec2(encBarX, curY), ImVec2(encBarX + encBarW, curY + encBarH),
|
||||
IM_COL32(255, 255, 255, 20), 2.0f);
|
||||
@@ -5664,7 +6070,8 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// 3c. Daemon crash error message
|
||||
// -------------------------------------------------------------------
|
||||
if (daemon_controller_ &&
|
||||
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Error) {
|
||||
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Error &&
|
||||
!wallet_auto_recovered_) { // a salvage is NOT a crash — it has its own recovery dialog
|
||||
curY += gap;
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
@@ -5679,50 +6086,25 @@ void App::renderLoadingOverlay(float contentH)
|
||||
IM_COL32(255, 90, 90, 255), errTitle);
|
||||
curY += ts.y + gap * 0.5f;
|
||||
|
||||
// Wallet auto-recovery/salvage takes over the error card: a concise message + prominent one-click
|
||||
// actions, RIGHT HERE in the overlay the user is looking at (the separate dialog can be occluded
|
||||
// by this full-frame overlay while the node is down). Skip the verbose daemon dump so the buttons
|
||||
// stay on-screen. Same handlers as the dialog.
|
||||
if (wallet_auto_recovered_) {
|
||||
const char* msg = TR("wallet_recovered_warn");
|
||||
float wrapW = ws.x * 0.8f; if (wrapW > 640.0f) wrapW = 640.0f;
|
||||
ImVec2 ms = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, wrapW, msg);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(wp.x + cx - wrapW * 0.5f, curY),
|
||||
IM_COL32(230, 210, 210, 235), msg, nullptr, wrapW);
|
||||
curY += ms.y + gap;
|
||||
const float dpi = ui::Layout::dpiScale();
|
||||
const float bw = 340.0f * dpi;
|
||||
auto placeBtn = [&](const char* label) -> bool {
|
||||
ImGui::SetCursorScreenPos(ImVec2(wp.x + cx - bw * 0.5f, curY));
|
||||
const bool clicked = ui::material::TactileButton(label, ImVec2(bw, 0));
|
||||
curY = ImGui::GetItemRectMax().y + gap * 0.4f;
|
||||
return clicked;
|
||||
};
|
||||
if (walletRebuildAvailable() && placeBtn(TR("wallet_recovered_rebuild"))) rebuildWalletDatabase();
|
||||
if (placeBtn(TR("wallet_recovered_restore"))) restoreOriginalWallet();
|
||||
if (placeBtn(TR("wallet_recovered_open_folder")))
|
||||
util::Platform::openFolder(util::Platform::getDragonXDataDir());
|
||||
} else {
|
||||
// Error details (wrapped) — full diagnostic info.
|
||||
const std::string& errDetail = daemon_controller_->lastError();
|
||||
if (!errDetail.empty()) {
|
||||
float wrapW = ws.x * 0.8f;
|
||||
if (wrapW > 700.0f) wrapW = 700.0f;
|
||||
ImVec2 es = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, wrapW, errDetail.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(wp.x + cx - wrapW * 0.5f, curY),
|
||||
IM_COL32(255, 180, 180, 220), errDetail.c_str(), nullptr, wrapW);
|
||||
curY += es.y + gap;
|
||||
}
|
||||
// Crash count hint
|
||||
if (daemon_controller_->crashCount() >= 3) {
|
||||
const char* hint = "Use Settings > Restart Daemon to try again";
|
||||
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
|
||||
IM_COL32(200, 200, 200, 180), hint);
|
||||
curY += hs2.y + gap;
|
||||
}
|
||||
// Error details (wrapped) — full diagnostic info for a genuine (non-recovery) crash.
|
||||
const std::string& errDetail = daemon_controller_->lastError();
|
||||
if (!errDetail.empty()) {
|
||||
float wrapW = ws.x * 0.8f;
|
||||
if (wrapW > 700.0f * dpi) wrapW = 700.0f * dpi;
|
||||
ImVec2 es = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, wrapW, errDetail.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(wp.x + cx - wrapW * 0.5f, curY),
|
||||
IM_COL32(255, 180, 180, 220), errDetail.c_str(), nullptr, wrapW);
|
||||
curY += es.y + gap;
|
||||
}
|
||||
// Crash count hint
|
||||
if (daemon_controller_->crashCount() >= 3) {
|
||||
const char* hint = "Use Settings > Restart Daemon to try again";
|
||||
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
|
||||
IM_COL32(200, 200, 200, 180), hint);
|
||||
curY += hs2.y + gap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5757,7 +6139,7 @@ void App::renderLoadingOverlay(float contentH)
|
||||
snprintf(stallBody, sizeof(stallBody), TR("loading_stall_body"),
|
||||
(float)(ImGui::GetTime() - connect_stall_since_));
|
||||
float wrapW = ws.x * 0.8f;
|
||||
if (wrapW > 640.0f) wrapW = 640.0f;
|
||||
if (wrapW > 640.0f * dpi) wrapW = 640.0f * dpi;
|
||||
ImVec2 bs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, wrapW, stallBody);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(wp.x + cx - wrapW * 0.5f, curY),
|
||||
@@ -5778,19 +6160,19 @@ void App::renderLoadingOverlay(float contentH)
|
||||
// -------------------------------------------------------------------
|
||||
// 4. Daemon output snippet (last few lines, if embedded)
|
||||
// -------------------------------------------------------------------
|
||||
if (daemon_controller_) {
|
||||
if (daemon_controller_ && !wallet_auto_recovered_) { // recovery moves the log behind the dialog's disclosure
|
||||
auto lines = daemon_controller_->recentLines(8);
|
||||
if (!lines.empty()) {
|
||||
curY += gap;
|
||||
|
||||
float panelW = ws.x * 0.85f;
|
||||
if (panelW > 900.0f) panelW = 900.0f;
|
||||
if (panelW > 900.0f * dpi) panelW = 900.0f * dpi;
|
||||
float panelX = wp.x + cx - panelW * 0.5f;
|
||||
float panelPad = 8.0f;
|
||||
float panelPad = 8.0f * dpi;
|
||||
|
||||
ImFont* capFont = Type().caption();
|
||||
if (!capFont) capFont = ImGui::GetFont();
|
||||
float panelLineH = capFont->LegacySize + 4.0f;
|
||||
float panelLineH = capFont->LegacySize + 4.0f * dpi;
|
||||
float panelContentH = panelPad * 2.0f + panelLineH * (float)lines.size();
|
||||
|
||||
ImVec2 panelMin(panelX, curY);
|
||||
|
||||
Reference in New Issue
Block a user