fix(ux): land quick-win UX audit fixes across settings, exports, toasts, nav
Batch of safe, isolated fixes from the 10-surface UX audit: - settings: Auto-shield / Save-z-transactions toggles now persist on change (they discarded CB()'s return and relied on a Save button that only renders on the About tab, so the choice was silently lost on relaunch). - exports: show the resolved Platform::getConfigDir() instead of a hardcoded ~/.config path (wrong on Windows/macOS) in both the all-keys and CSV dialogs; all-keys also reveals the file path on a partial export. - notifications: error/warning toasts last 9s/7s (were 4s/3.5s) so failures are readable. - qr popup / tx-details: copy-to-clipboard now confirms with a toast. - about: Escape-to-close + readable heading contrast via OnSurfaceMedium(). - sidebar: suppress single-key nav (J/K/arrows/[) while an overlay dialog or ImGui popup is open, so it no longer switches the tab behind a modal. - i18n: strip the stale hardcoded path out of file_save_location in i18n.cpp and all eight res/lang/*.json locales (additive, one key each). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Preise abrufen",
|
||||
"file": "Datei",
|
||||
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
|
||||
"file_save_location": "Datei wird gespeichert in:",
|
||||
"filter": "Filtern...",
|
||||
"font_scale": "Schriftgröße",
|
||||
"from": "Von",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Obtener precios",
|
||||
"file": "Archivo",
|
||||
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
|
||||
"file_save_location": "El archivo se guardará en:",
|
||||
"filter": "Filtrar...",
|
||||
"font_scale": "Escala de fuente",
|
||||
"from": "Desde",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Récupérer les prix",
|
||||
"file": "Fichier",
|
||||
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
|
||||
"file_save_location": "Le fichier sera enregistré dans :",
|
||||
"filter": "Filtrer...",
|
||||
"font_scale": "Taille de police",
|
||||
"from": "De",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "通常",
|
||||
"fetch_prices": "価格を取得",
|
||||
"file": "ファイル",
|
||||
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
|
||||
"file_save_location": "ファイルの保存先:",
|
||||
"filter": "フィルター...",
|
||||
"font_scale": "フォントサイズ",
|
||||
"from": "送信元",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "보통",
|
||||
"fetch_prices": "가격 조회",
|
||||
"file": "파일",
|
||||
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
|
||||
"file_save_location": "파일 저장 위치:",
|
||||
"filter": "필터...",
|
||||
"font_scale": "글꼴 크기",
|
||||
"from": "보낸 곳",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Buscar preços",
|
||||
"file": "Arquivo",
|
||||
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
|
||||
"file_save_location": "O arquivo será salvo em:",
|
||||
"filter": "Filtrar...",
|
||||
"font_scale": "Escala da Fonte",
|
||||
"from": "De",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "Обычная",
|
||||
"fetch_prices": "Получить цены",
|
||||
"file": "Файл",
|
||||
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
|
||||
"file_save_location": "Файл будет сохранён в:",
|
||||
"filter": "Фильтр...",
|
||||
"font_scale": "Масштаб шрифта",
|
||||
"from": "От",
|
||||
|
||||
+1
-1
@@ -876,7 +876,7 @@
|
||||
"fee_normal": "普通",
|
||||
"fetch_prices": "获取价格",
|
||||
"file": "文件",
|
||||
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
|
||||
"file_save_location": "文件将保存至:",
|
||||
"filter": "筛选...",
|
||||
"font_scale": "字体大小",
|
||||
"from": "从",
|
||||
|
||||
+2
-2
@@ -1229,8 +1229,8 @@ glass-border-alpha = { size = 35.0 }
|
||||
progress-bar-height = { size = 2.0 }
|
||||
duration-info = { size = 1.0 }
|
||||
duration-success = { size = 2.5 }
|
||||
duration-warning = { size = 3.5 }
|
||||
duration-error = { size = 4.0 }
|
||||
duration-warning = { size = 7.0 }
|
||||
duration-error = { size = 9.0 }
|
||||
window = { width = 350, height = 60, border-radius = 8, border-width = 1 }
|
||||
viewport-padding = { size = 20.0 }
|
||||
stack-gap = { size = 10.0 }
|
||||
|
||||
@@ -1298,9 +1298,14 @@ void RenderSettingsPage(App* app) {
|
||||
if (c == 1) { rowY += fh + gp; c = 0; } else { c = 1; }
|
||||
return changed;
|
||||
};
|
||||
CB(TrId("save_z_transactions", "save_ztx"), &s_settingsState.save_ztxs);
|
||||
// These persist immediately on toggle (like the daemon/verbose options below). The old
|
||||
// code discarded CB()'s return and relied on a Save button that only renders on the About
|
||||
// tab, so a user who enabled Auto-shield (a money/privacy intent) silently lost it on relaunch.
|
||||
if (CB(TrId("save_z_transactions", "save_ztx"), &s_settingsState.save_ztxs))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx"));
|
||||
CB(TrId("auto_shield", "auto_shld"), &s_settingsState.auto_shield);
|
||||
if (CB(TrId("auto_shield", "auto_shld"), &s_settingsState.auto_shield))
|
||||
saveSettingsPageState(app->settings());
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield"));
|
||||
{
|
||||
// Not implemented: disabled and never shown as "on", even for a stored use_tor=true.
|
||||
|
||||
+5
-1
@@ -763,7 +763,11 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
}
|
||||
|
||||
// ---- Keyboard navigation ----
|
||||
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
// Suppress single-key nav while a dialog/overlay (send-confirm, seed backup, unlock, …) or any
|
||||
// ImGui popup is open — otherwise Arrow/J/K/[ silently switch the tab *behind* the modal.
|
||||
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl &&
|
||||
!material::IsOverlayDialogBlockingInput() &&
|
||||
!ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopup)) {
|
||||
int idx = static_cast<int>(current);
|
||||
bool nav = false;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
|
||||
|
||||
@@ -38,6 +38,12 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape closes the dialog (click-outside and the Close button already work; this matches the FAQ
|
||||
// dialog's affordance so Escape behaves consistently across modal dialogs).
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
*p_open = false;
|
||||
}
|
||||
|
||||
// Use Body2 font for all dialog text
|
||||
ImGui::PushFont(Type().body2());
|
||||
|
||||
@@ -107,7 +113,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Credits
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", TR("about_credits"));
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()), "%s", TR("about_credits"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::BulletText("The Hush Developers");
|
||||
@@ -121,7 +127,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// License
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", TR("about_license"));
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()), "%s", TR("about_license"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", TR("about_license_text"));
|
||||
|
||||
|
||||
@@ -93,7 +93,10 @@ void ExportAllKeysDialog::render(App* app)
|
||||
ImGui::Spacing();
|
||||
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
||||
// Show the real, resolved config directory (not a hardcoded ~/.config path, which was wrong on
|
||||
// Windows/macOS) so the user knows where this plaintext key file lands before they export it.
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s %s",
|
||||
TR("file_save_location"), util::Platform::getConfigDir().c_str());
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Progress / result.
|
||||
@@ -232,7 +235,10 @@ void ExportAllKeysDialog::render(App* app)
|
||||
Notifications::instance().error(TR("export_keys_write_fail"));
|
||||
} else if (exported < total) {
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_partial"), exported, total);
|
||||
s_status = buf;
|
||||
// A partial export still wrote a plaintext key file to disk — show WHERE,
|
||||
// so the user can find it to move or delete it (previously only a full
|
||||
// success revealed the path, leaving partial-export files orphaned).
|
||||
s_status = std::string(buf) + "\n" + filepath;
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_partial_toast"), exported, total);
|
||||
Notifications::instance().warning(buf);
|
||||
} else {
|
||||
|
||||
@@ -81,7 +81,9 @@ void ExportTransactionsDialog::render(App* app)
|
||||
// Filename + save-location hint.
|
||||
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
||||
// Show the real, resolved config directory (the old hardcoded ~/.config path was wrong on Windows/macOS).
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s %s",
|
||||
TR("file_save_location"), util::Platform::getConfigDir().c_str());
|
||||
|
||||
// Result — success shows the saved path as a copy field; failure shows a red line.
|
||||
if (!s_saved_path.empty()) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../layout.h"
|
||||
#include "../theme.h"
|
||||
#include "../notifications.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
@@ -138,6 +139,9 @@ void QRPopupDialog::render(App* app)
|
||||
material::DialogActionFooter(TR("copy_address"), true, TR("close"), doCopy, doClose, btnW);
|
||||
if (doCopy) {
|
||||
ImGui::SetClipboardText(s_address.c_str());
|
||||
// Confirm the copy — a silent copy here risks a stale clipboard being pasted into an
|
||||
// exchange withdrawal. Matches the Receive tab's feedback.
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
if (doClose) {
|
||||
close();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../theme.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../notifications.h"
|
||||
#include "imgui.h"
|
||||
#include <ctime>
|
||||
|
||||
@@ -126,6 +127,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
ImGui::SetClipboardText(tx.txid.c_str());
|
||||
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -152,6 +154,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
ImGui::SetClipboardText(tx.address.c_str());
|
||||
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1606,7 +1606,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["date_label"] = "Date:";
|
||||
strings_["delete"] = "Delete";
|
||||
strings_["fee_label"] = "Fee:";
|
||||
strings_["file_save_location"] = "File will be saved in: ~/.config/ObsidianDragon/";
|
||||
strings_["file_save_location"] = "File will be saved in:";
|
||||
strings_["hide"] = "Hide";
|
||||
strings_["label"] = "Label:";
|
||||
strings_["loading"] = "Loading...";
|
||||
|
||||
Reference in New Issue
Block a user