feat(ui): implement UI/UX audit — i18n, HiDPI, theme, destructive-action & overflow fixes
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user