feat(settings): migrate Export All Keys modal to the reference design
Phase 1. DialogWarningHeader care header (was a hand-rolled red icon+text); centered DialogActionFooter (Export + Close) replacing the StyledButtons + two separators; on full success the saved path shows in a copy field with a green check, partial/error render inline; export progress uses LoadingDots. Full i18n: the previously-hardcoded notification + status strings now use TR (8 new export_keys_* keys in en + 8 langs). Added hide() + a modal-export-all-keys sweep surface. Also fixes a latent Begin/EndOverlayDialog imbalance on the 0-address early return. No change to the export RPC logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
#include "ui/windows/send_tab.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/export_transactions_dialog.h"
|
||||
#include "ui/windows/export_all_keys_dialog.h"
|
||||
#include "ui/windows/daemon_download_dialog.h"
|
||||
#include "ui/windows/xmrig_download_dialog.h"
|
||||
#include "ui/pages/settings_page.h"
|
||||
@@ -269,6 +270,8 @@ void App::buildSweepCatalog()
|
||||
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
||||
add("modal-export-transactions", ui::NavPage::Settings,
|
||||
[](App&) { ui::ExportTransactionsDialog::show(); }, [](App&) { ui::ExportTransactionsDialog::hide(); });
|
||||
add("modal-export-all-keys", ui::NavPage::Settings,
|
||||
[](App&) { ui::ExportAllKeysDialog::show(); }, [](App&) { ui::ExportAllKeysDialog::hide(); });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||
add("modal-about", ui::NavPage::Overview,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../widgets/copy_field.h"
|
||||
#include "../theme.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
@@ -29,7 +30,8 @@ using json = nlohmann::json;
|
||||
// Static state
|
||||
static bool s_open = false;
|
||||
static bool s_exporting = false;
|
||||
static std::string s_status;
|
||||
static std::string s_status; // error / partial message (non-empty => red text)
|
||||
static std::string s_saved_path; // full path on full success (=> green check + copy field)
|
||||
static std::string s_exported_keys;
|
||||
static int s_total_addresses = 0;
|
||||
static int s_exported_count = 0;
|
||||
@@ -42,6 +44,7 @@ void ExportAllKeysDialog::show()
|
||||
s_open = true;
|
||||
s_exporting = false;
|
||||
s_status.clear();
|
||||
s_saved_path.clear();
|
||||
s_exported_keys.clear();
|
||||
s_total_addresses = 0;
|
||||
s_exported_count = 0;
|
||||
@@ -60,74 +63,77 @@ bool ExportAllKeysDialog::isOpen()
|
||||
return s_open;
|
||||
}
|
||||
|
||||
void ExportAllKeysDialog::hide()
|
||||
{
|
||||
s_open = false;
|
||||
s_status.clear();
|
||||
s_saved_path.clear();
|
||||
}
|
||||
|
||||
void ExportAllKeysDialog::render(App* app)
|
||||
{
|
||||
if (!s_open) return;
|
||||
namespace m = material;
|
||||
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.export-all-keys");
|
||||
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
|
||||
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
|
||||
m::OverlayDialogSpec ov;
|
||||
ov.title = TR("export_keys_title");
|
||||
ov.p_open = &s_open;
|
||||
ov.style = m::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 600.0f;
|
||||
ov.idSuffix = "exportallkeys";
|
||||
if (m::BeginOverlayDialog(ov)) {
|
||||
m::DialogWarningHeader(TR("export_keys_danger"));
|
||||
ImGui::Spacing();
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("export_keys_title"); ov.p_open = &s_open;
|
||||
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
||||
ov.cardWidth = win.width; ov.cardBottomViewportRatio = 0.94f; // keep authored width
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
// Warning
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::Text(ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f);
|
||||
ImGui::TextWrapped("%s", TR("export_keys_danger"));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (s_exporting) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
// Options
|
||||
ImGui::Text("%s", TR("export_keys_options"));
|
||||
ImGui::BeginDisabled(s_exporting);
|
||||
ImGui::TextUnformatted(TR("export_keys_options"));
|
||||
ImGui::Checkbox(TR("export_keys_include_z"), &s_include_z);
|
||||
ImGui::Checkbox(TR("export_keys_include_t"), &s_include_t);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Filename
|
||||
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
|
||||
m::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
||||
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
||||
ImGui::EndDisabled();
|
||||
|
||||
// Progress / result.
|
||||
if (s_exporting) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), TR("export_keys_progress"),
|
||||
s_exported_count, s_total_addresses);
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), "%s", m::LoadingDots());
|
||||
} else if (!s_saved_path.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushFont(m::Type().iconSmall());
|
||||
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
||||
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_keys_success"));
|
||||
widgets::AddressCopyField("##exportedkeyspath", s_saved_path);
|
||||
} else if (!s_status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_status.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Export button
|
||||
if (s_exporting) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (material::StyledButton(TR("export_keys_btn"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
||||
|
||||
bool doExport = false, doClose = false;
|
||||
m::DialogActionFooter(TR("export_keys_btn"), !s_exporting, TR("close"), doExport, doClose);
|
||||
if (doClose) s_open = false;
|
||||
if (doExport) {
|
||||
if (!s_include_z && !s_include_t) {
|
||||
Notifications::instance().warning("Select at least one address type");
|
||||
Notifications::instance().warning(TR("export_keys_select_type"));
|
||||
} else if (!app->rpc() || !app->rpc()->isConnected()) {
|
||||
Notifications::instance().error("Not connected to daemon");
|
||||
Notifications::instance().error(TR("export_keys_not_connected"));
|
||||
} else {
|
||||
s_exporting = true;
|
||||
s_exported_keys.clear();
|
||||
s_exported_count = 0;
|
||||
s_status = "Exporting keys...";
|
||||
|
||||
s_status.clear();
|
||||
s_saved_path.clear();
|
||||
|
||||
const auto& state = app->getWalletState();
|
||||
|
||||
// Count total addresses to export
|
||||
@@ -137,7 +143,8 @@ void ExportAllKeysDialog::render(App* app)
|
||||
|
||||
if (s_total_addresses == 0) {
|
||||
s_exporting = false;
|
||||
s_status = "No addresses to export";
|
||||
s_status = TR("export_keys_none_addrs");
|
||||
m::EndOverlayDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -216,21 +223,21 @@ void ExportAllKeysDialog::render(App* app)
|
||||
return [exported, total, filepath, writeOk]() {
|
||||
s_exported_count = exported;
|
||||
s_exporting = false;
|
||||
char buf[320];
|
||||
if (exported == 0) {
|
||||
s_status = "No keys exported (0 of " + std::to_string(total) +
|
||||
") — unlock the wallet (if encrypted) and try again.";
|
||||
Notifications::instance().error("No keys could be exported — is the wallet unlocked?");
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_none_result"), total);
|
||||
s_status = buf;
|
||||
Notifications::instance().error(TR("export_keys_none_toast"));
|
||||
} else if (!writeOk) {
|
||||
s_status = "Failed to write file";
|
||||
Notifications::instance().error("Failed to save key file");
|
||||
s_status = TR("export_keys_write_fail");
|
||||
Notifications::instance().error(TR("export_keys_write_fail"));
|
||||
} else if (exported < total) {
|
||||
s_status = "Exported " + std::to_string(exported) + " of " +
|
||||
std::to_string(total) + " keys to: " + filepath +
|
||||
" (INCOMPLETE — some addresses had no spending key or the wallet is locked)";
|
||||
Notifications::instance().warning("Partial export: " + std::to_string(exported) +
|
||||
" of " + std::to_string(total) + " keys");
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_partial"), exported, total);
|
||||
s_status = buf;
|
||||
snprintf(buf, sizeof(buf), TR("export_keys_partial_toast"), exported, total);
|
||||
Notifications::instance().warning(buf);
|
||||
} else {
|
||||
s_status = "Exported to: " + filepath;
|
||||
s_saved_path = filepath;
|
||||
Notifications::instance().success(TR("export_keys_success"), 5.0f);
|
||||
}
|
||||
};
|
||||
@@ -238,24 +245,8 @@ void ExportAllKeysDialog::render(App* app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_exporting) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(TR("export_keys_progress"), s_exported_count, s_total_addresses);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// Status
|
||||
if (!s_status.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", s_status.c_str());
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
|
||||
m::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ public:
|
||||
|
||||
// Check if dialog is open
|
||||
static bool isOpen();
|
||||
|
||||
// Close the dialog (used by the UI sweep teardown)
|
||||
static void hide();
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -1327,6 +1327,14 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["export_keys_include_z"] = "Include Z-addresses (shielded)";
|
||||
strings_["export_keys_options"] = "Export options:";
|
||||
strings_["export_keys_success"] = "Keys exported successfully";
|
||||
strings_["export_keys_select_type"] = "Select at least one address type";
|
||||
strings_["export_keys_not_connected"] = "Not connected to the daemon";
|
||||
strings_["export_keys_none_addrs"] = "No addresses to export";
|
||||
strings_["export_keys_none_result"] = "No keys exported (0 of %d) \xE2\x80\x94 unlock the wallet and try again.";
|
||||
strings_["export_keys_none_toast"] = "No keys could be exported \xE2\x80\x94 is the wallet unlocked?";
|
||||
strings_["export_keys_write_fail"] = "Failed to write the key file.";
|
||||
strings_["export_keys_partial"] = "Exported %d of %d keys \xE2\x80\x94 incomplete (some had no spending key, or the wallet is locked).";
|
||||
strings_["export_keys_partial_toast"] = "Partial export: %d of %d keys";
|
||||
strings_["export_keys_title"] = "Export All Private Keys";
|
||||
|
||||
// --- Export Transactions Dialog ---
|
||||
|
||||
Reference in New Issue
Block a user