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 <noreply@anthropic.com>
135 lines
4.4 KiB
C++
135 lines
4.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 "../../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 <string>
|
|
#include <ctime>
|
|
|
|
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
|