Files
ObsidianDragon/src/ui/windows/export_transactions_dialog.cpp
DanS bf81ec1702 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>
2026-07-13 21:22:41 -05:00

168 lines
5.4 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#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"
#include <string>
#include <fstream>
#include <sstream>
#include <ctime>
#include <iomanip>
namespace dragonx {
namespace ui {
// Static state
static bool s_open = false;
static char s_filename[256] = "";
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
// Export button so a second synchronous write can't be kicked off).
static bool s_exporting = false;
// Helper to escape CSV field
static std::string escapeCSV(const std::string& field)
{
if (field.find(',') != std::string::npos ||
field.find('"') != std::string::npos ||
field.find('\n') != std::string::npos) {
// Escape quotes and wrap in quotes
std::string escaped;
escaped.reserve(field.size() + 4);
escaped += '"';
for (char c : field) {
if (c == '"') escaped += "\"\"";
else escaped += c;
}
escaped += '"';
return escaped;
}
return field;
}
void ExportTransactionsDialog::show()
{
s_open = true;
s_saved_path.clear();
s_error.clear();
// Generate default filename with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
snprintf(s_filename, sizeof(s_filename), "dragonx_transactions_%s.csv", timebuf);
}
bool ExportTransactionsDialog::isOpen()
{
return s_open;
}
void ExportTransactionsDialog::hide()
{
s_open = false;
s_saved_path.clear();
s_error.clear();
}
void ExportTransactionsDialog::render(App* app)
{
if (!s_open) return;
namespace m = material;
m::OverlayDialogSpec ov;
ov.title = TR("export_tx_title");
ov.p_open = &s_open;
ov.style = m::OverlayStyle::BlurFloat;
ov.cardWidth = 520.0f;
ov.idSuffix = "exporttx";
if (!m::BeginOverlayDialog(ov)) return;
const auto& state = app->getWalletState();
ImGui::Text(TR("export_tx_count"), state.transactions.size());
ImGui::Spacing();
// 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"));
// 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();
bool doExport = false, doClose = false;
m::DialogActionFooter(TR("export"), !s_exporting, TR("close"), doExport, doClose);
if (doClose) s_open = false;
if (doExport) {
s_saved_path.clear();
s_error.clear();
if (state.transactions.empty()) {
Notifications::instance().warning(TR("export_tx_none"));
} else {
s_exporting = true;
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
std::ofstream file(filepath);
if (!file.is_open()) {
s_error = TR("export_tx_file_fail");
Notifications::instance().error(TR("export_tx_file_fail"));
} else {
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
for (const auto& tx : state.transactions) {
std::time_t t = static_cast<std::time_t>(tx.timestamp);
char datebuf[32];
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
file << datebuf << ",";
file << escapeCSV(tx.type) << ",";
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << tx.amount;
file << amt.str() << ",";
file << escapeCSV(tx.address) << ",";
file << escapeCSV(tx.txid) << ",";
file << tx.confirmations << ",";
file << escapeCSV(tx.memo) << "\n";
}
file.close();
s_saved_path = filepath;
Notifications::instance().success(TR("export_tx_success"), 5.0f);
}
s_exporting = false;
}
}
m::EndOverlayDialog();
}
} // namespace ui
} // namespace dragonx