// 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 "../../data/transaction_csv.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 #include 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 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::tm nowTm{}; #ifdef _WIN32 const bool nowOk = localtime_s(&nowTm, &now) == 0; #else const bool nowOk = localtime_r(&now, &nowTm) != nullptr; #endif timebuf[0] = '\0'; if (nowOk) std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", &nowTm); 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(); // 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()) { 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"), true, 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 { std::string configDir = util::Platform::getConfigDir(); std::string filepath = configDir + "/" + s_filename; // Build in memory, then write atomically (temp + fsync + rename): a disk-full / // removable-media failure is reported instead of leaving a truncated CSV behind a success toast. const std::string csv = data::formatTransactionsCsv(state.transactions); if (!util::Platform::writeFileAtomically(filepath, csv)) { s_error = TR("export_tx_file_fail"); Notifications::instance().error(TR("export_tx_file_fail")); } else { s_saved_path = filepath; Notifications::instance().success(TR("export_tx_success"), 5.0f); } } } m::EndOverlayDialog(); } } // namespace ui } // namespace dragonx