From de11850c74e23734c02ca9251d7514a39118b0cd Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 17:29:12 -0500 Subject: [PATCH] =?UTF-8?q?refactor(audit):=20batch=204=20=E2=80=94=20UI?= =?UTF-8?q?=20design-system=20helpers=20+=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical, behavior-preserving consolidations from the audit. Rendering extractions were kept byte-equivalent; sites that couldn't be made identical were left as-is (noted below). Shared helpers: - util::truncateMiddle(s,maxLen) / (s,front,back) in text_format.h — replaces 6 file-local middle-ellipsis truncators + several inline substr sites (send/receive/transactions/balance_recent_tx/explorer + app.cpp + 3 dialogs). Carries the maxLen<=3 guard, fixing the latent unsigned-underflow copies. - material::LoadingDots() in draw_helpers.h — one animated-ellipsis source for 8 copy-pasted spinner sites (identical GetTime()*3 phase preserved). - Reuse the existing FormatHashrate() in explorer_tab + peers_tab (dropped two inline hashrate ladders). - material::DrawButtonGlassOverlay() — the glass-fill/rim/tactile overlay block shared by TactileButton / TactileSmallButton / schema TactileButton. - material::CollapsibleHeader() — the invisible-button + label + chevron idiom (3 of 5 settings_page sites; RPC/Debug headers skipped — non-identical). - material::GlassCardScope (RAII) — the ChannelsSplit/Indent/DrawGlassPanel/ ChannelsMerge card scaffold (5 settings_page cards; About/send/receive skipped — different padding / logo interleaving). - material::DialogWarningHeader()/DialogConfirmFooter() — warning header + 50/50 Cancel/danger footer across the confirm dialogs; button height moved from a hardcoded 40px into ui.toml (components.overlay-dialog.confirm-btn-height). - File-local enterLowSpec()/exitLowSpec() collapse the 3 low-spec snapshot copies. i18n: wrapped hardcoded English in the shared balance render paths and the whole Lite lifecycle/security section in TR(), with English defaults added to loadBuiltinEnglish() (res/lang/*.json left for the translation tooling — TR falls back to English, so output is unchanged). Full-node + Lite build clean; ctest 1/1; hygiene clean. These are rendering changes — the Settings page (cards/headers/confirm dialogs), all tactile buttons, and the Lite settings section warrant a screenshot check. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/themes/ui.toml | 3 + src/app.cpp | 3 +- src/app_security.cpp | 4 +- src/ui/material/draw_helpers.h | 198 ++++++++-- src/ui/pages/settings_page.cpp | 486 ++++++++----------------- src/ui/windows/address_book_dialog.cpp | 4 +- src/ui/windows/address_label_dialog.h | 3 +- src/ui/windows/balance_components.cpp | 10 +- src/ui/windows/balance_recent_tx.cpp | 9 +- src/ui/windows/balance_tab.cpp | 16 +- src/ui/windows/balance_tab_helpers.cpp | 4 +- src/ui/windows/block_info_dialog.cpp | 5 +- src/ui/windows/explorer_tab.cpp | 31 +- src/ui/windows/peers_tab.cpp | 14 +- src/ui/windows/receive_tab.cpp | 23 +- src/ui/windows/send_tab.cpp | 24 +- src/ui/windows/settings_window.cpp | 22 +- src/ui/windows/transactions_tab.cpp | 17 +- src/util/i18n.cpp | 52 +++ src/util/text_format.cpp | 15 + src/util/text_format.h | 9 + 21 files changed, 452 insertions(+), 500 deletions(-) diff --git a/res/themes/ui.toml b/res/themes/ui.toml index 09fd9a4..95e1f1c 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -1219,6 +1219,9 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 } fill-alpha = { size = 12.0 } noise-alpha = { size = 14.0 } +[components.overlay-dialog] +confirm-btn-height = { size = 40.0 } + [components.qr-code] module-scale = { size = 4 } border-modules = { size = 2 } diff --git a/src/app.cpp b/src/app.cpp index 4b54642..3fa6f55 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -66,6 +66,7 @@ #include "ui/notifications.h" #include "util/i18n.h" #include "util/platform.h" +#include "util/text_format.h" #include "util/payment_uri.h" #include "util/texture_loader.h" #include "util/bootstrap.h" @@ -2722,7 +2723,7 @@ void App::renderExportKeyDialog() if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) { for (const auto& addr : all_addrs) { bool selected = (export_address_ == addr); - std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen); + std::string display = util::truncateMiddle(addr, addrFrontLen, addrBackLen); if (ImGui::Selectable(display.c_str(), selected)) { export_address_ = addr; export_result_.clear(); diff --git a/src/app_security.cpp b/src/app_security.cpp index ac37085..de0204c 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -935,10 +935,8 @@ void App::renderLockScreen() { float rowH = captionFont->LegacySize + 8.0f; if (lock_unlock_in_progress_) { // Animated spinner dots - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; char msg[64]; - snprintf(msg, sizeof(msg), "Unlocking%s", dotStr[dots]); + snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots()); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 6cb35b1..56410ad 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -35,6 +35,13 @@ inline ImU32 ScaleAlpha(ImU32 col, float scale) { return (col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } +// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase. +inline const char* LoadingDots() { + int n = ((int)(ImGui::GetTime() * 3.0f)) % 4; + static const char* kDots[4] = { "", ".", "..", "..." }; + return kDots[n]; +} + // ============================================================================ // Text Drop Shadow // ============================================================================ @@ -210,6 +217,30 @@ inline void DrawTactileOverlay(ImDrawList* dl, const ImVec2& bMin, dl->PopClipRect(); } +// Glass-card button surface: frosted-glass hover/idle fill (skipped while +// pressed) + rim light + tactile depth overlay. Shared by the tactile +// button wrappers. Call after computing the button rect / rounding / +// active / hovered state. +inline void DrawButtonGlassOverlay(ImDrawList* dl, const ImVec2& bMin, + const ImVec2& bMax, float rounding, + bool active, bool hovered) +{ + // Frosted glass highlight — subtle fill on top + if (!active) { + ImU32 col = hovered + ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) + : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); + dl->AddRectFilled(bMin, bMax, col, rounding); + } + // Rim light + ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); + dl->AddRect(bMin, bMax, + active ? ScaleAlpha(rim, 0.6f) : rim, + rounding, 0, 1.0f); + // Tactile depth + DrawTactileOverlay(dl, bMin, bMax, rounding, active); +} + // Convenience: call right after any ImGui::Button() / SmallButton() call // to add tactile depth. Uses the last item rect automatically. inline void ApplyTactile(ImDrawList* dl = nullptr) @@ -278,20 +309,7 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0), float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); bool hovered = ImGui::IsItemHovered(); - // Frosted glass highlight — subtle fill on top - if (!active) { - ImU32 col = hovered - ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) - : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); - dl->AddRectFilled(bMin, bMax, col, rounding); - } - // Rim light - ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); - dl->AddRect(bMin, bMax, - active ? ScaleAlpha(rim, 0.6f) : rim, - rounding, 0, 1.0f); - // Tactile depth - DrawTactileOverlay(dl, bMin, bMax, rounding, active); + DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered); return pressed; } @@ -306,20 +324,44 @@ inline bool TactileSmallButton(const char* label, ImFont* font = nullptr) float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); bool hovered = ImGui::IsItemHovered(); - if (!active) { - ImU32 col = hovered - ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) - : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); - dl->AddRectFilled(bMin, bMax, col, rounding); - } - ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); - dl->AddRect(bMin, bMax, - active ? ScaleAlpha(rim, 0.6f) : rim, - rounding, 0, 1.0f); - DrawTactileOverlay(dl, bMin, bMax, rounding, active); + DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered); return pressed; } +// ============================================================================ +// Collapsible section header +// ============================================================================ +// Renders a full-width transparent clickable header row: `label` drawn on the +// left and an expand/collapse chevron on the right. Toggles `expanded` when +// clicked and returns the (possibly toggled) state. The label font governs +// the row height (frame height) and text metrics; the chevron uses the small +// icon font. `arrowInset` shifts the chevron left of the right edge. +inline bool CollapsibleHeader(ImDrawList* dl, const char* id, const char* label, + bool& expanded, float width, ImFont* labelFont, + ImU32 labelCol, float arrowInset = 0.0f) +{ + const char* arrow = expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; + ImGui::PushFont(labelFont); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); + { + ImVec2 hdrPos = ImGui::GetCursorScreenPos(); + if (ImGui::Button(id, ImVec2(width, ImGui::GetFrameHeight()))) { + expanded = !expanded; + } + float textY = hdrPos.y + (ImGui::GetFrameHeight() - labelFont->LegacySize) * 0.5f; + dl->AddText(labelFont, labelFont->LegacySize, ImVec2(hdrPos.x, textY), labelCol, label); + ImFont* iconFont = Type().iconSmall(); + if (!iconFont) iconFont = labelFont; + float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; + dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + width - arrowW - arrowInset, textY), labelCol, arrow); + } + ImGui::PopStyleColor(3); + ImGui::PopFont(); + return expanded; +} + // ============================================================================ // Glass Panel (glassmorphism card) // ============================================================================ @@ -434,6 +476,57 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin, } } +// ============================================================================ +// Glass Card Scope (RAII) — channel-split glass-card scaffold +// ============================================================================ +// Captures the current cursor as the card's top-left, splits the draw list +// into two channels (content on 1, glass on 0), insets the content by `pad`, +// and — on scope exit — appends the bottom pad, sizes the card to its content, +// draws the glass panel behind it on channel 0, merges, and advances the +// cursor past the card. `width` is the full card width, `bottomPad` the +// trailing pad. Reproduces the hand-written prologue/epilogue exactly. +// +// { +// material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); +// ... card content ... +// } +struct GlassCardScope { + ImDrawList* dl; + ImVec2 cardMin; + float width; + float pad; + float bottomPad; + GlassPanelSpec spec; + + GlassCardScope(ImDrawList* dl_, float width_, float pad_, float bottomPad_, + const GlassPanelSpec& spec_) + : dl(dl_), width(width_), pad(pad_), bottomPad(bottomPad_), spec(spec_) + { + cardMin = ImGui::GetCursorScreenPos(); + dl->ChannelsSplit(2); + dl->ChannelsSetCurrent(1); + ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); + ImGui::Indent(pad); + } + + ~GlassCardScope() + { + ImGui::Dummy(ImVec2(0, bottomPad)); + ImGui::Unindent(pad); + + ImVec2 cardMax(cardMin.x + width, ImGui::GetCursorScreenPos().y); + dl->ChannelsSetCurrent(0); + DrawGlassPanel(dl, cardMin, cardMax, spec); + dl->ChannelsMerge(); + + ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); + ImGui::Dummy(ImVec2(width, 0)); + } + + GlassCardScope(const GlassCardScope&) = delete; + GlassCardScope& operator=(const GlassCardScope&) = delete; +}; + // ============================================================================ // Stat Card — reusable card with overline / value / subtitle + accent stripe // ============================================================================ @@ -656,17 +749,7 @@ inline bool TactileButton(const char* label, const schema::ButtonStyle& style, float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); bool hovered = ImGui::IsItemHovered(); - if (!active) { - ImU32 col = hovered - ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) - : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); - dl->AddRectFilled(bMin, bMax, col, rounding); - } - ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); - dl->AddRect(bMin, bMax, - active ? ScaleAlpha(rim, 0.6f) : rim, - rounding, 0, 1.0f); - DrawTactileOverlay(dl, bMin, bMax, rounding, active); + DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered); ImGui::PopStyleVar(styleCount); ImGui::PopStyleColor(colorCount); @@ -1069,6 +1152,49 @@ inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator PlaceOverlayDialogActions(totalActionWidth); } +// ── Confirmation dialog header / footer ───────────────────────────────── +// Shared scaffolding for the "warning + Cancel/confirm" overlay dialogs. + +// Warning icon + "Warning" label row, drawn in `col` (the caller owns the +// exact accent colour; new callers can omit it to use the theme warning). +// Reproduces: PushFont(iconLarge) + ICON_MD_WARNING, PopFont, SameLine, +// TextColored(label). The label text is passed in (already translated). +inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4()) +{ + ImGui::PushFont(Type().iconLarge()); + ImGui::TextColored(col, ICON_MD_WARNING); + ImGui::PopFont(); + ImGui::SameLine(); + ImGui::TextColored(col, "%s", warningLabel); +} + +// 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` / +// `outConfirm` when the respective button is pressed (the caller runs the +// action bodies). When `danger`, the confirm button gets the red danger +// style. Button height comes from ui.toml (falls back to 40). Reproduces +// the hand-written footer exactly (btnW split + SameLine + optional danger +// PushStyleColor/PopStyleColor(2)). +inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel, + bool danger, bool& outCancel, bool& outConfirm) +{ + float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f); + float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; + if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) { + outCancel = true; + } + ImGui::SameLine(); + if (danger) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); + } + if (ImGui::Button(confirmLabel, ImVec2(btnW, btnH))) { + outConfirm = true; + } + if (danger) { + ImGui::PopStyleColor(2); + } +} + } // namespace material } // namespace ui } // namespace dragonx diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index cfb1caf..bb2f2fb 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -170,6 +170,51 @@ struct SettingsPageState { static SettingsPageState s_settingsState; +// Low-spec mode enter: snapshot the current effects prefs, then force the +// low-cost overrides. When applyEffects is true, also push the overrides to +// the live effects subsystems (the runtime-sync path passes false since it +// only mirrors an already-applied hotkey toggle into the checkbox state). +static void enterLowSpec(bool applyEffects) { + s_settingsState.low_spec_snapshot.valid = true; + s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled; + s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount; + s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity; + s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity; + s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled; + s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled; + s_settingsState.acrylic_enabled = false; + s_settingsState.blur_amount = 0.0f; + s_settingsState.ui_opacity = 1.0f; + s_settingsState.window_opacity = 1.0f; + s_settingsState.theme_effects_enabled = false; + s_settingsState.scanline_enabled = false; + if (applyEffects) { + effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); + effects::ImGuiAcrylic::SetUIOpacity(1.0f); + effects::ThemeEffects::instance().setEnabled(false); + ConsoleTab::s_scanline_enabled = false; + } +} + +// Low-spec mode exit: restore the previously snapshotted effects prefs and +// clear the snapshot. When applyEffects is true, also push them to the live +// effects subsystems. Callers guard this on low_spec_snapshot.valid. +static void exitLowSpec(bool applyEffects) { + s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount; + s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); + s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity; + s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity; + s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled; + s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled; + if (applyEffects) { + effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount); + effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); + effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled); + ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled; + } + s_settingsState.low_spec_snapshot.valid = false; +} + // Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input. static int liteSeedWordCount(const char* s) { int words = 0; @@ -274,7 +319,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) { } if (started) { s_settingsState.lite_lifecycle_pending = true; - s_settingsState.lite_lifecycle_status = "Working…"; + s_settingsState.lite_lifecycle_status = TR("lite_working"); s_settingsState.lite_lifecycle_summary.clear(); } else { // Rejected before any thread launched (wallet already open, an attempt in flight, or no @@ -428,28 +473,10 @@ void RenderSettingsPage(App* app) { if (s_settingsState.low_spec_mode != runtimeLowSpec) { if (runtimeLowSpec) { // Hotkey turned low-spec ON — save snapshot, override statics - s_settingsState.low_spec_snapshot.valid = true; - s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled; - s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount; - s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity; - s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity; - s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled; - s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled; - s_settingsState.acrylic_enabled = false; - s_settingsState.blur_amount = 0.0f; - s_settingsState.ui_opacity = 1.0f; - s_settingsState.window_opacity = 1.0f; - s_settingsState.theme_effects_enabled = false; - s_settingsState.scanline_enabled = false; + enterLowSpec(false); } else if (s_settingsState.low_spec_snapshot.valid) { // Hotkey turned low-spec OFF — restore snapshot - s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount; - s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); - s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity; - s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity; - s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled; - s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled; - s_settingsState.low_spec_snapshot.valid = false; + exitLowSpec(false); } else if (app->settings()) { // No snapshot — read prefs from settings file s_settingsState.blur_amount = app->settings()->getBlurMultiplier(); @@ -561,12 +588,7 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - dl->ChannelsSplit(2); - dl->ChannelsSetCurrent(1); - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); - ImGui::Indent(pad); + material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); float contentW = availWidth - pad * 2; float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size; @@ -767,27 +789,9 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Advanced Effects... --- - { - const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; - ImGui::PushFont(body2); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); - { - ImVec2 hdrPos = ImGui::GetCursorScreenPos(); - if (ImGui::Button("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) { - s_settingsState.effects_expanded = !s_settingsState.effects_expanded; - } - float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; - dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects")); - ImFont* iconFont = Type().iconSmall(); - if (!iconFont) iconFont = body2; - float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; - dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); - } - ImGui::PopStyleColor(3); - ImGui::PopFont(); - } + material::CollapsibleHeader(dl, "##EffectsToggle", TR("advanced_effects"), + s_settingsState.effects_expanded, contentW, + body2, OnSurfaceMedium()); if (s_settingsState.effects_expanded) { ImGui::PushFont(body2); @@ -796,35 +800,9 @@ void RenderSettingsPage(App* app) { if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) { effects::setLowSpecMode(s_settingsState.low_spec_mode); if (s_settingsState.low_spec_mode) { - s_settingsState.low_spec_snapshot.valid = true; - s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled; - s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount; - s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity; - s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity; - s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled; - s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled; - s_settingsState.acrylic_enabled = false; - s_settingsState.blur_amount = 0.0f; - s_settingsState.ui_opacity = 1.0f; - s_settingsState.window_opacity = 1.0f; - s_settingsState.theme_effects_enabled = false; - s_settingsState.scanline_enabled = false; - effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); - effects::ImGuiAcrylic::SetUIOpacity(1.0f); - effects::ThemeEffects::instance().setEnabled(false); - ConsoleTab::s_scanline_enabled = false; + enterLowSpec(true); } else if (s_settingsState.low_spec_snapshot.valid) { - s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount; - s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); - s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity; - s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity; - s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled; - s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled; - effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount); - effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); - effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled); - ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled; - s_settingsState.low_spec_snapshot.valid = false; + exitLowSpec(true); } saveSettingsPageState(app->settings()); } @@ -1065,26 +1043,10 @@ void RenderSettingsPage(App* app) { // --- Collapsible: Advanced Effects... --- { - const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; - ImGui::PushFont(body2); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); - { - float narrowContentW = availWidth - pad * 2; - ImVec2 hdrPos = ImGui::GetCursorScreenPos(); - if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) { - s_settingsState.effects_expanded = !s_settingsState.effects_expanded; - } - float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; - dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects")); - ImFont* iconFont = Type().iconSmall(); - if (!iconFont) iconFont = body2; - float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; - dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow); - } - ImGui::PopStyleColor(3); - ImGui::PopFont(); + float narrowContentW = availWidth - pad * 2; + material::CollapsibleHeader(dl, "##EffectsToggleN", TR("advanced_effects"), + s_settingsState.effects_expanded, narrowContentW, + body2, OnSurfaceMedium()); } if (s_settingsState.effects_expanded) { @@ -1093,35 +1055,9 @@ void RenderSettingsPage(App* app) { if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) { effects::setLowSpecMode(s_settingsState.low_spec_mode); if (s_settingsState.low_spec_mode) { - s_settingsState.low_spec_snapshot.valid = true; - s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled; - s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount; - s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity; - s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity; - s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled; - s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled; - s_settingsState.acrylic_enabled = false; - s_settingsState.blur_amount = 0.0f; - s_settingsState.ui_opacity = 1.0f; - s_settingsState.window_opacity = 1.0f; - s_settingsState.theme_effects_enabled = false; - s_settingsState.scanline_enabled = false; - effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); - effects::ImGuiAcrylic::SetUIOpacity(1.0f); - effects::ThemeEffects::instance().setEnabled(false); - ConsoleTab::s_scanline_enabled = false; + enterLowSpec(true); } else if (s_settingsState.low_spec_snapshot.valid) { - s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount; - s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f); - s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity; - s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity; - s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled; - s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled; - effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount); - effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity); - effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled); - ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled; - s_settingsState.low_spec_snapshot.valid = false; + exitLowSpec(true); } saveSettingsPageState(app->settings()); } @@ -1219,19 +1155,6 @@ void RenderSettingsPage(App* app) { ImGui::PopFont(); } // s_settingsState.effects_expanded } - - // Bottom padding - ImGui::Dummy(ImVec2(0, bottomPad)); - ImGui::Unindent(pad); - - // Draw glass panel behind content (auto-sized) - ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); - dl->ChannelsSetCurrent(0); - DrawGlassPanel(dl, cardMin, cardMax, glassSpec); - dl->ChannelsMerge(); - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); - ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); @@ -1243,11 +1166,8 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - dl->ChannelsSplit(2); - dl->ChannelsSetCurrent(1); - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); - ImGui::Indent(pad); + material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); + const ImVec2& cardMin = card.cardMin; float contentW = availWidth - pad * 2; @@ -1307,27 +1227,9 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Tools & Actions... --- - { - const char* arrow = s_settingsState.tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; - ImGui::PushFont(body2); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); - { - ImVec2 hdrPos = ImGui::GetCursorScreenPos(); - if (ImGui::Button("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) { - s_settingsState.tools_expanded = !s_settingsState.tools_expanded; - } - float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; - dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions")); - ImFont* iconFont = Type().iconSmall(); - if (!iconFont) iconFont = body2; - float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; - dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); - } - ImGui::PopStyleColor(3); - ImGui::PopFont(); - } + material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"), + s_settingsState.tools_expanded, contentW, + body2, OnSurfaceMedium()); if (s_settingsState.tools_expanded) { float btnSpacing = Layout::spacingMd(); @@ -1435,17 +1337,6 @@ void RenderSettingsPage(App* app) { if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } - - ImGui::Dummy(ImVec2(0, bottomPad)); - ImGui::Unindent(pad); - - ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); - dl->ChannelsSetCurrent(0); - DrawGlassPanel(dl, cardMin, cardMax, glassSpec); - dl->ChannelsMerge(); - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); - ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); @@ -1458,11 +1349,7 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - dl->ChannelsSplit(2); - dl->ChannelsSetCurrent(1); - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); - ImGui::Indent(pad); + material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); float contentW = availWidth - pad * 2; float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f); @@ -1618,18 +1505,18 @@ void RenderSettingsPage(App* app) { // Lite-server selection lives in the dedicated Network tab now. Type().textColored(TypeStyle::Body2, OnSurfaceMedium(), - "Lite servers are managed in the Network tab."); + TR("lite_servers_network_tab")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) { + if (ImGui::Button(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) { s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded; } if (s_settingsState.lite_lifecycle_expanded) { - const char* lifecycleLabels[] = {"Create", "Open", "Restore"}; + const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")}; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Action"); + ImGui::TextUnformatted(TR("lite_action")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); if (ImGui::BeginCombo("##LiteLifecycleOperation", @@ -1650,7 +1537,7 @@ void RenderSettingsPage(App* app) { s_settingsState.lite_lifecycle_operation == 2) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Wallet"); + ImGui::TextUnformatted(TR("lite_wallet_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path, @@ -1660,7 +1547,7 @@ void RenderSettingsPage(App* app) { if (s_settingsState.lite_lifecycle_operation == 2) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Seed"); + ImGui::TextUnformatted(TR("lite_seed_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed, @@ -1670,38 +1557,38 @@ void RenderSettingsPage(App* app) { { const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed); char wbuf[32]; - snprintf(wbuf, sizeof(wbuf), "%d / 24 words", wc); + snprintf(wbuf, sizeof(wbuf), TR("lite_word_count"), wc); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Birthday"); + ImGui::TextUnformatted(TR("lite_birthday_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday); if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), - "Block height to start scanning from. Leave 0 if unknown (slower full scan)."); + TR("lite_birthday_hint")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Account"); + ImGui::TextUnformatted(TR("lite_account_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account); if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - ImGui::Checkbox("Overwrite##LiteRestoreOverwrite", + ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(), &s_settingsState.lite_restore_overwrite); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Passphrase"); + ImGui::TextUnformatted(TR("lite_passphrase_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteLifecyclePassphrase", @@ -1719,7 +1606,7 @@ void RenderSettingsPage(App* app) { const auto& result = lite->lastLifecycleResult(); s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted; if (result.walletReady) { - s_settingsState.lite_lifecycle_status = "Wallet ready"; + s_settingsState.lite_lifecycle_status = TR("lite_wallet_ready"); } else { s_settingsState.lite_lifecycle_status = result.error.empty() ? result.status.message @@ -1735,7 +1622,7 @@ void RenderSettingsPage(App* app) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending; if (liteLifecycleBusy) ImGui::BeginDisabled(); - if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { evaluateLiteLifecycleRequestFromPageState(app); } if (liteLifecycleBusy) ImGui::EndDisabled(); @@ -1765,17 +1652,15 @@ void RenderSettingsPage(App* app) { if (app->liteWallet() && app->liteWallet()->walletOpen()) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - Type().text(TypeStyle::Body2, "Backup & keys"); + Type().text(TypeStyle::Body2, TR("lite_backup_keys")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_show_seed", "LiteExportSeed").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { auto r = app->liteWallet()->exportSeed(); wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); if (r.ok) { s_settingsState.lite_export_secret = r.seedPhrase; - s_settingsState.lite_export_label = - "Seed phrase — the ONLY way to restore your wallet. " - "Write it down, store it offline, never share it."; + s_settingsState.lite_export_label = TR("lite_seed_warning"); s_settingsState.lite_export_is_seed = true; s_settingsState.lite_export_birthday = r.birthday; s_settingsState.lite_backup_status.clear(); @@ -1787,13 +1672,13 @@ void RenderSettingsPage(App* app) { wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy } ImGui::SameLine(0, Layout::spacingSm()); - if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_show_private_keys", "LiteExportKeys").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { auto r = app->liteWallet()->exportPrivateKeys(); wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_is_seed = false; // keys, not a seed if (r.ok) { s_settingsState.lite_export_secret = r.privateKeysJson; - s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds"; + s_settingsState.lite_export_label = TR("lite_private_keys_warning"); s_settingsState.lite_backup_status.clear(); } else { s_settingsState.lite_export_label.clear(); @@ -1814,19 +1699,19 @@ void RenderSettingsPage(App* app) { // The seed's birthday is needed to restore quickly — show + back it up too. if (s_settingsState.lite_export_is_seed) { char bday[64]; - snprintf(bday, sizeof(bday), "Birthday: %llu (back this up too)", + snprintf(bday, sizeof(bday), TR("lite_birthday_backup"), s_settingsState.lite_export_birthday); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { app->copySecretToClipboard(s_settingsState.lite_export_secret); } ImGui::SameLine(0, Layout::spacingSm()); // Save the seed (+ birthday) to an owner-only (0600) file in the config dir. if (s_settingsState.lite_export_is_seed && - TactileButton("Save to file##LiteExportSave", ImVec2(0, 0), S.resolveFont("button"))) { + TactileButton(TrId("lite_save_to_file", "LiteExportSave").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { std::string path = util::Platform::getConfigDir() + "/lite-seed-backup.txt"; std::string content = s_settingsState.lite_export_secret + "\nBirthday: " + std::to_string(s_settingsState.lite_export_birthday) + "\n"; @@ -1834,11 +1719,11 @@ void RenderSettingsPage(App* app) { path, content, /*restrictPermissions=*/true); wallet::secureWipeLiteSecret(content); s_settingsState.lite_backup_status = ok - ? ("Saved (plaintext, owner-only) to " + path) - : ("Could not write " + path); + ? (std::string(TR("lite_saved_to")) + path) + : (std::string(TR("lite_could_not_write")) + path); } if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm()); - if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); s_settingsState.lite_export_is_seed = false; @@ -1848,18 +1733,18 @@ void RenderSettingsPage(App* app) { // Import a spending/viewing key (history appears after the next sync). ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Import key"); + ImGui::TextUnformatted(TR("lite_import_key_label")); ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key); sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); s_settingsState.lite_backup_status = - r.ok ? "Key imported — run a sync to scan its history" : r.error; + r.ok ? TR("lite_key_imported") : r.error; } if (!s_settingsState.lite_backup_status.empty()) { @@ -1871,57 +1756,57 @@ void RenderSettingsPage(App* app) { // ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ---- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - Type().text(TypeStyle::Body2, "Security"); + Type().text(TypeStyle::Body2, TR("lite_security")); const auto& wstate = app->getWalletState(); const float encLabelX = leftX - sectionOrigin.x + liteLabelW; if (!wstate.isEncrypted()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Passphrase"); + ImGui::TextUnformatted(TR("lite_passphrase_label")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Encrypt wallet##LiteEncrypt", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); - s_settingsState.lite_encryption_status = r.ok ? "Wallet encrypted" : r.error; + s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error; } } else { if (wstate.isLocked()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Unlock"); + ImGui::TextUnformatted(TR("lite_unlock")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Unlock##LiteUnlock", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); - s_settingsState.lite_encryption_status = ok ? "Wallet unlocked" : "Unlock failed"; + s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed"); } } else { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { s_settingsState.lite_encryption_status = - app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed"; + app->liteWallet()->lockWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed"); } } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Passphrase"); + ImGui::TextUnformatted(TR("lite_passphrase_label")); ImGui::SameLine(encLabelX); ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton("Remove encryption##LiteDecrypt", ImVec2(0, 0), S.resolveFont("button"))) { + if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass); sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); - s_settingsState.lite_encryption_status = r.ok ? "Encryption removed" : r.error; + s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error; } } if (!s_settingsState.lite_encryption_status.empty()) { @@ -2347,17 +2232,6 @@ void RenderSettingsPage(App* app) { ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y)); } } - - ImGui::Dummy(ImVec2(0, bottomPad)); - ImGui::Unindent(pad); - - ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); - dl->ChannelsSetCurrent(0); - DrawGlassPanel(dl, cardMin, cardMax, glassSpec); - dl->ChannelsMerge(); - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); - ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); @@ -2369,11 +2243,7 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_section")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - dl->ChannelsSplit(2); - dl->ChannelsSetCurrent(1); - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); - ImGui::Indent(pad); + material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); float contentW = availWidth - pad * 2; ImGui::PushFont(body2); @@ -2413,17 +2283,6 @@ void RenderSettingsPage(App* app) { util::Platform::openUrl("https://explorer.dragonx.is"); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer")); - - ImGui::Dummy(ImVec2(0, bottomPad)); - ImGui::Unindent(pad); - - ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); - dl->ChannelsSetCurrent(0); - DrawGlassPanel(dl, cardMin, cardMax, glassSpec); - dl->ChannelsMerge(); - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); - ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); @@ -2597,11 +2456,7 @@ void RenderSettingsPage(App* app) { if (s_settingsState.debug_expanded) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - dl->ChannelsSplit(2); - dl->ChannelsSetCurrent(1); - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); - ImGui::Indent(pad); + material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select")); ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note")); @@ -2684,17 +2539,6 @@ void RenderSettingsPage(App* app) { } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon")); } - - ImGui::Dummy(ImVec2(0, bottomPad)); - ImGui::Unindent(pad); - - ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); - dl->ChannelsSetCurrent(0); - DrawGlassPanel(dl, cardMin, cardMax, glassSpec); - dl->ChannelsMerge(); - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); - ImGui::Dummy(ImVec2(availWidth, 0)); } } @@ -2739,12 +2583,8 @@ void RenderSettingsPage(App* app) { // Confirmation dialog for clearing z-tx history if (s_settingsState.confirm_clear_ztx) { if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning")); - + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); + ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1")); ImGui::Spacing(); @@ -2752,15 +2592,14 @@ void RenderSettingsPage(App* app) { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) { + + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_btn").c_str(), + true, doCancel, doConfirm); + if (doCancel) { s_settingsState.confirm_clear_ztx = false; } - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); - if (ImGui::Button(TrId("clear_anyway", "clear_ztx_btn").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; if (util::Platform::deleteFile(ztx_file)) { Notifications::instance().success("Z-transaction history cleared"); @@ -2769,7 +2608,6 @@ void RenderSettingsPage(App* app) { } s_settingsState.confirm_clear_ztx = false; } - ImGui::PopStyleColor(2); EndOverlayDialog(); } } @@ -2777,12 +2615,8 @@ void RenderSettingsPage(App* app) { // Confirmation dialog for deleting blockchain data if (s_settingsState.confirm_delete_blockchain) { if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning")); - + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg")); ImGui::Spacing(); @@ -2790,20 +2624,18 @@ void RenderSettingsPage(App* app) { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) { + + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TR("cancel"), TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), + true, doCancel, doConfirm); + if (doCancel) { s_settingsState.confirm_delete_blockchain = false; } - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); - if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { if (app->supportsFullNodeLifecycleActions()) app->deleteBlockchainData(); s_settingsState.confirm_delete_blockchain = false; } - ImGui::PopStyleColor(2); EndOverlayDialog(); } } @@ -2825,11 +2657,7 @@ void RenderSettingsPage(App* app) { } if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); @@ -2877,11 +2705,7 @@ void RenderSettingsPage(App* app) { // Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan) if (s_settingsState.confirm_repair_wallet) { if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg")); @@ -2891,12 +2715,14 @@ void RenderSettingsPage(App* app) { ImGui::Separator(); ImGui::Spacing(); - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TrId("cancel", "repair_wallet_cancel").c_str(), ImVec2(btnW, 40))) { + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TrId("cancel", "repair_wallet_cancel").c_str(), + TrId("repair_wallet", "repair_wallet_confirm").c_str(), + false, doCancel, doConfirm); + if (doCancel) { s_settingsState.confirm_repair_wallet = false; } - ImGui::SameLine(); - if (ImGui::Button(TrId("repair_wallet", "repair_wallet_confirm").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { app->repairWallet(); s_settingsState.confirm_repair_wallet = false; } @@ -2907,11 +2733,7 @@ void RenderSettingsPage(App* app) { // Confirm: reinstall the bundled daemon binary (stop → overwrite → restart) if (s_settingsState.confirm_reinstall_daemon) { if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg")); @@ -2921,12 +2743,14 @@ void RenderSettingsPage(App* app) { ImGui::Separator(); ImGui::Spacing(); - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) { + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TrId("cancel", "reinstall_daemon_cancel").c_str(), + TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), + false, doCancel, doConfirm); + if (doCancel) { s_settingsState.confirm_reinstall_daemon = false; } - ImGui::SameLine(); - if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { app->reinstallBundledDaemon(); s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap s_settingsState.confirm_reinstall_daemon = false; @@ -2938,11 +2762,7 @@ void RenderSettingsPage(App* app) { // Confirm: restart daemon (briefly drops the connection to apply changed options) if (s_settingsState.confirm_restart_daemon) { if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg")); @@ -2950,12 +2770,14 @@ void RenderSettingsPage(App* app) { ImGui::Separator(); ImGui::Spacing(); - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TrId("cancel", "restartd_cancel").c_str(), ImVec2(btnW, 40))) { + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TrId("cancel", "restartd_cancel").c_str(), + TrId("settings_restart_daemon", "restartd_confirm").c_str(), + false, doCancel, doConfirm); + if (doCancel) { s_settingsState.confirm_restart_daemon = false; } - ImGui::SameLine(); - if (ImGui::Button(TrId("settings_restart_daemon", "restartd_confirm").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { s_settingsState.debug_cats_dirty = false; app->restartDaemon(); s_settingsState.confirm_restart_daemon = false; @@ -2967,11 +2789,7 @@ void RenderSettingsPage(App* app) { // Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe) if (s_settingsState.confirm_lite_redownload) { if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) { - ImGui::PushFont(Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg")); @@ -2981,12 +2799,14 @@ void RenderSettingsPage(App* app) { ImGui::Separator(); ImGui::Spacing(); - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TrId("cancel", "lite_redl_cancel").c_str(), ImVec2(btnW, 40))) { + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TrId("cancel", "lite_redl_cancel").c_str(), + TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), + false, doCancel, doConfirm); + if (doCancel) { s_settingsState.confirm_lite_redownload = false; } - ImGui::SameLine(); - if (ImGui::Button(TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { if (auto* lite = app->liteWallet()) lite->startRescan(); s_settingsState.confirm_lite_redownload = false; } diff --git a/src/ui/windows/address_book_dialog.cpp b/src/ui/windows/address_book_dialog.cpp index c2e24c7..6924456 100644 --- a/src/ui/windows/address_book_dialog.cpp +++ b/src/ui/windows/address_book_dialog.cpp @@ -6,6 +6,7 @@ #include "../../app.h" #include "../../data/address_book.h" #include "../../util/i18n.h" +#include "../../util/text_format.h" #include "../notifications.h" #include "../schema/ui_schema.h" #include "../material/draw_helpers.h" @@ -253,8 +254,7 @@ void AddressBookDialog::render(App* app) std::string addr_display = entry.address; int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40; if (addr_display.length() > static_cast(addrTruncLen)) { - addr_display = addr_display.substr(0, addrFrontLbl.truncate) + "..." + - addr_display.substr(addr_display.length() - addrBackLbl.truncate); + addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate); } ImGui::TextDisabled("%s", addr_display.c_str()); if (ImGui::IsItemHovered()) { diff --git a/src/ui/windows/address_label_dialog.h b/src/ui/windows/address_label_dialog.h index 51e5619..1c1dc23 100644 --- a/src/ui/windows/address_label_dialog.h +++ b/src/ui/windows/address_label_dialog.h @@ -11,6 +11,7 @@ #include #include "../../app.h" #include "../../util/i18n.h" +#include "../../util/text_format.h" #include "../material/draw_helpers.h" #include "../material/project_icons.h" #include "../theme.h" @@ -69,7 +70,7 @@ public: ImGui::Spacing(); { std::string display = s_address; - if (display.size() > 42) display = display.substr(0, 20) + "..." + display.substr(display.size() - 16); + if (display.size() > 42) display = util::truncateMiddle(display, 20, 16); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str()); } ImGui::Spacing(); diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 89cfcca..242a595 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -200,10 +200,8 @@ void RenderSharedAddressList(App* app, float listH, float availW, bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced(); ImGui::BeginDisabled(sharedAddrSyncing); if (s_generating_z_address) { - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; char genLabel[64]; - snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]); + snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots()); TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font)); } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), @@ -774,9 +772,9 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float const auto& state = app->state(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS"); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions")); ImGui::SameLine(); - if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) { + if (TactileSmallButton(TR("view_all"), S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) { app->setCurrentPage(NavPage::History); } ImGui::Spacing(); @@ -791,7 +789,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float int count = (int)txs.size(); if (count == 0) { - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet"); + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet")); } else { ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* capFont = Type().caption(); diff --git a/src/ui/windows/balance_recent_tx.cpp b/src/ui/windows/balance_recent_tx.cpp index e4311c1..a57751b 100644 --- a/src/ui/windows/balance_recent_tx.cpp +++ b/src/ui/windows/balance_recent_tx.cpp @@ -10,13 +10,6 @@ namespace dragonx { namespace ui { namespace { -std::string truncateRecentAddress(const std::string& address, int maxLen) -{ - if (maxLen <= 3 || address.length() <= static_cast(maxLen)) return address; - int half = (maxLen - 3) / 2; - return address.substr(0, half) + "..." + address.substr(address.length() - half); -} - std::string formatRecentAmount(const std::string& type, double amount) { char buffer[32]; @@ -36,7 +29,7 @@ RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLe { return { tx.getTypeDisplay(), - truncateRecentAddress(tx.address, addressMaxLen), + util::truncateMiddle(tx.address, addressMaxLen), formatRecentAmount(tx.type, tx.amount), recentTimeAgo(tx.timestamp) }; diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index 23ee6a1..eafdb91 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -733,10 +733,8 @@ static void RenderBalanceClassic(App* app) bool addrSyncing = state.sync.syncing && !state.sync.isSynced(); ImGui::BeginDisabled(addrSyncing); if (s_generating_z_address) { - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; char genLabel[64]; - snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]); + snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), material::LoadingDots()); TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font)); } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { s_generating_z_address = true; @@ -2189,9 +2187,9 @@ static void RenderBalanceShield(App* app) { // Label below gauge ImFont* capFont = Type().caption(); const char* statusMsg; - if (privPct >= 80.0f) statusMsg = "Great privacy!"; - else if (privPct >= 50.0f) statusMsg = "Consider shielding more"; - else statusMsg = "Low privacy — shield funds"; + if (privPct >= 80.0f) statusMsg = TR("privacy_great"); + else if (privPct >= 50.0f) statusMsg = TR("privacy_medium"); + else statusMsg = TR("privacy_low"); ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000, 0, statusMsg); dl->AddText(capFont, capFont->LegacySize, ImVec2(gaugeCx - msgSz.x * 0.5f, gaugeCy + 4 * dp), @@ -2307,7 +2305,7 @@ static void RenderBalanceTimeline(App* app) { DrawGlassPanel(dl, chartMin, chartMax, spec); ImFont* capFont = Type().caption(); - const char* msg = "Balance history — collecting data..."; + const char* msg = TR("balance_history_collecting"); ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, msg); dl->AddText(capFont, capFont->LegacySize, ImVec2(chartMin.x + (availW - msgSz.x) * 0.5f, @@ -2629,10 +2627,10 @@ static void RenderBalanceMinimal(App* app) { ImGui::Dummy(heroSz); // Line 2: Shielded + Transparent - snprintf(buf, sizeof(buf), "Shielded: %.8f", s_dispShielded); + snprintf(buf, sizeof(buf), TR("balance_shielded_fmt"), s_dispShielded); Type().textColored(TypeStyle::Caption, Success(), buf); ImGui::SameLine(0, Layout::spacingLg()); - snprintf(buf, sizeof(buf), "Transparent: %.8f", s_dispTransparent); + snprintf(buf, sizeof(buf), TR("balance_transparent_fmt"), s_dispTransparent); Type().textColored(TypeStyle::Caption, Warning(), buf); // USD value diff --git a/src/ui/windows/balance_tab_helpers.cpp b/src/ui/windows/balance_tab_helpers.cpp index c7a677b..d5ee44b 100644 --- a/src/ui/windows/balance_tab_helpers.cpp +++ b/src/ui/windows/balance_tab_helpers.cpp @@ -36,9 +36,7 @@ std::string timeAgo(int64_t timestamp) std::string truncateAddress(const std::string& address, int maxLen) { - if (maxLen <= 3 || address.length() <= static_cast(maxLen)) return address; - int half = (maxLen - 3) / 2; - return address.substr(0, half) + "..." + address.substr(address.length() - half); + return util::truncateMiddle(address, maxLen); } ImU32 recentTxIconColor(const std::string& type) diff --git a/src/ui/windows/block_info_dialog.cpp b/src/ui/windows/block_info_dialog.cpp index e5410eb..ab94e01 100644 --- a/src/ui/windows/block_info_dialog.cpp +++ b/src/ui/windows/block_info_dialog.cpp @@ -7,6 +7,7 @@ #include "../../rpc/rpc_client.h" #include "../../rpc/rpc_worker.h" #include "../../util/i18n.h" +#include "../../util/text_format.h" #include "../notifications.h" #include "../schema/ui_schema.h" #include "../material/draw_helpers.h" @@ -251,7 +252,7 @@ void BlockInfoDialog::render(App* app) // Truncate for display std::string prev_short = s_prev_hash; if (prev_short.length() > static_cast(hashLbl.truncate)) { - prev_short = prev_short.substr(0, hashFrontLbl.truncate) + "..." + prev_short.substr(prev_short.length() - hashBackLbl.truncate); + prev_short = util::truncateMiddle(prev_short, hashFrontLbl.truncate, hashBackLbl.truncate); } ImGui::Text("%s", prev_short.c_str()); ImGui::PopStyleColor(); @@ -273,7 +274,7 @@ void BlockInfoDialog::render(App* app) // Truncate for display std::string next_short = s_next_hash; if (next_short.length() > static_cast(hashLbl.truncate)) { - next_short = next_short.substr(0, hashFrontLbl.truncate) + "..." + next_short.substr(next_short.length() - hashBackLbl.truncate); + next_short = util::truncateMiddle(next_short, hashFrontLbl.truncate, hashBackLbl.truncate); } ImGui::Text("%s", next_short.c_str()); ImGui::PopStyleColor(); diff --git a/src/ui/windows/explorer_tab.cpp b/src/ui/windows/explorer_tab.cpp index 96ba62f..f6594cd 100644 --- a/src/ui/windows/explorer_tab.cpp +++ b/src/ui/windows/explorer_tab.cpp @@ -8,6 +8,8 @@ #include "../../rpc/rpc_worker.h" #include "../../data/wallet_state.h" #include "../../util/i18n.h" +#include "../../util/text_format.h" +#include "mining_tab_helpers.h" #include "../explorer/explorer_block_cache.h" #include "../schema/ui_schema.h" #include "../material/type.h" @@ -117,9 +119,7 @@ static const char* relativeTime(int64_t timestamp) { } static std::string truncateHash(const std::string& hash, int front, int back) { - if (hash.length() <= static_cast(front + back + 3)) - return hash; - return hash.substr(0, front) + "..." + hash.substr(hash.length() - back); + return util::truncateMiddle(hash, front, back); } // Adaptive hash truncation based on available pixel width @@ -176,21 +176,6 @@ static std::string formatSize(int bytes) { return b; } -static std::string formatHashrate(double hashrate) { - char b[32]; - if (hashrate >= 1e12) - snprintf(b, sizeof(b), "%.2f TH/s", hashrate / 1e12); - else if (hashrate >= 1e9) - snprintf(b, sizeof(b), "%.2f GH/s", hashrate / 1e9); - else if (hashrate >= 1e6) - snprintf(b, sizeof(b), "%.2f MH/s", hashrate / 1e6); - else if (hashrate >= 1e3) - snprintf(b, sizeof(b), "%.2f KH/s", hashrate / 1e3); - else - snprintf(b, sizeof(b), "%.0f H/s", hashrate); - return b; -} - // Copy icon button — draws a small icon that copies text to clipboard on click static void copyButton(const char* id, const std::string& text, float x, float y) { ImFont* iconFont = Type().iconSmall(); @@ -698,7 +683,7 @@ static void renderChainStats(App* app, float availWidth) { MetricSpec metrics[4] = { { ICON_MD_SPEED, TR("difficulty"), difficulty, "", Warning() }, - { ICON_MD_SHOW_CHART, TR("peers_hashrate"), formatHashrate(state.mining.networkHashrate), "", Success() }, + { ICON_MD_SHOW_CHART, TR("peers_hashrate"), FormatHashrate(state.mining.networkHashrate), "", Success() }, { ICON_MD_CONFIRMATION_NUMBER, TR("peers_notarized"), notarized, "", Primary() }, { ICON_MD_STORAGE, TR("explorer_mempool"), mempoolTxs, s_mempool_loading ? std::string("") : formatSize(static_cast(s_mempool_size)), Secondary() }, }; @@ -1030,10 +1015,8 @@ static void renderBlockDetailModal(App* app) { // Show a loading modal while fetching if (BeginOverlayDialog(TR("loading"), &s_detail_loading, 300.0f, 0.85f)) { ImGui::Spacing(); - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; char loadBuf[64]; - snprintf(loadBuf, sizeof(loadBuf), "%s%s", TR("loading"), dotStr[dots]); + snprintf(loadBuf, sizeof(loadBuf), "%s%s", TR("loading"), material::LoadingDots()); ImGui::TextDisabled("%s", loadBuf); ImGui::Spacing(); EndOverlayDialog(); @@ -1091,9 +1074,7 @@ static void renderBlockDetailModal(App* app) { // Show loading state inside the modal when navigating between blocks if (s_detail_loading) { - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; - snprintf(buf, sizeof(buf), "%s%s", TR("loading"), dotStr[dots]); + snprintf(buf, sizeof(buf), "%s%s", TR("loading"), material::LoadingDots()); ImGui::TextDisabled("%s", buf); EndOverlayDialog(); return; diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index 967d62c..da73c89 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -6,6 +6,7 @@ #include "../../app.h" #include "../../data/wallet_state.h" #include "../../util/i18n.h" +#include "mining_tab_helpers.h" #include "../theme.h" #include "../effects/imgui_acrylic.h" #include "../effects/low_spec.h" @@ -218,17 +219,8 @@ void RenderPeersTab(App* app) dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate")); float valY = ry + capFont->LegacySize + Layout::spacingXs(); if (mining.networkHashrate > 0) { - if (mining.networkHashrate >= 1e12) - snprintf(buf, sizeof(buf), "%.2f TH/s", mining.networkHashrate / 1e12); - else if (mining.networkHashrate >= 1e9) - snprintf(buf, sizeof(buf), "%.2f GH/s", mining.networkHashrate / 1e9); - else if (mining.networkHashrate >= 1e6) - snprintf(buf, sizeof(buf), "%.2f MH/s", mining.networkHashrate / 1e6); - else if (mining.networkHashrate >= 1e3) - snprintf(buf, sizeof(buf), "%.2f KH/s", mining.networkHashrate / 1e3); - else - snprintf(buf, sizeof(buf), "%.2f H/s", mining.networkHashrate); - dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), buf); + std::string hr = FormatHashrate(mining.networkHashrate); + dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), hr.c_str()); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94"); } diff --git a/src/ui/windows/receive_tab.cpp b/src/ui/windows/receive_tab.cpp index c24be6d..12329f4 100644 --- a/src/ui/windows/receive_tab.cpp +++ b/src/ui/windows/receive_tab.cpp @@ -13,6 +13,7 @@ #include "../../config/settings.h" #include "../../util/i18n.h" #include "../../util/platform.h" +#include "../../util/text_format.h" #include "../../config/version.h" #include "../../data/wallet_state.h" #include "../../ui/widgets/qr_code.h" @@ -71,14 +72,6 @@ static bool s_generating_address = false; // ============================================================================ // Helpers // ============================================================================ -static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) { - // Guard maxLen <= 3 before the unsigned (maxLen - 3): otherwise it wraps and the - // trailing substr(length - halfLen) throws std::out_of_range on short inputs. - if (maxLen <= 3 || addr.length() <= maxLen) return addr; - size_t halfLen = (maxLen - 3) / 2; - return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen); -} - static void OpenExplorerURL(App* app, const std::string& address) { std::string url = app->settings()->getAddressExplorerUrl() + address; dragonx::util::Platform::openUrl(url); @@ -201,8 +194,8 @@ static void RenderAddressDropdown(App* app, float width) { const auto& addr = state.addresses[s_selected_address_idx]; bool isZ = addr.type == "shielded"; const char* tag = isZ ? "[Z]" : "[T]"; - std::string trunc = TruncateAddress(addr.address, - static_cast(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size))); + std::string trunc = util::truncateMiddle(addr.address, + static_cast(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size))); snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s", tag, trunc.c_str(), addr.balance, DRAGONX_TICKER); s_source_preview = buf; @@ -258,7 +251,7 @@ static void RenderAddressDropdown(App* app, float width) { auto lblIt = s_address_labels.find(addr.address); bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty()); - std::string trunc = TruncateAddress(addr.address, addrTruncLen); + std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen); if (hasLabel) { snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s", tag, lblIt->second.c_str(), trunc.c_str(), @@ -304,10 +297,8 @@ static void RenderAddressDropdown(App* app, float width) { ImGui::SameLine(0, Layout::spacingSm()); ImGui::BeginDisabled(!app->isConnected() || s_generating_address); if (s_generating_address) { - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; char genLabel[64]; - snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), dotStr[dots]); + snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots()); TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button")); } else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) { s_generating_address = true; @@ -418,8 +409,8 @@ static void RenderRecentReceived(const AddressInfo& /* addr */, // Address (second line) float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f); - std::string addrDisplay = TruncateAddress(tx.address, - (size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); + std::string addrDisplay = util::truncateMiddle(tx.address, + (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); rowDL->AddText(capFont, capFont->LegacySize, ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str()); diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index f66bafb..8d7ce29 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -147,14 +147,6 @@ static double GetAvailableBalance(App* app) { return 0.0; } -static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) { - // Guard maxLen <= 3 before the unsigned (maxLen - 3): otherwise it wraps and the - // trailing substr(length - halfLen) throws std::out_of_range on short inputs. - if (maxLen <= 3 || addr.length() <= maxLen) return addr; - size_t halfLen = (maxLen - 3) / 2; - return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen); -} - // Recipient validity = prefix/length pre-filter AND a real encoding-checksum check, so a // transcription error that still matches the prefix/length is no longer labelled "Valid". // The checksum verifiers are version-agnostic, so they never reject a genuine address. @@ -256,8 +248,8 @@ static void RenderSourceDropdown(App* app, float width) { const auto& addr = state.addresses[s_selected_from_idx]; bool isZ = addr.type == "shielded"; const char* tag = isZ ? "[Z]" : "[T]"; - std::string trunc = TruncateAddress(addr.address, - static_cast(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size))); + std::string trunc = util::truncateMiddle(addr.address, + static_cast(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size))); snprintf(buf, sizeof(buf), "%s %s — %.8f %s", tag, trunc.c_str(), addr.balance, DRAGONX_TICKER); s_source_preview = buf; @@ -287,7 +279,7 @@ static void RenderSourceDropdown(App* app, float width) { bool isZ = addr.type == "shielded"; const char* tag = isZ ? "[Z]" : "[T]"; - std::string trunc = TruncateAddress(addr.address, addrTruncLen); + std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen); snprintf(buf, sizeof(buf), "%s %s — %.8f %s", tag, trunc.c_str(), addr.balance, DRAGONX_TICKER); @@ -347,7 +339,7 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons ImGui::BeginChild(childId, ImVec2(width, sugH), true); for (size_t si = 0; si < suggestions.size(); si++) { int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size; - std::string dispSug = TruncateAddress(suggestions[si], sugTrunc > 0 ? sugTrunc : 50); + std::string dispSug = util::truncateMiddle(suggestions[si], sugTrunc > 0 ? sugTrunc : 50); ImGui::PushID((int)si); if (ImGui::Selectable(dispSug.c_str())) { snprintf(s_to_address, sizeof(s_to_address), "%s", suggestions[si].c_str()); @@ -710,7 +702,7 @@ void RenderSendConfirmPopup(App* app) { DrawGlassPanel(popDl, cMin, cMax, gs); popDl->AddText(body2, body2->LegacySize, ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()), - Success(), TruncateAddress(s_from_address, (size_t)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str()); + Success(), util::truncateMiddle(s_from_address, (int)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str()); ImGui::Dummy(ImVec2(popW, addrCardH)); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); } @@ -724,7 +716,7 @@ void RenderSendConfirmPopup(App* app) { DrawGlassPanel(popDl, cMin, cMax, gs); popDl->AddText(body2, body2->LegacySize, ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()), - Success(), TruncateAddress(s_to_address, (size_t)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str()); + Success(), util::truncateMiddle(s_to_address, (int)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str()); ImGui::Dummy(ImVec2(popW, addrCardH)); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); } @@ -1060,8 +1052,8 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap // Address (second line) float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f); - std::string addrDisplay = TruncateAddress(tx.address, - (size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); + std::string addrDisplay = util::truncateMiddle(tx.address, + (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); rowDL->AddText(capFont, capFont->LegacySize, ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str()); diff --git a/src/ui/windows/settings_window.cpp b/src/ui/windows/settings_window.cpp index cdd6789..96fae64 100644 --- a/src/ui/windows/settings_window.cpp +++ b/src/ui/windows/settings_window.cpp @@ -487,12 +487,8 @@ void RenderSettingsWindow(App* app, bool* p_open) // Confirmation dialog if (s_confirm_clear_ztx) { if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) { - ImGui::PushFont(material::Type().iconLarge()); - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING); - ImGui::PopFont(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning")); - + material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f)); + ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1")); ImGui::Spacing(); @@ -500,15 +496,14 @@ void RenderSettingsWindow(App* app, bool* p_open) ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - - float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) { + + bool doCancel = false, doConfirm = false; + material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_w").c_str(), + true, doCancel, doConfirm); + if (doCancel) { s_confirm_clear_ztx = false; } - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); - if (ImGui::Button(TrId("clear_anyway", "clear_ztx_w").c_str(), ImVec2(btnW, 40))) { + if (doConfirm) { std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; if (util::Platform::deleteFile(ztx_file)) { Notifications::instance().success("Z-transaction history cleared"); @@ -517,7 +512,6 @@ void RenderSettingsWindow(App* app, bool* p_open) } s_confirm_clear_ztx = false; } - ImGui::PopStyleColor(2); material::EndOverlayDialog(); } } diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index b6fe493..82e31a0 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -37,13 +37,6 @@ static std::string TrId(const char* key, const char* id) { return std::string(TR(key)) + "##" + id; } -// Helper to truncate strings -static std::string truncateString(const std::string& str, int maxLen = 16) { - if (str.length() <= static_cast(maxLen)) return str; - int half = (maxLen - 3) / 2; - return str.substr(0, half) + "..." + str.substr(str.length() - half); -} - // Case-insensitive string search static bool containsIgnoreCase(const std::string& str, const std::string& search) { return dragonx::util::containsIgnoreCase(str, search); @@ -358,9 +351,7 @@ void RenderTransactionsTab(App* app) ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; - ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]); + ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), material::LoadingDots()); } ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs())); @@ -679,9 +670,7 @@ void RenderTransactionsTab(App* app) } else if (state.transactions.empty()) { ImGui::Dummy(ImVec2(0, 20)); if (txLoading) { - int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; - const char* dotStr[] = {"", ".", "..", "..."}; - snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]); + snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), material::LoadingDots()); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); } else { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions")); @@ -756,7 +745,7 @@ void RenderTransactionsTab(App* app) OnSurfaceDisabled(), ago.c_str()); // Address (second line, left side) - std::string addr_display = truncateString(tx.address, (addrLabel.truncate > 0) ? addrLabel.truncate : 20); + std::string addr_display = util::truncateMiddle(tx.address, (addrLabel.truncate > 0) ? addrLabel.truncate : 20); dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceMedium(), addr_display.c_str()); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 44bf405..97484d4 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -452,6 +452,49 @@ void I18n::loadBuiltinEnglish() strings_["confirm_lite_redownload_title"] = "Redownload Blocks"; strings_["confirm_lite_redownload_msg"] = "This clears the wallet's downloaded block data and re-fetches and re-scans every block from the lite server. It can take a while; the wallet shows sync progress until it finishes."; strings_["confirm_lite_redownload_safe"] = "Your wallet, keys, and seed are not affected — only the block data is re-downloaded."; + // Lite wallet lifecycle / backup / security section + strings_["lite_servers_network_tab"] = "Lite servers are managed in the Network tab."; + strings_["lite_wallet_request"] = "Lite wallet request"; + strings_["lite_action"] = "Action"; + strings_["lite_op_create"] = "Create"; + strings_["lite_op_open"] = "Open"; + strings_["lite_op_restore"] = "Restore"; + strings_["lite_wallet_label"] = "Wallet"; + strings_["lite_seed_label"] = "Seed"; + strings_["lite_word_count"] = "%d / 24 words"; + strings_["lite_birthday_label"] = "Birthday"; + strings_["lite_birthday_hint"] = "Block height to start scanning from. Leave 0 if unknown (slower full scan)."; + strings_["lite_account_label"] = "Account"; + strings_["lite_overwrite"] = "Overwrite"; + strings_["lite_passphrase_label"] = "Passphrase"; + strings_["lite_validate"] = "Validate"; + strings_["lite_working"] = "Working…"; + strings_["lite_wallet_ready"] = "Wallet ready"; + strings_["lite_backup_keys"] = "Backup & keys"; + strings_["lite_show_seed"] = "Show seed"; + strings_["lite_seed_warning"] = "Seed phrase — the ONLY way to restore your wallet. Write it down, store it offline, never share it."; + strings_["lite_show_private_keys"] = "Show private keys"; + strings_["lite_private_keys_warning"] = "Private keys — anyone with these can spend your funds"; + strings_["lite_birthday_backup"] = "Birthday: %llu (back this up too)"; + strings_["lite_copy"] = "Copy"; + strings_["lite_save_to_file"] = "Save to file"; + strings_["lite_saved_to"] = "Saved (plaintext, owner-only) to "; + strings_["lite_could_not_write"] = "Could not write "; + strings_["lite_hide_wipe"] = "Hide & wipe"; + strings_["lite_import_key_label"] = "Import key"; + strings_["lite_import"] = "Import"; + strings_["lite_key_imported"] = "Key imported — run a sync to scan its history"; + strings_["lite_security"] = "Security"; + strings_["lite_encrypt_wallet"] = "Encrypt wallet"; + strings_["lite_wallet_encrypted"] = "Wallet encrypted"; + strings_["lite_unlock"] = "Unlock"; + strings_["lite_wallet_unlocked"] = "Wallet unlocked"; + strings_["lite_unlock_failed"] = "Unlock failed"; + strings_["lite_lock_now"] = "Lock now"; + strings_["lite_wallet_locked"] = "Wallet locked"; + strings_["lite_lock_failed"] = "Lock failed"; + strings_["lite_remove_encryption"] = "Remove encryption"; + strings_["lite_encryption_removed"] = "Encryption removed"; strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase"; strings_["tt_change_pass"] = "Change the wallet encryption passphrase"; strings_["tt_lock"] = "Lock the wallet immediately"; @@ -652,6 +695,15 @@ void I18n::loadBuiltinEnglish() strings_["transparent"] = "Transparent"; strings_["total"] = "Total"; strings_["unconfirmed"] = "Unconfirmed"; + strings_["recent_transactions"] = "RECENT TRANSACTIONS"; + strings_["view_all"] = "View All"; + strings_["no_transactions_yet"] = "No transactions yet"; + strings_["privacy_great"] = "Great privacy!"; + strings_["privacy_medium"] = "Consider shielding more"; + strings_["privacy_low"] = "Low privacy — shield funds"; + strings_["balance_history_collecting"] = "Balance history — collecting data..."; + strings_["balance_shielded_fmt"] = "Shielded: %.8f"; + strings_["balance_transparent_fmt"] = "Transparent: %.8f"; strings_["your_addresses"] = "Your Addresses"; strings_["z_addresses"] = "Z-Addresses"; strings_["t_addresses"] = "T-Addresses"; diff --git a/src/util/text_format.cpp b/src/util/text_format.cpp index 26b774e..4beff82 100644 --- a/src/util/text_format.cpp +++ b/src/util/text_format.cpp @@ -45,6 +45,21 @@ std::string formatTimeAgoShort(std::int64_t timestamp) return buf; } +std::string truncateMiddle(const std::string& s, int maxLen) +{ + // Guard maxLen <= 3 before the unsigned (maxLen - 3): otherwise it wraps and the + // trailing substr(length - half) throws std::out_of_range on short inputs. + if (maxLen <= 3 || s.length() <= static_cast(maxLen)) return s; + int half = (maxLen - 3) / 2; + return s.substr(0, half) + "..." + s.substr(s.length() - half); +} + +std::string truncateMiddle(const std::string& s, int front, int back) +{ + if (s.length() <= static_cast(front + back + 3)) return s; + return s.substr(0, front) + "..." + s.substr(s.length() - back); +} + bool containsIgnoreCase(std::string_view haystack, std::string_view needle) { if (needle.empty()) return true; diff --git a/src/util/text_format.h b/src/util/text_format.h index b888d29..5ab5999 100644 --- a/src/util/text_format.h +++ b/src/util/text_format.h @@ -24,5 +24,14 @@ std::string formatTimeAgoShort(std::int64_t timestamp); // An empty needle matches everything. Safe for per-frame filtering of large lists. bool containsIgnoreCase(std::string_view haystack, std::string_view needle); +// Middle-ellipsis truncation, half/half form: "prefix...suffix" so the total (including "...") +// is at most maxLen. Guards maxLen <= 3 (returns s unchanged) to avoid unsigned underflow on the +// (maxLen - 3) split. Strings already at/under maxLen are returned unchanged. +std::string truncateMiddle(const std::string& s, int maxLen); + +// Middle-ellipsis truncation with explicit prefix/suffix lengths: "...". +// Returns s unchanged when it is no longer than front + back + 3 (the "..." would not save space). +std::string truncateMiddle(const std::string& s, int front, int back); + } // namespace util } // namespace dragonx