From a99f71a0cd74eeaad9dc4fa7a709eec7ae0d6087 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 17 Oct 2026 16:27:58 -0500 Subject: [PATCH] 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 --- res/lang/de.json | 2 +- res/lang/es.json | 2 +- res/lang/fr.json | 2 +- res/lang/ja.json | 2 +- res/lang/ko.json | 2 +- res/lang/pt.json | 2 +- res/lang/ru.json | 2 +- res/lang/zh.json | 2 +- res/themes/ui.toml | 4 ++-- src/ui/pages/settings_page.cpp | 9 +++++++-- src/ui/sidebar.h | 6 +++++- src/ui/windows/about_dialog.cpp | 10 ++++++++-- src/ui/windows/export_all_keys_dialog.cpp | 10 ++++++++-- src/ui/windows/export_transactions_dialog.cpp | 4 +++- src/ui/windows/qr_popup_dialog.cpp | 4 ++++ src/ui/windows/transaction_details_dialog.cpp | 3 +++ src/util/i18n.cpp | 2 +- 17 files changed, 49 insertions(+), 19 deletions(-) diff --git a/res/lang/de.json b/res/lang/de.json index ea47d51..8859ffc 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -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", diff --git a/res/lang/es.json b/res/lang/es.json index 1ace328..d232743 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -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", diff --git a/res/lang/fr.json b/res/lang/fr.json index aa30c1c..20b36fe 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -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", diff --git a/res/lang/ja.json b/res/lang/ja.json index f251b10..9208fe0 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -876,7 +876,7 @@ "fee_normal": "通常", "fetch_prices": "価格を取得", "file": "ファイル", - "file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/", + "file_save_location": "ファイルの保存先:", "filter": "フィルター...", "font_scale": "フォントサイズ", "from": "送信元", diff --git a/res/lang/ko.json b/res/lang/ko.json index a32a70c..6ca004d 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -876,7 +876,7 @@ "fee_normal": "보통", "fetch_prices": "가격 조회", "file": "파일", - "file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/", + "file_save_location": "파일 저장 위치:", "filter": "필터...", "font_scale": "글꼴 크기", "from": "보낸 곳", diff --git a/res/lang/pt.json b/res/lang/pt.json index 049c605..1a5a60e 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -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", diff --git a/res/lang/ru.json b/res/lang/ru.json index de851e0..87fc204 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -876,7 +876,7 @@ "fee_normal": "Обычная", "fetch_prices": "Получить цены", "file": "Файл", - "file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/", + "file_save_location": "Файл будет сохранён в:", "filter": "Фильтр...", "font_scale": "Масштаб шрифта", "from": "От", diff --git a/res/lang/zh.json b/res/lang/zh.json index 059178c..434a3a8 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -876,7 +876,7 @@ "fee_normal": "普通", "fetch_prices": "获取价格", "file": "文件", - "file_save_location": "文件将保存至:~/.config/ObsidianDragon/", + "file_save_location": "文件将保存至:", "filter": "筛选...", "font_scale": "字体大小", "from": "从", diff --git a/res/themes/ui.toml b/res/themes/ui.toml index 7446821..023019c 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -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 } diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 9707b46..412ad4d 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -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. diff --git a/src/ui/sidebar.h b/src/ui/sidebar.h index 924f825..34ee717 100644 --- a/src/ui/sidebar.h +++ b/src/ui/sidebar.h @@ -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(current); bool nav = false; if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) { diff --git a/src/ui/windows/about_dialog.cpp b/src/ui/windows/about_dialog.cpp index f58ce5c..67a6611 100644 --- a/src/ui/windows/about_dialog.cpp +++ b/src/ui/windows/about_dialog.cpp @@ -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")); diff --git a/src/ui/windows/export_all_keys_dialog.cpp b/src/ui/windows/export_all_keys_dialog.cpp index cdb3c98..0a9d7d5 100644 --- a/src/ui/windows/export_all_keys_dialog.cpp +++ b/src/ui/windows/export_all_keys_dialog.cpp @@ -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 { diff --git a/src/ui/windows/export_transactions_dialog.cpp b/src/ui/windows/export_transactions_dialog.cpp index 216090f..5172037 100644 --- a/src/ui/windows/export_transactions_dialog.cpp +++ b/src/ui/windows/export_transactions_dialog.cpp @@ -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()) { diff --git a/src/ui/windows/qr_popup_dialog.cpp b/src/ui/windows/qr_popup_dialog.cpp index 809b852..04f988e 100644 --- a/src/ui/windows/qr_popup_dialog.cpp +++ b/src/ui/windows/qr_popup_dialog.cpp @@ -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(); diff --git a/src/ui/windows/transaction_details_dialog.cpp b/src/ui/windows/transaction_details_dialog.cpp index c533f41..dbb33d3 100644 --- a/src/ui/windows/transaction_details_dialog.cpp +++ b/src/ui/windows/transaction_details_dialog.cpp @@ -10,6 +10,7 @@ #include "../theme.h" #include "../schema/ui_schema.h" #include "../material/draw_helpers.h" +#include "../notifications.h" #include "imgui.h" #include @@ -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")); } } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index f132766..eab6dde 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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...";