refactor(audit): batch 4 — UI design-system helpers + i18n

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:29:12 -05:00
parent 280a71e973
commit de11850c74
21 changed files with 452 additions and 500 deletions

View File

@@ -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 }

View File

@@ -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();

View File

@@ -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),

View File

@@ -35,6 +35,13 @@ inline ImU32 ScaleAlpha(ImU32 col, float scale) {
return (col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(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

View File

@@ -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;
}

View File

@@ -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<size_t>(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()) {

View File

@@ -11,6 +11,7 @@
#include <cstddef>
#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();

View File

@@ -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();

View File

@@ -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<size_t>(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)
};

View File

@@ -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 historycollecting 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

View File

@@ -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<size_t>(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)

View File

@@ -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<size_t>(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<size_t>(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();

View File

@@ -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<size_t>(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<int>(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;

View File

@@ -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");
}

View File

@@ -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<size_t>(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<int>(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());

View File

@@ -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<size_t>(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<int>(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());

View File

@@ -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();
}
}

View File

@@ -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<size_t>(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());

View File

@@ -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";

View File

@@ -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<std::size_t>(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<std::size_t>(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;

View File

@@ -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: "<front chars>...<back chars>".
// 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