From e24ca015d15d794d7cf917e3f91e0d39ab7d2c4d Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 18 Aug 2026 17:54:15 -0500 Subject: [PATCH] =?UTF-8?q?feat(ui):=20implement=20UI/UX=20audit=20?= =?UTF-8?q?=E2=80=94=20i18n,=20HiDPI,=20theme,=20destructive-action=20&=20?= =?UTF-8?q?overflow=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements all 26 confirmed UI/UX audit findings plus the 4 dashboard/timeline tile labels. Verified across full-node + Lite + Windows builds (ctest green) and an adversarial diff review (one market-tab delete regression caught + fixed). i18n coverage: - Balance hero/quick-actions/toasts + dashboard & timeline tiles (Total Balance, Shielded, Transparent, Quick Send, Quick Receive, Click to open, Market) - Send: Review Send / Cancel / Paste / view-only tooltip / memo byte counter - Settings: Lite lifecycle errors, plaintext-RPC security warning, 8 toasts - Mining pool-payout tooltip - Resolve daemon_update_title double-assignment collision (new daemon_update_prompt_title) - 23 new keys translated into all 8 locales; CJK subset font rebuilt HiDPI: DPI-scale the Send amount bar, mining thread-tile clamp bounds, receive loading skeleton, and sidebar notification-badge insets. Light theme: theme-aware material::SurfaceOverlay() for balance bar tracks and row-hover highlights; contacts active-pill foreground uses OnPrimary(). Destructive actions: arm/confirm for portfolio-group delete, saved pool/worker remove, and avatar-image delete. Interaction/overflow: route balance star/eye buttons and the console fold-toggle through popup-safe guards; clip peer addr/subver; truncate the balance custom label; measure Settings tool-button widths; mining stepper disabled-state feedback; +/- stepper buttons restyled to match the thread tiles. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 671092 -> 671376 bytes res/lang/de.json | 24 ++++++ res/lang/es.json | 24 ++++++ res/lang/fr.json | 24 ++++++ res/lang/ja.json | 24 ++++++ res/lang/ko.json | 24 ++++++ res/lang/pt.json | 24 ++++++ res/lang/ru.json | 24 ++++++ res/lang/zh.json | 24 ++++++ src/app.cpp | 2 +- src/ui/material/colors.h | 20 +++++ src/ui/pages/settings_page.cpp | 55 +++++++++---- src/ui/sidebar.h | 6 +- src/ui/windows/balance_components.cpp | 30 ++++++-- src/ui/windows/balance_tab.cpp | 106 ++++++++++++++++++-------- src/ui/windows/console_tab.cpp | 3 + src/ui/windows/contacts_tab.cpp | 24 ++++-- src/ui/windows/market_tab.cpp | 31 +++++++- src/ui/windows/mining_controls.cpp | 56 +++++++++----- src/ui/windows/mining_mode_toggle.cpp | 96 +++++++++++++++-------- src/ui/windows/peers_tab.cpp | 63 +++++++++++---- src/ui/windows/receive_tab.cpp | 20 +++-- src/ui/windows/send_tab.cpp | 18 +++-- src/util/i18n.cpp | 25 +++++- 24 files changed, 599 insertions(+), 148 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 955157817d2cad9dfb6c0a8c38449b4cf8afbd6d..2bb3213d7758faa87984afefe439eeadac89efdc 100644 GIT binary patch delta 12360 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 delta 12268 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 IM_COL32_R_SHIFT) & 0xFF) / 255.0f; + float g = ((bg >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f; + float b = ((bg >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f; + bool light = (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f; + return light ? IM_COL32(0, 0, 0, alpha) : IM_COL32(255, 255, 255, alpha); +} + /** * @brief Get color with applied state overlay * diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 237c89d..21b65c8 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -309,7 +309,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) { std::string trimmedPath(s_settingsState.lite_wallet_path); const auto first = trimmedPath.find_first_not_of(" \t\r\n"); if (first == std::string::npos) { - s_settingsState.lite_lifecycle_status = "Enter a wallet path"; + s_settingsState.lite_lifecycle_status = TR("lite_enter_wallet_path"); s_settingsState.lite_lifecycle_summary.clear(); return; } @@ -320,8 +320,9 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) { if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) { const int words = liteSeedWordCount(s_settingsState.lite_restore_seed); if (!dragonx::util::isCompleteRecoveryPhrase(words)) { - s_settingsState.lite_lifecycle_status = - "Enter all 24 seed words to restore (got " + std::to_string(words) + ")"; + char seedBuf[128]; + snprintf(seedBuf, sizeof(seedBuf), TR("lite_enter_all_seed_words"), words); + s_settingsState.lite_lifecycle_status = seedBuf; s_settingsState.lite_lifecycle_summary.clear(); return; } @@ -353,7 +354,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) { // Rejected before any thread launched (wallet already open, an attempt in flight, or no // usable server). The controller's status carries the reason. s_settingsState.lite_lifecycle_status = lite->status().message.empty() - ? "Could not start the operation" + ? TR("lite_could_not_start") : lite->status().message; Notifications::instance().warning(s_settingsState.lite_lifecycle_status); } @@ -364,7 +365,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) { // failed to load / rollout-disabled). The live path above returns when a backend is // present, so reaching here means there is nothing to run. s_settingsState.lite_lifecycle_summary.clear(); - s_settingsState.lite_lifecycle_status = "Lite wallet backend unavailable"; + s_settingsState.lite_lifecycle_status = TR("lite_backend_unavailable"); Notifications::instance().warning(s_settingsState.lite_lifecycle_status); } @@ -819,7 +820,7 @@ void RenderSettingsPage(App* app) { ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); - Notifications::instance().info("Theme list refreshed"); + Notifications::instance().info(TR("settings_theme_refreshed")); } if (ImGui::IsItemHovered()) { material::Tooltip(TR("tt_scan_themes"), @@ -1064,7 +1065,7 @@ void RenderSettingsPage(App* app) { ImGui::SameLine(); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); - Notifications::instance().info("Theme list refreshed"); + Notifications::instance().info(TR("settings_theme_refreshed")); } if (ImGui::IsItemHovered()) { material::Tooltip(TR("tt_scan_themes"), @@ -1386,6 +1387,30 @@ void RenderSettingsPage(App* app) { float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow; float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f); bw = std::max(minBtnW, bw); + // Grow the column so the longest translated label (e.g. German) isn't clipped inside + // ImGui::Button. Measure every label with the actual button font (LegacySize is already + // DPI-scaled — don't scale it again) and add the button's own FramePadding on both sides; + // if that exceeds bw, drop to fewer columns rather than overflow the card width. + { + ImFont* toolsFont = S.resolveFont("button"); + if (!toolsFont) toolsFont = Type().button(); + const char* toolLabels[] = { + TR("settings_address_book"), TR("settings_validate_address"), + TR("settings_request_payment"), TR("settings_shield_mining"), + TR("settings_merge_to_address"), TR("settings_clear_ztx"), + }; + float widestLabel = 0.0f; + for (const char* lbl : toolLabels) + widestLabel = std::max(widestLabel, + toolsFont->CalcTextSizeA(toolsFont->LegacySize, FLT_MAX, 0, lbl).x); + const float needW = widestLabel + ImGui::GetStyle().FramePadding.x * 2.0f + + 8.0f * Layout::dpiScale(); + // Shed columns until the widest label fits (or we're down to a single column). + while (btnsPerRow > 1 && + ((contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow) < needW) + --btnsPerRow; + bw = std::max({minBtnW, (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow, needW}); + } if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button"))) app->setCurrentPage(ui::NavPage::Contacts); // now a top-level tab @@ -2225,7 +2250,7 @@ void RenderSettingsPage(App* app) { if (s_settingsState.rpc_plaintext_remote) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); ImGui::PushTextWrapPos(sectionOrigin.x + contentW); - ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS."); + ImGui::TextWrapped("%s", TR("rpc_plaintext_remote_warning")); ImGui::PopTextWrapPos(); ImGui::PopStyleColor(); } @@ -2367,14 +2392,14 @@ void RenderSettingsPage(App* app) { try { rpc::RPCClient::TraceScope trace("Settings / Test connection"); rpc->call("getinfo"); - return []() { Notifications::instance().success("RPC connection OK"); }; + return []() { Notifications::instance().success(TR("settings_rpc_ok")); }; } catch (const std::exception& e) { std::string err = e.what(); - return [err]() { Notifications::instance().error("RPC error: " + err); }; + return [err]() { Notifications::instance().error(std::string(TR("settings_rpc_error_prefix")) + err); }; } }); } else { - Notifications::instance().warning("Not connected to daemon"); + Notifications::instance().warning(TR("settings_not_connected")); } } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); @@ -2587,14 +2612,14 @@ void RenderSettingsPage(App* app) { if (aboutGrid) ImGui::Dummy(ImVec2(0, Layout::spacingXs())); else ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { saveSettingsPageState(app->settings()); - Notifications::instance().success("Settings saved"); + Notifications::instance().success(TR("settings_saved")); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { if (app->settings()) { loadSettingsPageState(app->settings()); - Notifications::instance().info("Settings reloaded from disk"); + Notifications::instance().info(TR("settings_reloaded")); } } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings")); @@ -2913,9 +2938,9 @@ void RenderSettingsPage(App* app) { if (doConfirm) { std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; if (util::Platform::deleteFile(ztx_file)) { - Notifications::instance().success("Z-transaction history cleared"); + Notifications::instance().success(TR("settings_ztx_cleared")); } else { - Notifications::instance().info("No history file found"); + Notifications::instance().info(TR("settings_ztx_not_found")); } s_settingsState.confirm_clear_ztx = false; } diff --git a/src/ui/sidebar.h b/src/ui/sidebar.h index 14a56d4..e07ada9 100644 --- a/src/ui/sidebar.h +++ b/src/ui/sidebar.h @@ -722,8 +722,10 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei if (badgeCount > 0 || dotOnly) { float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber; - float bx = indMax.x - badgeR - 6.0f; - float by = indMin.y + badgeR + 5.0f; + float badgeInsetX = sde("badge-inset-x", 6.0f); + float badgeInsetY = sde("badge-inset-y", 5.0f); + float bx = indMax.x - badgeR - badgeInsetX; + float by = indMin.y + badgeR + badgeInsetY; dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol); if (!dotOnly && showLabels) { char buf[16]; diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 2a836bf..4ef26d8 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -466,7 +466,8 @@ void RenderSharedAddressList(App* app, float listH, float availW, { const auto& starRect = rowLayout.favoriteButton; ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height); - bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax); + // material::IsRectHovered so the button inherits overlay/popup input blocking + bool bHov = material::IsRectHovered(bMin, bMax); dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound); dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp); ImFont* iconFont = Type().iconSmall(); @@ -492,7 +493,8 @@ void RenderSharedAddressList(App* app, float listH, float availW, if (showEye) { const auto& eyeRect = rowLayout.visibilityButton; ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height); - bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax); + // material::IsRectHovered so the button inherits overlay/popup input blocking + bool bHov = material::IsRectHovered(bMin, bMax); dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound); dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp); ImFont* iconFont = Type().iconSmall(); @@ -539,12 +541,24 @@ void RenderSharedAddressList(App* app, float listH, float availW, snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag); dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf); - // User label next to type + // User label next to type — clip to the gap before the right-aligned + // balance so a long custom label can't overrun the balance on this line + // (mirrors the address-line width guard below). if (!row.label.empty()) { float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x; - dl->AddText(capFont, capFont->LegacySize, - ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy), - OnSurfaceMedium(), row.label.c_str()); + float userLabelX = labelX + typeLabelW + Layout::spacingLg(); + // Balance is drawn right-aligned at contentRight; recompute its left edge here. + char balBufPeek[32]; + snprintf(balBufPeek, sizeof(balBufPeek), "%.8f", addr.balance); + float balW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBufPeek).x; + float userLabelAvailW = (contentRight - balW - Layout::spacingMd()) - userLabelX; + std::string userLabel = material::TruncateToWidth( + row.label, capFont, capFont->LegacySize, userLabelAvailW); + if (userLabelAvailW > 0.0f) { + dl->AddText(capFont, capFont->LegacySize, + ImVec2(userLabelX, cy), + OnSurfaceMedium(), userLabel.c_str()); + } } } @@ -841,7 +855,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float float rowW = ImGui::GetContentRegionAvail().x; ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH); if (material::IsRectHovered(rowPos, rowEnd)) { - dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f)); + dl->AddRectFilled(rowPos, rowEnd, SurfaceOverlay(15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) app->setCurrentPage(NavPage::History); @@ -868,7 +882,7 @@ void RenderSyncBar(App* app, ImDrawList* dl, float vs) { ImVec2 barPos = ImGui::GetCursorScreenPos(); dl->AddRectFilled(barPos, ImVec2(barPos.x + barW, barPos.y + barH), - IM_COL32(255, 255, 255, 15), 1.0f * dp); + SurfaceOverlay(15), 1.0f * dp); dl->AddRectFilled(barPos, ImVec2(barPos.x + barW * prog, barPos.y + barH), WithAlpha(Warning(), 200), 1.0f * dp); diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index 1e07228..3bfde21 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -31,6 +31,7 @@ #include "imgui.h" #include #include +#include #include #include #include @@ -196,7 +197,9 @@ void RenderBalanceTab(App* app) for (const auto& l : allLayouts) { if (l.id == layoutId) { displayName = l.name; break; } } - Notifications::instance().info("Layout: " + displayName); + char layoutToast[128]; + snprintf(layoutToast, sizeof(layoutToast), TR("balance_layout_switched"), displayName.c_str()); + Notifications::instance().info(layoutToast); } } } @@ -359,8 +362,11 @@ static void RenderBalanceClassic(App* app) IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.classic", "logo-opacity").sizeOr(180.0f))); } + std::string totalLabelUpper = TR("total_balance_label"); + std::transform(totalLabelUpper.begin(), totalLabelUpper.end(), totalLabelUpper.begin(), + [](unsigned char c){ return (char)std::toupper(c); }); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), - OnSurfaceMedium(), "TOTAL BALANCE"); + OnSurfaceMedium(), totalLabelUpper.c_str()); cy += ovFont->LegacySize + ovGap; snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); @@ -387,7 +393,7 @@ static void RenderBalanceClassic(App* app) // Sync progress or mining indicator (whichever fits) if (state.sync.syncing && state.sync.headers > 0) { float pct = static_cast(state.sync.verification_progress) * 100.0f; - snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct); + snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), Warning(), buf); @@ -401,7 +407,7 @@ static void RenderBalanceClassic(App* app) dl->PushClipRect(ImVec2(cMin.x, barTop), cMax, true); // Background track dl->AddRectFilled(cMin, cMax, - IM_COL32(255, 255, 255, 15), cardSpec.rounding); + SurfaceOverlay(15), cardSpec.rounding); // Progress fill — additional horizontal clip float progRight = cMin.x + (cMax.x - cMin.x) * prog; dl->PushClipRect(ImVec2(cMin.x, barTop), ImVec2(progRight, cMax.y), true); @@ -418,7 +424,11 @@ static void RenderBalanceClassic(App* app) dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f), S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol); double hr = state.mining.localHashrate; - snprintf(buf, sizeof(buf), " Mining %s", FormatHashrate(hr).c_str()); + // Leading indent clears the mining dot drawn at cx+4dp; the text + // itself starts at cx+12dp so keep the two spaces for spacing parity. + char mineFmt[64]; + snprintf(mineFmt, sizeof(mineFmt), " %s", TR("balance_mining_rate")); + snprintf(buf, sizeof(buf), mineFmt, FormatHashrate(hr).c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), WithAlpha(Success(), 200), buf); @@ -574,15 +584,18 @@ static void RenderBalanceClassic(App* app) ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "USD"); // Measure widest text line to determine sparkline left edge + std::string marketLabel = TR("market"); + std::transform(marketLabel.begin(), marketLabel.end(), marketLabel.begin(), + [](unsigned char c){ return (char)std::toupper(c); }); float textW = std::max(pSz.x + tickGap + usdSz.x, - ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, "MARKET").x); + ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x); float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f); float sparkLeft = cx + textW + sparkGap; float sparkRight = cMax.x - cardPadLg; // Left side: label + price + 24h change dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), - OnSurfaceMedium(), "MARKET"); + OnSurfaceMedium(), marketLabel.c_str()); cy += ovFont->LegacySize + ovGap; dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), @@ -682,7 +695,7 @@ static void RenderBalanceDonut(App* app) { else ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.donut", "hero-pad-ratio").sizeOr(8.0f) * vs)); { - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE"); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label")); ImGui::Dummy(ImVec2(0, 2 * dp)); snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); ImFont* heroFont = Type().h2(); @@ -947,10 +960,13 @@ static void RenderBalanceConsolidated(App* app) { // Shielded bar float shieldX = cardMin.x + pad; - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), "SHIELDED"); + std::string shieldLabelUpper = TR("shielded"); + std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(), + [](unsigned char c){ return (char)std::toupper(c); }); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), shieldLabelUpper.c_str()); barY += ovFont->LegacySize + 4 * dp; dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW, barY + barH), - IM_COL32(255, 255, 255, 15), barH * 0.5f); + SurfaceOverlay(15), barH * 0.5f); dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW * shieldRatio, barY + barH), WithAlpha(Success(), 180), barH * 0.5f); barY += barH + 2 * dp; @@ -960,10 +976,13 @@ static void RenderBalanceConsolidated(App* app) { // Transparent bar float transX = cardMin.x + pad * 2 + halfW; barY = divY + Layout::spacingSm(); - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), "TRANSPARENT"); + std::string transLabelUpper = TR("transparent"); + std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(), + [](unsigned char c){ return (char)std::toupper(c); }); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), transLabelUpper.c_str()); barY += ovFont->LegacySize + 4 * dp; dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW, barY + barH), - IM_COL32(255, 255, 255, 15), barH * 0.5f); + SurfaceOverlay(15), barH * 0.5f); dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW * transRatio, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f); barY += barH + 2 * dp; @@ -982,7 +1001,7 @@ static void RenderBalanceConsolidated(App* app) { dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), cardMax, true); // Background track dl->AddRectFilled(cardMin, cardMax, - IM_COL32(255, 255, 255, 15), glassRound); + SurfaceOverlay(15), glassRound); // Progress fill — additional horizontal clip float progRight = cardMin.x + (cardMax.x - cardMin.x) * prog; dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), ImVec2(progRight, cardMax.y), true); @@ -1075,11 +1094,20 @@ static void RenderBalanceDashboard(App* app) { snprintf(shBuf, sizeof(shBuf), "%.8f", s_dispShielded); snprintf(trBuf, sizeof(trBuf), "%.8f", s_dispTransparent); + // Localized captions — raw dl->AddText (no auto-uppercase), so uppercase to preserve the caption look. + auto upperTR = [](const char* key) { + std::string s = TR(key); + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); }); + return s; + }; + std::string lblShielded = upperTR("shielded"), lblTransparent = upperTR("transparent"); + std::string lblQuickSend = upperTR("quick_send"), lblQuickReceive = upperTR("quick_receive"); + TileInfo tiles[4] = { - {"SHIELDED", shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false}, - {"TRANSPARENT", trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false}, - {"QUICK SEND", "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true}, - {"QUICK RECEIVE", "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true}, + {lblShielded.c_str(), shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false}, + {lblTransparent.c_str(), trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false}, + {lblQuickSend.c_str(), "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true}, + {lblQuickReceive.c_str(), "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true}, }; for (int i = 0; i < 4; i++) { @@ -1125,7 +1153,7 @@ static void RenderBalanceDashboard(App* app) { tiles[i].accent, tiles[i].value); } else { dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py), - OnSurfaceMedium(), "Click to open"); + OnSurfaceMedium(), TR("tile_click_to_open")); } // Click @@ -1279,7 +1307,7 @@ static void RenderBalanceVerticalStack(App* app) { float barW = barRight - barLeft; float barY = rowPos.y + (rowH - barH) * 0.5f; dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barRight, barY + barH), - IM_COL32(255, 255, 255, 15), barH * 0.5f); + SurfaceOverlay(15), barH * 0.5f); dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barLeft + barW * rowInfos[i].ratio, barY + barH), WithAlpha(rowInfos[i].accent, 180), barH * 0.5f); @@ -1473,7 +1501,7 @@ static void RenderBalanceVertical2x2(App* app) { float barX = cellMax.x - amtSz.x - rowPad - barW - Layout::spacingSm(); float barY = cellMin.y + (rowH - barH) * 0.5f; dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH), - IM_COL32(255, 255, 255, 15), barH * 0.5f); + SurfaceOverlay(15), barH * 0.5f); dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * cell.ratio, barY + barH), WithAlpha(cell.accent, 180), barH * 0.5f); @@ -1551,7 +1579,7 @@ static void RenderBalanceShield(App* app) { else ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs)); { - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE"); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label")); ImGui::Dummy(ImVec2(0, 2 * dp)); snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); ImFont* heroFont = Type().h2(); @@ -1665,13 +1693,19 @@ static void RenderBalanceShield(App* app) { float infoY = panelMin.y + shieldPad; ImFont* ovFont = Type().overline(); - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), "SHIELDED"); + std::string shieldLabelUpper = TR("shielded"); + std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(), + [](unsigned char c){ return (char)std::toupper(c); }); + std::string transLabelUpper = TR("transparent"); + std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(), + [](unsigned char c){ return (char)std::toupper(c); }); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), shieldLabelUpper.c_str()); infoY += ovFont->LegacySize + 2 * dp; snprintf(buf, sizeof(buf), "%.8f", s_dispShielded); dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Success(), buf); infoY += capFont->LegacySize + 6 * dp; - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), "TRANSPARENT"); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), transLabelUpper.c_str()); infoY += ovFont->LegacySize + 2 * dp; snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent); dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Warning(), buf); @@ -1727,7 +1761,7 @@ static void RenderBalanceTimeline(App* app) { else ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs)); { - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE"); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label")); ImGui::Dummy(ImVec2(0, 2 * dp)); snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); ImFont* heroFont = Type().h2(); @@ -1816,10 +1850,16 @@ static void RenderBalanceTimeline(App* app) { spec.rounding = glassRound; struct SumCard { const char* label; ImU32 col; double val; bool isMoney; }; + auto upperTR = [](const char* key) { + std::string s = TR(key); + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); }); + return s; + }; + std::string cShielded = upperTR("shielded"), cTransparent = upperTR("transparent"), cMarket = upperTR("market"); SumCard cards[3] = { - {"SHIELDED", Success(), s_dispShielded, false}, - {"TRANSPARENT", Warning(), s_dispTransparent, false}, - {"MARKET", Primary(), state.market.price_usd, true}, + {cShielded.c_str(), Success(), s_dispShielded, false}, + {cTransparent.c_str(), Warning(), s_dispTransparent, false}, + {cMarket.c_str(), Primary(), state.market.price_usd, true}, }; for (int i = 0; i < 3; i++) { ImVec2 cMin(origin.x + i * (cardW + cGap), origin.y); @@ -1887,13 +1927,13 @@ static void RenderBalanceTwoRow(App* app) { ImFont* capFont = Type().caption(); if (state.sync.syncing && state.sync.headers > 0) { float pct = static_cast(state.sync.verification_progress) * 100.0f; - snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct); + snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct); Type().textColored(TypeStyle::Caption, Warning(), buf); ImGui::SameLine(); } if (state.mining.generate) { double hr = state.mining.localHashrate; - snprintf(buf, sizeof(buf), "Mining %s", FormatHashrate(hr).c_str()); + snprintf(buf, sizeof(buf), TR("balance_mining_rate"), FormatHashrate(hr).c_str()); Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf); ImGui::SameLine(); } @@ -1902,11 +1942,15 @@ static void RenderBalanceTwoRow(App* app) { float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f); float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg(); ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm()); - if (TactileButton("Send", ImVec2(btnW, 0), S.resolveFont("button"))) { + // Stable ## ids keep the button identity fixed across translations. + char sendBtn[64], recvBtn[64]; + snprintf(sendBtn, sizeof(sendBtn), "%s##tworow-send", TR("send")); + snprintf(recvBtn, sizeof(recvBtn), "%s##tworow-receive", TR("receive")); + if (TactileButton(sendBtn, ImVec2(btnW, 0), S.resolveFont("button"))) { app->setCurrentPage(NavPage::Send); } ImGui::SameLine(); - if (TactileButton("Receive", ImVec2(btnW, 0), S.resolveFont("button"))) { + if (TactileButton(recvBtn, ImVec2(btnW, 0), S.resolveFont("button"))) { app->setCurrentPage(NavPage::Receive); } } diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index c57e04f..45e8ff0 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -1050,8 +1050,11 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt ImVec2(cx, cy + sz * 0.7f), triCol); // ▼ expanded } // Click anywhere in the gutter cell for this line's first row toggles the fold. + // Guard against a click that is actually dismissing the ConsoleContextMenu (or any + // popup) — the same popup guard the text-selection path uses (see mouse_in_output). ImVec2 mp = ImGui::GetIO().MousePos; if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && + !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup) && mp.x >= output_origin_.x - padX && mp.x < output_origin_.x && mp.y >= lineOrigin.y && mp.y < lineOrigin.y + lineHeight) { pendingFoldToggle = i; diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index 4cf5078..fbc4f6f 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -40,6 +40,7 @@ static bool s_show_edit_dialog = false; static bool s_show_contacts_settings = false; // Contacts customization modal (gear button) static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms +static int s_confirm_avatar_del_idx = -1; // armed avatar-library index; a 2nd badge click confirms the (irreversible) file delete static char s_edit_label[128] = ""; static char s_edit_address[512] = ""; static char s_edit_notes[512] = ""; @@ -555,7 +556,7 @@ void RenderContactsTab(App* app) sdl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp), ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp), material::WithAlpha(material::Primary(), 210), (segH - 4.0f * dp) * 0.5f); - ImU32 fg = active ? IM_COL32(255, 255, 255, 255) : (hov ? material::OnSurface() : material::OnSurfaceMedium()); + ImU32 fg = active ? material::OnPrimary() : (hov ? material::OnSurface() : material::OnSurfaceMedium()); float igW = segIcoF->CalcTextSizeA(segIcoF->LegacySize, FLT_MAX, 0, segIco[i]).x; float lbW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, segLbl[i]).x; float gapI = 5.0f * dp; @@ -747,12 +748,16 @@ void RenderContactsTab(App* app) float dr = std::max(7.0f * dp, cell * 0.15f); ImVec2 dcc(mx.x - dr - 3.0f * dp, mn.y + dr + 3.0f * dp); bool dhov = ImGui::IsMouseHoveringRect(ImVec2(dcc.x-dr, dcc.y-dr), ImVec2(dcc.x+dr, dcc.y+dr)); + const int libIdx = n - 1; + const bool delArmed = (s_confirm_avatar_del_idx == libIdx); ImGui::PushID(n); bool thumbClicked = ImGui::InvisibleButton("##avthumb", ImVec2(cell, cell)); bool thumbHov = ImGui::IsItemHovered(); ImGui::PopID(); - if (thumbHov || sel || hov) { // draw the delete badge - gdl->AddCircleFilled(dcc, dr, dhov ? material::ReadableError() : IM_COL32(0, 0, 0, 175)); + if (thumbHov || sel || hov || delArmed) { // draw the delete badge (persist while armed) + gdl->AddCircleFilled(dcc, dr, (dhov || delArmed) ? material::ReadableError() : IM_COL32(0, 0, 0, 175)); + // While armed, ring the badge so the "click again to delete" state is unmistakable. + if (delArmed) gdl->AddCircle(dcc, dr + 1.5f * dp, material::ReadableError(), 0, 1.5f * dp); ImFont* xf = material::Type().iconSmall(); float xsz = dr * 1.35f; ImVec2 xs = xf->CalcTextSizeA(xsz, FLT_MAX, 0, ICON_MD_CLOSE); @@ -760,11 +765,18 @@ void RenderContactsTab(App* app) } if (dhov) { // badge takes priority over selecting the thumbnail ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - material::Tooltip("%s", TR("delete")); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) pendingDelete = n - 1; + // Two-stage confirm: the first click arms this badge; a second click on the SAME + // badge deletes the image file (fs::remove is irreversible — no undo). + material::Tooltip("%s", delArmed ? TR("address_book_confirm_delete") : TR("delete")); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (delArmed) { pendingDelete = libIdx; s_confirm_avatar_del_idx = -1; } + else s_confirm_avatar_del_idx = libIdx; // arm; requires a 2nd deliberate click + } } else { if (thumbHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (thumbClicked) s_edit_avatar = "img:" + path; + // Any click that lands off this badge (selecting the thumbnail or elsewhere) + // disarms it, so a stale armed badge can't be confirmed by an unrelated click. + if (thumbClicked) { s_edit_avatar = "img:" + path; s_confirm_avatar_del_idx = -1; } } } col = (col + 1) % cols; diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 844efdd..5265d83 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -137,6 +137,12 @@ struct PfEditState { }; static PfEditState s_pfEdit; +// Two-stage delete arm/confirm for the master-list per-row delete icon (mirrors the +// contacts tab's s_confirm_delete_idx): first click on a row's trash arms it (icon turns +// red); a second click on the SAME row within the loop confirms the erase. Hovering/clicking +// a different row, or clicking off the icons, disarms. -1 = nothing armed. +static int s_pfConfirmDelIdx = -1; + // The pure price-series math lives in data/market_series.h; the selected chart range is // s_mkt.chartInterval (0=Live 1=1H 2=1D 3=1W 4=1M, session-scoped, default 1M). @@ -837,6 +843,7 @@ static void RenderPortfolioEditor(App* app) float rowH = 46.0f * dp; float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop + bool armedThisFrame = false; // an arm-click also fires the row Selectable; don't let it disarm for (int vi = 0; vi < (int)vis.size(); vi++) { int i = vis[vi]; ImGui::PushID(i); @@ -877,6 +884,7 @@ static void RenderPortfolioEditor(App* app) // Per-row delete icon (larger, inset from the edge). Hit-tested manually so it is // NOT an overlapping ImGui item over the row Selectable (which asserted on hover). if (rowHov || selRow) { + bool armed = (s_pfConfirmDelIdx == i); // this row is arm-confirmed ImFont* delFont = Type().iconMed(); float ds = delFont->LegacySize; ImVec2 dc(rmx.x - Layout::spacingMd() - ds * 0.5f, rmn.y + rowH * 0.5f); @@ -884,11 +892,20 @@ static void RenderPortfolioEditor(App* app) bool dhov = ImGui::IsMouseHoveringRect(dmn, dmx); if (dhov) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) delRow = i; + // Hovering a *different* row's trash disarms the previously armed one. + if (!armed && s_pfConfirmDelIdx >= 0) s_pfConfirmDelIdx = -1; + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (armed) delRow = i; // 2nd click on the armed row erases + else { s_pfConfirmDelIdx = i; // 1st click arms this row... + armedThisFrame = true; } // ...and must survive the row-select this same click triggers + } } - ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE); + // A filled trash glyph while armed (vs outline) reinforces the recolor. + const char* delGlyph = armed ? ICON_MD_DELETE : ICON_MD_DELETE_OUTLINE; + ImU32 delCol = armed ? ReadableError() : (dhov ? Error() : OnSurfaceMedium()); + ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, delGlyph); mdl->AddText(delFont, ds, ImVec2(dc.x - isz.x * 0.5f, dc.y - isz.y * 0.5f), - dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE); + delCol, delGlyph); } ImGui::PopID(); } @@ -905,10 +922,16 @@ static void RenderPortfolioEditor(App* app) else if (delRow == s_pfEdit.sel) s_pfEdit.sel = std::min(delRow, (int)es.size() - 1); PortfolioBeginEdit(app, s_pfEdit.sel); } - } else if (clickedSel != -999) { + s_pfConfirmDelIdx = -1; // erase done — storage indices shifted, drop the arm + } else if (clickedSel != -999 && !armedThisFrame) { pfCommitIfNeeded(app); // auto-save the current group before switching PortfolioBeginEdit(app, clickedSel); + s_pfConfirmDelIdx = -1; // switching rows disarms any pending delete } + // A left-click that hit no row/action (empty list space) disarms a pending delete. + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && + !ImGui::IsAnyItemHovered()) + s_pfConfirmDelIdx = -1; } ImGui::EndChild(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index a06aeeb..37b16c0 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -83,7 +83,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& // over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%). float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap; float innerW = estControlsW - pad * 2; - float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f)); + float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size * dp, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f) * dp); float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size); int cols = std::max(1, std::min(nOpts, (int)(innerW / (cellSz + cellGap)))); int rows = (nOpts + cols - 1) / cols; @@ -162,14 +162,35 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& const float sy = curY; ImVec2 savedCur = ImGui::GetCursorScreenPos(); - material::IconButtonStyle sideStyle; - sideStyle.color = OnSurfaceMedium(); - sideStyle.hoverBg = StateHover(); + // -/+ buttons drawn in the same style as the thread tiles (rounded rect + border + centered + // glyph, same fill/border/rounding). Hit-tested with a real InvisibleButton so they stay + // popup-safe. + const float stepRound = schema::UI().drawElement("tabs.mining", "cell-rounding").size; + // atBound: the stepper can't move further (already at 1 for "-" or max for "+"). When + // at the bound the control is a no-op, so render it disabled: dim glyph/border, no hand + // cursor, no tooltip — and swallow the click so it reads as inert. + auto rectStepBtn = [&](const char* id, const char* glyph, float x, const char* tip, bool atBound) -> bool { + ImGui::SetCursorScreenPos(ImVec2(x, sy)); + ImGui::InvisibleButton(id, ImVec2(sideW, fieldH)); + const bool hov = ImGui::IsItemHovered() && !atBound; + const bool clk = ImGui::IsItemClicked() && !atBound; + const ImVec2 mn(x, sy), mx(x + sideW, sy + fieldH); + dl->AddRectFilled(mn, mx, hov ? WithAlpha(OnSurface(), 25) : WithAlpha(OnSurface(), 8), stepRound); + dl->AddRect(mn, mx, WithAlpha(OnSurface(), atBound ? 15 : (hov ? 80 : 35)), stepRound); + const ImVec2 gsz = stepFont->CalcTextSizeA(stepFont->LegacySize, FLT_MAX, 0, glyph); + dl->AddText(stepFont, stepFont->LegacySize, + ImVec2(x + (sideW - gsz.x) * 0.5f, sy + (fieldH - gsz.y) * 0.5f), + WithAlpha(OnSurface(), atBound ? 30 : (hov ? 160 : 80)), glyph); + if (hov) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (tip && tip[0]) material::Tooltip("%s", tip); + } + return clk; + }; - // "-" button - sideStyle.tooltip = TR("mining_threads_minus_tooltip"); - ImGui::SetCursorScreenPos(ImVec2(sx, sy)); - if (material::IconButton("##ThreadMinus", ICON_MD_REMOVE, stepFont, ImVec2(sideW, fieldH), sideStyle)) + // "-" button (disabled at 1 thread) + if (rectStepBtn("##ThreadMinus", ICON_MD_REMOVE, sx, TR("mining_threads_minus_tooltip"), + s_selected_threads <= 1)) applyThreads(s_selected_threads - 1); sx += sideW + g; @@ -188,10 +209,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& ImGui::PopStyleVar(); sx += fieldW + g; - // "+" button - sideStyle.tooltip = TR("mining_threads_plus_tooltip"); - ImGui::SetCursorScreenPos(ImVec2(sx, sy)); - if (material::IconButton("##ThreadPlus", ICON_MD_ADD, stepFont, ImVec2(sideW, fieldH), sideStyle)) + // "+" button (disabled at max_threads) + if (rectStepBtn("##ThreadPlus", ICON_MD_ADD, sx, TR("mining_threads_plus_tooltip"), + s_selected_threads >= max_threads)) applyThreads(s_selected_threads + 1); ImGui::SetCursorScreenPos(savedCur); @@ -738,15 +758,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size); } else if (active) { // Active but not mining: solid primary fill - ImU32 pri = Primary(); - int priR = (pri >> 0) & 0xFF; - int priG = (pri >> 8) & 0xFF; - int priB = (pri >> 16) & 0xFF; - ImU32 fillCol = hovered - ? IM_COL32(priR, priG, priB, 220) - : IM_COL32(priR, priG, priB, 180); + ImU32 fillCol = material::WithAlpha(Primary(), hovered ? 220 : 180); dl->AddRectFilled(cMin, cMax, fillCol, rounding); - dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size); + dl->AddRect(cMin, cMax, material::WithAlpha(Primary(), 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size); } else { // Inactive: dim outline ImU32 fillCol = hovered @@ -938,7 +952,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& else if (poolBlockedBySolo) material::Tooltip("%s", TR("mining_stop_solo_for_pool")); else if (poolNeedsPayout) - material::Tooltip("%s", "Enter a payout address first (generate a Z address)"); + material::Tooltip("%s", TR("mining_pool_needs_payout_tooltip")); else material::Tooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining")); } diff --git a/src/ui/windows/mining_mode_toggle.cpp b/src/ui/windows/mining_mode_toggle.cpp index 65f7e2c..75029fb 100644 --- a/src/ui/windows/mining_mode_toggle.cpp +++ b/src/ui/windows/mining_mode_toggle.cpp @@ -42,6 +42,20 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256], bool& s_pool_settings_dirty) { + // Arm/confirm for the destructive X (delete) on saved pools & workers: a first click on a + // row's X arms it (the X turns red + relabels via the trash glyph) and a second click within + // a short window actually removes it; moving to another row or letting the window lapse disarms. + // Same idiom as contacts_tab.cpp's s_confirm_delete_idx, keyed by row index within each list. + static int s_armed_pool_idx = -1; // armed saved-pool row (-1 = none) + static int s_armed_worker_idx = -1; // armed saved-worker row (-1 = none) + static double s_armed_pool_time = 0.0; + static double s_armed_worker_time = 0.0; + const double kArmWindowSecs = 3.0; // second click must land within this window + const double nowTime = ImGui::GetTime(); + // Lapse the arm if the confirm window elapsed. + if (s_armed_pool_idx >= 0 && nowTime - s_armed_pool_time > kArmWindowSecs) s_armed_pool_idx = -1; + if (s_armed_worker_idx >= 0 && nowTime - s_armed_worker_time > kArmWindowSecs) s_armed_worker_idx = -1; + const bool soloMiningAvailable = app->supportsSoloMining(); float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs; float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size; @@ -296,9 +310,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo ImFont* rowFont = ImGui::GetFont(); float rowFontSz = ImGui::GetFontSize(); float rowH = ImGui::GetFrameHeight(); + int rowIdx = 0; for (const auto& url : savedUrls) { ImGui::PushID(url.c_str()); bool isCurrent = (std::string(s_pool_url) == url); + bool armed = (s_armed_pool_idx == rowIdx); // this row's X is arm-confirmed ImVec2 rowMin = ImGui::GetCursorScreenPos(); ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH); ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH)); @@ -319,40 +335,46 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo pdl->AddText(rowFont, rowFontSz, ImVec2(rowMin.x + textPadX, textY), isCurrent ? Primary() : OnSurface(), urlDisp.c_str()); - // X button — flush with right edge, icon centered + // X (delete) button — flush with right edge, icon centered. First click on the X + // arms it (filled trash glyph + readable-error tint); a second click within the + // arm window actually removes. Armed row stays lit even when not hovered so the + // pending-delete state is visible. { ImVec2 xMin(rowMax.x - xZoneW, rowMin.y); ImVec2 xMax(rowMax.x, rowMax.y); if (inXZone) { pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - material::Tooltip("%s", TR("mining_remove")); - } else if (rowHov) { - // Show faint X when row is hovered - ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; - ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); - ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); - pdl->AddText(icoF, icoF->LegacySize, - ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - OnSurfaceDisabled(), xIcon); + material::Tooltip("%s", armed ? TR("address_book_confirm_delete") + : TR("mining_remove")); } - // Always draw icon when hovering X zone - if (inXZone) { + if (armed || inXZone || rowHov) { ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; + // Filled trash while armed reinforces the recolor; otherwise a plain X. + const char* xIcon = armed ? ICON_MD_DELETE : ICON_MD_CLOSE; + ImU32 xCol = armed ? ReadableError() + : (inXZone ? Error() : OnSurfaceDisabled()); ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); pdl->AddText(icoF, icoF->LegacySize, ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - Error(), xIcon); + xCol, xIcon); } } + // Hovering a *different* row's X disarms the previously armed one. + if (inXZone && !armed && s_armed_pool_idx >= 0) + s_armed_pool_idx = -1; // Click handling if (rowClk) { if (inXZone) { - urlToRemove = url; + if (armed) { + urlToRemove = url; // 2nd click on the armed row removes + } else { + s_armed_pool_idx = rowIdx; // 1st click arms this row + s_armed_pool_time = nowTime; + } } else { + s_armed_pool_idx = -1; // selecting a row disarms strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1); s_pool_url[sizeof(s_pool_url) - 1] = '\0'; s_pool_settings_dirty = true; @@ -362,10 +384,12 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo if (rowHov && !inXZone) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); + rowIdx++; } if (!urlToRemove.empty()) { app->settings()->removeSavedPoolUrl(urlToRemove); app->settings()->save(); + s_armed_pool_idx = -1; // removal shifts indices — drop the arm } } ImGui::EndPopup(); @@ -460,9 +484,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo ImFont* wRowFont = ImGui::GetFont(); float wRowFontSz = ImGui::GetFontSize(); float wRowH = ImGui::GetFrameHeight(); + int wRowIdx = 0; for (const auto& addr : savedWorkers) { ImGui::PushID(addr.c_str()); bool isCurrent = (std::string(s_pool_worker) == addr); + bool armed = (s_armed_worker_idx == wRowIdx); // this row's X is arm-confirmed ImVec2 rowMin = ImGui::GetCursorScreenPos(); ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH); ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH)); @@ -486,38 +512,46 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo // Tooltip for long addresses if (rowHov && !inXZone) material::Tooltip("%s", addr.c_str()); - // X button — flush with right edge, icon centered + // X (delete) button — flush with right edge, icon centered. First click on the X + // arms it (filled trash glyph + readable-error tint); a second click within the + // arm window actually removes. Armed row stays lit even when not hovered so the + // pending-delete state is visible. { ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y); ImVec2 xMax(rowMax.x, rowMax.y); if (inXZone) { pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - material::Tooltip("%s", TR("mining_remove")); - } else if (rowHov) { - ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; - ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); - ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); - pdl->AddText(icoF, icoF->LegacySize, - ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - OnSurfaceDisabled(), xIcon); + material::Tooltip("%s", armed ? TR("address_book_confirm_delete") + : TR("mining_remove")); } - if (inXZone) { + if (armed || inXZone || rowHov) { ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; + // Filled trash while armed reinforces the recolor; otherwise a plain X. + const char* xIcon = armed ? ICON_MD_DELETE : ICON_MD_CLOSE; + ImU32 xCol = armed ? ReadableError() + : (inXZone ? Error() : OnSurfaceDisabled()); ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); pdl->AddText(icoF, icoF->LegacySize, ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - Error(), xIcon); + xCol, xIcon); } } + // Hovering a *different* row's X disarms the previously armed one. + if (inXZone && !armed && s_armed_worker_idx >= 0) + s_armed_worker_idx = -1; // Click handling if (rowClk) { if (inXZone) { - addrToRemove = addr; + if (armed) { + addrToRemove = addr; // 2nd click on the armed row removes + } else { + s_armed_worker_idx = wRowIdx; // 1st click arms this row + s_armed_worker_time = nowTime; + } } else { + s_armed_worker_idx = -1; // selecting a row disarms strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1); s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_settings_dirty = true; @@ -527,10 +561,12 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo if (rowHov && !inXZone) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); + wRowIdx++; } if (!addrToRemove.empty()) { app->settings()->removeSavedPoolWorker(addrToRemove); app->settings()->save(); + s_armed_worker_idx = -1; // removal shifts indices — drop the arm } } ImGui::EndPopup(); diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index c823a74..8c6b118 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -665,14 +665,26 @@ void RenderPeersTab(App* app) dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol); float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size; + // Reserve the line-1 trailing zone (ping + direction pill live on the far right, + // the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port + // addr must clip before it so it can't run under those. Mirror the ping formula + // conservatively and leave a gap. + float line1RightLimit = rowPos.x + innerW - Layout::spacingXl() * 3 - + capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "0000ms").x - + Layout::spacingSm(); + dl->PushClipRect(ImVec2(addrX, rowPos.y), ImVec2(line1RightLimit, rowEnd.y), true); dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str()); + dl->PopClipRect(); - // Seed node icon — rendered right after the IP address + // Seed node icon — rendered right after the IP address, but never past the + // reserved trailing zone (a long addr would otherwise push it into the ping text). if (IsSeedNode(peer.addr)) { ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str()); ImFont* iconFont = Type().iconSmall(); float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f; - dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS); + float seedIconX = std::min(addrX + addrSz.x + Layout::spacingSm(), + line1RightLimit - iconFont->LegacySize); + dl->AddText(iconFont, iconFont->LegacySize, ImVec2(seedIconX, iconY), WithAlpha(Success(), 200), ICON_MD_GRASS); } { @@ -696,30 +708,55 @@ void RenderPeersTab(App* app) } float cy2 = cy + body2->LegacySize + Layout::spacingXs(); - dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy2), - OnSurfaceDisabled(), peer.subver.c_str()); + float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size; + + // Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long + // remote subver cannot push the TLS badge / ban-score off the row. Order right→left: + // ban-score pinned far right, then the TLS/no-TLS badge, then the clipped subver text. + float line2RightLimit = rowPos.x + innerW - Layout::spacingLg(); + float banScoreLeftX = line2RightLimit; + char banBuf[16]; + ImU32 banCol = 0; + bool haveBanScore = peer.banscore > 0; + if (haveBanScore) { + snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore); + banCol = peer.banscore > 50 ? Error() : Warning(); + float banW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf).x; + banScoreLeftX = line2RightLimit - banW; + // The TLS/no-TLS badge sits to the LEFT of the ban score with a gap. + line2RightLimit = banScoreLeftX - Layout::spacingLg(); + } + + float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs); + float noTlsW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("peers_no_tls")).x; + float badgeW = peer.tls_cipher.empty() ? noTlsW : tlsBadgeW; + // Hard right boundary for the subver text column, leaving room for the badge. + float subverMaxX = line2RightLimit - badgeW - Layout::spacingSm(); + if (subverMaxX < subverX) subverMaxX = subverX; float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x; - float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs); + dl->PushClipRect(ImVec2(subverX, rowPos.y), ImVec2(subverMaxX, rowEnd.y), true); + dl->AddText(capFont, capFont->LegacySize, ImVec2(subverX, cy2), + OnSurfaceDisabled(), peer.subver.c_str()); + dl->PopClipRect(); + + // Pin the badge just after the (clipped) subver, but never past the reserved zone. + float badgeX = std::min(subverX + verW + Layout::spacingSm(), subverMaxX + Layout::spacingSm()); if (!peer.tls_cipher.empty()) { ImU32 tlsBg = WithAlpha(Success(), 25); ImU32 tlsFg = WithAlpha(Success(), 200); - ImVec2 tlsMin(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2); + ImVec2 tlsMin(badgeX, cy2); ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2); dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size); 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), + dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2), WithAlpha(Error(), 140), TR("peers_no_tls")); } - if (peer.banscore > 0) { - char banBuf[16]; - 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); + if (haveBanScore) { dl->AddText(capFont, capFont->LegacySize, - ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingLg(), cy2), banCol, banBuf); + ImVec2(banScoreLeftX, cy2), banCol, banBuf); } ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH)); diff --git a/src/ui/windows/receive_tab.cpp b/src/ui/windows/receive_tab.cpp index 2269850..f4f0707 100644 --- a/src/ui/windows/receive_tab.cpp +++ b/src/ui/windows/receive_tab.cpp @@ -514,21 +514,25 @@ void RenderReceiveTab(App* app) if (state.addresses.empty()) { ImVec2 emptyMin = ImGui::GetCursorScreenPos(); - float emptyH = S.drawElement("tabs.receive", "skeleton-height").size; + // Hand-drawn skeleton geometry is absolute px — scale heights/offsets/rounding by + // dpiScale() so it doesn't render native-thin at >100% (width ratios multiply formW, leave as-is). + float dp = Layout::dpiScale(); + float emptyH = S.drawElement("tabs.receive", "skeleton-height").size * dp; ImVec2 emptyMax(emptyMin.x + formW, emptyMin.y + emptyH); DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec); float alpha = (float)(schema::UI().drawElement("animations", "skeleton-base").size + schema::UI().drawElement("animations", "skeleton-amp").size * std::sin(ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size)); ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255)); + float skelRound = schema::UI().drawElement("tabs.receive", "skeleton-rounding").size * dp; dl->AddRectFilled( ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()), - ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size), - skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size); + ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size * dp), + skelCol, skelRound); dl->AddRectFilled( - ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size), - ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size), - skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size); + ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size * dp), + ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size * dp), + skelCol, skelRound); dl->AddText(capFont, capFont->LegacySize, - ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size), + ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size * dp), OnSurfaceDisabled(), TR("loading_addresses")); ImGui::Dummy(ImVec2(formW, emptyH)); ImGui::EndGroup(); @@ -778,7 +782,7 @@ void RenderReceiveTab(App* app) size_t memo_len = strlen(s_request_memo); size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size; bool memoAtCap = memo_len + 1 >= memoMax; - snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax); + snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax); Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf); } diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index f71a515..4d2940d 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -424,11 +424,13 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW, ? std::clamp((float)((s_amount + s_fee) / available), 0.0f, 1.0f) : 0.0f; - float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size; + const float dp = Layout::dpiScale(); + float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size * dp; float gap = Layout::spacingMd(); float barW = innerW - maxBtnW - gap; - if (barW < schema::UI().drawElement("tabs.send", "progress-bar-min-width").size) barW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size; - float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size; + float minBarW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size * dp; + if (barW < minBarW) barW = minBarW; + float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size * dp; float barRound = barH * 0.5f; ImVec2 barMin = ImGui::GetCursorScreenPos(); @@ -1000,7 +1002,7 @@ static void RenderActionButtons(App* app, float width, float vScale, ImGui::BeginDisabled(!can_send); char sendId[64]; - snprintf(sendId, sizeof(sendId), "Review Send%s", suffix); + snprintf(sendId, sizeof(sendId), "%s##ReviewSend%s", TR("review_send"), suffix); if (TactileButton(sendId, ImVec2(sendBtnW, btnH), S.resolveFont(S.button("tabs.send", "send-button").font))) { s_show_confirm = true; } @@ -1020,7 +1022,7 @@ static void RenderActionButtons(App* app, float width, float vScale, else if (total > available) material::Tooltip("%s", TR("send_tooltip_exceeds_balance")); else if (!sourceSpendable) - material::Tooltip("%s", "View-only address — no spending key, cannot send"); + material::Tooltip("%s", TR("send_tooltip_view_only")); else if (s_sending) material::Tooltip("%s", TR("send_tooltip_in_progress")); } @@ -1033,7 +1035,7 @@ static void RenderActionButtons(App* app, float width, float vScale, ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled())); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.send", "cancel-btn-border-size").size); char clearId[64]; - snprintf(clearId, sizeof(clearId), "Cancel%s", suffix); + snprintf(clearId, sizeof(clearId), "%s##Cancel%s", TR("cancel"), suffix); if (TactileButton(clearId, ImVec2(cancelBtnW, btnH), S.resolveFont(S.button("tabs.send", "clear-button").font))) { if (FormHasData()) { s_clear_confirm_pending = true; @@ -1377,7 +1379,7 @@ void RenderSendTab(App* app) s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), (size_t)S.drawElement("tabs.send", "paste-preview-max-chars").size)); } - if (TactileButton("Paste##to", ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) { + if (TactileButton((std::string(TR("paste")) + "##to").c_str(), ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) { if (s_paste_previewing) { // Commit the preview snprintf(s_to_address, sizeof(s_to_address), "%s", s_preview_text.c_str()); @@ -1547,7 +1549,7 @@ void RenderSendTab(App* app) size_t memo_len = strlen(s_memo); size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size; bool memoAtCap = memo_len + 1 >= memoMax; - snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax); + snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax); Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf); } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 9a126f4..46a1010 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -823,7 +823,7 @@ void I18n::loadBuiltinEnglish() strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon"; strings_["confirm_reinstall_daemon_msg"] = "This stops the daemon, overwrites the installed dragonxd (and dragonx-cli/dragonx-tx) with the versions bundled in this wallet build, then restarts the node. Use this to recover or update the node binary."; strings_["confirm_reinstall_daemon_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced."; - strings_["daemon_update_title"] = "Update the node daemon?"; + strings_["daemon_update_prompt_title"] = "Update the node daemon?"; strings_["daemon_update_body"] = "This wallet build bundles a newer DragonX node than the one currently installed. Updating replaces the installed dragonxd (and dragonx-cli/dragonx-tx), then stops and restarts the node so the new version takes effect. Recommended — a newer node can add features (e.g. seed-phrase support) the old one lacks."; strings_["daemon_update_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced. If you deliberately run a custom node, choose Keep current."; strings_["daemon_update_now"] = "Update now"; @@ -855,6 +855,10 @@ void I18n::loadBuiltinEnglish() strings_["lite_passphrase_label"] = "Passphrase"; strings_["lite_validate"] = "Validate"; strings_["lite_working"] = "Working…"; + strings_["lite_enter_wallet_path"] = "Enter a wallet path"; + strings_["lite_enter_all_seed_words"] = "Enter all 24 seed words to restore (got %d)"; + strings_["lite_could_not_start"] = "Could not start the operation"; + strings_["lite_backend_unavailable"] = "Lite wallet backend unavailable"; strings_["lite_wallet_ready"] = "Wallet ready"; strings_["lite_backup_keys"] = "Backup & keys"; strings_["lite_show_seed"] = "Show seed"; @@ -1101,6 +1105,14 @@ void I18n::loadBuiltinEnglish() strings_["balance_history_collecting"] = "Balance history — collecting data..."; strings_["balance_shielded_fmt"] = "Shielded: %.8f"; strings_["balance_transparent_fmt"] = "Transparent: %.8f"; + strings_["total_balance_label"] = "Total Balance"; + strings_["balance_syncing_pct"] = "Syncing %.1f%%"; + strings_["balance_mining_rate"] = "Mining %s"; + strings_["balance_layout_switched"] = "Layout: %s"; + strings_["byte_count_fmt"] = "%zu / %zu bytes"; + strings_["quick_send"] = "Quick Send"; + strings_["quick_receive"] = "Quick Receive"; + strings_["tile_click_to_open"] = "Click to open"; strings_["your_addresses"] = "Your Addresses"; strings_["z_addresses"] = "Z-Addresses"; strings_["t_addresses"] = "T-Addresses"; @@ -1428,9 +1440,18 @@ void I18n::loadBuiltinEnglish() strings_["loading_stall_body"] = "The daemon has been initializing for %.0fs. This can be normal after an update or on first launch (loading the block index or rescanning) — it will connect automatically once ready."; strings_["loading_stall_hint"] = "Still stuck? Open Settings and use Restart Daemon, or check the Console for details."; strings_["sb_plaintext_remote_blocked"] = "Refusing to send RPC credentials over plaintext to a remote host. Add rpcallowplaintext=1 to DRAGONX.conf to allow it, or enable TLS with rpctls=1."; + strings_["rpc_plaintext_remote_warning"] = "Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS."; strings_["settings_open_log_folder"] = "Open log folder"; strings_["settings_copy_diagnostics"] = "Copy diagnostics"; strings_["settings_diagnostics_copied"] = "Diagnostics copied to clipboard"; + strings_["settings_rpc_ok"] = "RPC connection OK"; + strings_["settings_rpc_error_prefix"] = "RPC error: "; + strings_["settings_not_connected"] = "Not connected to daemon"; + strings_["settings_theme_refreshed"] = "Theme list refreshed"; + strings_["settings_saved"] = "Settings saved"; + strings_["settings_reloaded"] = "Settings reloaded from disk"; + strings_["settings_ztx_cleared"] = "Z-transaction history cleared"; + strings_["settings_ztx_not_found"] = "No history file found"; strings_["tt_open_log_folder"] = "Open the folder containing the debug and crash logs"; strings_["tt_copy_diagnostics"] = "Copy a support snapshot (version, daemon/wallet/log state — no secrets) to the clipboard"; strings_["sb_dragonxd_running"] = "dragonxd running"; @@ -1886,6 +1907,7 @@ void I18n::loadBuiltinEnglish() strings_["mining_payout_invalid"] = "Not a valid DragonX address — fix it before starting, or mining rewards are lost."; strings_["mining_est_daily_pool_sub"] = "rough solo-equivalent, before pool fee"; strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address"; + strings_["mining_pool_needs_payout_tooltip"] = "Enter a payout address first (generate a Z address)"; strings_["mining_pool"] = "Pool"; strings_["mining_payout_foreign"] = "⚠ This payout address isn't in your current wallet — mined rewards would go to a different wallet. Update it if you switched wallets."; strings_["mining_pool_hashrate"] = "Pool Hashrate"; @@ -2155,6 +2177,7 @@ void I18n::loadBuiltinEnglish() strings_["send_tooltip_not_connected"] = "Not connected to daemon"; strings_["send_tooltip_select_source"] = "Select a source address first"; strings_["send_tooltip_syncing"] = "Wait for blockchain to sync"; + strings_["send_tooltip_view_only"] = "View-only address — no spending key, cannot send"; strings_["send_total"] = "Total"; strings_["send_tx_failed"] = "Transaction failed"; strings_["send_tx_sent"] = "Transaction sent!";