feat: Full UI internationalization, pool hashrate stats, and layout caching

- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
This commit is contained in:
dan_s
2026-03-11 00:40:50 -05:00
parent cc617dd5be
commit 96c27bb949
71 changed files with 43567 additions and 5267 deletions

View File

@@ -572,12 +572,12 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
float u1 = localX1 / viewportWidth_;
float v1 = 1.0f - localY1 / viewportHeight_; // V at bottom-right (low)
// Draw the blurred background. The glass opacity comes from
// fallbackColor.w — blur always renders at full strength.
// UI opacity is handled separately by card surface alpha.
// Draw the blurred background. Scale by both the glass preset
// opacity (fallbackColor.w) AND the user's UI opacity slider so
// that lowering card opacity lets the sharp background through.
ImTextureID blurTex = getBlurredTexture();
uint8_t glassAlpha = static_cast<uint8_t>(
std::min(255.0f, std::max(0.0f, params.fallbackColor.w * 255.0f)));
std::min(255.0f, std::max(0.0f, params.fallbackColor.w * settings_.uiOpacity * 255.0f)));
if (blurTex) {
drawList->AddImageRounded(
@@ -774,11 +774,11 @@ AcrylicFallback AcrylicMaterial::detectFallback() const
void AcrylicMaterial::drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax,
const AcrylicParams& params, float rounding)
{
// Draw semi-transparent tint without blur
// This is the "Tinted Only" fallback mode (not affected by UI opacity)
// Draw semi-transparent tint without blur — scale by UI opacity
// so the tinted fallback also respects the card transparency slider.
ImU32 tintCol = ImGui::ColorConvertFloat4ToU32(
ImVec4(params.tintColor.x, params.tintColor.y, params.tintColor.z,
params.tintColor.w * params.tintOpacity * 0.9f)
params.tintColor.w * params.tintOpacity * 0.9f * settings_.uiOpacity)
);
drawList->AddRectFilled(pMin, pMax, tintCol, rounding);
@@ -1528,11 +1528,11 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
(void*)blurTex, u0, v0, u1, v1);
}
// Draw the blurred background. Glass opacity from fallbackColor
// alpha. Blur always renders at full strength; UI opacity is
// handled separately by card surface alpha.
// Draw the blurred background. Scale by both the glass preset
// opacity (fallbackColor.w) AND the user's UI opacity slider so
// that lowering card opacity lets the sharp background through.
uint8_t glassAlpha = (uint8_t)std::min(255.f,
std::max(0.f, params.fallbackColor.w * 255.f));
std::max(0.f, params.fallbackColor.w * settings_.uiOpacity * 255.f));
if (blurTex) {
drawList->AddImageRounded(
@@ -1559,7 +1559,7 @@ void AcrylicMaterial::drawTintedRect(ImDrawList* drawList, const ImVec2& pMin, c
{
ImU32 tintCol = ImGui::ColorConvertFloat4ToU32(
ImVec4(params.tintColor.x, params.tintColor.y, params.tintColor.z,
params.tintColor.w * params.tintOpacity * 0.9f));
params.tintColor.w * params.tintOpacity * 0.9f * settings_.uiOpacity));
drawList->AddRectFilled(pMin, pMax, tintCol, rounding);
// Noise grain overlay — single draw call via pre-tiled texture

View File

@@ -105,6 +105,12 @@ public:
bool hasSandstorm() const { return enabled_ && sandstorm_.enabled; }
bool hasViewportOverlay() const { return enabled_ && (viewport_overlay_.colorWashEnabled || viewport_overlay_.vignetteEnabled); }
/// True when any per-panel visual effect is active (rainbow, shimmer, specular, edge, ember, gradient)
bool hasAnyPanelEffect() const {
return hasRainbowBorder() || hasShimmer() || hasSpecularGlare()
|| hasEdgeTrace() || hasEmberRise() || hasGradientBorder();
}
/// True when any time-dependent effect is active and needs continuous redraws
bool hasActiveAnimation() const {
if (!enabled_) return false;

View File

@@ -185,6 +185,102 @@ inline float kItemSpacing() { return schema::UI().drawElement("spacing", "it
inline float kLabelValueGap() { return schema::UI().drawElement("spacing", "label-value").sizeOr(4.0f) * dpiScale(); }
inline float kSeparatorGap() { return schema::UI().drawElement("spacing", "separator").sizeOr(20.0f) * dpiScale(); }
// ============================================================================
// Per-frame cache — populated once via beginFrame(), avoids repeated
// schema hash-map lookups for the hottest accessors.
// ============================================================================
namespace detail {
struct FrameCache {
uint32_t gen = 0; // schema generation when last populated
float rawDp = 1.0f; // rawDpiScale() snapshot
float dp = 1.0f; // dpiScale() snapshot
// Spacing tokens (raw, unscaled)
float spXs = 2.0f;
float spSm = 4.0f;
float spMd = 8.0f;
float spLg = 12.0f;
float spXl = 16.0f;
float spXxl = 24.0f;
// Responsive scale config
float refW = 1200.0f;
float refH = 700.0f;
float minHS = 0.5f;
float maxHS = 1.5f;
float minVS = 0.5f;
float maxVS = 1.4f;
float minDen = 0.6f;
float maxDen = 1.2f;
// Breakpoint thresholds (raw, unscaled)
float compactW = 500.0f;
float compactH = 450.0f;
float expandedW = 900.0f;
float expandedH = 750.0f;
// Glass / card helpers (raw, unscaled)
float glassRnd = 8.0f;
float cardPad = 12.0f;
float cardGap = 8.0f;
};
inline FrameCache& frameCache() { static FrameCache c; return c; }
} // namespace detail
/**
* @brief Refresh the per-frame layout cache.
*
* Call once per frame (e.g., from App::preFrame) BEFORE any rendering.
* Reads all high-frequency TOML values into fast statics. Invalidated
* automatically when the schema generation changes (hot-reload).
*/
inline void beginFrame() {
auto& c = detail::frameCache();
uint32_t g = schema::UI().generation();
// Also capture DPI each frame (can change on display-scale events)
float curRawDp = rawDpiScale();
float curDp = dpiScale();
if (g == c.gen && curRawDp == c.rawDp && curDp == c.dp) return;
c.gen = g;
c.rawDp = curRawDp;
c.dp = curDp;
const auto& S = schema::UI();
// Spacing tokens
c.spXs = S.drawElement("spacing-tokens", "xs").sizeOr(2.0f);
c.spSm = S.drawElement("spacing-tokens", "sm").sizeOr(4.0f);
c.spMd = S.drawElement("spacing-tokens", "md").sizeOr(8.0f);
c.spLg = S.drawElement("spacing-tokens", "lg").sizeOr(12.0f);
c.spXl = S.drawElement("spacing-tokens", "xl").sizeOr(16.0f);
c.spXxl = S.drawElement("spacing-tokens", "xxl").sizeOr(24.0f);
// Responsive config
c.refW = S.drawElement("responsive", "ref-width").sizeOr(1200.0f);
c.refH = S.drawElement("responsive", "ref-height").sizeOr(700.0f);
c.minHS = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
c.maxHS = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
c.minVS = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
c.maxVS = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
c.minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
c.maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
// Breakpoints
c.compactW = S.drawElement("responsive", "compact-width").sizeOr(500.0f);
c.compactH = S.drawElement("responsive", "compact-height").sizeOr(450.0f);
c.expandedW = S.drawElement("responsive", "expanded-width").sizeOr(900.0f);
c.expandedH = S.drawElement("responsive", "expanded-height").sizeOr(750.0f);
// Glass / card
c.glassRnd = S.drawElement("responsive", "glass-rounding").sizeOr(8.0f);
c.cardPad = S.drawElement("responsive", "card-inner-padding").sizeOr(12.0f);
c.cardGap = S.drawElement("responsive", "card-gap").sizeOr(8.0f);
}
// ============================================================================
// Layout Tier (responsive breakpoints)
// ============================================================================
@@ -203,12 +299,12 @@ enum class LayoutTier { Compact, Normal, Expanded };
* Call after ImGui::BeginChild for the content area, or pass explicit avail.
*/
inline LayoutTier currentTier() {
const auto& S = schema::UI();
float dp = dpiScale();
float cw = S.drawElement("responsive", "compact-width").sizeOr(500.0f) * dp;
float ch = S.drawElement("responsive", "compact-height").sizeOr(450.0f) * dp;
float ew = S.drawElement("responsive", "expanded-width").sizeOr(900.0f) * dp;
float eh = S.drawElement("responsive", "expanded-height").sizeOr(750.0f) * dp;
const auto& c = detail::frameCache();
float dp = c.dp;
float cw = c.compactW * dp;
float ch = c.compactH * dp;
float ew = c.expandedW * dp;
float eh = c.expandedH * dp;
ImVec2 avail = ImGui::GetContentRegionAvail();
if (avail.x < cw || avail.y < ch) return LayoutTier::Compact;
if (avail.x > ew && avail.y > eh) return LayoutTier::Expanded;
@@ -216,12 +312,12 @@ inline LayoutTier currentTier() {
}
inline LayoutTier currentTier(float availW, float availH) {
const auto& S = schema::UI();
float dp = dpiScale();
float cw = S.drawElement("responsive", "compact-width").sizeOr(500.0f) * dp;
float ch = S.drawElement("responsive", "compact-height").sizeOr(450.0f) * dp;
float ew = S.drawElement("responsive", "expanded-width").sizeOr(900.0f) * dp;
float eh = S.drawElement("responsive", "expanded-height").sizeOr(750.0f) * dp;
const auto& c = detail::frameCache();
float dp = c.dp;
float cw = c.compactW * dp;
float ch = c.compactH * dp;
float ew = c.expandedW * dp;
float eh = c.expandedH * dp;
if (availW < cw || availH < ch) return LayoutTier::Compact;
if (availW > ew && availH > eh) return LayoutTier::Expanded;
return LayoutTier::Normal;
@@ -240,15 +336,10 @@ inline LayoutTier currentTier(float availW, float availH) {
* while emitting physical-pixel results.
*/
inline float hScale(float availWidth) {
const auto& S = schema::UI();
float rawDp = rawDpiScale(); // reference uses hardware DPI only
float dp = dpiScale(); // output includes user font scale
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * rawDp;
float minH = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
float maxH = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
// Clamp the logical (DPI-neutral) portion, then apply effective DPI.
float logical = std::clamp(availWidth / rw, minH, maxH);
return logical * dp;
const auto& c = detail::frameCache();
float rw = c.refW * c.rawDp;
float logical = std::clamp(availWidth / rw, c.minHS, c.maxHS);
return logical * c.dp;
}
inline float hScale() {
@@ -261,14 +352,10 @@ inline float hScale() {
* Same decomposition as hScale — logical clamp × DPI.
*/
inline float vScale(float availHeight) {
const auto& S = schema::UI();
float rawDp = rawDpiScale(); // reference uses hardware DPI only
float dp = dpiScale(); // output includes user font scale
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
float minV = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
float maxV = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
float logical = std::clamp(availHeight / rh, minV, maxV);
return logical * dp;
const auto& c = detail::frameCache();
float rh = c.refH * c.rawDp;
float logical = std::clamp(availHeight / rh, c.minVS, c.maxVS);
return logical * c.dp;
}
inline float vScale() {
@@ -282,14 +369,10 @@ inline float vScale() {
* values scale proportionally with fonts and style.
*/
inline float densityScale(float availHeight) {
const auto& S = schema::UI();
float rawDp = rawDpiScale(); // reference uses hardware DPI only
float dp = dpiScale(); // output includes user font scale
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
float minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
float maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
float logical = std::clamp(availHeight / rh, minDen, maxDen);
return logical * dp;
const auto& c = detail::frameCache();
float rh = c.refH * c.rawDp;
float logical = std::clamp(availHeight / rh, c.minDen, c.maxDen);
return logical * c.dp;
}
inline float densityScale() {
@@ -301,33 +384,33 @@ inline float densityScale() {
// ============================================================================
/** @brief Get spacing token scaled by current density. */
inline float spacingXs() { return schema::UI().drawElement("spacing-tokens", "xs").sizeOr(2.0f) * densityScale(); }
inline float spacingSm() { return schema::UI().drawElement("spacing-tokens", "sm").sizeOr(4.0f) * densityScale(); }
inline float spacingMd() { return schema::UI().drawElement("spacing-tokens", "md").sizeOr(8.0f) * densityScale(); }
inline float spacingLg() { return schema::UI().drawElement("spacing-tokens", "lg").sizeOr(12.0f) * densityScale(); }
inline float spacingXl() { return schema::UI().drawElement("spacing-tokens", "xl").sizeOr(16.0f) * densityScale(); }
inline float spacingXxl() { return schema::UI().drawElement("spacing-tokens", "xxl").sizeOr(24.0f) * densityScale(); }
inline float spacingXs() { return detail::frameCache().spXs * densityScale(); }
inline float spacingSm() { return detail::frameCache().spSm * densityScale(); }
inline float spacingMd() { return detail::frameCache().spMd * densityScale(); }
inline float spacingLg() { return detail::frameCache().spLg * densityScale(); }
inline float spacingXl() { return detail::frameCache().spXl * densityScale(); }
inline float spacingXxl() { return detail::frameCache().spXxl * densityScale(); }
/** @brief Get raw (unscaled) spacing token. */
inline float spacingXsRaw() { return schema::UI().drawElement("spacing-tokens", "xs").sizeOr(2.0f); }
inline float spacingSmRaw() { return schema::UI().drawElement("spacing-tokens", "sm").sizeOr(4.0f); }
inline float spacingMdRaw() { return schema::UI().drawElement("spacing-tokens", "md").sizeOr(8.0f); }
inline float spacingLgRaw() { return schema::UI().drawElement("spacing-tokens", "lg").sizeOr(12.0f); }
inline float spacingXlRaw() { return schema::UI().drawElement("spacing-tokens", "xl").sizeOr(16.0f); }
inline float spacingXxlRaw() { return schema::UI().drawElement("spacing-tokens", "xxl").sizeOr(24.0f); }
inline float spacingXsRaw() { return detail::frameCache().spXs; }
inline float spacingSmRaw() { return detail::frameCache().spSm; }
inline float spacingMdRaw() { return detail::frameCache().spMd; }
inline float spacingLgRaw() { return detail::frameCache().spLg; }
inline float spacingXlRaw() { return detail::frameCache().spXl; }
inline float spacingXxlRaw() { return detail::frameCache().spXxl; }
// ============================================================================
// Responsive Globals Helpers
// ============================================================================
/** @brief Default glass panel rounding (8.0 default). */
inline float glassRounding() { return schema::UI().drawElement("responsive", "glass-rounding").sizeOr(8.0f) * dpiScale(); }
inline float glassRounding() { return detail::frameCache().glassRnd * dpiScale(); }
/** @brief Default card inner padding (12.0 default). */
inline float cardInnerPadding() { return schema::UI().drawElement("responsive", "card-inner-padding").sizeOr(12.0f) * dpiScale(); }
inline float cardInnerPadding() { return detail::frameCache().cardPad * dpiScale(); }
/** @brief Default card gap (8.0 default). */
inline float cardGap() { return schema::UI().drawElement("responsive", "card-gap").sizeOr(8.0f) * dpiScale(); }
inline float cardGap() { return detail::frameCache().cardGap * dpiScale(); }
/**
* @brief Compute a responsive card height from a base value.

View File

@@ -351,18 +351,24 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
// Noise grain overlay — drawn OVER the surface overlay so card
// opacity doesn't hide it. Gives cards a tactile paper feel.
// Noise tint is cached per-generation, opacity checked each panel.
{
float noiseMul = dragonx::ui::effects::ImGuiAcrylic::GetNoiseOpacity();
if (noiseMul > 0.0f) {
uint8_t origAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF;
uint8_t scaledAlpha = static_cast<uint8_t>(std::min(255.0f, origAlpha * noiseMul));
// Tint base color changes only on theme reload; opacity slider may change per-frame
static uint32_t s_noiseGen = 0;
static uint8_t s_baseAlpha = 0;
if (curGen != s_noiseGen) {
s_noiseGen = curGen;
s_baseAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF;
}
uint8_t scaledAlpha = static_cast<uint8_t>(std::min(255.0f, s_baseAlpha * noiseMul));
ImU32 noiseTint = (s_glassNoiseTint & ~(0xFFu << IM_COL32_A_SHIFT)) | (scaledAlpha << IM_COL32_A_SHIFT);
float inset = spec.rounding * 0.3f;
ImVec2 clipMin(pMin.x + inset, pMin.y + inset);
ImVec2 clipMax(pMax.x - inset, pMax.y - inset);
dl->PushClipRect(clipMin, clipMax, true);
dragonx::util::DrawTiledNoiseRect(dl, clipMin, clipMax, noiseTint);
dl->PopClipRect();
ImVec2 noiseMin(pMin.x + inset, pMin.y + inset);
ImVec2 noiseMax(pMax.x - inset, pMax.y - inset);
// Image rect matches clip bounds exactly — no PushClipRect needed
dragonx::util::DrawTiledNoiseRect(dl, noiseMin, noiseMax, noiseTint);
}
}
@@ -375,18 +381,20 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
// Theme visual effects drawn on ForegroundDrawList so they
// render above card content (text, values, etc.), not below.
auto& fx = effects::ThemeEffects::instance();
ImDrawList* fxDl = ImGui::GetForegroundDrawList();
if (fx.hasRainbowBorder()) {
fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth);
if (fx.hasAnyPanelEffect()) {
ImDrawList* fxDl = ImGui::GetForegroundDrawList();
if (fx.hasRainbowBorder()) {
fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth);
}
if (fx.hasShimmer()) {
fx.drawShimmer(fxDl, pMin, pMax, spec.rounding);
}
if (fx.hasSpecularGlare()) {
fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding);
}
// Per-panel theme effects: edge trace + ember rise
fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding);
}
if (fx.hasShimmer()) {
fx.drawShimmer(fxDl, pMin, pMax, spec.rounding);
}
if (fx.hasSpecularGlare()) {
fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding);
}
// Per-panel theme effects: edge trace + ember rise
fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding);
} else {
// Low-spec opaque fallback
dl->AddRectFilled(pMin, pMax,
@@ -749,8 +757,10 @@ inline void ApplySmoothScroll(float speed = 12.0f)
s.current = actualY;
}
// Capture mouse wheel when hovered
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
// Capture mouse wheel when hovered, but not when a popup (combo dropdown
// etc.) is open — let the popup handle its own scrolling exclusively.
bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
if (!popupOpen && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f;

View File

@@ -246,11 +246,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
cfg.PixelSnapH = true;
// Include default ASCII + Latin, Latin Extended (for Spanish/multilingual),
// plus arrows (⇄ U+21C4), math (≈ U+2248),
// general punctuation (— U+2014, … U+2026, etc.)
// Cyrillic (for Russian), Greek, plus arrows (⇄ U+21C4),
// math (≈ U+2248), general punctuation (— U+2014, … U+2026, etc.)
static const ImWchar glyphRanges[] = {
0x0020, 0x00FF, // Basic Latin + Latin-1 Supplement
0x0100, 0x024F, // Latin Extended-A + Latin Extended-B (Spanish, etc.)
0x0370, 0x03FF, // Greek and Coptic
0x0400, 0x04FF, // Cyrillic (Russian, Ukrainian, etc.)
0x0500, 0x052F, // Cyrillic Supplement
0x2000, 0x206F, // General Punctuation (em dash, ellipsis, etc.)
0x2190, 0x21FF, // Arrows (includes ⇄ U+21C4, ↻ U+21BB)
0x2200, 0x22FF, // Mathematical Operators (includes ≈ U+2248)
@@ -269,6 +272,36 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
if (font) {
DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name);
// Merge CJK fallback glyphs (Chinese/Japanese/Korean) from subset font
if (g_noto_cjk_subset_size > 0) {
void* cjkCopy = IM_ALLOC(g_noto_cjk_subset_size);
memcpy(cjkCopy, g_noto_cjk_subset_data, g_noto_cjk_subset_size);
ImFontConfig cjkCfg;
cjkCfg.FontDataOwnedByAtlas = true;
cjkCfg.MergeMode = true; // merge into the font we just loaded
cjkCfg.OversampleH = 1;
cjkCfg.OversampleV = 1;
cjkCfg.PixelSnapH = true;
cjkCfg.GlyphMinAdvanceX = 0;
// CJK Unified Ideographs + Hiragana + Katakana + Hangul + fullwidth punctuation
static const ImWchar cjkRanges[] = {
0x2E80, 0x2FDF, // CJK Radicals
0x3000, 0x30FF, // CJK Symbols, Hiragana, Katakana
0x3100, 0x312F, // Bopomofo
0x31F0, 0x31FF, // Katakana Extensions
0x3400, 0x4DBF, // CJK Extension A
0x4E00, 0x9FFF, // CJK Unified Ideographs
0xAC00, 0xD7AF, // Hangul Syllables
0xFF00, 0xFFEF, // Fullwidth Forms
0,
};
cjkCfg.GlyphRanges = cjkRanges;
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
}
} else {
DEBUG_LOGF("Typography: Failed to load %s\n", name);
IM_FREE(fontDataCopy);

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,7 @@ bool UISchema::loadFromFile(const std::string& path) {
// Clear previous data
elements_.clear();
styleCache_.clear();
backgroundImagePath_.clear();
logoImagePath_.clear();
@@ -92,6 +93,7 @@ bool UISchema::loadFromFile(const std::string& path) {
overlayPath_.clear(); // No overlay yet
loaded_ = true;
dirty_ = false;
++generation_;
// Record initial modification time
try {
@@ -126,6 +128,7 @@ bool UISchema::loadFromString(const std::string& tomlStr, const std::string& lab
// Clear previous data
elements_.clear();
styleCache_.clear();
backgroundImagePath_.clear();
logoImagePath_.clear();
@@ -222,6 +225,7 @@ bool UISchema::mergeOverlayFromFile(const std::string& path) {
// Track overlay file for hot-reload
currentPath_ = path;
++generation_;
styleCache_.clear(); // Invalidate after overlay merges new elements
try {
lastModTime_ = std::filesystem::last_write_time(path);
} catch (const std::filesystem::filesystem_error&) {}
@@ -495,6 +499,9 @@ void UISchema::applyIfDirty() {
if (!dirty_) return;
dirty_ = false;
// Clear style cache before snapshot so we re-read from TOML
styleCache_.clear();
// Snapshot font sizes before reload for change detection
static const char* fontKeys[] = {
"h1", "h2", "h3", "h4", "h5", "h6",
@@ -513,10 +520,17 @@ void UISchema::applyIfDirty() {
DEBUG_LOGF("[UISchema] Hot-reload: re-parsing %s\n", currentPath_.c_str());
// Save overlay path before reloading — loadFromFile/loadFromString clear it
std::string savedOverlay = overlayPath_;
// If an overlay is active, reload base first then re-merge overlay
if (!overlayPath_.empty() && !basePath_.empty()) {
if (!savedOverlay.empty() && !basePath_.empty()) {
loadFromFile(basePath_);
mergeOverlayFromFile(overlayPath_);
mergeOverlayFromFile(savedOverlay);
} else if (!savedOverlay.empty() && !embeddedTomlStr_.empty()) {
// Embedded base (e.g. Windows single-file): reload from stored string
loadFromString(embeddedTomlStr_, "embedded-reload");
mergeOverlayFromFile(savedOverlay);
} else {
loadFromFile(currentPath_);
}
@@ -836,15 +850,26 @@ SeparatorStyle UISchema::separator(const std::string& section, const std::string
return result;
}
DrawElementStyle UISchema::drawElement(const std::string& section, const std::string& name) const {
DrawElementStyle result;
const DrawElementStyle& UISchema::drawElement(const std::string& section, const std::string& name) const {
static const DrawElementStyle s_empty{};
const void* elem = findElement(section, name);
if (elem) {
detail::parseDrawElementStyle(elem, result);
std::string key = section + "." + name;
// Return from cache if already parsed
auto cit = styleCache_.find(key);
if (cit != styleCache_.end()) {
return cit->second;
}
return result;
// Parse from TOML and cache
const void* elem = findElement(section, name);
if (!elem) {
return s_empty;
}
auto [it, _] = styleCache_.emplace(std::move(key), DrawElementStyle{});
detail::parseDrawElementStyle(elem, it->second);
return it->second;
}
} // namespace schema

View File

@@ -255,8 +255,12 @@ public:
/**
* @brief Look up a DrawList custom element style
*
* Returns a cached const reference. The cache is invalidated on
* hot-reload (applyIfDirty) and full loads. For missing elements
* a static empty sentinel is returned.
*/
DrawElementStyle drawElement(const std::string& section, const std::string& name) const;
const DrawElementStyle& drawElement(const std::string& section, const std::string& name) const;
/**
* @brief Find a raw stored element by section and name.
@@ -365,6 +369,10 @@ private:
std::any data; // holds toml::table at runtime
};
std::unordered_map<std::string, StoredElement> elements_;
// Parsed DrawElementStyle cache — avoids repeated TOML table iteration.
// Populated lazily on first drawElement() lookup, cleared on reload.
mutable std::unordered_map<std::string, DrawElementStyle> styleCache_;
};
// Convenience alias

View File

@@ -11,6 +11,7 @@
#include "layout.h"
#include "schema/ui_schema.h"
#include "../embedded/IconsMaterialDesign.h"
#include "../util/i18n.h"
#include <cstdio>
#include <cmath>
@@ -34,25 +35,35 @@ enum class NavPage {
};
struct NavItem {
const char* label;
const char* label; // fallback label (English)
NavPage page;
const char* section_label; // if non-null, render section label above this item
const char* section_label; // if non-null, render section label above this item
const char* tr_key; // i18n key for label
const char* section_tr_key; // i18n key for section_label
};
inline const NavItem kNavItems[] = {
{ "Overview", NavPage::Overview, nullptr },
{ "Send", NavPage::Send, nullptr },
{ "Receive", NavPage::Receive, nullptr },
{ "History", NavPage::History, nullptr },
{ "Mining", NavPage::Mining, "TOOLS" },
{ "Market", NavPage::Market, nullptr },
{ "Console", NavPage::Console, "ADVANCED" },
{ "Network", NavPage::Peers, nullptr },
{ "Settings", NavPage::Settings, nullptr },
{ "Overview", NavPage::Overview, nullptr, "overview", nullptr },
{ "Send", NavPage::Send, nullptr, "send", nullptr },
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
{ "History", NavPage::History, nullptr, "history", nullptr },
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
{ "Market", NavPage::Market, nullptr, "market", nullptr },
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
};
static_assert(sizeof(kNavItems) / sizeof(kNavItems[0]) == (int)NavPage::Count_,
"kNavItems must match NavPage::Count_");
// Get translated nav label at runtime
inline const char* NavLabel(const NavItem& item) {
return item.tr_key ? TR(item.tr_key) : item.label;
}
inline const char* NavSectionLabel(const NavItem& item) {
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
}
// Get the Material Design icon string for a navigation page.
inline const char* GetNavIconMD(NavPage page)
{
@@ -541,7 +552,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float olFsz = ScaledFontSize(olFont);
dl->AddText(olFont, olFsz,
ImVec2(wp.x + sbSectionLabelPadLeft, labelY),
ImGui::ColorConvertFloat4ToU32(olCol), item.section_label);
ImGui::ColorConvertFloat4ToU32(olCol), NavSectionLabel(item));
ImGui::Dummy(ImVec2(0, olFsz + 2.0f));
} else if (item.section_label && !showLabels) {
// Collapsed: thin separator instead of label
@@ -609,11 +620,19 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
ImU32 textCol = selected ? Primary() : OnSurfaceMedium();
if (showLabels) {
// Measure total width of icon + gap + label, then center
// Measure total width of icon + gap + label, then center.
// If the translated label is too wide, shrink the font to fit.
ImFont* font = selected ? Type().subtitle2() : Type().body2();
float gap = iconLabelGap;
float lblFsz = ScaledFontSize(font);
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, item.label);
float btnW = indMax.x - indMin.x;
float maxLabelW = btnW - iconS * 2.0f - gap - Layout::spacingXs() * 2;
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
if (labelSz.x > maxLabelW && maxLabelW > 0) {
float shrink = maxLabelW / labelSz.x;
lblFsz *= shrink;
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
}
float totalW = iconS * 2.0f + gap + labelSz.x;
float btnCX = (indMin.x + indMax.x) * 0.5f;
float startX = btnCX - totalW * 0.5f;
@@ -625,7 +644,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
ImVec4 lc = ImGui::ColorConvertU32ToFloat4(textCol);
lc.w *= expandFrac;
dl->AddText(font, lblFsz, ImVec2(labelX, textY),
ImGui::ColorConvertFloat4ToU32(lc), item.label);
ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item));
} else {
float iconCX = (indMin.x + indMax.x) * 0.5f;
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
@@ -633,7 +652,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
// Tooltip when collapsed + hovered
if (!showLabels && hovered) {
ImGui::SetTooltip("%s", item.label);
ImGui::SetTooltip("%s", NavLabel(item));
}
// ---- Badge indicator ----

View File

@@ -5,6 +5,7 @@
#include "about_dialog.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../util/i18n.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
@@ -23,7 +24,7 @@ void RenderAboutDialog(App* app, bool* p_open)
auto versionLbl = S.label("dialogs.about", "version-label");
auto editionLbl = S.label("dialogs.about", "edition-label");
if (!material::BeginOverlayDialog("About ObsidianDragon", p_open, win.width, 0.94f)) {
if (!material::BeginOverlayDialog(TR("about_title"), p_open, win.width, 0.94f)) {
return;
}
@@ -38,33 +39,33 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::PopStyleColor();
ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position);
ImGui::TextDisabled("ImGui Edition");
ImGui::TextDisabled("%s", TR("about_edition"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Version info
ImGui::Text("Version:");
ImGui::Text("%s", TR("about_version"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("%s", DRAGONX_VERSION);
ImGui::Text("ImGui:");
ImGui::Text("%s", TR("about_imgui"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("%s", IMGUI_VERSION);
ImGui::Text("Build Date:");
ImGui::Text("%s", TR("about_build_date"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("%s %s", __DATE__, __TIME__);
#ifdef DRAGONX_DEBUG
ImGui::Text("Build Type:");
ImGui::Text("%s", TR("about_build_type"));
ImGui::SameLine(versionLbl.position);
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Debug");
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug"));
#else
ImGui::Text("Build Type:");
ImGui::Text("%s", TR("about_build_type"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("Release");
ImGui::Text("%s", TR("about_release"));
#endif
// Daemon info
@@ -73,22 +74,22 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Daemon:");
ImGui::Text("%s", TR("about_daemon"));
ImGui::SameLine(versionLbl.position);
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Connected");
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected"));
const auto& state = app->getWalletState();
ImGui::Text("Chain:");
ImGui::Text("%s", TR("about_chain"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("ObsidianDragon");
ImGui::Text("Block Height:");
ImGui::Text("%s", TR("about_block_height"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("%d", state.sync.blocks);
ImGui::Text("Connections:");
ImGui::Text("%s", TR("about_connections"));
ImGui::SameLine(versionLbl.position);
ImGui::Text("%zu peers", state.peers.size());
ImGui::Text(TR("about_peers_count"), state.peers.size());
}
ImGui::Spacing();
@@ -96,7 +97,7 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::Spacing();
// Credits
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Credits");
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", TR("about_credits"));
ImGui::Spacing();
ImGui::BulletText("The Hush Developers");
@@ -110,19 +111,16 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::Spacing();
// License
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "License");
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", TR("about_license"));
ImGui::Spacing();
ImGui::TextWrapped(
"This software is released under the GNU General Public License v3 (GPLv3). "
"You are free to use, modify, and distribute this software under the terms of the license."
);
ImGui::TextWrapped("%s", TR("about_license_text"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Links
if (material::StyledButton("Website", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32
system("start https://dragonx.is");
#elif __APPLE__
@@ -132,7 +130,7 @@ void RenderAboutDialog(App* app, bool* p_open)
#endif
}
ImGui::SameLine();
if (material::StyledButton("GitHub", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32
system("start https://git.dragonx.is/dragonx/ObsidianDragon");
#elif __APPLE__
@@ -142,7 +140,7 @@ void RenderAboutDialog(App* app, bool* p_open)
#endif
}
ImGui::SameLine();
if (material::StyledButton("Block Explorer", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32
system("start https://explorer.dragonx.is");
#elif __APPLE__
@@ -157,7 +155,7 @@ void RenderAboutDialog(App* app, bool* p_open)
// Close button
float button_width = closeBtn.width;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
*p_open = false;
}

View File

@@ -5,6 +5,7 @@
#include "address_book_dialog.h"
#include "../../app.h"
#include "../../data/address_book.h"
#include "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
@@ -65,11 +66,11 @@ void AddressBookDialog::render(App* app)
auto notesInput = S.input("dialogs.address-book", "notes-input");
auto actionBtn = S.button("dialogs.address-book", "action-button");
if (material::BeginOverlayDialog("Address Book", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("address_book_title"), &s_open, win.width, 0.94f)) {
auto& book = getAddressBook();
// Toolbar
if (material::StyledButton("Add New", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
s_show_add_dialog = true;
s_edit_label[0] = '\0';
s_edit_address[0] = '\0';
@@ -82,7 +83,7 @@ void AddressBookDialog::render(App* app)
if (!has_selection) ImGui::BeginDisabled();
if (material::StyledButton("Edit", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) {
const auto& entry = book.entries()[s_selected_index];
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1);
@@ -94,20 +95,20 @@ void AddressBookDialog::render(App* app)
ImGui::SameLine();
if (material::StyledButton("Delete", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) {
book.removeEntry(s_selected_index);
s_selected_index = -1;
Notifications::instance().success("Entry deleted");
Notifications::instance().success(TR("address_book_deleted"));
}
}
ImGui::SameLine();
if (material::StyledButton("Copy Address", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("copy_address"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) {
ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str());
Notifications::instance().info("Address copied to clipboard");
Notifications::instance().info(TR("address_copied"));
}
}
@@ -125,16 +126,16 @@ void AddressBookDialog::render(App* app)
{
float labelColW = (addrTable.columns.count("label") && addrTable.columns.at("label").width > 0) ? addrTable.columns.at("label").width : 150;
float notesColW = (addrTable.columns.count("notes") && addrTable.columns.at("notes").width > 0) ? addrTable.columns.at("notes").width : 150;
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, labelColW);
ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Notes", ImGuiTableColumnFlags_WidthFixed, notesColW);
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthFixed, labelColW);
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthFixed, notesColW);
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
if (book.empty()) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextDisabled("No saved addresses. Click 'Add New' to add one.");
ImGui::TextDisabled("%s", TR("address_book_empty"));
} else {
for (size_t i = 0; i < book.size(); i++) {
const auto& entry = book.entries()[i];
@@ -182,7 +183,7 @@ void AddressBookDialog::render(App* app)
}
// Status line
ImGui::TextDisabled("%zu addresses saved", book.size());
ImGui::TextDisabled(TR("address_book_count"), book.size());
material::EndOverlayDialog();
}
@@ -196,20 +197,20 @@ void AddressBookDialog::render(App* app)
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Add Address", &s_show_add_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
material::Type().text(material::TypeStyle::H6, "Add Address");
material::Type().text(material::TypeStyle::H6, TR("address_book_add"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Text("Label:");
ImGui::Text("%s", TR("label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##AddLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("Address:");
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##AddAddress", s_edit_address, sizeof(s_edit_address));
ImGui::SameLine();
if (material::StyledButton("Paste##Add", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_edit_address, clipboard, sizeof(s_edit_address) - 1);
@@ -218,7 +219,7 @@ void AddressBookDialog::render(App* app)
ImGui::Spacing();
ImGui::Text("Notes (optional):");
ImGui::Text("%s", TR("notes_optional"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputTextMultiline("##AddNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
@@ -229,20 +230,20 @@ void AddressBookDialog::render(App* app)
bool can_add = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
if (!can_add) ImGui::BeginDisabled();
if (material::StyledButton("Add", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("add"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (getAddressBook().addEntry(entry)) {
Notifications::instance().success("Address added to book");
Notifications::instance().success(TR("address_book_added"));
s_show_add_dialog = false;
} else {
Notifications::instance().error("Address already exists in book");
Notifications::instance().error(TR("address_book_exists"));
}
}
if (!can_add) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_show_add_dialog = false;
}
@@ -257,22 +258,22 @@ void AddressBookDialog::render(App* app)
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Edit Address", &s_show_edit_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
material::Type().text(material::TypeStyle::H6, "Edit Address");
material::Type().text(material::TypeStyle::H6, TR("address_book_edit"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Text("Label:");
ImGui::Text("%s", TR("label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##EditLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("Address:");
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##EditAddress", s_edit_address, sizeof(s_edit_address));
ImGui::Spacing();
ImGui::Text("Notes (optional):");
ImGui::Text("%s", TR("notes_optional"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputTextMultiline("##EditNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
@@ -283,20 +284,20 @@ void AddressBookDialog::render(App* app)
bool can_save = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
if (!can_save) ImGui::BeginDisabled();
if (material::StyledButton("Save", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("save"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (getAddressBook().updateEntry(s_selected_index, entry)) {
Notifications::instance().success("Address updated");
Notifications::instance().success(TR("address_book_updated"));
s_show_edit_dialog = false;
} else {
Notifications::instance().error("Failed to update - address may be duplicate");
Notifications::instance().error(TR("address_book_update_failed"));
}
}
if (!can_save) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_show_edit_dialog = false;
}

View File

@@ -61,11 +61,8 @@ void BackupWalletDialog::render(App* app)
auto backupBtn = S.button("dialogs.backup-wallet", "backup-button");
auto closeBtn = S.button("dialogs.backup-wallet", "close-button");
if (material::BeginOverlayDialog("Backup Wallet", &s_open, win.width, 0.94f)) {
ImGui::TextWrapped(
"Create a backup of your wallet.dat file. This file contains all your "
"private keys and transaction history. Store the backup in a secure location."
);
if (material::BeginOverlayDialog(TR("backup_title"), &s_open, win.width, 0.94f)) {
ImGui::TextWrapped("%s", TR("backup_description"));
ImGui::Spacing();
ImGui::Separator();
@@ -76,7 +73,7 @@ void BackupWalletDialog::render(App* app)
}
// Destination path
ImGui::Text("Backup destination:");
ImGui::Text("%s", TR("backup_destination"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Destination", s_destination, sizeof(s_destination));
@@ -84,13 +81,13 @@ void BackupWalletDialog::render(App* app)
// Show wallet.dat location
std::string walletPath = util::Platform::getDataDir() + "/wallet.dat";
ImGui::TextDisabled("Source: %s", walletPath.c_str());
ImGui::TextDisabled(TR("backup_source"), walletPath.c_str());
// Check if source exists
bool sourceExists = fs::exists(walletPath);
if (!sourceExists) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::Text("Warning: wallet.dat not found at expected location");
ImGui::Text("%s", TR("backup_wallet_not_found"));
ImGui::PopStyleColor();
}
@@ -107,7 +104,7 @@ void BackupWalletDialog::render(App* app)
ImGui::BeginDisabled();
}
if (material::StyledButton("Create Backup", ImVec2(backupBtn.width, 0), S.resolveFont(backupBtn.font))) {
if (material::StyledButton(TR("backup_create"), ImVec2(backupBtn.width, 0), S.resolveFont(backupBtn.font))) {
if (strlen(s_destination) == 0) {
Notifications::instance().warning("Please enter a destination path");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
@@ -147,7 +144,7 @@ void BackupWalletDialog::render(App* app)
s_status = statusMsg;
s_backing_up = false;
if (success) {
Notifications::instance().success("Wallet backup created");
Notifications::instance().success(TR("backup_created"));
} else {
Notifications::instance().warning(statusMsg);
}
@@ -160,11 +157,11 @@ void BackupWalletDialog::render(App* app)
if (s_backing_up) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Backing up...");
ImGui::TextDisabled("%s", TR("backup_backing_up"));
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
@@ -179,10 +176,10 @@ void BackupWalletDialog::render(App* app)
ImGui::Spacing();
// Tips
ImGui::TextDisabled("Tips:");
ImGui::BulletText("Store backups on external drives or cloud storage");
ImGui::BulletText("Create multiple backups in different locations");
ImGui::BulletText("Test restoring from backup periodically");
ImGui::TextDisabled("%s", TR("backup_tips"));
ImGui::BulletText("%s", TR("backup_tip_external"));
ImGui::BulletText("%s", TR("backup_tip_multiple"));
ImGui::BulletText("%s", TR("backup_tip_test"));
material::EndOverlayDialog();
}
}

View File

@@ -30,6 +30,14 @@
namespace dragonx {
namespace ui {
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
static std::string TrId(const char* tr_key, const char* id) {
std::string s = TR(tr_key);
s += "##";
s += id;
return s;
}
// Case-insensitive substring search
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
if (search.empty()) return true;
@@ -774,13 +782,13 @@ static void RenderBalanceClassic(App* app)
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox("Hide 0 Balances", &s_hideZeroBalances);
ImGui::Checkbox(TrId("hide_zero_balances", "hide0").c_str(), &s_hideZeroBalances);
{
int hc = app->getHiddenAddressCount();
if (hc > 0) {
ImGui::SameLine(0, Layout::spacingLg());
char hlbl[64];
snprintf(hlbl, sizeof(hlbl), "Show Hidden (%d)", hc);
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
ImGui::Checkbox(hlbl, &s_showHidden);
} else {
s_showHidden = false;
@@ -883,7 +891,8 @@ static void RenderBalanceClassic(App* app)
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
float rowPadLeft = Layout::spacingLg();
float rowPadLeft = Layout::cardInnerPadding();
float rowPadRight = Layout::cardInnerPadding();
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, S.drawElement("tabs.balance", "address-icon-size").size * hs);
float innerW = ImGui::GetContentRegionAvail().x;
@@ -931,10 +940,10 @@ static void RenderBalanceClassic(App* app)
// --- Button zone (right edge): [eye] [star] ---
float btnH = rowH - Layout::spacingSm() * 2.0f;
float btnW = btnH;
float btnGap = Layout::spacingXs();
float btnGap = Layout::spacingSm();
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
float rightEdge = rowPos.x + innerW;
float starX = rightEdge - btnW - Layout::spacingSm();
float starX = rightEdge - btnW - rowPadRight;
float eyeX = starX - btnGap - btnW;
float btnRound = 6.0f * dp;
bool btnClicked = false;
@@ -958,7 +967,7 @@ static void RenderBalanceClassic(App* app)
else app->favoriteAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.favorite ? "Remove favorite" : "Favorite address");
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
}
// Eye button (zero balance or hidden)
@@ -980,7 +989,7 @@ static void RenderBalanceClassic(App* app)
else app->hideAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.hidden ? "Restore address" : "Hide address");
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
}
// Content zone ends before buttons
@@ -1103,17 +1112,17 @@ static void RenderBalanceClassic(App* app)
}
ImGui::Separator();
if (row.hidden) {
if (ImGui::MenuItem("Restore Address"))
if (ImGui::MenuItem(TR("restore_address")))
app->unhideAddress(addr.address);
} else if (addr.balance < 1e-9) {
if (ImGui::MenuItem("Hide Address"))
if (ImGui::MenuItem(TR("hide_address")))
app->hideAddress(addr.address);
}
if (row.favorite) {
if (ImGui::MenuItem("Remove Favorite"))
if (ImGui::MenuItem(TR("remove_favorite")))
app->unfavoriteAddress(addr.address);
} else {
if (ImGui::MenuItem("Favorite Address"))
if (ImGui::MenuItem(TR("favorite_address")))
app->favoriteAddress(addr.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
@@ -1387,13 +1396,13 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::SetNextItemWidth(searchW);
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox("Hide 0 Balances", &s_hideZeroBalances);
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
{
int hc = app->getHiddenAddressCount();
if (hc > 0) {
ImGui::SameLine(0, Layout::spacingLg());
char hlbl[64];
snprintf(hlbl, sizeof(hlbl), "Show Hidden (%d)", hc);
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
ImGui::Checkbox(hlbl, &s_showHidden);
} else {
s_showHidden = false;
@@ -1461,10 +1470,10 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
if (addr_search[0]) {
ImVec2 textSz = ImGui::CalcTextSize("No matching addresses");
ImVec2 textSz = ImGui::CalcTextSize(TR("no_addresses_match"));
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("No matching addresses");
ImGui::TextDisabled("%s", TR("no_addresses_match"));
} else {
const char* msg = "No addresses yet";
ImVec2 msgSz = ImGui::CalcTextSize(msg);
@@ -1482,7 +1491,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
addrScrollMaxY = ImGui::GetScrollMaxY();
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
float rowPadLeft = Layout::spacingLg();
float rowPadLeft = Layout::cardInnerPadding();
float rowPadRight = Layout::cardInnerPadding();
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
S.drawElement("tabs.balance", "address-icon-size").size * hs);
float innerW = ImGui::GetContentRegionAvail().x;
@@ -1522,10 +1532,10 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
// --- Button zone (right edge): [eye] [star] ---
float btnH = rowH - Layout::spacingSm() * 2.0f;
float btnW = btnH;
float btnGap = Layout::spacingXs();
float btnGap = Layout::spacingSm();
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
float rightEdge = rowPos.x + innerW;
float starX = rightEdge - btnW - Layout::spacingSm();
float starX = rightEdge - btnW - rowPadRight;
float eyeX = starX - btnGap - btnW;
float btnRound = 6.0f * dp;
bool btnClicked = false;
@@ -1549,7 +1559,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
else app->favoriteAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.favorite ? "Remove favorite" : "Favorite address");
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
}
// Eye button (zero balance or hidden)
@@ -1571,7 +1581,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
else app->hideAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.hidden ? "Restore address" : "Hide address");
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
}
// Content zone ends before buttons
@@ -1672,17 +1682,17 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
QRPopupDialog::show(addr.address, row.isZ ? "Z-Address" : "T-Address");
ImGui::Separator();
if (row.hidden) {
if (ImGui::MenuItem("Restore Address"))
if (ImGui::MenuItem(TR("restore_address")))
app->unhideAddress(addr.address);
} else if (addr.balance < 1e-9) {
if (ImGui::MenuItem("Hide Address"))
if (ImGui::MenuItem(TR("hide_address")))
app->hideAddress(addr.address);
}
if (row.favorite) {
if (ImGui::MenuItem("Remove Favorite"))
if (ImGui::MenuItem(TR("remove_favorite")))
app->unfavoriteAddress(addr.address);
} else {
if (ImGui::MenuItem("Favorite Address"))
if (ImGui::MenuItem(TR("favorite_address")))
app->favoriteAddress(addr.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();

View File

@@ -99,12 +99,12 @@ void BlockInfoDialog::render(App* app)
auto hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
auto closeBtn = S.button("dialogs.block-info", "close-button");
if (material::BeginOverlayDialog("Block Information", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("block_info_title"), &s_open, win.width, 0.94f)) {
auto* rpc = app->rpc();
const auto& state = app->getWalletState();
// Height input
ImGui::Text("Block Height:");
ImGui::Text("%s", TR("block_height"));
ImGui::SetNextItemWidth(heightInput.width);
ImGui::InputInt("##Height", &s_height);
if (s_height < 1) s_height = 1;
@@ -123,7 +123,7 @@ void BlockInfoDialog::render(App* app)
ImGui::BeginDisabled();
}
if (material::StyledButton("Get Block Info", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (rpc && rpc->isConnected()) {
s_loading = true;
s_error.clear();
@@ -138,7 +138,7 @@ void BlockInfoDialog::render(App* app)
if (s_loading) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Loading...");
ImGui::TextDisabled("%s", TR("loading"));
}
ImGui::Spacing();
@@ -155,23 +155,23 @@ void BlockInfoDialog::render(App* app)
// Block info display
if (s_has_data) {
// Block hash
ImGui::Text("Block Hash:");
ImGui::Text("%s", TR("block_hash"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
ImGui::TextWrapped("%s", s_block_hash.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Click to copy");
ImGui::SetTooltip("%s", TR("click_to_copy"));
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(s_block_hash.c_str());
Notifications::instance().success("Block hash copied");
Notifications::instance().success(TR("block_hash_copied"));
}
ImGui::Spacing();
// Timestamp
ImGui::Text("Timestamp:");
ImGui::Text("%s", TR("block_timestamp"));
ImGui::SameLine(lbl.position);
if (s_block_time > 0) {
std::time_t t = static_cast<std::time_t>(s_block_time);
@@ -179,21 +179,21 @@ void BlockInfoDialog::render(App* app)
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
ImGui::Text("%s", time_buf);
} else {
ImGui::TextDisabled("Unknown");
ImGui::TextDisabled("%s", TR("unknown"));
}
// Confirmations
ImGui::Text("Confirmations:");
ImGui::Text("%s", TR("confirmations"));
ImGui::SameLine(lbl.position);
ImGui::Text("%d", s_confirmations);
// Transaction count
ImGui::Text("Transactions:");
ImGui::Text("%s", TR("block_transactions"));
ImGui::SameLine(lbl.position);
ImGui::Text("%d", s_tx_count);
// Size
ImGui::Text("Size:");
ImGui::Text("%s", TR("block_size"));
ImGui::SameLine(lbl.position);
if (s_block_size > 1024 * 1024) {
ImGui::Text("%.2f MB", s_block_size / (1024.0 * 1024.0));
@@ -204,12 +204,12 @@ void BlockInfoDialog::render(App* app)
}
// Difficulty
ImGui::Text("Difficulty:");
ImGui::Text("%s", TR("difficulty"));
ImGui::SameLine(lbl.position);
ImGui::Text("%.4f", s_difficulty);
// Bits
ImGui::Text("Bits:");
ImGui::Text("%s", TR("block_bits"));
ImGui::SameLine(lbl.position);
ImGui::Text("%s", s_bits.c_str());
@@ -218,7 +218,7 @@ void BlockInfoDialog::render(App* app)
ImGui::Spacing();
// Merkle root
ImGui::Text("Merkle Root:");
ImGui::Text("%s", TR("block_merkle_root"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
ImGui::TextWrapped("%s", s_merkle_root.c_str());
ImGui::PopStyleColor();
@@ -227,7 +227,7 @@ void BlockInfoDialog::render(App* app)
// Previous block
if (!s_prev_hash.empty()) {
ImGui::Text("Previous Block:");
ImGui::Text("%s", TR("block_previous"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
// Truncate for display
@@ -239,7 +239,7 @@ void BlockInfoDialog::render(App* app)
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Click to view previous block");
ImGui::SetTooltip("%s", TR("block_click_prev"));
}
if (ImGui::IsItemClicked() && s_height > 1) {
s_height--;
@@ -249,7 +249,7 @@ void BlockInfoDialog::render(App* app)
// Next block
if (!s_next_hash.empty()) {
ImGui::Text("Next Block:");
ImGui::Text("%s", TR("block_next"));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
// Truncate for display
@@ -261,7 +261,7 @@ void BlockInfoDialog::render(App* app)
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Click to view next block");
ImGui::SetTooltip("%s", TR("block_click_next"));
}
if (ImGui::IsItemClicked()) {
s_height++;
@@ -276,7 +276,7 @@ void BlockInfoDialog::render(App* app)
// Navigation buttons
if (s_has_data) {
if (s_height > 1) {
if (material::StyledButton("<< Previous", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height--;
s_has_data = false;
s_error.clear();
@@ -285,7 +285,7 @@ void BlockInfoDialog::render(App* app)
}
if (!s_next_hash.empty()) {
if (material::StyledButton("Next >>", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height++;
s_has_data = false;
s_error.clear();
@@ -295,7 +295,7 @@ void BlockInfoDialog::render(App* app)
// Close button at bottom
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40);
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();

View File

@@ -13,6 +13,7 @@
#include "../material/color_theme.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/i18n.h"
#include <imgui.h>
#include <cstring>
@@ -75,8 +76,8 @@ ConsoleTab::ConsoleTab()
refreshColors();
// Add welcome message
addLine("DragonX Wallet Console", COLOR_INFO);
addLine("Type 'help' for available commands", COLOR_INFO);
addLine(TR("console_welcome"), COLOR_INFO);
addLine(TR("console_type_help"), COLOR_INFO);
addLine("", COLOR_RESULT);
}
@@ -117,27 +118,38 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
if (current_state == daemon::EmbeddedDaemon::State::Starting &&
last_daemon_state_ == daemon::EmbeddedDaemon::State::Stopped) {
addLine("", COLOR_RESULT);
addLine("=== Starting DragonX Full Node ===", COLOR_INFO);
addLine("Capturing daemon output...", COLOR_INFO);
addLine(TR("console_starting_node"), COLOR_INFO);
addLine(TR("console_capturing_output"), COLOR_INFO);
addLine("", COLOR_RESULT);
shown_startup_message_ = true;
}
else if (current_state == daemon::EmbeddedDaemon::State::Running &&
last_daemon_state_ != daemon::EmbeddedDaemon::State::Running) {
addLine("=== Daemon is running ===", COLOR_INFO);
addLine(TR("console_daemon_started"), COLOR_INFO);
}
else if (current_state == daemon::EmbeddedDaemon::State::Stopped &&
last_daemon_state_ == daemon::EmbeddedDaemon::State::Running) {
addLine("", COLOR_RESULT);
addLine("=== Daemon stopped ===", COLOR_INFO);
addLine(TR("console_daemon_stopped"), COLOR_INFO);
}
else if (current_state == daemon::EmbeddedDaemon::State::Error) {
addLine("=== Daemon error: " + daemon->getLastError() + " ===", COLOR_ERROR);
addLine(std::string(TR("console_daemon_error")) + daemon->getLastError() + " ===", COLOR_ERROR);
}
last_daemon_state_ = current_state;
}
// Track RPC connection state and show a message when connected
if (rpc) {
bool connected_now = rpc->isConnected();
if (connected_now && !last_rpc_connected_) {
addLine(TR("console_connected"), COLOR_INFO);
} else if (!connected_now && last_rpc_connected_) {
addLine(TR("console_disconnected"), COLOR_ERROR);
}
last_rpc_connected_ = connected_now;
}
// Check for new daemon output — always capture so toggle works as a live filter
if (daemon) {
std::string new_output = daemon->getOutputSince(last_daemon_output_size_);
@@ -372,31 +384,31 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Daemon status with colored dot
if (daemon) {
auto state = daemon->getState();
const char* status_text = "Unknown";
const char* status_text = TR("console_status_unknown");
ImU32 dotCol = IM_COL32(150, 150, 150, 255);
bool pulse = false;
switch (state) {
case daemon::EmbeddedDaemon::State::Stopped:
status_text = "Stopped";
status_text = TR("console_status_stopped");
dotCol = IM_COL32(150, 150, 150, 255);
break;
case daemon::EmbeddedDaemon::State::Starting:
status_text = "Starting";
status_text = TR("console_status_starting");
dotCol = Warning();
pulse = true;
break;
case daemon::EmbeddedDaemon::State::Running:
status_text = "Running";
status_text = TR("console_status_running");
dotCol = Success();
break;
case daemon::EmbeddedDaemon::State::Stopping:
status_text = "Stopping";
status_text = TR("console_status_stopping");
dotCol = Warning();
pulse = true;
break;
case daemon::EmbeddedDaemon::State::Error:
status_text = "Error";
status_text = TR("console_status_error");
dotCol = Error();
break;
}
@@ -418,7 +430,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, dotCol, status_text);
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No daemon");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon"));
}
ImGui::SameLine();
@@ -426,7 +438,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
// Auto-scroll toggle
if (ImGui::Checkbox("Auto-scroll", &auto_scroll_)) {
if (ImGui::Checkbox(TR("console_auto_scroll"), &auto_scroll_)) {
if (auto_scroll_) {
scroll_to_bottom_ = true;
new_lines_since_scroll_ = 0;
@@ -440,7 +452,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Daemon messages toggle
{
static bool s_prev_daemon_enabled = true;
ImGui::Checkbox("Daemon", &s_daemon_messages_enabled);
ImGui::Checkbox(TR("console_daemon"), &s_daemon_messages_enabled);
// When toggling daemon filter while auto-scroll is active, scroll to bottom
if (s_prev_daemon_enabled != s_daemon_messages_enabled && auto_scroll_) {
scroll_to_bottom_ = true;
@@ -448,7 +460,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
s_prev_daemon_enabled = s_daemon_messages_enabled;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show daemon output messages");
ImGui::SetTooltip("%s", TR("console_show_daemon_output"));
}
ImGui::SameLine();
@@ -458,7 +470,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Errors-only toggle
{
static bool s_prev_errors_only = false;
ImGui::Checkbox("Errors", &s_errors_only_enabled);
ImGui::Checkbox(TR("console_errors"), &s_errors_only_enabled);
// When toggling errors filter while auto-scroll is active, scroll to bottom
if (s_prev_errors_only != s_errors_only_enabled && auto_scroll_) {
scroll_to_bottom_ = true;
@@ -466,7 +478,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
s_prev_errors_only = s_errors_only_enabled;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show only error messages");
ImGui::SetTooltip("%s", TR("console_show_errors_only"));
}
ImGui::SameLine();
@@ -474,7 +486,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
// Clear button
if (TactileButton("Clear", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
clear();
clearSelection();
}
@@ -482,7 +494,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
ImGui::SameLine();
// Copy button — material styled
if (TactileButton("Copy", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("copy"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
std::lock_guard<std::mutex> lock(lines_mutex_);
if (has_selection_) {
std::string selected = getSelectedText();
@@ -501,17 +513,17 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(has_selection_ ? "Copy selected text" : "Copy all output");
ImGui::SetTooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
}
ImGui::SameLine();
// Commands reference button
if (TactileButton("Commands", ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
show_commands_popup_ = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show RPC command reference");
ImGui::SetTooltip("%s", TR("console_show_rpc_ref"));
}
ImGui::SameLine();
@@ -519,7 +531,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
// Line count
{
std::lock_guard<std::mutex> lock(lines_mutex_);
ImGui::TextDisabled("(%zu lines)", lines_.size());
ImGui::TextDisabled(TR("console_line_count"), lines_.size());
}
ImGui::SameLine();
@@ -531,7 +543,7 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
float filterAvail = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
ImGui::SetNextItemWidth(filterW);
ImGui::InputTextWithHint("##ConsoleFilter", "Filter output...", filter_text_, sizeof(filter_text_));
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
// Zoom +/- buttons (right side of toolbar)
ImGui::SameLine();
@@ -548,14 +560,14 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Zoom out (%.0f%%)", s_console_zoom * 100.0f);
ImGui::SetTooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
}
ImGui::SameLine();
if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Zoom in (%.0f%%)", s_console_zoom * 100.0f);
ImGui::SetTooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
}
}
}
@@ -889,7 +901,7 @@ void ConsoleTab::renderOutput()
// Filter indicator (text filter only — daemon toggle is already visible in toolbar)
if (has_text_filter) {
char filterBuf[128];
snprintf(filterBuf, sizeof(filterBuf), "Showing %d of %zu lines",
snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
visible_count, lines_.size());
ImVec2 indicatorPos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddText(indicatorPos,
@@ -899,13 +911,13 @@ void ConsoleTab::renderOutput()
// Right-click context menu
if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) {
if (ImGui::MenuItem("Copy", "Ctrl+C", false, has_selection_)) {
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) {
std::string selected = getSelectedText();
if (!selected.empty()) {
ImGui::SetClipboardText(selected.c_str());
}
}
if (ImGui::MenuItem("Select All", "Ctrl+A")) {
if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
if (!lines_.empty()) {
sel_anchor_ = {0, 0};
sel_end_ = {static_cast<int>(lines_.size()) - 1,
@@ -914,7 +926,7 @@ void ConsoleTab::renderOutput()
}
}
ImGui::Separator();
if (ImGui::MenuItem("Clear Console")) {
if (ImGui::MenuItem(TR("console_clear_console"))) {
// Can't call clear() here (already holding lock), just mark for clearing
lines_.clear();
has_selection_ = false;
@@ -938,7 +950,7 @@ void ConsoleTab::renderOutput()
dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);
char buf[48];
snprintf(buf, sizeof(buf), " %d new line%s",
snprintf(buf, sizeof(buf), TR("console_new_lines"),
new_lines_since_scroll_, new_lines_since_scroll_ != 1 ? "s" : "");
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
@@ -1229,7 +1241,7 @@ void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker)
data->InsertChars(0, matches[0]);
} else if (matches.size() > 1) {
// Multiple matches — show list in console and complete common prefix
console->addLine("Completions:", ConsoleTab::COLOR_INFO);
console->addLine(TR("console_completions"), ConsoleTab::COLOR_INFO);
std::string line = " ";
for (size_t m = 0; m < matches.size(); m++) {
if (m > 0) line += " ";
@@ -1284,17 +1296,17 @@ void ConsoleTab::renderCommandsPopup()
using namespace material;
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
if (!material::BeginOverlayDialog("RPC Command Reference", &show_commands_popup_, popW, 0.94f)) {
if (!material::BeginOverlayDialog(TR("console_rpc_reference"), &show_commands_popup_, popW, 0.94f)) {
return;
}
Type().text(TypeStyle::H6, "RPC Command Reference");
Type().text(TypeStyle::H6, TR("console_rpc_reference"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Search filter
static char cmdFilter[128] = {0};
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##CmdSearch", "Search commands...", cmdFilter, sizeof(cmdFilter));
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Command entries
@@ -1507,9 +1519,9 @@ void ConsoleTab::renderCommandsPopup()
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (cmd.params[0] != '\0')
ImGui::SetTooltip("Click to insert '%s %s' into input", cmd.name, cmd.params);
ImGui::SetTooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
else
ImGui::SetTooltip("Click to insert '%s' into input", cmd.name);
ImGui::SetTooltip(TR("console_click_insert"), cmd.name);
}
ImGui::PopStyleColor(3);
if (showParams) {
@@ -1575,7 +1587,7 @@ void ConsoleTab::renderCommandsPopup()
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Close button
if (ImGui::Button("Close", ImVec2(-1, 0))) {
if (ImGui::Button(TR("console_close"), ImVec2(-1, 0))) {
cmdFilter[0] = '\0';
show_commands_popup_ = false;
}
@@ -1605,28 +1617,28 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
}
if (cmd == "help") {
addLine("Available commands:", COLOR_INFO);
addLine(" clear - Clear console output", COLOR_RESULT);
addLine(" help - Show this help", COLOR_RESULT);
addLine(TR("console_available_commands"), COLOR_INFO);
addLine(TR("console_help_clear"), COLOR_RESULT);
addLine(TR("console_help_help"), COLOR_RESULT);
addLine("", COLOR_RESULT);
addLine("Common RPC commands (when connected):", COLOR_INFO);
addLine(" getinfo - Get daemon info", COLOR_RESULT);
addLine(" getbalance - Get transparent balance", COLOR_RESULT);
addLine(" z_gettotalbalance - Get total balance", COLOR_RESULT);
addLine(" getblockcount - Get current block height", COLOR_RESULT);
addLine(" getpeerinfo - Get connected peers", COLOR_RESULT);
addLine(" setgenerate true/false [threads] - Mining on/off", COLOR_RESULT);
addLine(" getmininginfo - Get mining status", COLOR_RESULT);
addLine(" stop - Stop the daemon", COLOR_RESULT);
addLine(TR("console_common_rpc"), COLOR_INFO);
addLine(TR("console_help_getinfo"), COLOR_RESULT);
addLine(TR("console_help_getbalance"), COLOR_RESULT);
addLine(TR("console_help_gettotalbalance"), COLOR_RESULT);
addLine(TR("console_help_getblockcount"), COLOR_RESULT);
addLine(TR("console_help_getpeerinfo"), COLOR_RESULT);
addLine(TR("console_help_setgenerate"), COLOR_RESULT);
addLine(TR("console_help_getmininginfo"), COLOR_RESULT);
addLine(TR("console_help_stop"), COLOR_RESULT);
addLine("", COLOR_RESULT);
addLine("Click 'Commands' in the toolbar for full RPC reference", COLOR_INFO);
addLine("Use Tab for command completion, Up/Down for history", COLOR_INFO);
addLine(TR("console_click_commands"), COLOR_INFO);
addLine(TR("console_tab_completion"), COLOR_INFO);
return;
}
// Execute RPC command
if (!rpc || !rpc->isConnected()) {
addLine("Error: Not connected to daemon", COLOR_ERROR);
addLine(TR("console_not_connected"), COLOR_ERROR);
return;
}
@@ -1839,7 +1851,7 @@ void ConsoleTab::clear()
last_xmrig_output_size_ = 0;
}
// addLine() takes the lock itself, so call it outside the locked scope
addLine("Console cleared", COLOR_INFO);
addLine(TR("console_cleared"), COLOR_INFO);
}
} // namespace ui

View File

@@ -119,6 +119,7 @@ private:
size_t last_xmrig_output_size_ = 0;
bool shown_startup_message_ = false;
daemon::EmbeddedDaemon::State last_daemon_state_ = daemon::EmbeddedDaemon::State::Stopped;
bool last_rpc_connected_ = false;
// Text selection state
bool is_selecting_ = false;

View File

@@ -69,16 +69,14 @@ void ExportAllKeysDialog::render(App* app)
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
if (material::BeginOverlayDialog("Export All Private Keys", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("export_keys_title"), &s_open, win.width, 0.94f)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("DANGER: This will export ALL private keys from your wallet! "
"Anyone with access to this file can steal your funds. "
"Store it securely and delete after use.");
ImGui::TextWrapped("%s", TR("export_keys_danger"));
ImGui::PopStyleColor();
ImGui::Spacing();
@@ -90,19 +88,19 @@ void ExportAllKeysDialog::render(App* app)
}
// Options
ImGui::Text("Export options:");
ImGui::Checkbox("Include Z-addresses (shielded)", &s_include_z);
ImGui::Checkbox("Include T-addresses (transparent)", &s_include_t);
ImGui::Text("%s", TR("export_keys_options"));
ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z);
ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t);
ImGui::Spacing();
// Filename
ImGui::Text("Output filename:");
ImGui::Text("%s", TR("output_filename"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("File will be saved in: ~/.config/ObsidianDragon/");
ImGui::TextDisabled("%s", TR("file_save_location"));
if (s_exporting) {
ImGui::EndDisabled();
@@ -117,7 +115,7 @@ void ExportAllKeysDialog::render(App* app)
ImGui::BeginDisabled();
}
if (material::StyledButton("Export Keys", ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (material::StyledButton(TR("export_keys_btn"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (!s_include_z && !s_include_t) {
Notifications::instance().warning("Select at least one address type");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
@@ -215,7 +213,7 @@ void ExportAllKeysDialog::render(App* app)
s_exporting = false;
if (writeOk) {
s_status = "Exported to: " + filepath;
Notifications::instance().success("Keys exported successfully");
Notifications::instance().success(TR("export_keys_success"));
} else {
s_status = "Failed to write file";
Notifications::instance().error("Failed to save key file");
@@ -229,11 +227,11 @@ void ExportAllKeysDialog::render(App* app)
if (s_exporting) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Exporting %d/%d...", s_exported_count, s_total_addresses);
ImGui::TextDisabled(TR("export_keys_progress"), s_exported_count, s_total_addresses);
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}

View File

@@ -72,31 +72,31 @@ void ExportTransactionsDialog::render(App* app)
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
if (material::BeginOverlayDialog("Export Transactions to CSV", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
ImGui::Text("Export %zu transactions to CSV file.", state.transactions.size());
ImGui::Text(TR("export_tx_count"), state.transactions.size());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Filename
ImGui::Text("Output filename:");
ImGui::Text("%s", TR("output_filename"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("File will be saved in: ~/.config/ObsidianDragon/");
ImGui::TextDisabled("%s", TR("file_save_location"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Export button
if (material::StyledButton("Export", ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (state.transactions.empty()) {
Notifications::instance().warning("No transactions to export");
Notifications::instance().warning(TR("export_tx_none"));
} else {
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
@@ -104,7 +104,7 @@ void ExportTransactionsDialog::render(App* app)
std::ofstream file(filepath);
if (!file.is_open()) {
s_status = "Failed to create file";
Notifications::instance().error("Failed to create CSV file");
Notifications::instance().error(TR("export_tx_file_fail"));
} else {
// Write CSV header
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
@@ -142,7 +142,7 @@ void ExportTransactionsDialog::render(App* app)
s_status = "Exported " + std::to_string(state.transactions.size()) +
" transactions to: " + filepath;
Notifications::instance().success("Transactions exported successfully");
Notifications::instance().success(TR("export_tx_success"));
}
}
}

View File

@@ -113,15 +113,14 @@ void ImportKeyDialog::render(App* app)
auto importBtn = S.button("dialogs.import-key", "import-button");
auto closeBtn = S.button("dialogs.import-key", "close-button");
if (material::BeginOverlayDialog("Import Private Key", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("import_key_title"), &s_open, win.width, 0.94f)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("Warning: Never share your private keys! "
"Importing keys from untrusted sources can compromise your wallet.");
ImGui::TextWrapped("%s", TR("import_key_warning"));
ImGui::PopStyleColor();
ImGui::Spacing();
@@ -129,13 +128,11 @@ void ImportKeyDialog::render(App* app)
ImGui::Spacing();
// Key input
ImGui::Text("Private Key(s):");
ImGui::Text("%s", TR("import_key_label"));
ImGui::SameLine();
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Enter one or more private keys, one per line.\n"
"Supports both z-address and t-address keys.\n"
"Lines starting with # are treated as comments.");
ImGui::SetTooltip("%s", TR("import_key_tooltip"));
}
if (s_importing) {
@@ -147,7 +144,7 @@ void ImportKeyDialog::render(App* app)
ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput);
// Paste button
if (material::StyledButton("Paste from Clipboard", ImVec2(0,0), S.resolveFont(importBtn.font))) {
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1);
@@ -155,24 +152,24 @@ void ImportKeyDialog::render(App* app)
}
ImGui::SameLine();
if (material::StyledButton("Clear", ImVec2(0,0), S.resolveFont(importBtn.font))) {
if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
s_key_input[0] = '\0';
}
ImGui::Spacing();
// Rescan options
ImGui::Checkbox("Rescan blockchain after import", &s_rescan);
ImGui::Checkbox(TR("import_key_rescan"), &s_rescan);
if (s_rescan) {
ImGui::Indent();
ImGui::Text("Start height:");
ImGui::Text("%s", TR("import_key_start_height"));
ImGui::SameLine();
ImGui::SetNextItemWidth(rescanInput.width);
ImGui::InputInt("##RescanHeight", &s_rescan_height);
if (s_rescan_height < 0) s_rescan_height = 0;
ImGui::SameLine();
ImGui::TextDisabled("(0 = full rescan)");
ImGui::TextDisabled("%s", TR("import_key_full_rescan"));
ImGui::Unindent();
}
@@ -189,13 +186,13 @@ void ImportKeyDialog::render(App* app)
ImGui::BeginDisabled();
}
if (material::StyledButton("Import Key(s)", ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
if (material::StyledButton(TR("import_key_btn"), ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
auto keys = splitKeys(s_key_input);
if (keys.empty()) {
Notifications::instance().warning("No valid keys found in input");
Notifications::instance().warning(TR("import_key_no_valid"));
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon");
Notifications::instance().error(TR("not_connected"));
} else {
s_importing = true;
s_total_keys = static_cast<int>(keys.size());
@@ -237,7 +234,7 @@ void ImportKeyDialog::render(App* app)
imported, failed);
s_status = buf;
if (imported > 0) {
Notifications::instance().success("Keys imported successfully");
Notifications::instance().success(TR("import_key_success"));
}
};
});
@@ -248,11 +245,11 @@ void ImportKeyDialog::render(App* app)
if (s_importing) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Importing %d/%d...", s_imported_keys + s_failed_keys, s_total_keys);
ImGui::TextDisabled(TR("import_key_progress"), s_imported_keys + s_failed_keys, s_total_keys);
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
@@ -271,9 +268,9 @@ void ImportKeyDialog::render(App* app)
ImGui::Spacing();
// Help text
ImGui::TextDisabled("Supported key formats:");
ImGui::BulletText("Z-address spending keys (secret-extended-key-...)");
ImGui::BulletText("T-address WIF private keys");
ImGui::TextDisabled("%s", TR("import_key_formats"));
ImGui::BulletText("%s", TR("import_key_z_format"));
ImGui::BulletText("%s", TR("import_key_t_format"));
material::EndOverlayDialog();
}
}

View File

@@ -63,15 +63,13 @@ void KeyExportDialog::render(App* app)
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
ImGui::BeginChild("WarningBox", ImVec2(-1, warningBox.height > 0 ? warningBox.height : 80), true);
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " WARNING!");
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " %s", TR("warning_upper"));
ImGui::Spacing();
if (s_key_type == KeyType::Private) {
ImGui::TextWrapped(" Keep this key SECRET! Anyone with this key can spend your "
"funds. Never share it online or with untrusted parties.");
ImGui::TextWrapped(" %s", TR("key_export_private_warning"));
} else {
ImGui::TextWrapped(" This viewing key allows others to see your incoming transactions "
"and balance, but NOT spend your funds. Share only with trusted parties.");
ImGui::TextWrapped(" %s", TR("key_export_viewing_warning"));
}
ImGui::EndChild();
@@ -82,7 +80,7 @@ void KeyExportDialog::render(App* app)
ImGui::Spacing();
// Address display
ImGui::Text("Address:");
ImGui::Text("%s", TR("address_label"));
// Determine if it's a z-address (longer) or t-address
bool is_zaddr = s_address.length() > 50;
@@ -105,16 +103,16 @@ void KeyExportDialog::render(App* app)
ImGui::Spacing();
// Key display section
const char* key_label = (s_key_type == KeyType::Private) ? "Private Key:" : "Viewing Key:";
const char* key_label = (s_key_type == KeyType::Private) ? TR("key_export_private_key") : TR("key_export_viewing_key");
ImGui::Text("%s", key_label);
if (s_fetching) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Fetching key from wallet...");
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
} else if (!s_error.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Error: %s", s_error.c_str());
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
} else if (s_key.empty()) {
// Show button to fetch key
if (material::StyledButton("Reveal Key", ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
if (material::StyledButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
s_fetching = true;
// Check if z-address or t-address
@@ -171,13 +169,13 @@ void KeyExportDialog::render(App* app)
});
}
} else {
s_error = "Viewing keys are only available for shielded (z) addresses";
s_error = TR("key_export_viewing_keys_zonly");
s_fetching = false;
}
}
}
ImGui::SameLine();
ImGui::TextDisabled("Click to retrieve the key from your wallet");
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
} else {
// Key has been fetched - display it
@@ -201,7 +199,7 @@ void KeyExportDialog::render(App* app)
// Show/Hide and Copy buttons
ImGui::Spacing();
if (material::StyledButton(s_show_key ? "Hide" : "Show", ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
s_show_key = !s_show_key;
}
@@ -222,7 +220,7 @@ void KeyExportDialog::render(App* app)
float avail_width = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f);
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
// Clear sensitive data
s_key.clear();

View File

@@ -8,6 +8,7 @@
#include "../../data/wallet_state.h"
#include "../../config/settings.h"
#include "../../data/exchange_info.h"
#include "../../util/i18n.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
@@ -147,15 +148,17 @@ void RenderMarketTab(App* app)
float mkCardBudget = std::max(200.0f, marketAvail.y - mkOverhead);
Layout::SectionBudget mb(mkCardBudget);
float statsMarketBudH = mb.allocate(0.14f, S.drawElement("tabs.market", "stats-card-min-height").size);
float portfolioBudgetH = mb.allocate(0.18f, 50.0f);
// ================================================================
// PRICE HERO — Large glass card with price + change badge
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
// ================================================================
{
float dp = Layout::dpiScale();
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = std::max(S.drawElement("tabs.market", "hero-card-min-height").size, S.drawElement("tabs.market", "hero-card-height").size * vs);
float heroMinH = S.drawElement("tabs.market", "hero-card-min-height").size;
float statsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad;
float cardH = std::max(heroMinH + statsRowH + pad, (S.drawElement("tabs.market", "hero-card-height").size + statsRowH + pad) * vs);
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
@@ -172,36 +175,118 @@ void RenderMarketTab(App* app)
float cy = cardMin.y + Layout::spacingLg();
if (market.price_usd > 0) {
// Large price with text shadow
// ---- HERO PRICE (large, prominent) ----
ImFont* h3 = Type().h3();
std::string priceStr = FormatPrice(market.price_usd);
ImU32 priceCol = Success();
DrawTextShadow(dl, h4, h4->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
// BTC price beside it
float priceW = h4->CalcTextSizeA(h4->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
snprintf(buf, sizeof(buf), "%.10f BTC", market.price_btc);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx + priceW + Layout::spacingXl(), cy + (h4->LegacySize - capFont->LegacySize)),
OnSurfaceMedium(), buf);
// Ticker label after price
float priceW = h3->CalcTextSizeA(h3->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
dl->AddText(body2, body2->LegacySize,
ImVec2(cx + priceW + Layout::spacingSm(), cy + (h3->LegacySize - body2->LegacySize)),
OnSurfaceMedium(), DRAGONX_TICKER);
// 24h change badge
float badgeY = cy + h4->LegacySize + 8;
// 24h change badge — to the right of ticker
float tickerW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, DRAGONX_TICKER).x;
float badgeX = cx + priceW + Layout::spacingSm() + tickerW + Layout::spacingMd();
ImU32 chgCol = market.change_24h >= 0 ? Success() : Error();
snprintf(buf, sizeof(buf), "%s%.2f%%", market.change_24h >= 0 ? "+" : "", market.change_24h);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float badgePad = Layout::spacingSm() + Layout::spacingXs();
ImVec2 bMin(cx, badgeY);
ImVec2 bMax(cx + txtSz.x + badgePad * 2, badgeY + txtSz.y + badgePad);
snprintf(buf, sizeof(buf), "%s%.2f%% 24h", market.change_24h >= 0 ? "+" : "", market.change_24h);
ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float badgePadH = Layout::spacingSm();
float badgePadV = Layout::spacingXs();
ImVec2 bMin(badgeX, cy + (h3->LegacySize - chgSz.y - badgePadV * 2) * 0.5f);
ImVec2 bMax(badgeX + chgSz.x + badgePadH * 2, bMin.y + chgSz.y + badgePadV * 2);
ImU32 badgeBg = market.change_24h >= 0 ? WithAlpha(Success(), 30) : WithAlpha(Error(), 30);
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + badgePad, badgeY + badgePad * 0.5f), chgCol, buf);
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
// "24h" label after badge
dl->AddText(capFont, capFont->LegacySize,
ImVec2(bMax.x + 6, badgeY + badgePad * 0.5f), OnSurfaceDisabled(), "24h");
// ---- SEPARATOR ----
float sepY = cy + h3->LegacySize + Layout::spacingMd();
float rnd = glassSpec.rounding;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, sepY),
ImVec2(cardMax.x - rnd * 0.5f, sepY),
WithAlpha(OnSurface(), 15), 1.0f * dp);
// ---- STATS ROW (BTC Price | Volume | Market Cap) ----
float statsY = sepY + Layout::spacingSm();
float colW = (availWidth - Layout::spacingLg() * 2) / 3.0f;
struct StatItem { const char* label; std::string value; ImU32 valueCol; };
StatItem stats[3] = {
{TR("market_btc_price"), "", OnSurface()},
{TR("market_24h_volume"), FormatCompactUSD(market.volume_24h), OnSurface()},
{TR("market_cap"), FormatCompactUSD(market.market_cap), OnSurface()},
};
snprintf(buf, sizeof(buf), "%.10f", market.price_btc);
stats[0].value = buf;
for (int i = 0; i < 3; i++) {
float sx = cardMin.x + Layout::spacingLg() + i * colW;
float centerX = sx + colW * 0.5f;
// Label (overline, centered)
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[i].label);
dl->AddText(ovFont, ovFont->LegacySize,
ImVec2(centerX - lblSz.x * 0.5f, statsY), OnSurfaceMedium(), stats[i].label);
// Value (subtitle1, centered)
float valY = statsY + ovFont->LegacySize + Layout::spacingXs();
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[i].value.c_str());
dl->AddText(sub1, sub1->LegacySize,
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
}
// ---- TRADE BUTTON (top-right of card) ----
if (!currentExchange.pairs.empty()) {
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
ImFont* iconFont = Type().iconSmall();
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, pairName);
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_OPEN_IN_NEW);
float iconGap = Layout::spacingSm();
float tradePadH = Layout::spacingMd();
float tradePadV = Layout::spacingSm();
float tradeBtnW = textSz.x + iconGap + iconSz.x + tradePadH * 2;
float tradeBtnH = std::max(textSz.y, iconSz.y) + tradePadV * 2;
float tradeBtnX = cardMax.x - pad - tradeBtnW;
float tradeBtnY = cardMin.y + Layout::spacingSm();
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
bool tradeHov = material::IsRectHovered(tMin, tMax);
// Glass pill background
GlassPanelSpec tradeBtnGlass;
tradeBtnGlass.rounding = tradeBtnH * 0.5f;
tradeBtnGlass.fillAlpha = tradeHov ? 35 : 20;
DrawGlassPanel(dl, tMin, tMax, tradeBtnGlass);
if (tradeHov)
dl->AddRectFilled(tMin, tMax, WithAlpha(Primary(), 20), tradeBtnH * 0.5f);
// Text (pair name with body2, icon with icon font)
ImU32 tradeCol = tradeHov ? OnSurface() : OnSurfaceMedium();
float contentY = tradeBtnY + tradePadV;
float curX = tradeBtnX + tradePadH;
dl->AddText(body2, body2->LegacySize,
ImVec2(curX, contentY), tradeCol, pairName);
curX += textSz.x + iconGap;
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(curX, contentY + (textSz.y - iconSz.y) * 0.5f), tradeCol, ICON_MD_OPEN_IN_NEW);
// Click
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(tMin);
ImGui::InvisibleButton("##TradeOnExchange", ImVec2(tradeBtnW, tradeBtnH));
if (ImGui::IsItemClicked()) {
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip(TR("market_trade_on"), currentExchange.name.c_str());
}
ImGui::SetCursorScreenPos(savedCur);
}
} else {
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), "Price data unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), TR("market_price_unavailable"));
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
@@ -209,42 +294,6 @@ void RenderMarketTab(App* app)
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// STATS — Three glass cards (Price | Volume | Market Cap)
// ================================================================
{
float cardW = (availWidth - 2 * gap) / 3.0f;
float cardH = std::min(StatCardHeight(vs), statsMarketBudH);
ImVec2 origin = ImGui::GetCursorScreenPos();
struct StatInfo { const char* label; std::string value; ImU32 col; ImU32 accent; };
StatInfo cards[3] = {
{"PRICE", market.price_usd > 0 ? FormatPrice(market.price_usd) : "N/A",
OnSurface(), WithAlpha(Success(), 200)},
{"24H VOLUME", FormatCompactUSD(market.volume_24h),
OnSurface(), WithAlpha(Secondary(), 200)},
{"MARKET CAP", FormatCompactUSD(market.market_cap),
OnSurface(), WithAlpha(Warning(), 200)},
};
for (int i = 0; i < 3; i++) {
float xOff = i * (cardW + gap);
ImVec2 cMin(origin.x + xOff, origin.y);
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
StatCardSpec sc;
sc.overline = cards[i].label;
sc.value = cards[i].value.c_str();
sc.valueCol = cards[i].col;
sc.accentCol = cards[i].accent;
sc.centered = true;
DrawStatCard(dl, cMin, cMax, sc, glassSpec);
}
ImGui::Dummy(ImVec2(availWidth, cardH));
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// PRICE CHART — Custom drawn inside glass panel (matches app design)
// ================================================================
@@ -340,7 +389,7 @@ void RenderMarketTab(App* app)
// X-axis labels
const int xlabels[] = {0, 6, 12, 18, 23};
const char* xlabelText[] = {"24h", "18h", "12h", "6h", "Now"};
const char* xlabelText[] = {TR("market_24h"), TR("market_18h"), TR("market_12h"), TR("market_6h"), TR("market_now")};
for (int xi = 0; xi < 5; xi++) {
int idx = xlabels[xi];
float t = (float)idx / (float)(n - 1);
@@ -395,7 +444,7 @@ void RenderMarketTab(App* app)
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
}
} else {
const char* msg = "No price history available";
const char* msg = TR("market_no_history");
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, chartMin.y + chartH * 0.45f),
@@ -429,7 +478,7 @@ void RenderMarketTab(App* app)
s_history_initialized = false;
s_last_refresh_time = ImGui::GetTime();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh price data");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("market_refresh_price"));
// Timestamp text to the left of refresh button
if (s_last_refresh_time > 0.0) {
@@ -452,7 +501,7 @@ void RenderMarketTab(App* app)
}
// ================================================================
// EXCHANGE SELECTOR — Combo dropdown + pair name + trade link
// EXCHANGE SELECTOR — Combo dropdown + attribution
// ================================================================
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
{
@@ -483,28 +532,9 @@ void RenderMarketTab(App* app)
}
ImGui::PopItemWidth();
// Show current pair name beside combo
if (!currentExchange.pairs.empty()) {
ImGui::SameLine(0, Layout::spacingLg());
Type().textColored(TypeStyle::Subtitle1, OnSurface(),
currentExchange.pairs[s_pair_idx].displayName.c_str());
// "Open on exchange" button
ImGui::SameLine(0, Layout::spacingSm());
ImGui::PushFont(material::Typography::instance().iconSmall());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
snprintf(buf, sizeof(buf), ICON_MD_OPEN_IN_NEW "##TradeLink");
if (ImGui::Button(buf)) {
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
}
ImGui::PopStyleVar();
ImGui::PopFont();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open on %s", currentExchange.name.c_str());
}
// Attribution
ImGui::SameLine(0, Layout::spacingLg());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "Price data from CoinGecko API");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("market_attribution"));
if (!market.last_updated.empty()) {
ImGui::SameLine(0, 12);
@@ -722,7 +752,7 @@ void RenderMarketTab(App* app)
// PORTFOLIO — Glass card with balance breakdown
// ================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PORTFOLIO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
double total_balance = state.totalBalance;
@@ -760,7 +790,7 @@ void RenderMarketTab(App* app)
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMax.x - valW - pad, cy + 2), OnSurfaceMedium(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), "No price data");
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price"));
}
cy += sub1->LegacySize + 8;
@@ -799,7 +829,7 @@ void RenderMarketTab(App* app)
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
int pct = (int)(shieldedRatio * 100.0f + 0.5f);
snprintf(buf, sizeof(buf), "%d%% Shielded", pct);
snprintf(buf, sizeof(buf), TR("market_pct_shielded"), pct);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf);
}

View File

@@ -4,6 +4,7 @@
#include "mining_tab.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../config/settings.h"
@@ -38,6 +39,9 @@ static bool s_threads_initialized = false;
static bool s_drag_active = false;
static int s_drag_anchor_thread = 0; // thread# where drag started
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
static int s_earnings_filter = 0;
// Pool mode state
static bool s_pool_mode = false;
static char s_pool_url[256] = "pool.dragonx.is:3433";
@@ -126,7 +130,13 @@ void RenderMiningTab(App* app)
int max_threads = GetMaxMiningThreads();
if (!s_threads_initialized) {
s_selected_threads = mining.generate ? std::max(1, mining.genproclimit) : 1;
int saved = app->settings()->getPoolThreads();
if (mining.generate)
s_selected_threads = std::max(1, mining.genproclimit);
else if (saved > 0)
s_selected_threads = std::min(saved, max_threads);
else
s_selected_threads = 1;
s_threads_initialized = true;
}
@@ -234,7 +244,7 @@ void RenderMiningTab(App* app)
dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd);
}
{
const char* label = "SOLO";
const char* label = TR("mining_solo");
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lx = soloMin.x + (toggleW - sz.x) * 0.5f;
float ly = soloMin.y + (toggleH - sz.y) * 0.5f;
@@ -255,7 +265,7 @@ void RenderMiningTab(App* app)
dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd);
}
{
const char* label = "POOL";
const char* label = TR("mining_pool");
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lx = poolMin.x + (toggleW - sz.x) * 0.5f;
float ly = poolMin.y + (toggleH - sz.y) * 0.5f;
@@ -287,7 +297,7 @@ void RenderMiningTab(App* app)
}
if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (poolHov && soloMiningActive && !s_pool_mode) {
ImGui::SetTooltip("Stop solo mining to use pool mining");
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
}
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
@@ -307,7 +317,7 @@ void RenderMiningTab(App* app)
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingSm());
ImGui::PushFont(capFont);
ImGui::TextUnformatted("Stop solo mining to configure pool settings");
ImGui::TextUnformatted(TR("mining_stop_solo_for_pool_settings"));
ImGui::PopFont();
ImGui::PopStyleColor();
} else if (s_pool_mode) {
@@ -339,7 +349,7 @@ void RenderMiningTab(App* app)
// === Pool URL input ===
ImGui::SetNextItemWidth(urlW);
if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) {
if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) {
s_pool_settings_dirty = true;
}
@@ -357,7 +367,7 @@ void RenderMiningTab(App* app)
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Saved pools");
ImGui::SetTooltip("%s", TR("mining_saved_pools"));
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
@@ -389,7 +399,7 @@ void RenderMiningTab(App* app)
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save pool URL");
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
@@ -416,11 +426,19 @@ void RenderMiningTab(App* app)
if (savedUrls.empty()) {
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("No saved pools");
ImGui::TextDisabled("%s", TR("mining_no_saved_pools"));
ImGui::PopFont();
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
ImGui::TextDisabled("%s", TR("mining_click"));
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().iconSmall());
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_to_save"));
ImGui::PopFont();
} else {
std::string urlToRemove;
@@ -457,7 +475,7 @@ void RenderMiningTab(App* app)
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Remove");
ImGui::SetTooltip("%s", TR("mining_remove"));
} else if (rowHov) {
// Show faint X when row is hovered
ImFont* icoF = Type().iconSmall();
@@ -508,11 +526,11 @@ void RenderMiningTab(App* app)
float wrkGroupW = wrkW + perGroupExtra;
ImGui::SetNextItemWidth(wrkW);
if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) {
if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) {
s_pool_settings_dirty = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts");
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
}
// --- Worker: Dropdown arrow button ---
@@ -529,7 +547,7 @@ void RenderMiningTab(App* app)
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Saved addresses");
ImGui::SetTooltip("%s", TR("mining_saved_addresses"));
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
@@ -561,7 +579,7 @@ void RenderMiningTab(App* app)
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save payout address");
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
@@ -589,11 +607,19 @@ void RenderMiningTab(App* app)
if (savedWorkers.empty()) {
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("No saved addresses");
ImGui::TextDisabled("%s", TR("mining_no_saved_addresses"));
ImGui::PopFont();
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
ImGui::TextDisabled("%s", TR("mining_click"));
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().iconSmall());
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_to_save"));
ImGui::PopFont();
} else {
std::string addrToRemove;
@@ -633,7 +659,7 @@ void RenderMiningTab(App* app)
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Remove");
ImGui::SetTooltip("%s", TR("mining_remove"));
} else if (rowHov) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
@@ -694,7 +720,7 @@ void RenderMiningTab(App* app)
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Reset to defaults");
ImGui::SetTooltip("%s", TR("mining_reset_defaults"));
}
// Icon
@@ -781,9 +807,9 @@ void RenderMiningTab(App* app)
// --- Header row: "THREADS 4 / 16" + RAM est + active indicator ---
{
ImVec2 labelPos(cardMin.x + pad, curY);
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), "THREADS");
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), TR("mining_threads"));
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, "THREADS").x;
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_threads")).x;
snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads);
ImVec2 countPos(labelPos.x + labelW, curY);
dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf);
@@ -804,17 +830,58 @@ void RenderMiningTab(App* app)
OnSurfaceDisabled(), buf);
}
// Active mining indicator (top-right)
// Idle mining toggle (top-right corner of card)
float idleRightEdge = cardMax.x - pad;
{
bool idleOn = app->settings()->getMineWhenIdle();
ImFont* icoFont = Type().iconSmall();
const char* idleIcon = ICON_MD_SCHEDULE;
float icoH = icoFont->LegacySize;
float btnSz = icoH + 8.0f * dp;
float btnX = idleRightEdge - btnSz;
float btnY = curY + (headerH - btnSz) * 0.5f;
// Pill background when active
if (idleOn) {
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
WithAlpha(Primary(), 60), btnSz * 0.5f);
}
// Icon centered in button
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
icoCol, idleIcon);
// Click target (save/restore cursor so layout is unaffected)
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setMineWhenIdle(!idleOn);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
}
ImGui::SetCursorScreenPos(savedCur);
idleRightEdge = btnX - 4.0f * dp;
}
// Active mining indicator (left of idle toggle)
if (mining.generate) {
float pulse = effects::isLowSpecMode()
? schema::UI().drawElement("animations", "pulse-base-normal").size
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse));
float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs;
dl->AddCircleFilled(ImVec2(cardMax.x - pad - dotR * 2, curY + dotR + 1 * dp), dotR, pulseCol);
dl->AddCircleFilled(ImVec2(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMax.x - pad - dotR * 2 - 60 * hs, curY),
WithAlpha(Success(), 200), "Mining");
ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY),
WithAlpha(Success(), 200), TR("mining_active"));
}
curY += headerH + secGap;
}
@@ -930,12 +997,16 @@ void RenderMiningTab(App* app)
dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf);
}
if (threads_changed && mining.generate) {
app->startMining(s_selected_threads);
}
if (threads_changed && s_pool_mode && state.pool_mining.xmrig_running) {
app->stopPoolMining();
app->startPoolMining(s_selected_threads);
if (threads_changed) {
app->settings()->setPoolThreads(s_selected_threads);
app->settings()->save();
if (mining.generate) {
app->startMining(s_selected_threads);
}
if (s_pool_mode && state.pool_mining.xmrig_running) {
app->stopPoolMining();
app->startPoolMining(s_selected_threads);
}
}
curY += gridH + secGap;
@@ -1059,20 +1130,20 @@ void RenderMiningTab(App* app)
const char* label;
ImU32 lblCol;
if (isToggling) {
label = isMiningActive ? "STOPPING" : "STARTING";
label = isMiningActive ? TR("mining_stopping") : TR("mining_starting");
// Animated dots effect via alpha pulse
float pulse = effects::isLowSpecMode()
? 0.7f
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
} else if (isMiningActive) {
label = "STOP";
label = TR("mining_stop");
lblCol = WithAlpha(Error(), 220);
} else if (disabled) {
label = "MINE";
label = TR("mining_mine");
lblCol = WithAlpha(OnSurface(), 50);
} else {
label = "MINE";
label = TR("mining_mine");
lblCol = WithAlpha(OnSurface(), 160);
}
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
@@ -1086,13 +1157,13 @@ void RenderMiningTab(App* app)
if (!disabled)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (isToggling)
ImGui::SetTooltip(isMiningActive ? "Stopping miner..." : "Starting miner...");
ImGui::SetTooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
else if (isSyncing && !s_pool_mode)
ImGui::SetTooltip("Syncing blockchain... (%.1f%%)", state.sync.verification_progress * 100.0);
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
else if (poolBlockedBySolo)
ImGui::SetTooltip("Stop solo mining before starting pool mining");
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
else
ImGui::SetTooltip(isMiningActive ? "Stop Mining" : "Start Mining");
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
}
// Click action — pool or solo
@@ -1201,23 +1272,15 @@ void RenderMiningTab(App* app)
int numStats = 3;
if (s_pool_mode) {
col1Label = "POOL HASHRATE";
col1Label = TR("mining_local_hashrate");
col1Str = FormatHashrate(state.pool_mining.hashrate_10s);
col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled();
col2Label = "THREADS / MEM";
{
char buf[64];
int64_t memMB = state.pool_mining.memory_used / (1024 * 1024);
if (memMB > 0)
snprintf(buf, sizeof(buf), "%d / %lld MB", state.pool_mining.threads_active, (long long)memMB);
else
snprintf(buf, sizeof(buf), "%d / --", state.pool_mining.threads_active);
col2Str = buf;
}
col2Col = OnSurface();
col2Label = TR("mining_pool_hashrate");
col2Str = FormatHashrate(state.pool_mining.pool_hashrate);
col2Col = state.pool_mining.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled();
col3Label = "SHARES";
col3Label = TR("mining_shares");
char sharesBuf[64];
snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld",
(long long)state.pool_mining.accepted,
@@ -1225,7 +1288,7 @@ void RenderMiningTab(App* app)
col3Str = sharesBuf;
col3Col = OnSurface();
col4Label = "UPTIME";
col4Label = TR("mining_uptime");
int64_t up = state.pool_mining.uptime_sec;
char uptBuf[64];
if (up <= 0)
@@ -1240,15 +1303,15 @@ void RenderMiningTab(App* app)
} else {
double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty);
col1Label = "LOCAL HASHRATE";
col1Label = TR("mining_local_hashrate");
col1Str = FormatHashrate(mining.localHashrate);
col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
col2Label = "NETWORK";
col2Label = TR("mining_network");
col2Str = FormatHashrate(mining.networkHashrate);
col2Col = OnSurface();
col3Label = "EST. BLOCK";
col3Label = TR("mining_est_block");
col3Str = FormatEstTime(est_hours);
col3Col = OnSurface();
}
@@ -1402,9 +1465,9 @@ void RenderMiningTab(App* app)
dl->AddText(capFont, capFont->LegacySize,
ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp),
OnSurfaceDisabled(),
chartHistory.size() >= 300 ? "5m ago" :
chartHistory.size() >= 60 ? "1m ago" : "start");
std::string nowLbl = "now";
chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") :
chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start"));
std::string nowLbl = TR("mining_chart_now");
ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp),
@@ -1421,7 +1484,7 @@ void RenderMiningTab(App* app)
if (hasLogContent || hasChartContent) {
ImFont* iconFont = Type().iconSmall();
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
const char* toggleTip = showLogFlag ? "Show hashrate chart" : "Show mining log";
const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log");
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
float btnSize = iconSz.y + 8 * dp;
float btnX = cardMax.x - pad - btnSize;
@@ -1486,6 +1549,10 @@ void RenderMiningTab(App* app)
&& !tx.memo.empty()
&& tx.memo.find("Mining Pool payout") != std::string::npos);
if (isSoloMined || isPoolPayout) {
// Apply earnings filter
if (s_earnings_filter == 1 && !isSoloMined) continue;
if (s_earnings_filter == 2 && !isPoolPayout) continue;
double amt = std::abs(tx.amount);
minedAllTime += amt;
minedAllTimeCount++;
@@ -1526,6 +1593,63 @@ void RenderMiningTab(App* app)
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + combinedCardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// === Earnings filter toggle button (top-right of card) ===
{
const char* filterLabels[] = { TR("mining_filter_all"), TR("mining_solo"), TR("mining_pool") };
const char* filterIcons[] = { ICON_MD_FUNCTIONS, ICON_MD_MEMORY, ICON_MD_CLOUD };
const char* curIcon = filterIcons[s_earnings_filter];
const char* curLabel = filterLabels[s_earnings_filter];
ImFont* icoFont = Type().iconSmall();
float icoH = icoFont->LegacySize;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, curIcon);
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, curLabel);
float padH = Layout::spacingSm();
float btnW = padH + icoSz.x + Layout::spacingXs() + lblSz.x + padH;
float btnH = icoH + 8.0f * dp;
float btnX = cardMax.x - pad - btnW;
float btnY = cardMin.y + (earningsRowH - btnH) * 0.5f;
ImVec2 bMin(btnX, btnY), bMax(btnX + btnW, btnY + btnH);
bool hov = material::IsRectHovered(bMin, bMax);
// Pill background
ImU32 pillBg = s_earnings_filter != 0
? WithAlpha(Primary(), 60)
: WithAlpha(OnSurface(), hov ? 25 : 12);
dl->AddRectFilled(bMin, bMax, pillBg, btnH * 0.5f);
// Icon
ImU32 icoCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceDisabled());
float cx = bMin.x + padH;
float cy = bMin.y + (btnH - icoSz.y) * 0.5f;
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cx, cy), icoCol, curIcon);
// Label
ImU32 lblCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceMedium());
float lx = cx + icoSz.x + Layout::spacingXs();
float ly = bMin.y + (btnH - lblSz.y) * 0.5f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), lblCol, curLabel);
// Click target
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(bMin);
ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH));
if (ImGui::IsItemClicked()) {
s_earnings_filter = (s_earnings_filter + 1) % 3;
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
const char* tips[] = {
TR("mining_filter_tip_all"),
TR("mining_filter_tip_solo"),
TR("mining_filter_tip_pool")
};
ImGui::SetTooltip("%s", tips[s_earnings_filter]);
}
ImGui::SetCursorScreenPos(savedCur);
}
// === Earnings section (top of combined card) ===
{
const int numCols = 4;
@@ -1561,10 +1685,10 @@ void RenderMiningTab(App* app)
snprintf(estVal, sizeof(estVal), "N/A");
EarningsEntry entries[] = {
{ "TODAY", todayVal, todaySub, greenCol2 },
{ "YESTERDAY", yesterdayVal, yesterdaySub, OnSurface() },
{ "ALL TIME", allVal, allSub, OnSurface() },
{ "EST. DAILY", estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
{ TR("mining_today"), todayVal, todaySub, greenCol2 },
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() },
{ TR("mining_all_time"), allVal, allSub, OnSurface() },
{ TR("mining_est_daily"), estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
};
for (int ei = 0; ei < numCols; ei++) {
@@ -1619,7 +1743,7 @@ void RenderMiningTab(App* app)
float col3X = cx + colW * 2.0f;
// -- Difficulty --
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), "Difficulty");
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), TR("difficulty"));
if (mining.difficulty > 0) {
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
@@ -1628,19 +1752,19 @@ void RenderMiningTab(App* app)
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy difficulty");
ImGui::SetTooltip("%s", TR("mining_click_copy_difficulty"));
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
Notifications::instance().info("Difficulty copied");
Notifications::instance().info(TR("mining_difficulty_copied"));
}
}
// -- Block --
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), "Block");
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), TR("block"));
if (mining.blocks > 0) {
snprintf(buf, sizeof(buf), "%d", mining.blocks);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
@@ -1649,19 +1773,19 @@ void RenderMiningTab(App* app)
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy block height");
ImGui::SetTooltip("%s", TR("mining_click_copy_block"));
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
Notifications::instance().info("Block height copied");
Notifications::instance().info(TR("mining_block_copied"));
}
}
// -- Mining Address --
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), "Mining Addr");
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr"));
std::string mining_address = "";
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
@@ -1685,14 +1809,14 @@ void RenderMiningTab(App* app)
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy mining address");
ImGui::SetTooltip("%s", TR("mining_click_copy_address"));
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(mining_address.c_str());
Notifications::instance().info("Mining address copied");
Notifications::instance().info(TR("mining_address_copied"));
}
}
}
@@ -1859,15 +1983,15 @@ void RenderMiningTab(App* app)
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (selfRAM >= 1024.0)
ImGui::Text("Wallet: %.1f GB", selfRAM / 1024.0);
ImGui::Text(TR("ram_wallet_gb"), selfRAM / 1024.0);
else
ImGui::Text("Wallet: %.0f MB", selfRAM);
ImGui::Text(TR("ram_wallet_mb"), selfRAM);
if (daemonRAM >= 1024.0)
ImGui::Text("Daemon: %.1f GB (%s)", daemonRAM / 1024.0, app->getDaemonMemDiag().c_str());
ImGui::Text(TR("ram_daemon_gb"), daemonRAM / 1024.0, app->getDaemonMemDiag().c_str());
else
ImGui::Text("Daemon: %.0f MB (%s)", daemonRAM, app->getDaemonMemDiag().c_str());
ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str());
ImGui::Separator();
ImGui::Text("System: %.1f / %.0f GB", usedRAM / 1024.0, totalRAM / 1024.0);
ImGui::Text(TR("ram_system_gb"), usedRAM / 1024.0, totalRAM / 1024.0);
ImGui::EndTooltip();
}
}
@@ -1881,7 +2005,7 @@ void RenderMiningTab(App* app)
// ============================================================
if (!recentMined.empty() || s_pool_mode) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
s_pool_mode ? "RECENT POOL PAYOUTS" : "RECENT BLOCKS");
s_pool_mode ? TR("mining_recent_payouts") : TR("mining_recent_blocks"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
@@ -1924,8 +2048,8 @@ void RenderMiningTab(App* app)
ImVec2(centerX - iSz.x * 0.5f, emptyY),
OnSurfaceDisabled(), emptyIcon);
const char* emptyMsg = s_pool_mode
? "No pool payouts yet"
: "No blocks found yet";
? TR("mining_no_payouts_yet")
: TR("mining_no_blocks_yet");
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg);
miningChildDL->AddText(capFont, capFont->LegacySize,
ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()),
@@ -1966,13 +2090,13 @@ void RenderMiningTab(App* app)
// Time
int64_t diff = now - mtx.timestamp;
if (diff < 60)
snprintf(buf, sizeof(buf), "%llds ago", (long long)diff);
snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff);
else if (diff < 3600)
snprintf(buf, sizeof(buf), "%lldm ago", (long long)(diff / 60));
snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60));
else if (diff < 86400)
snprintf(buf, sizeof(buf), "%lldh ago", (long long)(diff / 3600));
snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600));
else
snprintf(buf, sizeof(buf), "%lldd ago", (long long)(diff / 86400));
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400));
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + iSz.x + 8 * dp, ry), OnSurfaceDisabled(), buf);
// Amount
@@ -1984,9 +2108,9 @@ void RenderMiningTab(App* app)
float badgeX = rMax.x - pad - Layout::spacingXl() * 3.5f;
if (mtx.mature) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
WithAlpha(Success(), 180), "Mature");
WithAlpha(Success(), 180), TR("mature"));
} else {
snprintf(buf, sizeof(buf), "%d conf", mtx.confirmations);
snprintf(buf, sizeof(buf), TR("conf_count"), mtx.confirmations);
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
WithAlpha(Warning(), 200), buf);
}
@@ -2001,7 +2125,7 @@ void RenderMiningTab(App* app)
dragonx::util::Platform::openUrl(url);
}
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
ImGui::SetTooltip("Open in explorer");
ImGui::SetTooltip("%s", TR("mining_open_in_explorer"));
}
}
@@ -2026,8 +2150,8 @@ void RenderMiningTab(App* app)
const char* dotIcon = ICON_MD_CIRCLE;
ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200);
const char* statusText = state.pool_mining.connected
? (state.pool_mining.pool_url.empty() ? "Connected" : state.pool_mining.pool_url.c_str())
: "Connecting...";
? (state.pool_mining.pool_url.empty() ? TR("mining_connected") : state.pool_mining.pool_url.c_str())
: TR("mining_connecting");
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon);

View File

@@ -5,6 +5,7 @@
#include "peers_tab.h"
#include "../../app.h"
#include "../../data/wallet_state.h"
#include "../../util/i18n.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../effects/low_spec.h"
@@ -148,7 +149,7 @@ void RenderPeersTab(App* app)
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Card header
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "BLOCKCHAIN");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
float colW = (cardW - pad * 2) / 2.0f;
float ry = cardMin.y + pad * 0.5f + headerH;
@@ -165,10 +166,11 @@ void RenderPeersTab(App* app)
{
// Blocks
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
int blocks = state.sync.blocks;
if (blocks > 0) {
int blocksLeft = state.sync.headers - blocks;
int chainTip = state.longestchain > 0 ? state.longestchain : state.sync.headers;
int blocksLeft = chainTip - blocks;
if (blocksLeft < 0) blocksLeft = 0;
if (blocksLeft > 0) {
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
@@ -180,7 +182,7 @@ void RenderPeersTab(App* app)
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr);
// Draw "(X left)" in warning color
char leftStr[32];
snprintf(leftStr, sizeof(leftStr), "(%d left)", blocksLeft);
snprintf(leftStr, sizeof(leftStr), TR("peers_blocks_left"), blocksLeft);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
Warning(), leftStr);
@@ -194,7 +196,7 @@ void RenderPeersTab(App* app)
// Longest Chain
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Longest Chain");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest_chain"));
if (state.longestchain > 0) {
snprintf(buf, sizeof(buf), "%d", state.longestchain);
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
@@ -213,7 +215,7 @@ void RenderPeersTab(App* app)
{
// Hashrate
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Hashrate");
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)
@@ -233,7 +235,7 @@ void RenderPeersTab(App* app)
// Difficulty
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Difficulty");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (mining.difficulty > 0) {
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
@@ -251,7 +253,7 @@ void RenderPeersTab(App* app)
{
// Notarized
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Notarized");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.notarized > 0) {
snprintf(buf, sizeof(buf), "%d", state.notarized);
@@ -262,7 +264,7 @@ void RenderPeersTab(App* app)
// Protocol
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Protocol");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_protocol"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.protocol_version > 0) {
snprintf(buf, sizeof(buf), "%d", state.protocol_version);
@@ -280,7 +282,7 @@ void RenderPeersTab(App* app)
{
// Version
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Version");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_version"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.daemon_version > 0) {
int major = state.daemon_version / 1000000;
@@ -294,7 +296,7 @@ void RenderPeersTab(App* app)
// Memory
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Memory");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_memory"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
double memMb = state.mining.daemon_memory_mb;
if (memMb > 0) {
@@ -316,7 +318,7 @@ void RenderPeersTab(App* app)
{
// Longest Chain
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Longest");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.longestchain > 0) {
snprintf(buf, sizeof(buf), "%d", state.longestchain);
@@ -330,7 +332,7 @@ void RenderPeersTab(App* app)
// Best Block (truncated hash)
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Best Block");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (!state.sync.best_blockhash.empty()) {
// Truncate hash to fit: first 6 + "..." + last 6
@@ -349,14 +351,14 @@ void RenderPeersTab(App* app)
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy: %s", hash.c_str());
ImGui::SetTooltip("%s %s", TR("peers_click_copy"), hash.c_str());
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(hash.c_str());
ui::Notifications::instance().info("Block hash copied");
ui::Notifications::instance().info(TR("peers_hash_copied"));
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
@@ -373,7 +375,7 @@ void RenderPeersTab(App* app)
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Card header
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "PEERS");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
float colW = (cardW - pad * 2) / 2.0f;
float ry = cardMin.y + pad * 0.5f + headerH;
@@ -390,13 +392,13 @@ void RenderPeersTab(App* app)
{
// Connected
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Connected");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_connected"));
snprintf(buf, sizeof(buf), "%d", totalPeers);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
// In / Out
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "In / Out");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_in_out"));
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
}
@@ -426,7 +428,7 @@ void RenderPeersTab(App* app)
// Avg Ping
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Avg Ping");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_avg_ping"));
ImU32 pingCol;
if (avgPing < 100) pingCol = Success();
else if (avgPing < 500) pingCol = Warning();
@@ -443,13 +445,13 @@ void RenderPeersTab(App* app)
{
// Received
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Received");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_received"));
std::string recvStr = fmtBytes(totalBytesRecv);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str());
// Sent
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Sent");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_sent"));
std::string sentStr = fmtBytes(totalBytesSent);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str());
}
@@ -462,7 +464,7 @@ void RenderPeersTab(App* app)
{
// P2P Port
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "P2P Port");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_p2p_port"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.p2p_port > 0) {
snprintf(buf, sizeof(buf), "%d", state.p2p_port);
@@ -472,7 +474,7 @@ void RenderPeersTab(App* app)
}
// Banned count
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Banned");
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_banned"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
size_t bannedCount = state.bannedPeers.size();
snprintf(buf, sizeof(buf), "%zu", bannedCount);
@@ -488,7 +490,7 @@ void RenderPeersTab(App* app)
// Compute remaining space for peer list + footer
// ================================================================
float footerH = ImGui::GetFrameHeight() + Layout::spacingSm();
float toggleH = body2->LegacySize + Layout::spacingMd() * 2;
float toggleH = body2->LegacySize + Layout::spacingMd() * 2 + Layout::spacingSm();
float remainForPeers = std::max(60.0f, peersAvail.y - (ImGui::GetCursorScreenPos().y - ImGui::GetWindowPos().y) - footerH - Layout::spacingSm());
float peerPanelHeight = remainForPeers - toggleH;
peerPanelHeight = std::max(S.drawElement("tabs.peers", "peer-panel-min-height").size, peerPanelHeight);
@@ -502,8 +504,8 @@ void RenderPeersTab(App* app)
float toggleY = ImGui::GetCursorScreenPos().y;
{
char connLabel[64], banLabel[64];
snprintf(connLabel, sizeof(connLabel), "Connected (%zu)", state.peers.size());
snprintf(banLabel, sizeof(banLabel), "Banned (%zu)", state.bannedPeers.size());
snprintf(connLabel, sizeof(connLabel), TR("peers_connected_count"), state.peers.size());
snprintf(banLabel, sizeof(banLabel), TR("peers_banned_count"), state.bannedPeers.size());
ImVec2 connSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, connLabel);
ImVec2 banSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, banLabel);
@@ -544,9 +546,14 @@ void RenderPeersTab(App* app)
// Refresh button — top-right, glass panel style (similar to mining button)
{
bool isRefreshing = app->isPeerRefreshInProgress();
auto refreshBtn = S.drawElement("tabs.peers", "refresh-button");
float btnW = refreshBtn.size;
float btnH = toggleH - 4.0f * Layout::dpiScale();
ImFont* iconFont = Type().iconMed();
float iconSz = iconFont->LegacySize;
const char* label = isRefreshing ? TR("peers_refreshing") : TR("peers_refresh");
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float padH = Layout::spacingSm();
float padV = Layout::spacingSm();
float btnW = padH + iconSz + Layout::spacingXs() + lblSz.x + padH;
float btnH = std::max(iconSz, lblSz.y) + padV * 2;
float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm();
float btnY = toggleY + (toggleH - btnH) * 0.5f;
ImVec2 bMin(btnX, btnY);
@@ -574,10 +581,8 @@ void RenderPeersTab(App* app)
}
// Icon: spinner while refreshing, refresh icon otherwise
float cx = bMin.x + btnW * 0.35f;
float cx = bMin.x + padH + iconSz * 0.5f;
float cy = bMin.y + btnH * 0.5f;
ImFont* iconFont = Type().iconMed();
float iconSz = iconFont->LegacySize;
if (isRefreshing) {
// Spinning arc spinner (same style as mining toggle)
@@ -617,7 +622,6 @@ void RenderPeersTab(App* app)
// Label to the right of icon
{
const char* label = isRefreshing ? "REFRESHING" : "REFRESH";
ImU32 lblCol;
if (isRefreshing) {
float pulse = effects::isLowSpecMode()
@@ -627,7 +631,6 @@ void RenderPeersTab(App* app)
} else {
lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160);
}
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lblX = cx + iconSz * 0.5f + Layout::spacingXs();
float lblY = cy - lblSz.y * 0.5f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
@@ -645,7 +648,7 @@ void RenderPeersTab(App* app)
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (!isRefreshing)
ImGui::SetTooltip("Refresh peers & blockchain");
ImGui::SetTooltip("%s", TR("peers_refresh_tooltip"));
}
ImGui::PopID();
}
@@ -673,10 +676,10 @@ void RenderPeersTab(App* app)
// ---- Connected Peers ----
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.peers.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No connected peers");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected"));
} else {
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
float rowInset = Layout::spacingLg();
@@ -727,7 +730,7 @@ void RenderPeersTab(App* app)
}
{
const char* dirLabel = peer.inbound ? "In" : "Out";
const char* dirLabel = peer.inbound ? TR("peers_dir_in") : TR("peers_dir_out");
ImU32 dirBg = peer.inbound ? WithAlpha(Success(), 30) : WithAlpha(Secondary(), 30);
ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
@@ -761,12 +764,12 @@ void RenderPeersTab(App* app)
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2),
WithAlpha(Error(), 140), "No TLS");
WithAlpha(Error(), 140), TR("peers_no_tls"));
}
if (peer.banscore > 0) {
char banBuf[16];
snprintf(banBuf, sizeof(banBuf), "Ban: %d", peer.banscore);
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
ImU32 banCol = peer.banscore > 50 ? Error() : Warning();
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf);
dl->AddText(capFont, capFont->LegacySize,
@@ -780,16 +783,16 @@ void RenderPeersTab(App* app)
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme.menu)) {
ImGui::Text("Peer: %s", peer.addr.c_str());
ImGui::Text(TR("peers_peer_label"), peer.addr.c_str());
ImGui::Separator();
if (ImGui::MenuItem("Copy Address")) {
if (ImGui::MenuItem(TR("copy_address"))) {
ImGui::SetClipboardText(peer.addr.c_str());
}
if (ImGui::MenuItem("Copy IP")) {
if (ImGui::MenuItem(TR("peers_copy_ip"))) {
ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
}
ImGui::Separator();
if (ImGui::MenuItem("Ban Peer (24h)")) {
if (ImGui::MenuItem(TR("peers_ban_24h"))) {
app->banPeer(ExtractIP(peer.addr), 86400);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
@@ -808,18 +811,18 @@ void RenderPeersTab(App* app)
};
char ttBuf[128];
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.id);
TTRow("ID", ttBuf);
TTRow("Services", peer.services.c_str());
TTRow(TR("peers_tt_id"), ttBuf);
TTRow(TR("peers_tt_services"), peer.services.c_str());
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.startingheight);
TTRow("Start Height", ttBuf);
TTRow(TR("peers_tt_start_height"), ttBuf);
snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytessent);
TTRow("Sent", ttBuf);
TTRow(TR("peers_tt_sent"), ttBuf);
snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytesrecv);
TTRow("Received", ttBuf);
TTRow(TR("peers_tt_received"), ttBuf);
snprintf(ttBuf, sizeof(ttBuf), "%d / %d", peer.synced_headers, peer.synced_blocks);
TTRow("Synced H/B", ttBuf);
TTRow(TR("peers_tt_synced"), ttBuf);
if (!peer.tls_cipher.empty())
TTRow("TLS Cipher", peer.tls_cipher.c_str());
TTRow(TR("peers_tt_tls_cipher"), peer.tls_cipher.c_str());
ImGui::EndTable();
}
ImGui::PopStyleVar();
@@ -840,10 +843,10 @@ void RenderPeersTab(App* app)
// ---- Banned Peers ----
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.bannedPeers.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No banned peers");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned"));
} else {
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size;
float rowInsetB = pad;
@@ -886,7 +889,7 @@ void RenderPeersTab(App* app)
float btnX = rowPos.x + innerW - Layout::spacingXl() * S.drawElement("tabs.peers", "unban-btn-right-offset-multiplier").size;
ImGui::SetCursorScreenPos(ImVec2(btnX, cy - 1));
if (TactileSmallButton("Unban", S.resolveFont("button"))) {
if (TactileSmallButton(TR("peers_unban"), S.resolveFont("button"))) {
app->unbanPeer(banned.address);
}
@@ -898,10 +901,10 @@ void RenderPeersTab(App* app)
const auto& acrylicTheme2 = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
if (ImGui::MenuItem("Copy Address")) {
if (ImGui::MenuItem(TR("copy_address"))) {
ImGui::SetClipboardText(banned.address.c_str());
}
if (ImGui::MenuItem("Unban")) {
if (ImGui::MenuItem(TR("peers_unban"))) {
app->unbanPeer(banned.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
@@ -940,7 +943,7 @@ void RenderPeersTab(App* app)
// ================================================================
if (s_show_banned && !state.bannedPeers.empty()) {
ImGui::BeginDisabled(!app->isConnected());
if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) {
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button"))) {
app->clearBans();
}
ImGui::EndDisabled();

View File

@@ -4,6 +4,7 @@
#include "qr_popup_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../widgets/qr_code.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
@@ -63,7 +64,7 @@ void QRPopupDialog::render(App* app)
auto addrInput = S.input("dialogs.qr-popup", "address-input");
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
if (material::BeginOverlayDialog("QR Code", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("qr_title"), &s_open, win.width, 0.94f)) {
// Label if present
if (!s_label.empty()) {
@@ -86,7 +87,7 @@ void QRPopupDialog::render(App* app)
} else {
// Fallback: show error
ImGui::BeginChild("QRPlaceholder", ImVec2(qr_size, qr_size), true);
ImGui::TextWrapped("Failed to generate QR code");
ImGui::TextWrapped("%s", TR("qr_failed"));
ImGui::EndChild();
}
@@ -95,7 +96,7 @@ void QRPopupDialog::render(App* app)
ImGui::Spacing();
// Address display
ImGui::Text("Address:");
ImGui::Text("%s", TR("address_label"));
// Use multiline for z-addresses
if (s_address.length() > 50) {
@@ -120,13 +121,13 @@ void QRPopupDialog::render(App* app)
float start_x = (window_width - total_width) / 2.0f;
ImGui::SetCursorPosX(start_x);
if (material::StyledButton("Copy Address", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("copy_address"), ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
ImGui::SetClipboardText(s_address.c_str());
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
close();
}
material::EndOverlayDialog();

View File

@@ -11,6 +11,7 @@
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../ui/widgets/qr_code.h"
@@ -35,6 +36,10 @@ namespace ui {
using namespace material;
static std::string TrId(const char* key, const char* id) {
return std::string(TR(key)) + "##" + id;
}
// ============================================================================
// State
// ============================================================================
@@ -110,7 +115,7 @@ static void RenderSyncBanner(const WalletState& state) {
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf),
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
TR("blockchain_syncing"), syncPct);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.receive", "sync-banner-bg-color").color)));
float syncH = std::max(schema::UI().drawElement("tabs.receive", "sync-banner-min-height").size, schema::UI().drawElement("tabs.receive", "sync-banner-height").size * Layout::vScale());
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, syncH),
@@ -129,13 +134,13 @@ static void RenderAddressDropdown(App* app, float width) {
char buf[256];
// Header row: label + address type toggle buttons
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("address_upper"));
float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
ImGui::SameLine(width - toggleTotalW);
const char* filterLabels[] = { "All", "Z", "T" };
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
for (int i = 0; i < 3; i++) {
bool isActive = (s_addr_type_filter == i);
if (isActive) {
@@ -190,7 +195,7 @@ static void RenderAddressDropdown(App* app, float width) {
// Build preview string
if (!app->isConnected()) {
s_source_preview = "Not connected to daemon";
s_source_preview = TR("not_connected");
} else if (s_selected_address_idx >= 0 &&
s_selected_address_idx < (int)state.addresses.size()) {
const auto& addr = state.addresses[s_selected_address_idx];
@@ -202,7 +207,7 @@ static void RenderAddressDropdown(App* app, float width) {
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
s_source_preview = buf;
} else {
s_source_preview = "Select a receiving address...";
s_source_preview = TR("select_receiving_address");
}
float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width));
@@ -212,7 +217,7 @@ static void RenderAddressDropdown(App* app, float width) {
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
if (!app->isConnected() || state.addresses.empty()) {
ImGui::TextDisabled("No addresses available");
ImGui::TextDisabled("%s", TR("no_addresses_available"));
} else {
// Build filtered and sorted list
std::vector<size_t> sortedIdx;
@@ -229,7 +234,7 @@ static void RenderAddressDropdown(App* app, float width) {
});
if (sortedIdx.empty()) {
ImGui::TextDisabled("No addresses match filter");
ImGui::TextDisabled("%s", TR("no_addresses_match"));
} else {
size_t addrTruncLen = static_cast<size_t>(std::max(schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-divisor").size));
double now = ImGui::GetTime();
@@ -287,10 +292,10 @@ static void RenderAddressDropdown(App* app, float width) {
ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected() || s_selected_address_idx < 0 ||
s_selected_address_idx >= (int)state.addresses.size());
if (TactileButton("Copy##recvAddr", ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TrId("copy", "recvAddr").c_str(), ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) {
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
Notifications::instance().info("Address copied to clipboard");
Notifications::instance().info(TR("address_copied"));
}
}
ImGui::EndDisabled();
@@ -298,23 +303,23 @@ static void RenderAddressDropdown(App* app, float width) {
// New address button on same line
ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("+ New##recv", ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new shielded address");
Notifications::instance().error(TR("failed_create_shielded"));
else {
s_pending_select_address = addr;
Notifications::instance().success("New shielded address created");
Notifications::instance().success(TR("new_shielded_created"));
}
});
} else {
app->createNewTAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new transparent address");
Notifications::instance().error(TR("failed_create_transparent"));
else {
s_pending_select_address = addr;
Notifications::instance().success("New transparent address created");
Notifications::instance().success(TR("new_transparent_created"));
}
});
}
@@ -330,10 +335,11 @@ static std::string recvTimeAgo(int64_t timestamp) {
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
char buf[32];
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
}
static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) {
@@ -353,7 +359,7 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
const WalletState& state, float width,
ImFont* capFont, App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT RECEIVED");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float hs = Layout::hScale(width);
@@ -375,7 +381,7 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
}
if (recvs.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent receives");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
return;
}
@@ -416,11 +422,11 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
// Type label (first line)
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, "Received");
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, TR("received_label"));
// Time (next to type)
std::string ago = recvTimeAgo(tx.timestamp);
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Received").x;
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("received_label")).x;
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
OnSurfaceDisabled(), ago.c_str());
@@ -457,12 +463,12 @@ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = "Pending"; statusCol = Warning();
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < (int)schema::UI().drawElement("tabs.receive", "confirmed-threshold").size) {
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else {
statusStr = "Confirmed"; statusCol = greenCol;
statusStr = TR("confirmed"); statusCol = greenCol;
}
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
float statusX = amtX - sSz.x - Layout::spacingXxl();
@@ -545,10 +551,10 @@ void RenderReceiveTab(App* app)
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
OnSurfaceDisabled(), "Waiting for daemon connection...");
OnSurfaceDisabled(), TR("waiting_for_daemon"));
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + S.drawElement("tabs.receive", "empty-state-subtitle-gap").size),
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
OnSurfaceDisabled(), TR("addresses_appear_here"));
ImGui::Dummy(ImVec2(formW, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
@@ -572,7 +578,7 @@ void RenderReceiveTab(App* app)
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size),
OnSurfaceDisabled(), "Loading addresses...");
OnSurfaceDisabled(), TR("loading_addresses"));
ImGui::Dummy(ImVec2(formW, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
@@ -639,7 +645,7 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// ---- PAYMENT REQUEST ----
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Amount input with currency toggle
@@ -805,7 +811,7 @@ void RenderReceiveTab(App* app)
// Memo (z-addresses only)
if (isZ) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
@@ -841,7 +847,7 @@ void RenderReceiveTab(App* app)
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
ImGui::BeginDisabled(!hasData);
if (TactileSmallButton("Clear Request##recv", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("clear_request", "recv").c_str(), S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_usd_amount = 0.0;
s_request_memo[0] = '\0';
@@ -880,20 +886,20 @@ void RenderReceiveTab(App* app)
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), "QR unavailable");
OnSurfaceDisabled(), TR("qr_unavailable"));
}
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy %s",
s_request_amount > 0 ? "payment URI" : "address");
ImGui::SetTooltip("%s",
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? "Payment URI copied" : "Address copied");
? TR("payment_uri_copied") : TR("address_copied"));
}
ImGui::SetCursorScreenPos(ImVec2(rx, qrPanelMax.y));
@@ -926,9 +932,9 @@ void RenderReceiveTab(App* app)
if (s_request_amount > 0) {
if (!firstBtn) ImGui::SameLine(0, btnGap);
firstBtn = false;
if (TactileButton("Copy URI##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
if (TactileButton(TrId("copy_uri", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_cached_qr_data.c_str());
Notifications::instance().info("Payment URI copied");
Notifications::instance().info(TR("payment_uri_copied"));
}
}
@@ -939,14 +945,16 @@ void RenderReceiveTab(App* app)
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileButton("Share##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
if (TactileButton(TrId("share", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
char shareBuf[1024];
snprintf(shareBuf, sizeof(shareBuf),
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
"%s\n%s: %.8f %s\n%s: %s\nURI: %s",
TR("payment_request"), TR("amount"),
s_request_amount, DRAGONX_TICKER,
selected.address.c_str(), s_cached_qr_data.c_str());
TR("address"), selected.address.c_str(),
s_cached_qr_data.c_str());
ImGui::SetClipboardText(shareBuf);
Notifications::instance().info("Payment request copied");
Notifications::instance().info(TR("payment_request_copied"));
}
ImGui::PopStyleColor(3);
}
@@ -959,7 +967,7 @@ void RenderReceiveTab(App* app)
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileButton("Explorer##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
OpenExplorerURL(selected.address);
}
ImGui::PopStyleVar(); // FrameBorderSize
@@ -972,7 +980,7 @@ void RenderReceiveTab(App* app)
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
if (TactileButton("Send \xe2\x86\x97##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
if (TactileButton(TrId("send", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
SetSendFromAddress(selected.address);
app->setCurrentPage(NavPage::Send);
}
@@ -1001,7 +1009,7 @@ void RenderReceiveTab(App* app)
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
ImGui::Dummy(ImVec2(formW, 0));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// ================================================================

View File

@@ -128,22 +128,19 @@ void RequestPaymentDialog::render(App* app)
auto qr = S.drawElement("dialogs.request-payment", "qr-code");
auto actionBtn = S.button("dialogs.request-payment", "action-button");
if (material::BeginOverlayDialog("Request Payment", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("request_title"), &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
ImGui::TextWrapped(
"Generate a payment request that others can scan or copy. "
"The QR code contains your address and optional amount/memo."
);
ImGui::TextWrapped("%s", TR("request_description"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address selection
ImGui::Text("Receive Address:");
ImGui::Text("%s", TR("request_receive_address"));
std::string addr_display = s_address[0] ? s_address : "Select address...";
std::string addr_display = s_address[0] ? s_address : TR("request_select_address");
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
}
@@ -152,7 +149,7 @@ void RequestPaymentDialog::render(App* app)
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
// Z-addresses
if (!state.z_addresses.empty()) {
ImGui::TextDisabled("-- Shielded Addresses --");
ImGui::TextDisabled("%s", TR("request_shielded_addrs"));
for (size_t i = 0; i < state.z_addresses.size(); i++) {
const auto& addr = state.z_addresses[i];
std::string label = addr.address;
@@ -169,7 +166,7 @@ void RequestPaymentDialog::render(App* app)
// T-addresses
if (!state.t_addresses.empty()) {
ImGui::TextDisabled("-- Transparent Addresses --");
ImGui::TextDisabled("%s", TR("request_transparent_addrs"));
for (size_t i = 0; i < state.t_addresses.size(); i++) {
const auto& addr = state.t_addresses[i];
std::string label = addr.address;
@@ -189,7 +186,7 @@ void RequestPaymentDialog::render(App* app)
ImGui::Spacing();
// Amount (optional)
ImGui::Text("Amount (optional):");
ImGui::Text("%s", TR("request_amount"));
ImGui::SetNextItemWidth(amountInput.width);
if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) {
s_uri_dirty = true;
@@ -200,7 +197,7 @@ void RequestPaymentDialog::render(App* app)
ImGui::Spacing();
// Label (optional)
ImGui::Text("Label (optional):");
ImGui::Text("%s", TR("request_label"));
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##Label", s_label, sizeof(s_label))) {
s_uri_dirty = true;
@@ -211,7 +208,7 @@ void RequestPaymentDialog::render(App* app)
// Memo (optional, only for z-addr)
bool is_zaddr = (s_address[0] == 'z');
if (is_zaddr) {
ImGui::Text("Memo (optional):");
ImGui::Text("%s", TR("request_memo"));
ImGui::SetNextItemWidth(-1);
if (ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo), ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
s_uri_dirty = true;
@@ -245,7 +242,7 @@ void RequestPaymentDialog::render(App* app)
// Payment URI display
if (!s_payment_uri.empty()) {
ImGui::Text("Payment URI:");
ImGui::Text("%s", TR("request_payment_uri"));
ImGui::SetNextItemWidth(-1);
// Use a selectable text area for the URI
@@ -256,23 +253,23 @@ void RequestPaymentDialog::render(App* app)
ImGui::Spacing();
// Copy button
if (material::StyledButton("Copy URI", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("request_copy_uri"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
ImGui::SetClipboardText(s_payment_uri.c_str());
Notifications::instance().success("Payment URI copied to clipboard");
Notifications::instance().success(TR("request_uri_copied"));
}
ImGui::SameLine();
if (material::StyledButton("Copy Address", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("copy_address"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
ImGui::SetClipboardText(s_address);
Notifications::instance().success("Address copied to clipboard");
Notifications::instance().success(TR("address_copied"));
}
}
ImGui::Spacing();
// Close button
if (material::StyledButton("Close", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();

View File

@@ -214,7 +214,7 @@ static void RenderSourceDropdown(App* app, float width) {
auto& S = schema::UI();
char buf[256];
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SENDING FROM");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_sending_from"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Auto-select the address with the largest balance on first load
@@ -237,7 +237,7 @@ static void RenderSourceDropdown(App* app, float width) {
// Build preview string for selected address
if (!app->isConnected()) {
s_source_preview = "Not connected to daemon";
s_source_preview = TR("not_connected");
} else if (s_selected_from_idx >= 0 &&
s_selected_from_idx < (int)state.addresses.size()) {
const auto& addr = state.addresses[s_selected_from_idx];
@@ -249,7 +249,7 @@ static void RenderSourceDropdown(App* app, float width) {
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
s_source_preview = buf;
} else {
s_source_preview = "Select a source address...";
s_source_preview = TR("send_select_source");
}
ImGui::SetNextItemWidth(width);
@@ -257,7 +257,7 @@ static void RenderSourceDropdown(App* app, float width) {
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
if (ImGui::BeginCombo("##SendFrom", s_source_preview.c_str())) {
if (!app->isConnected() || state.addresses.empty()) {
ImGui::TextDisabled("No addresses available");
ImGui::TextDisabled("%s", TR("no_addresses_available"));
} else {
// Sort by balance descending, only show addresses with balance
std::vector<size_t> sortedIdx;
@@ -272,7 +272,7 @@ static void RenderSourceDropdown(App* app, float width) {
});
if (sortedIdx.empty()) {
ImGui::TextDisabled("No addresses with balance");
ImGui::TextDisabled("%s", TR("send_no_balance"));
} else {
size_t addrTruncLen = static_cast<size_t>(std::max(S.drawElement("tabs.send", "addr-dropdown-trunc-min").size, width / S.drawElement("tabs.send", "addr-dropdown-trunc-divisor").size));
@@ -352,7 +352,7 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI();
const char* feeLabels[] = { "Low", "Normal", "High" };
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
@@ -569,12 +569,12 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size);
if (TactileSmallButton("Copy Error##txErr", schema::UI().resolveFont("button"))) {
if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(s_tx_status.c_str());
Notifications::instance().info("Error copied to clipboard");
Notifications::instance().info(TR("send_error_copied"));
}
ImGui::SameLine();
if (TactileSmallButton("Dismiss##txErr", schema::UI().resolveFont("button"))) {
if (TactileSmallButton(TR("send_dismiss"), schema::UI().resolveFont("button"))) {
s_tx_status.clear();
s_result_txid.clear();
s_status_success = false;
@@ -615,7 +615,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(ix, iy), Primary(), spinIcon);
double elapsed = ImGui::GetTime() - s_send_start_time;
snprintf(buf, sizeof(buf), "Submitting transaction... (%.0fs)", elapsed);
snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed);
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), OnSurface(), buf);
} else {
// Success checkmark
@@ -624,7 +624,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(ix, iy), Success(), checkIcon);
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), "Transaction sent!");
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), TR("send_tx_sent"));
if (!s_result_txid.empty()) {
float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size;
@@ -633,13 +633,13 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
std::string dispTxid = (int)s_result_txid.length() > txidThreshold
? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen)
: s_result_txid;
snprintf(buf, sizeof(buf), "TxID: %s", dispTxid.c_str());
snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str());
dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size, txY),
OnSurfaceDisabled(), buf);
ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size));
if (TactileSmallButton("Copy##TxID", schema::UI().resolveFont("button"))) {
if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(s_result_txid.c_str());
Notifications::instance().info("TxID copied to clipboard");
Notifications::instance().info(TR("send_txid_copied"));
}
}
}
@@ -688,7 +688,7 @@ void RenderSendConfirmPopup(App* app) {
// FROM card
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
ImVec2 cMin = ImGui::GetCursorScreenPos();
ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
GlassPanelSpec gs; gs.rounding = popGlassRound;
@@ -702,7 +702,7 @@ void RenderSendConfirmPopup(App* app) {
// TO card
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("to_upper"));
ImVec2 cMin = ImGui::GetCursorScreenPos();
ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
GlassPanelSpec gs; gs.rounding = popGlassRound;
@@ -716,7 +716,7 @@ void RenderSendConfirmPopup(App* app) {
// Fee tier selector
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK FEE");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
RenderFeeTierSelector("##confirm");
// Recalculate total after potential fee change
@@ -729,7 +729,7 @@ void RenderSendConfirmPopup(App* app) {
float valX = std::max(schema::UI().drawElement("tabs.send", "confirm-val-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-val-col-x").size * popVs);
float usdX = popW - std::max(schema::UI().drawElement("tabs.send", "confirm-usd-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-usd-col-x").size * popVs);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT DETAILS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
ImVec2 cMin = ImGui::GetCursorScreenPos();
float cH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
ImVec2 cMax(cMin.x + popW, cMin.y + cH);
@@ -740,7 +740,7 @@ void RenderSendConfirmPopup(App* app) {
float cy = cMin.y + Layout::spacingSm() + Layout::spacingXs();
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Amount");
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_amount"));
snprintf(buf, sizeof(buf), "%.8f %s", s_amount, DRAGONX_TICKER);
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
if (market.price_usd > 0) {
@@ -749,7 +749,7 @@ void RenderSendConfirmPopup(App* app) {
}
cy += rowStep;
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Fee");
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_fee"));
snprintf(buf, sizeof(buf), "%.8f %s", s_fee, DRAGONX_TICKER);
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
if (market.price_usd > 0) {
@@ -762,7 +762,7 @@ void RenderSendConfirmPopup(App* app) {
ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "confirm-divider-thickness").size);
cy += rowStep;
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Total");
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_total"));
snprintf(buf, sizeof(buf), "%.8f %s", total, DRAGONX_TICKER);
DrawTextShadow(popDl, sub1, sub1->LegacySize, ImVec2(cx + valX, cy), Primary(), buf);
if (market.price_usd > 0) {
@@ -774,7 +774,7 @@ void RenderSendConfirmPopup(App* app) {
}
if (s_memo[0] != '\0' && is_valid_z) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
Type().textColored(TypeStyle::Caption, OnSurface(), s_memo);
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
@@ -782,7 +782,7 @@ void RenderSendConfirmPopup(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (s_sending) {
Type().text(TypeStyle::Body2, "Sending...");
Type().text(TypeStyle::Body2, TR("sending"));
} else {
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
s_sending = true;
@@ -801,26 +801,26 @@ void RenderSendConfirmPopup(App* app) {
s_sending = false;
s_status_timestamp = ImGui::GetTime();
if (success) {
s_tx_status = "Transaction sent!";
s_tx_status = TR("send_tx_sent");
s_result_txid = result;
s_status_success = true;
Notifications::instance().success("Transaction sent successfully!");
Notifications::instance().success(TR("send_tx_success"));
s_to_address[0] = '\0';
s_amount = 0.0;
s_memo[0] = '\0';
s_send_max = false;
} else {
s_tx_status = "Error: " + result;
s_tx_status = std::string(TR("send_error_prefix")) + result;
s_result_txid.clear();
s_status_success = false;
Notifications::instance().error("Transaction failed: " + result);
Notifications::instance().error(std::string(TR("send_tx_failed")) + result);
}
}
);
s_show_confirm = false;
}
ImGui::SameLine();
if (TactileButton("Cancel", ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
s_show_confirm = false;
}
}
@@ -851,13 +851,13 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
float cx = ctaMin.x + Layout::spacingXl();
float cy = ctaMin.y + Layout::spacingLg();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), "Your wallet is empty");
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), TR("send_wallet_empty"));
cy += sub1->LegacySize + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(),
"Switch to Receive to get your address and start receiving funds.");
TR("send_switch_to_receive"));
cy += capFont->LegacySize + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
if (TactileButton("Go to Receive", ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
app->setCurrentPage(NavPage::Receive);
}
ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg()));
@@ -904,19 +904,19 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (!can_send && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (!app->isConnected())
ImGui::SetTooltip("Not connected to daemon");
ImGui::SetTooltip("%s", TR("send_tooltip_not_connected"));
else if (state.sync.syncing)
ImGui::SetTooltip("Blockchain is still syncing");
ImGui::SetTooltip("%s", TR("send_tooltip_syncing"));
else if (s_from_address[0] == '\0')
ImGui::SetTooltip("Select a source address above");
ImGui::SetTooltip("%s", TR("send_tooltip_select_source"));
else if (!is_valid_address)
ImGui::SetTooltip("Enter a valid recipient address");
ImGui::SetTooltip("%s", TR("send_tooltip_invalid_address"));
else if (s_amount <= 0)
ImGui::SetTooltip("Enter an amount to send");
ImGui::SetTooltip("%s", TR("send_tooltip_enter_amount"));
else if (total > available)
ImGui::SetTooltip("Amount exceeds available balance");
ImGui::SetTooltip("%s", TR("send_tooltip_exceeds_balance"));
else if (s_sending)
ImGui::SetTooltip("Transaction in progress...");
ImGui::SetTooltip("%s", TR("send_tooltip_in_progress"));
}
if (!can_send) ImGui::PopStyleColor(3);
@@ -946,14 +946,14 @@ static void RenderActionButtons(App* app, float width, float vScale,
s_clear_confirm_pending = false;
}
if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("Clear all form fields?");
ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing();
if (TactileButton("Yes, Clear", ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
ClearFormWithUndo();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton("Keep", ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
@@ -972,7 +972,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot();
Notifications::instance().info("Form restored");
Notifications::instance().info(TR("send_form_restored"));
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
@@ -989,7 +989,7 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
float width, ImFont* capFont, App* app) {
auto& S = schema::UI();
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT SENDS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recent_sends"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 avail = ImGui::GetContentRegionAvail();
@@ -1012,7 +1012,7 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
}
if (sends.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent sends");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
return;
}
@@ -1055,11 +1055,11 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
// Type label (first line)
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, "Sent");
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, TR("sent_upper"));
// Time (next to type)
std::string ago = timeAgo(tx.timestamp);
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Sent").x;
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("sent_upper")).x;
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
OnSurfaceDisabled(), ago.c_str());
@@ -1096,12 +1096,12 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = "Pending"; statusCol = Warning();
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < (int)S.drawElement("tabs.send", "confirmed-threshold").size) {
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else {
statusStr = "Confirmed"; statusCol = greenCol;
statusStr = TR("confirmed"); statusCol = greenCol;
}
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
float statusX = amtX - sSz.x - Layout::spacingXxl();
@@ -1238,7 +1238,7 @@ void RenderSendTab(App* app)
static bool s_paste_previewing = false;
static std::string s_preview_text;
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECIPIENT");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recipient"));
// Validation indicator — inline next to title (no height change)
// Check the preview text during hover, otherwise check actual address
@@ -1254,9 +1254,9 @@ void RenderSendTab(App* app)
if (vz || vt) {
ImGui::SameLine();
if (vz)
Type().textColored(TypeStyle::Caption, Success(), "Valid shielded address");
Type().textColored(TypeStyle::Caption, Success(), TR("send_valid_shielded"));
else
Type().textColored(TypeStyle::Caption, Warning(), "Valid transparent address");
Type().textColored(TypeStyle::Caption, Warning(), TR("send_valid_transparent"));
} else if (!checkPreview) {
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
@@ -1341,7 +1341,7 @@ void RenderSendTab(App* app)
// ---- AMOUNT ----
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_upper"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Toggle between DRGX and USD input
@@ -1439,7 +1439,7 @@ void RenderSendTab(App* app)
// Amount error
if (s_amount > 0 && s_amount + s_fee > available && available > 0) {
snprintf(buf, sizeof(buf), "Exceeds available (%.8f)", available - s_fee);
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
Type().textColored(TypeStyle::Caption, Error(), buf);
}
}
@@ -1455,7 +1455,7 @@ void RenderSendTab(App* app)
}
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.send", "memo-label-gap").size));
float memoInputH = std::max(S.drawElement("tabs.send", "memo-min-height").size, S.drawElement("tabs.send", "memo-base-height").size * vScale);
@@ -1502,7 +1502,7 @@ void RenderSendTab(App* app)
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
ImGui::Dummy(ImVec2(formW, 0));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Pass card bottom Y so error overlay can anchor to it
float cardBottom = containerMax.y;

View File

@@ -27,6 +27,14 @@
namespace dragonx {
namespace ui {
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
static std::string TrId(const char* tr_key, const char* id) {
std::string s = TR(tr_key);
s += "##";
s += id;
return s;
}
// Settings state - these get loaded from Settings on window open
static bool s_initialized = false;
static int s_language_index = 0;
@@ -141,17 +149,17 @@ void RenderSettingsWindow(App* app, bool* p_open)
auto saveBtn = S.button("dialogs.settings", "save-button");
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
if (!material::BeginOverlayDialog("Settings", p_open, win.width, 0.94f)) {
if (!material::BeginOverlayDialog(TR("settings"), p_open, win.width, 0.94f)) {
return;
}
if (ImGui::BeginTabBar("SettingsTabs")) {
// General settings tab
if (ImGui::BeginTabItem("General")) {
if (ImGui::BeginTabItem(TR("general"))) {
ImGui::Spacing();
// Skin/theme selection
ImGui::Text("Theme:");
ImGui::Text("%s", TR("theme"));
ImGui::SameLine(lbl.position);
// Active skin combo (populated from SkinManager)
@@ -172,7 +180,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
// Bundled themes header
ImGui::TextDisabled("Built-in");
ImGui::TextDisabled("%s", TR("settings_builtin"));
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
@@ -198,7 +206,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
if (has_custom) {
ImGui::Spacing();
ImGui::TextDisabled("Custom");
ImGui::TextDisabled("%s", TR("settings_custom"));
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
@@ -236,7 +244,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
// Show indicator if custom theme is active
if (active_is_custom) {
@@ -245,7 +253,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Custom theme active");
ImGui::SetTooltip("%s", TR("tt_custom_theme"));
}
}
@@ -257,14 +265,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
ImGui::SetTooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::Spacing();
// Language selection
ImGui::Text("Language:");
ImGui::Text("%s", TR("language"));
ImGui::SameLine(lbl.position);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
@@ -283,17 +291,17 @@ void RenderSettingsWindow(App* app, bool* p_open)
std::advance(it, s_language_index);
i18n.loadLanguage(it->first);
}
ImGui::TextDisabled(" Note: Some text requires restart to update");
ImGui::TextDisabled(" %s", TR("settings_language_note"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Acrylic Effects settings
ImGui::Text("Visual Effects");
ImGui::Text("%s", TR("settings_visual_effects"));
ImGui::Spacing();
ImGui::Text("Acrylic Level:");
ImGui::Text("%s", TR("settings_acrylic_level"));
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
@@ -309,10 +317,10 @@ void RenderSettingsWindow(App* app, bool* p_open)
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
}
}
ImGui::TextDisabled(" Blur amount (0%% = off, 100%% = maximum)");
ImGui::TextDisabled(" %s", TR("tt_blur"));
ImGui::Spacing();
ImGui::Text("Noise Opacity:");
ImGui::Text("%s", TR("settings_noise_opacity"));
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
@@ -326,86 +334,86 @@ void RenderSettingsWindow(App* app, bool* p_open)
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
}
}
ImGui::TextDisabled(" Grain texture intensity (0%% = off, 100%% = maximum)");
ImGui::TextDisabled(" %s", TR("tt_noise"));
ImGui::Spacing();
// Accessibility: Reduced transparency
if (ImGui::Checkbox("Reduce transparency", &s_reduced_transparency)) {
if (ImGui::Checkbox(TrId("settings_reduce_transparency", "reduce_trans").c_str(), &s_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
}
ImGui::TextDisabled(" Use solid colors instead of blur effects (accessibility)");
ImGui::TextDisabled(" %s", TR("settings_solid_colors_desc"));
ImGui::Spacing();
if (ImGui::Checkbox("Simple background", &s_gradient_background)) {
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_gradient_background);
}
ImGui::TextDisabled(" Replace textured backgrounds with smooth gradients");
ImGui::TextDisabled(" %s", TR("settings_gradient_desc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Privacy settings
ImGui::Text("Privacy");
ImGui::Text("%s", TR("settings_privacy"));
ImGui::Spacing();
ImGui::Checkbox("Save shielded transaction history locally", &s_save_ztxs);
ImGui::TextDisabled(" Stores z-addr transactions in a local file for viewing");
ImGui::Checkbox(TrId("settings_save_shielded_local", "save_ztx_w").c_str(), &s_save_ztxs);
ImGui::TextDisabled(" %s", TR("settings_save_shielded_desc"));
ImGui::Spacing();
ImGui::Checkbox("Auto-shield transparent funds", &s_auto_shield);
ImGui::TextDisabled(" Automatically move transparent funds to shielded addresses");
ImGui::Checkbox(TrId("settings_auto_shield_funds", "auto_shld_w").c_str(), &s_auto_shield);
ImGui::TextDisabled(" %s", TR("settings_auto_shield_desc"));
ImGui::Spacing();
ImGui::Checkbox("Use Tor for network connections", &s_use_tor);
ImGui::TextDisabled(" Route all connections through Tor for enhanced privacy");
ImGui::Checkbox(TrId("settings_use_tor_network", "tor_w").c_str(), &s_use_tor);
ImGui::TextDisabled(" %s", TR("settings_tor_desc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Other settings
ImGui::Text("Other");
ImGui::Text("%s", TR("settings_other"));
ImGui::Spacing();
ImGui::Checkbox("Allow custom transaction fees", &s_allow_custom_fees);
ImGui::Checkbox("Fetch price data from CoinGecko", &s_fetch_prices);
ImGui::Checkbox(TrId("custom_fees", "fees_w").c_str(), &s_allow_custom_fees);
ImGui::Checkbox(TrId("fetch_prices", "prices_w").c_str(), &s_fetch_prices);
ImGui::EndTabItem();
}
// Connection settings tab
if (ImGui::BeginTabItem("Connection")) {
if (ImGui::BeginTabItem(TR("settings_connection"))) {
ImGui::Spacing();
ImGui::Text("RPC Connection");
ImGui::TextDisabled("Configure connection to dragonxd daemon");
ImGui::Text("%s", TR("settings_rpc_connection"));
ImGui::TextDisabled("%s", TR("settings_configure_rpc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Host:");
ImGui::Text("%s", TR("rpc_host"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
ImGui::Text("Port:");
ImGui::Text("%s", TR("rpc_port"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(portInput.width);
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
ImGui::Spacing();
ImGui::Text("Username:");
ImGui::Text("%s", TR("rpc_user"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
ImGui::Text("Password:");
ImGui::Text("%s", TR("rpc_pass"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
@@ -415,11 +423,11 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("Note: Connection settings are usually auto-detected from DRAGONX.conf");
ImGui::TextDisabled("%s", TR("settings_rpc_note"));
ImGui::Spacing();
if (material::StyledButton("Test Connection", ImVec2(0,0), S.resolveFont("button"))) {
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
if (app->rpc()) {
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
@@ -439,15 +447,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
// Wallet tab
if (ImGui::BeginTabItem("Wallet")) {
if (ImGui::BeginTabItem(TR("wallet"))) {
ImGui::Spacing();
ImGui::Text("Wallet Maintenance");
ImGui::Text("%s", TR("settings_wallet_maintenance"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (material::StyledButton("Rescan Blockchain", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (material::StyledButton(TR("rescan"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (app->rpc()) {
// Start rescan from block 0
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
@@ -465,45 +473,41 @@ void RenderSettingsWindow(App* app, bool* p_open)
Notifications::instance().error("RPC client not initialized");
}
}
ImGui::TextDisabled(" Rescan blockchain for missing transactions");
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
ImGui::Spacing();
static bool s_confirm_clear_ztx = false;
if (material::StyledButton("Clear Saved Z-Transaction History", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (material::StyledButton(TR("settings_clear_ztx_long"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
s_confirm_clear_ztx = true;
}
ImGui::TextDisabled(" Delete locally stored shielded transaction data");
ImGui::TextDisabled(" %s", TR("settings_clear_ztx_desc"));
// Confirmation dialog
if (s_confirm_clear_ztx) {
if (material::BeginOverlayDialog("Confirm Clear Z-Tx History", &s_confirm_clear_ztx, 480.0f, 0.94f)) {
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), "Warning");
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped(
"Clearing z-transaction history may cause your shielded balance to show as 0 "
"until a wallet rescan is performed.");
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
ImGui::Spacing();
ImGui::TextWrapped(
"If this happens, you will need to re-import your z-address private keys with "
"rescan enabled to recover your balance.");
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
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("Clear Anyway", ImVec2(btnW, 40))) {
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_w").c_str(), ImVec2(btnW, 40))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
@@ -521,7 +525,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Wallet Info");
ImGui::Text("%s", TR("settings_wallet_info"));
ImGui::Spacing();
// Get actual wallet size
@@ -529,35 +533,35 @@ void RenderSettingsWindow(App* app, bool* p_open)
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::Text("Wallet file size: %s", size_str.c_str());
ImGui::Text(TR("settings_wallet_file_size"), size_str.c_str());
} else {
ImGui::TextDisabled("Wallet file not found");
ImGui::TextDisabled("%s", TR("settings_wallet_not_found"));
}
ImGui::Text("Wallet location: %s", wallet_path.c_str());
ImGui::Text(TR("settings_wallet_location"), wallet_path.c_str());
ImGui::EndTabItem();
}
// Explorer tab
if (ImGui::BeginTabItem("Explorer")) {
if (ImGui::BeginTabItem(TR("explorer"))) {
ImGui::Spacing();
ImGui::Text("Block Explorer URLs");
ImGui::TextDisabled("Configure external block explorer links");
ImGui::Text("%s", TR("settings_block_explorer_urls"));
ImGui::TextDisabled("%s", TR("settings_configure_explorer"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Transaction URL:");
ImGui::Text("%s", TR("transaction_url"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
ImGui::Text("Address URL:");
ImGui::Text("%s", TR("address_url"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
ImGui::Spacing();
ImGui::TextDisabled("URLs should include a trailing slash. The txid/address will be appended.");
ImGui::TextDisabled("%s", TR("settings_explorer_hint"));
ImGui::EndTabItem();
}
@@ -570,13 +574,13 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Spacing();
// Save/Cancel buttons
if (material::StyledButton("Save", ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
*p_open = false;
}
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
// Reload settings to revert changes
loadSettingsToUI(app->settings());
// Revert skin to what was active when settings opened

View File

@@ -76,24 +76,17 @@ void ShieldDialog::render(App* app)
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
const char* title = (s_mode == Mode::ShieldCoinbase)
? "Shield Coinbase Rewards"
: "Merge to Address";
? TR("shield_title")
: TR("merge_title");
if (material::BeginOverlayDialog(title, &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
// Description
if (s_mode == Mode::ShieldCoinbase) {
ImGui::TextWrapped(
"Shield your mining rewards by sending coinbase outputs from "
"transparent addresses to a shielded address. This improves "
"privacy by hiding your mining income."
);
ImGui::TextWrapped("%s", TR("shield_description"));
} else {
ImGui::TextWrapped(
"Merge multiple UTXOs into a single shielded address. This can "
"help reduce wallet size and improve privacy."
);
ImGui::TextWrapped("%s", TR("merge_description"));
}
ImGui::Spacing();
@@ -102,18 +95,18 @@ void ShieldDialog::render(App* app)
// From address (for shield coinbase)
if (s_mode == Mode::ShieldCoinbase) {
ImGui::Text("From Address:");
ImGui::Text("%s", TR("shield_from_address"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##FromAddr", s_from_address, sizeof(s_from_address));
ImGui::TextDisabled("Use '*' to shield from all transparent addresses");
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
ImGui::Spacing();
}
// To address (z-address dropdown)
ImGui::Text("To Address (Shielded):");
ImGui::Text("%s", TR("shield_to_address"));
// Get z-addresses for dropdown
std::string to_display = s_to_address[0] ? s_to_address : "Select z-address...";
std::string to_display = s_to_address[0] ? s_to_address : TR("shield_select_z");
if (to_display.length() > static_cast<size_t>(addrLbl.truncate)) {
to_display = to_display.substr(0, addrFrontLbl.truncate) + "..." + to_display.substr(to_display.length() - addrBackLbl.truncate);
}
@@ -142,7 +135,7 @@ void ShieldDialog::render(App* app)
ImGui::Spacing();
// Fee
ImGui::Text("Fee:");
ImGui::Text("%s", TR("fee_label"));
ImGui::SetNextItemWidth(feeInput.width);
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
ImGui::SameLine();
@@ -151,11 +144,11 @@ void ShieldDialog::render(App* app)
ImGui::Spacing();
// UTXO limit
ImGui::Text("UTXO Limit:");
ImGui::Text("%s", TR("shield_utxo_limit"));
ImGui::SetNextItemWidth(utxoInput.width);
ImGui::InputInt("##Limit", &s_utxo_limit);
ImGui::SameLine();
ImGui::TextDisabled("Max UTXOs per operation");
ImGui::TextDisabled("%s", TR("shield_max_utxos"));
if (s_utxo_limit < 1) s_utxo_limit = 1;
if (s_utxo_limit > 100) s_utxo_limit = 100;
@@ -178,7 +171,7 @@ void ShieldDialog::render(App* app)
if (!can_submit) ImGui::BeginDisabled();
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? "Shield Funds" : "Merge Funds";
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
if (material::StyledButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
s_operation_pending = true;
s_status_message = "Submitting operation...";
@@ -201,7 +194,7 @@ void ShieldDialog::render(App* app)
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = "Operation submitted: " + s_operation_id;
Notifications::instance().success("Shield operation started");
Notifications::instance().success(TR("shield_started"));
} else {
s_status_message = "Error: " + error;
Notifications::instance().error("Shield failed: " + error);
@@ -231,7 +224,7 @@ void ShieldDialog::render(App* app)
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = "Operation submitted: " + s_operation_id;
Notifications::instance().success("Merge operation started");
Notifications::instance().success(TR("merge_started"));
} else {
s_status_message = "Error: " + error;
Notifications::instance().error("Merge failed: " + error);
@@ -246,7 +239,7 @@ void ShieldDialog::render(App* app)
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
s_open = false;
}
@@ -256,9 +249,9 @@ void ShieldDialog::render(App* app)
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Operation ID: %s", s_operation_id.c_str());
ImGui::Text(TR("shield_operation_id"), s_operation_id.c_str());
if (material::StyledButton("Check Status", ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
if (material::StyledButton(TR("shield_check_status"), ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
std::string opid = s_operation_id;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
@@ -276,14 +269,14 @@ void ShieldDialog::render(App* app)
auto& op = result[0];
std::string status = op.value("status", "unknown");
if (status == "success") {
s_status_message = "Operation completed successfully!";
Notifications::instance().success("Shield/merge completed!");
s_status_message = TR("shield_completed");
Notifications::instance().success(TR("shield_merge_done"));
} else if (status == "failed") {
std::string errMsg = op.value("error", nlohmann::json{}).value("message", "Unknown error");
s_status_message = "Operation failed: " + errMsg;
Notifications::instance().error("Operation failed: " + errMsg);
} else if (status == "executing") {
s_status_message = "Operation in progress...";
s_status_message = TR("shield_in_progress");
} else {
s_status_message = "Status: " + status;
}

View File

@@ -44,7 +44,7 @@ void TransactionDetailsDialog::render(App* app)
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
auto bottomBtn = S.button("dialogs.transaction-details", "bottom-button");
if (material::BeginOverlayDialog("Transaction Details", &s_open, win.width, 0.94f)) {
if (material::BeginOverlayDialog(TR("tx_details_title"), &s_open, win.width, 0.94f)) {
const auto& tx = s_transaction;
// Type indicator with color
@@ -52,16 +52,16 @@ void TransactionDetailsDialog::render(App* app)
std::string type_display;
if (tx.type == "receive") {
type_color = ImVec4(0.3f, 0.8f, 0.3f, 1.0f);
type_display = "RECEIVED";
type_display = TR("tx_received");
} else if (tx.type == "send") {
type_color = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
type_display = "SENT";
type_display = TR("tx_sent");
} else if (tx.type == "generate" || tx.type == "mined") {
type_color = ImVec4(0.3f, 0.6f, 0.9f, 1.0f);
type_display = "MINED";
type_display = TR("tx_mined");
} else if (tx.type == "immature") {
type_color = ImVec4(0.8f, 0.8f, 0.3f, 1.0f);
type_display = "IMMATURE";
type_display = TR("tx_immature");
} else {
type_color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
type_display = tx.type;
@@ -72,11 +72,11 @@ void TransactionDetailsDialog::render(App* app)
// Confirmations badge
if (tx.confirmations == 0) {
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Pending");
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "%s", TR("pending"));
} else if (tx.confirmations < 10) {
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), "%d confirmations", tx.confirmations);
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), TR("tx_confirmations"), tx.confirmations);
} else {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%d confirmations", tx.confirmations);
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), TR("tx_confirmations"), tx.confirmations);
}
ImGui::Spacing();
@@ -84,7 +84,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::Spacing();
// Amount (prominent display)
ImGui::Text("Amount:");
ImGui::Text("%s", TR("amount_label"));
ImGui::SameLine(lbl.position);
if (tx.amount >= 0) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "+%.8f DRGX", tx.amount);
@@ -102,7 +102,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::Spacing();
// Date/Time
ImGui::Text("Date:");
ImGui::Text("%s", TR("date_label"));
ImGui::SameLine(lbl.position);
ImGui::Text("%s", tx.getTimeString().c_str());
@@ -111,7 +111,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::Spacing();
// Transaction ID
ImGui::Text("Transaction ID:");
ImGui::Text("%s", TR("tx_id_label"));
char txid_buf[128];
strncpy(txid_buf, tx.txid.c_str(), sizeof(txid_buf) - 1);
txid_buf[sizeof(txid_buf) - 1] = '\0';
@@ -126,7 +126,7 @@ void TransactionDetailsDialog::render(App* app)
// Address
if (!tx.address.empty()) {
ImGui::Text(tx.type == "send" ? "To Address:" : "From Address:");
ImGui::Text("%s", tx.type == "send" ? TR("tx_to_address") : TR("tx_from_address"));
// Use multiline for z-addresses
if (tx.address.length() > 50) {
@@ -155,7 +155,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Memo:");
ImGui::Text("%s", TR("memo_label"));
char memo_buf[512];
strncpy(memo_buf, tx.memo.c_str(), sizeof(memo_buf) - 1);
memo_buf[sizeof(memo_buf) - 1] = '\0';
@@ -173,7 +173,7 @@ void TransactionDetailsDialog::render(App* app)
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
ImGui::SetCursorPosX(start_x);
if (material::StyledButton("View on Explorer", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
// Platform-specific URL opening
#ifdef _WIN32
@@ -189,7 +189,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();

View File

@@ -6,6 +6,7 @@
#include "transaction_details_dialog.h"
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../config/settings.h"
#include "../../config/version.h"
#include "../theme.h"
@@ -28,6 +29,10 @@ namespace ui {
using namespace material;
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;
@@ -66,7 +71,7 @@ struct DisplayTx {
};
std::string DisplayTx::getTimeString() const {
if (timestamp <= 0) return "Pending";
if (timestamp <= 0) return TR("pending");
std::time_t t = static_cast<std::time_t>(timestamp);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", std::localtime(&t));
@@ -79,10 +84,11 @@ static std::string timeAgo(int64_t timestamp) {
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
char buf[32];
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
}
// Draw a small transaction-type icon
@@ -191,10 +197,10 @@ void RenderTransactionsTab(App* app)
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "RECEIVED");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", recvCount);
snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
@@ -221,10 +227,10 @@ void RenderTransactionsTab(App* app)
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "SENT");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", sendCount);
snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
@@ -251,10 +257,10 @@ void RenderTransactionsTab(App* app)
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "MINED");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", minedCount);
snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
@@ -292,23 +298,23 @@ void RenderTransactionsTab(App* app)
ImGui::SetNextItemWidth(searchWidth);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
ImGui::InputTextWithHint("##TxSearch", "Search...", search_filter, sizeof(search_filter));
ImGui::InputTextWithHint("##TxSearch", TR("search_placeholder"), search_filter, sizeof(search_filter));
ImGui::PopStyleVar();
float filterGap = std::max(8.0f, ((filterGapEl.size > 0) ? filterGapEl.size : 20.0f) * hs);
ImGui::SameLine(0, filterGap);
float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
ImGui::SetNextItemWidth(comboW);
const char* types[] = { "All", "Sent", "Received", "Mined" };
const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") };
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
ImGui::SameLine(0, filterGap);
if (TactileButton("Refresh", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("refresh", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
app->refreshNow();
}
ImGui::SameLine(0, filterGap);
if (TactileButton("Export CSV", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("export_csv", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
ExportTransactionsDialog::show();
}
@@ -442,7 +448,7 @@ void RenderTransactionsTab(App* app)
// ---- Heading line: "TRANSACTIONS" left, pagination right ----
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTIONS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transactions_upper"));
if (totalPages > 1) {
float paginationH = ImGui::GetFrameHeight();
@@ -557,13 +563,13 @@ void RenderTransactionsTab(App* app)
{
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.transactions.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No transactions found");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
} else if (filtered_indices.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No matching transactions");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
} else {
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float innerW = ImGui::GetContentRegionAvail().x;
@@ -592,15 +598,15 @@ void RenderTransactionsTab(App* app)
ImU32 iconCol;
const char* typeStr;
if (tx.display_type == "shield") {
iconCol = Primary(); typeStr = "Shielded";
iconCol = Primary(); typeStr = TR("shielded_type");
} else if (tx.display_type == "receive") {
iconCol = greenCol; typeStr = "Recv";
iconCol = greenCol; typeStr = TR("recv_type");
} else if (tx.display_type == "send") {
iconCol = redCol; typeStr = "Sent";
iconCol = redCol; typeStr = TR("sent_type");
} else if (tx.display_type == "immature") {
iconCol = Warning(); typeStr = "Immature";
iconCol = Warning(); typeStr = TR("immature_type");
} else {
iconCol = goldCol; typeStr = "Mined";
iconCol = goldCol; typeStr = TR("mined_type");
}
// Expanded selection accent
@@ -669,14 +675,14 @@ void RenderTransactionsTab(App* app)
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = "Pending"; statusCol = Warning();
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < 10) {
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
statusStr = "Mature"; statusCol = greenCol;
statusStr = TR("mature"); statusCol = greenCol;
} else {
statusStr = "Confirmed"; statusCol = WithAlpha(Success(), 140);
statusStr = TR("confirmed"); statusCol = WithAlpha(Success(), 140);
}
// Position status badge in the middle-right area
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
@@ -707,14 +713,14 @@ void RenderTransactionsTab(App* app)
// Context menu
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
if (ImGui::MenuItem("Copy Address") && !tx.address.empty()) {
if (ImGui::MenuItem(TR("copy_address")) && !tx.address.empty()) {
ImGui::SetClipboardText(tx.address.c_str());
}
if (ImGui::MenuItem("Copy TxID")) {
if (ImGui::MenuItem(TR("copy_txid"))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::Separator();
if (ImGui::MenuItem("View on Explorer")) {
if (ImGui::MenuItem(TR("view_on_explorer"))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
@@ -726,7 +732,7 @@ void RenderTransactionsTab(App* app)
system(cmd.c_str());
#endif
}
if (ImGui::MenuItem("View Details")) {
if (ImGui::MenuItem(TR("view_details"))) {
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
}
@@ -747,19 +753,19 @@ void RenderTransactionsTab(App* app)
// From address
if (!tx.from_address.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
ImGui::TextWrapped("%s", tx.from_address.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// To address
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
tx.display_type == "send" ? "TO" : (tx.display_type == "shield" ? "SHIELDED TO" : "ADDRESS"));
tx.display_type == "send" ? TR("to_upper") : (tx.display_type == "shield" ? TR("shielded_to") : TR("address_upper")));
ImGui::TextWrapped("%s", tx.address.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// TxID (full, copyable)
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTION ID");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transaction_id"));
ImGui::TextWrapped("%s", tx.txid.c_str());
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsItemClicked()) ImGui::SetClipboardText(tx.txid.c_str());
@@ -767,13 +773,13 @@ void RenderTransactionsTab(App* app)
// Memo
if (!tx.memo.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
ImGui::TextWrapped("%s", tx.memo.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// Confirmations + time
snprintf(buf, sizeof(buf), "%d confirmations | %s",
snprintf(buf, sizeof(buf), TR("confirmations_display"),
tx.confirmations, tx.getTimeString().c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
@@ -782,15 +788,15 @@ void RenderTransactionsTab(App* app)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
if (TactileSmallButton("Copy TxID##detail", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("copy_txid", "detail").c_str(), S.resolveFont("button"))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::SameLine();
if (!tx.address.empty() && TactileSmallButton("Copy Address##detail", S.resolveFont("button"))) {
if (!tx.address.empty() && TactileSmallButton(TrId("copy_address", "detail").c_str(), S.resolveFont("button"))) {
ImGui::SetClipboardText(tx.address.c_str());
}
ImGui::SameLine();
if (TactileSmallButton("Explorer##detail", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
@@ -803,7 +809,7 @@ void RenderTransactionsTab(App* app)
#endif
}
ImGui::SameLine();
if (TactileSmallButton("Full Details##detail", S.resolveFont("button"))) {
if (TactileSmallButton(TrId("full_details", "detail").c_str(), S.resolveFont("button"))) {
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
}
@@ -849,7 +855,7 @@ void RenderTransactionsTab(App* app)
}
// Status line with page info
snprintf(buf, sizeof(buf), "Showing %d\xe2\x80\x93%d of %d transactions (total: %zu)",
snprintf(buf, sizeof(buf), TR("showing_transactions"),
filtered_count > 0 ? pageStart + 1 : 0, pageEnd, filtered_count, state.transactions.size());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
}

View File

@@ -6,6 +6,7 @@
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
@@ -52,15 +53,15 @@ void ValidateAddressDialog::render(App* app)
auto lbl = S.label("dialogs.validate-address", "label");
auto closeBtn = S.button("dialogs.validate-address", "close-button");
if (material::BeginOverlayDialog("Validate Address", &s_open, win.width, 0.94f)) {
ImGui::TextWrapped("Enter a DragonX address to check if it's valid and whether it belongs to this wallet.");
if (material::BeginOverlayDialog(TR("validate_title"), &s_open, win.width, 0.94f)) {
ImGui::TextWrapped("%s", TR("validate_description"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address input
ImGui::Text("Address:");
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(-1);
bool enter_pressed = ImGui::InputText("##ValidateAddr", s_address_input, sizeof(s_address_input),
ImGuiInputTextFlags_EnterReturnsTrue);
@@ -74,7 +75,7 @@ void ValidateAddressDialog::render(App* app)
ImGui::BeginDisabled();
}
if (material::StyledButton("Validate", ImVec2(valBtn.width, 0), S.resolveFont(valBtn.font)) || (enter_pressed && can_validate)) {
if (material::StyledButton(TR("validate_btn"), ImVec2(valBtn.width, 0), S.resolveFont(valBtn.font)) || (enter_pressed && can_validate)) {
s_validating = true;
s_validated = false;
s_error_message.clear();
@@ -100,7 +101,7 @@ void ValidateAddressDialog::render(App* app)
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = "Shielded (z-address)";
s_address_type = TR("validate_shielded_type");
} else {
s_error_message = error;
s_is_valid = false;
@@ -126,7 +127,7 @@ void ValidateAddressDialog::render(App* app)
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = "Transparent (t-address)";
s_address_type = TR("validate_transparent_type");
} else {
s_error_message = error;
s_is_valid = false;
@@ -145,7 +146,7 @@ void ValidateAddressDialog::render(App* app)
ImGui::SameLine();
if (material::StyledButton("Paste", ImVec2(pasteBtn.width, 0), S.resolveFont(pasteBtn.font))) {
if (material::StyledButton(TR("paste"), ImVec2(pasteBtn.width, 0), S.resolveFont(pasteBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_address_input, clipboard, sizeof(s_address_input) - 1);
@@ -156,7 +157,7 @@ void ValidateAddressDialog::render(App* app)
if (s_validating) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Validating...");
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("validating"));
}
ImGui::Spacing();
@@ -165,39 +166,39 @@ void ValidateAddressDialog::render(App* app)
// Results
if (s_validated) {
ImGui::Text("Results:");
ImGui::Text("%s", TR("validate_results"));
ImGui::Spacing();
if (!s_error_message.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Error: %s", s_error_message.c_str());
} else {
// Valid/Invalid indicator
ImGui::Text("Status:");
ImGui::Text("%s", TR("validate_status"));
ImGui::SameLine(lbl.position);
if (s_is_valid) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "VALID");
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_valid"));
} else {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "INVALID");
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("validate_invalid"));
}
if (s_is_valid) {
// Address type
ImGui::Text("Type:");
ImGui::Text("%s", TR("validate_type"));
ImGui::SameLine(lbl.position);
ImGui::Text("%s", s_address_type.c_str());
// Is mine?
ImGui::Text("Ownership:");
ImGui::Text("%s", TR("validate_ownership"));
ImGui::SameLine(lbl.position);
if (s_is_mine) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "This wallet owns this address");
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
} else {
ImGui::TextDisabled("Not owned by this wallet");
ImGui::TextDisabled("%s", TR("validate_not_mine"));
}
}
}
} else if (!app->isConnected()) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Not connected to daemon");
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected"));
}
ImGui::Spacing();
@@ -205,7 +206,7 @@ void ValidateAddressDialog::render(App* app)
// Close button at bottom
float button_width = closeBtn.width;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) / 2.0f);
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();