feat(settings): migrate Export-Transactions-CSV modal to the reference design
Phase 1 (first modal). Bring the CSV export dialog onto the key-import reference:
- Struct-form OverlayDialogSpec / BlurFloat (was the legacy positional overload).
- material::DialogActionFooter (centered TactileButton pair) replacing the
StyledButtons + the two ImGui::Separators.
- Full i18n: TR("close") and the success/error status now via TR (were hardcoded
English); on success the saved path shows in a widgets::AddressCopyField with a
green check.
- Add ExportTransactionsDialog::hide() + a modal-export-transactions sweep surface
so the dialog is captured (it wasn't before).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/send_tab.h"
|
#include "ui/windows/send_tab.h"
|
||||||
#include "ui/windows/wallets_dialog.h"
|
#include "ui/windows/wallets_dialog.h"
|
||||||
|
#include "ui/windows/export_transactions_dialog.h"
|
||||||
#include "ui/windows/daemon_download_dialog.h"
|
#include "ui/windows/daemon_download_dialog.h"
|
||||||
#include "ui/windows/xmrig_download_dialog.h"
|
#include "ui/windows/xmrig_download_dialog.h"
|
||||||
#include "ui/pages/settings_page.h"
|
#include "ui/pages/settings_page.h"
|
||||||
@@ -266,6 +267,8 @@ void App::buildSweepCatalog()
|
|||||||
[](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
|
[](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
|
||||||
add("modal-export-key", ui::NavPage::Overview,
|
add("modal-export-key", ui::NavPage::Overview,
|
||||||
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
[](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-backup", ui::NavPage::Overview,
|
add("modal-backup", ui::NavPage::Overview,
|
||||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||||
add("modal-about", ui::NavPage::Overview,
|
add("modal-about", ui::NavPage::Overview,
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../widgets/copy_field.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -24,7 +27,8 @@ namespace ui {
|
|||||||
// Static state
|
// Static state
|
||||||
static bool s_open = false;
|
static bool s_open = false;
|
||||||
static char s_filename[256] = "";
|
static char s_filename[256] = "";
|
||||||
static std::string s_status;
|
static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
|
||||||
|
static std::string s_error; // error message on failure; empty otherwise
|
||||||
// Re-entrancy guard: true while a CSV write is in flight (disables the
|
// Re-entrancy guard: true while a CSV write is in flight (disables the
|
||||||
// Export button so a second synchronous write can't be kicked off).
|
// Export button so a second synchronous write can't be kicked off).
|
||||||
static bool s_exporting = false;
|
static bool s_exporting = false;
|
||||||
@@ -52,7 +56,8 @@ static std::string escapeCSV(const std::string& field)
|
|||||||
void ExportTransactionsDialog::show()
|
void ExportTransactionsDialog::show()
|
||||||
{
|
{
|
||||||
s_open = true;
|
s_open = true;
|
||||||
s_status.clear();
|
s_saved_path.clear();
|
||||||
|
s_error.clear();
|
||||||
|
|
||||||
// Generate default filename with timestamp
|
// Generate default filename with timestamp
|
||||||
std::time_t now = std::time(nullptr);
|
std::time_t now = std::time(nullptr);
|
||||||
@@ -66,37 +71,60 @@ bool ExportTransactionsDialog::isOpen()
|
|||||||
return s_open;
|
return s_open;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ExportTransactionsDialog::hide()
|
||||||
|
{
|
||||||
|
s_open = false;
|
||||||
|
s_saved_path.clear();
|
||||||
|
s_error.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void ExportTransactionsDialog::render(App* app)
|
void ExportTransactionsDialog::render(App* app)
|
||||||
{
|
{
|
||||||
if (!s_open) return;
|
if (!s_open) return;
|
||||||
|
namespace m = material;
|
||||||
|
|
||||||
auto& S = schema::UI();
|
m::OverlayDialogSpec ov;
|
||||||
auto win = S.window("dialogs.export-transactions");
|
ov.title = TR("export_tx_title");
|
||||||
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
|
ov.p_open = &s_open;
|
||||||
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
|
ov.style = m::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 520.0f;
|
||||||
|
ov.idSuffix = "exporttx";
|
||||||
|
if (!m::BeginOverlayDialog(ov)) return;
|
||||||
|
|
||||||
if (material::BeginOverlayDialog(TR("export_tx_title"), &s_open, win.width, 0.94f)) {
|
|
||||||
const auto& state = app->getWalletState();
|
const auto& state = app->getWalletState();
|
||||||
|
|
||||||
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Filename
|
// Filename + save-location hint.
|
||||||
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::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("file_save_location"));
|
||||||
|
|
||||||
|
// Result — success shows the saved path as a copy field; failure shows a red line.
|
||||||
|
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_tx_success"));
|
||||||
|
widgets::AddressCopyField("##exportedpath", s_saved_path);
|
||||||
|
} else if (!s_error.empty()) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_error.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextDisabled("%s", TR("file_save_location"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
bool doExport = false, doClose = false;
|
||||||
ImGui::Separator();
|
m::DialogActionFooter(TR("export"), !s_exporting, TR("close"), doExport, doClose);
|
||||||
ImGui::Spacing();
|
if (doClose) s_open = false;
|
||||||
|
if (doExport) {
|
||||||
// Export button (disabled while a write is already in flight)
|
s_saved_path.clear();
|
||||||
ImGui::BeginDisabled(s_exporting);
|
s_error.clear();
|
||||||
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
|
|
||||||
if (state.transactions.empty()) {
|
if (state.transactions.empty()) {
|
||||||
Notifications::instance().warning(TR("export_tx_none"));
|
Notifications::instance().warning(TR("export_tx_none"));
|
||||||
} else {
|
} else {
|
||||||
@@ -106,64 +134,33 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
|
|
||||||
std::ofstream file(filepath);
|
std::ofstream file(filepath);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
s_status = "Failed to create file";
|
s_error = TR("export_tx_file_fail");
|
||||||
Notifications::instance().error(TR("export_tx_file_fail"));
|
Notifications::instance().error(TR("export_tx_file_fail"));
|
||||||
} else {
|
} else {
|
||||||
// Write CSV header
|
|
||||||
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
|
||||||
|
|
||||||
// Write transactions
|
|
||||||
for (const auto& tx : state.transactions) {
|
for (const auto& tx : state.transactions) {
|
||||||
// Date
|
|
||||||
std::time_t t = static_cast<std::time_t>(tx.timestamp);
|
std::time_t t = static_cast<std::time_t>(tx.timestamp);
|
||||||
char datebuf[32];
|
char datebuf[32];
|
||||||
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||||
file << datebuf << ",";
|
file << datebuf << ",";
|
||||||
|
|
||||||
// Type
|
|
||||||
file << escapeCSV(tx.type) << ",";
|
file << escapeCSV(tx.type) << ",";
|
||||||
|
|
||||||
// Amount
|
|
||||||
std::ostringstream amt;
|
std::ostringstream amt;
|
||||||
amt << std::fixed << std::setprecision(8) << tx.amount;
|
amt << std::fixed << std::setprecision(8) << tx.amount;
|
||||||
file << amt.str() << ",";
|
file << amt.str() << ",";
|
||||||
|
|
||||||
// Address
|
|
||||||
file << escapeCSV(tx.address) << ",";
|
file << escapeCSV(tx.address) << ",";
|
||||||
|
|
||||||
// TXID
|
|
||||||
file << escapeCSV(tx.txid) << ",";
|
file << escapeCSV(tx.txid) << ",";
|
||||||
|
|
||||||
// Confirmations
|
|
||||||
file << tx.confirmations << ",";
|
file << tx.confirmations << ",";
|
||||||
|
|
||||||
// Memo
|
|
||||||
file << escapeCSV(tx.memo) << "\n";
|
file << escapeCSV(tx.memo) << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
s_saved_path = filepath;
|
||||||
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
|
||||||
" transactions to: " + filepath;
|
|
||||||
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
||||||
}
|
}
|
||||||
s_exporting = false;
|
s_exporting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
m::EndOverlayDialog();
|
||||||
if (material::StyledButton("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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public:
|
|||||||
|
|
||||||
// Check if dialog is open
|
// Check if dialog is open
|
||||||
static bool isOpen();
|
static bool isOpen();
|
||||||
|
|
||||||
|
// Close the dialog (used by the UI sweep teardown)
|
||||||
|
static void hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
Reference in New Issue
Block a user