Follow-up to 5246898. A transparent address only ever receives mining
rewards on DragonX (private chain), so:
- One explainer ("Receives solo-mining rewards only - the network rejects
payments to it. Rewards are shielded automatically once they mature.")
on the Overview row hover, the Receive picker hover, the Receive card
and the Validate Address result (now typed "Mining address
(transparent)").
- Receive: a selected mining address shows the explainer and a "Use a
shielded address" button (creates one if none exists) instead of the
payment request, and a "Not for payments" panel instead of a QR code.
The default selection prefers a shielded address; the "All" filter
hides empty mining addresses unless solo mining or marked as a mining
address; the filter reads All / Shielded / Mining.
- Request Payment: no transparent section, never prefilled with one.
- Overview: no "Mining - Mining" tag on a marked transparent row.
- FAQ: address answers rewritten; the send and memo answers no longer
imply you can pay a transparent address. All 8 languages.
i18n: 4 new keys, 6 FAQ answers updated in place, 2 dead keys removed,
CJK subset rebuilt. UI sweep: receive-mining-address and
modal-validate-mining targets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
843 lines
46 KiB
C++
843 lines
46 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// Screenshot sweep: capture the UI under every skin. Two modes share one state machine:
|
|
// - startScreenshotSweep() — the legacy tab-only sweep (<config>/screenshots).
|
|
// - startFullUiSweep() — ALSO drives every modal / dialog / multi-step flow / state overlay
|
|
// into view (offline, with injected demo data, firing no live ops) and captures each under
|
|
// every skin (<config>/screenshots-full/<surface>/<skin>.png + index.md).
|
|
// The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces
|
|
// a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the
|
|
// settled frame and calls onScreenshotCaptured() to advance.
|
|
|
|
#include "app.h"
|
|
|
|
#include "config/settings.h"
|
|
#include "data/address_book.h"
|
|
#include "ui/schema/skin_manager.h"
|
|
#include "ui/notifications.h"
|
|
#include "ui/sidebar.h"
|
|
#include "ui/windows/send_tab.h"
|
|
#include "ui/windows/receive_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/bootstrap_download_dialog.h"
|
|
#include "ui/windows/daemon_download_dialog.h"
|
|
#include "ui/windows/xmrig_download_dialog.h"
|
|
#include "ui/windows/qr_popup_dialog.h"
|
|
#include "ui/windows/request_payment_dialog.h"
|
|
#include "ui/windows/validate_address_dialog.h"
|
|
#include "ui/windows/address_label_dialog.h"
|
|
#include "ui/windows/shield_dialog.h"
|
|
#include "ui/windows/address_transfer_dialog.h"
|
|
#include "ui/windows/key_export_dialog.h"
|
|
#include "ui/windows/contacts_tab.h"
|
|
#include "ui/pages/settings_page.h"
|
|
#include "util/platform.h"
|
|
#include "wallet/wallet_capabilities.h"
|
|
|
|
#include "imgui.h"
|
|
#include "imgui_internal.h" // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown
|
|
|
|
#include <sodium.h>
|
|
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
|
|
namespace dragonx {
|
|
|
|
namespace {
|
|
namespace fs = std::filesystem;
|
|
|
|
// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
|
|
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
|
|
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
|
|
int g_pfStyleSnapshot = 0;
|
|
bool g_pfSnapshotValid = false;
|
|
std::vector<double> g_marketHistorySnapshot; // real price history, restored at sweep end
|
|
|
|
// Filesystem-safe one-word tab name.
|
|
const char* sweepPageName(ui::NavPage page)
|
|
{
|
|
switch (page) {
|
|
case ui::NavPage::Overview: return "overview";
|
|
case ui::NavPage::Send: return "send";
|
|
case ui::NavPage::Receive: return "receive";
|
|
case ui::NavPage::History: return "history";
|
|
case ui::NavPage::Contacts: return "contacts";
|
|
case ui::NavPage::Chat: return "chat";
|
|
case ui::NavPage::Mining: return "mining";
|
|
case ui::NavPage::Market: return "market";
|
|
case ui::NavPage::Console: return "console";
|
|
case ui::NavPage::LiteConsole: return "console";
|
|
case ui::NavPage::Peers: return "network";
|
|
case ui::NavPage::LiteNetwork: return "network";
|
|
case ui::NavPage::Explorer: return "explorer";
|
|
case ui::NavPage::Settings: return "settings";
|
|
default: return "page";
|
|
}
|
|
}
|
|
|
|
bool sweepPageEnabled(ui::NavPage page)
|
|
{
|
|
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
|
|
}
|
|
|
|
// Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display).
|
|
const char* kDemoMnemonic =
|
|
"select milk exit banana type alcohol comic moral drama federal just green "
|
|
"elevator render stumble lesson convince organ category caution panther misery pelican immune";
|
|
const char* kDemoZAddr =
|
|
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
|
|
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
|
|
|
|
// Contacts-view sweep: seed a few demo contacts (Z + T types, some global) so the Cards/List/Table
|
|
// modes render with data, and restore the real book afterward. Uses sweepSetEntries (no disk write),
|
|
// so the user's persisted address book is never touched even if the sweep is interrupted.
|
|
std::vector<data::AddressBookEntry> s_contactsSweepBackup;
|
|
void seedSweepContacts(App& a)
|
|
{
|
|
s_contactsSweepBackup = a.addressBook().entries();
|
|
data::AddressBookEntry e1("drgx pool payout address", kDemoZAddr, "mining pool payouts"); // Z, global
|
|
e1.avatar = "icon:account_balance"; // icon avatar (verifies the icon-badge render path)
|
|
data::AddressBookEntry e2("mining rig", "t1DemoTransparentAddressForUiSweep00000", ""); // T (mining only)
|
|
// Scope to the active wallet so it's visible (the tab hides contacts not in the active wallet);
|
|
// non-empty hash also keeps it non-global -> no globe badge, so the list shows a global/non-global mix.
|
|
e2.scope = a.activeWalletIdentityHash();
|
|
data::AddressBookEntry e3("cold savings",
|
|
"zs1sweepdemocoldsavingsaddressforuicapture0000000000000000000000000000", "long-term storage"); // Z, global
|
|
a.addressBook().sweepSetEntries({ e1, e2, e3 });
|
|
}
|
|
void restoreSweepContacts(App& a)
|
|
{
|
|
a.addressBook().sweepSetEntries(s_contactsSweepBackup);
|
|
s_contactsSweepBackup.clear();
|
|
if (a.settings()) a.settings()->setContactsViewMode(0);
|
|
}
|
|
} // namespace
|
|
|
|
std::string App::screenshotDir() const
|
|
{
|
|
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
|
|
}
|
|
|
|
std::string App::screenshotFullDir() const
|
|
{
|
|
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string();
|
|
}
|
|
|
|
// ── Demo data (full sweep only): make every data-dependent screen render offline ─────────────
|
|
void App::installDemoWalletData()
|
|
{
|
|
// Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases).
|
|
auto& s = sweep_state_snapshot_;
|
|
s.connected = state_.connected; s.warming_up = state_.warming_up;
|
|
s.daemon_initializing = state_.daemon_initializing;
|
|
s.encrypted = state_.encrypted; s.locked = state_.locked;
|
|
s.encryption_state_known = state_.encryption_state_known;
|
|
s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description;
|
|
s.sync = state_.sync;
|
|
s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance;
|
|
s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance;
|
|
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
|
|
s.transactions = state_.transactions;
|
|
s.market_price_usd = state_.market.price_usd;
|
|
s.market_change_24h = state_.market.change_24h;
|
|
s.valid = true;
|
|
|
|
applyHealthyDemoState();
|
|
state_.sync.blocks = state_.sync.headers = 3124322;
|
|
state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
|
|
// Legacy (pre-seed) wallet so the Settings "Migrate to seed" button glows in the sweep.
|
|
wallet_seed_status_ = WalletSeedStatus::NoMnemonic;
|
|
|
|
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
|
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
|
state_.market.price_usd = 0.01336200;
|
|
state_.market.change_24h = 5.24000000;
|
|
// Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
|
|
g_marketHistorySnapshot = state_.market.price_history;
|
|
{
|
|
std::vector<double> hist;
|
|
for (int i = 0; i < 48; i++) {
|
|
double t = static_cast<double>(i);
|
|
hist.push_back(0.01250 + 0.0000180 * t
|
|
+ 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
|
|
}
|
|
state_.market.price_history = hist;
|
|
}
|
|
|
|
auto zaddr = [](const char* a, double bal, const char* label) {
|
|
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "shielded"; i.label = label; return i;
|
|
};
|
|
auto taddr = [](const char* a, double bal, const char* label) {
|
|
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "transparent"; i.label = label; return i;
|
|
};
|
|
state_.z_addresses = {
|
|
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
|
zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""),
|
|
};
|
|
state_.t_addresses = {
|
|
taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"),
|
|
taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""),
|
|
};
|
|
state_.rebuildAddressList();
|
|
|
|
auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf,
|
|
const char* addr, const char* memo) {
|
|
TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts;
|
|
t.confirmations = conf; t.address = addr; t.memo = memo; return t;
|
|
};
|
|
state_.transactions = {
|
|
tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42,
|
|
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"),
|
|
tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000,
|
|
1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""),
|
|
tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000,
|
|
1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""),
|
|
tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000,
|
|
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
|
|
};
|
|
|
|
// Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
|
|
// the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
|
|
// here), and clearDemoWalletData() restores them at sweep end.
|
|
if (settings_) {
|
|
g_pfSnapshot = settings_->getPortfolioEntries();
|
|
g_pfStyleSnapshot = settings_->getPortfolioStyle();
|
|
g_pfSnapshotValid = true;
|
|
auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
|
|
bool drgx, bool value, bool ch, bool spark) {
|
|
config::Settings::PortfolioEntry e;
|
|
e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
|
|
e.addresses = { addr }; e.priceBasis = 0;
|
|
e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
|
|
return e;
|
|
};
|
|
settings_->setPortfolioEntries({
|
|
grp("Savings", "savings", 0xFFFF9D4Fu,
|
|
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
|
|
grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
|
|
"t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
|
|
grp("Cold storage", "diamond", 0xFFFF7BB0u,
|
|
"zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
|
|
});
|
|
settings_->setPortfolioStyle(0);
|
|
}
|
|
|
|
seedChatDemoData();
|
|
}
|
|
|
|
void App::applyHealthyDemoState()
|
|
{
|
|
state_.connected = true;
|
|
state_.warming_up = false;
|
|
state_.daemon_initializing = false;
|
|
state_.encryption_state_known = true;
|
|
state_.encrypted = false;
|
|
state_.locked = false;
|
|
state_.warmup_status.clear();
|
|
state_.warmup_description.clear();
|
|
// Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set
|
|
// wizard_phase_ themselves and restore None on teardown).
|
|
wizard_phase_ = WizardPhase::None;
|
|
}
|
|
|
|
void App::clearDemoWalletData()
|
|
{
|
|
auto& s = sweep_state_snapshot_;
|
|
if (!s.valid) return;
|
|
wallet_seed_status_ = WalletSeedStatus::Unknown; // re-probe on the next real connect
|
|
state_.connected = s.connected; state_.warming_up = s.warming_up;
|
|
state_.daemon_initializing = s.daemon_initializing;
|
|
state_.encrypted = s.encrypted; state_.locked = s.locked;
|
|
state_.encryption_state_known = s.encryption_state_known;
|
|
state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description;
|
|
state_.sync = s.sync;
|
|
state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance;
|
|
state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance;
|
|
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
|
|
state_.transactions = s.transactions;
|
|
state_.market.price_usd = s.market_price_usd;
|
|
state_.market.change_24h = s.market_change_24h;
|
|
state_.market.price_history = g_marketHistorySnapshot;
|
|
g_marketHistorySnapshot.clear();
|
|
s = SweepStateSnapshot{}; // invalidate
|
|
|
|
// Restore the user's real portfolio (demo groups were in-memory only).
|
|
if (g_pfSnapshotValid && settings_) {
|
|
settings_->setPortfolioEntries(g_pfSnapshot);
|
|
settings_->setPortfolioStyle(g_pfStyleSnapshot);
|
|
}
|
|
g_pfSnapshot.clear();
|
|
g_pfSnapshotValid = false;
|
|
}
|
|
|
|
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
|
|
// Lambdas defined in this member function may touch App's private members through the App& arg.
|
|
void App::buildSweepCatalog()
|
|
{
|
|
sweep_targets_.clear();
|
|
|
|
// Tabs (both sweeps).
|
|
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
|
|
ui::NavPage pg = static_cast<ui::NavPage>(p);
|
|
if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 });
|
|
}
|
|
if (!sweep_full_) return;
|
|
|
|
// Full sweep: modals / flows / states. Blur overlays need more settle frames.
|
|
const int kOverlaySettle = 8;
|
|
auto add = [&](const char* name, ui::NavPage pg, std::function<void(App&)> setup,
|
|
std::function<void(App&)> teardown) {
|
|
sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle });
|
|
};
|
|
|
|
// Simple bool-flag modals.
|
|
add("modal-import-key", ui::NavPage::Overview,
|
|
[](App& a) { a.import_view_mode_ = false; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
|
|
add("modal-import-viewkey", ui::NavPage::Overview,
|
|
[](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,
|
|
[](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-bootstrap", ui::NavPage::Settings,
|
|
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::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-faq", ui::NavPage::Overview,
|
|
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
|
|
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
|
|
add("modal-encrypt", ui::NavPage::Settings,
|
|
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
|
|
[](App& a) {
|
|
a.show_encrypt_dialog_ = false; a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
|
|
a.encrypt_status_.clear();
|
|
memset(a.encrypt_pass_buf_, 0, sizeof(a.encrypt_pass_buf_));
|
|
memset(a.encrypt_confirm_buf_, 0, sizeof(a.encrypt_confirm_buf_));
|
|
});
|
|
add("modal-change-passphrase", ui::NavPage::Settings,
|
|
[](App& a) { a.show_change_passphrase_ = true; },
|
|
[](App& a) {
|
|
a.show_change_passphrase_ = false; a.encrypt_status_.clear();
|
|
memset(a.change_old_pass_buf_, 0, sizeof(a.change_old_pass_buf_));
|
|
memset(a.change_new_pass_buf_, 0, sizeof(a.change_new_pass_buf_));
|
|
memset(a.change_confirm_buf_, 0, sizeof(a.change_confirm_buf_));
|
|
});
|
|
// Remove-encryption dialog — the redesigned passphrase-entry phase (reset() keeps the
|
|
// workflow in PassphraseEntry; nothing fires the async unlock/export/restart pyramid).
|
|
add("modal-decrypt", ui::NavPage::Settings,
|
|
[](App& a) { a.wallet_security_workflow_.reset(); a.show_decrypt_dialog_ = true; },
|
|
[](App& a) {
|
|
a.show_decrypt_dialog_ = false; a.wallet_security_workflow_.reset();
|
|
memset(a.decrypt_pass_buf_, 0, sizeof(a.decrypt_pass_buf_));
|
|
});
|
|
// PIN setup / change / remove dialogs (never fire the async vault store/verify).
|
|
add("modal-pin-setup", ui::NavPage::Settings,
|
|
[](App& a) { a.show_pin_setup_ = true; },
|
|
[](App& a) {
|
|
a.show_pin_setup_ = false; a.pin_status_.clear();
|
|
memset(a.pin_passphrase_buf_, 0, sizeof(a.pin_passphrase_buf_));
|
|
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
|
|
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
|
|
});
|
|
add("modal-pin-change", ui::NavPage::Settings,
|
|
[](App& a) { a.show_pin_change_ = true; },
|
|
[](App& a) {
|
|
a.show_pin_change_ = false; a.pin_status_.clear();
|
|
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
|
|
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
|
|
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
|
|
});
|
|
add("modal-pin-remove", ui::NavPage::Settings,
|
|
[](App& a) { a.show_pin_remove_ = true; },
|
|
[](App& a) {
|
|
a.show_pin_remove_ = false; a.pin_status_.clear();
|
|
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
|
|
});
|
|
// Wave-1 standalone dialogs (rendered globally from App::render, so any page works).
|
|
add("modal-qr-popup", ui::NavPage::Receive,
|
|
[](App&) { ui::QRPopupDialog::show(kDemoZAddr, "Savings"); },
|
|
[](App&) { ui::QRPopupDialog::close(); });
|
|
add("modal-request-payment", ui::NavPage::Receive,
|
|
[](App&) { ui::RequestPaymentDialog::show(kDemoZAddr); },
|
|
[](App&) { ui::RequestPaymentDialog::hide(); });
|
|
add("modal-validate-address", ui::NavPage::Receive,
|
|
[](App&) { ui::ValidateAddressDialog::show(); },
|
|
[](App&) { ui::ValidateAddressDialog::hide(); });
|
|
add("modal-validate-mining", ui::NavPage::Receive,
|
|
[](App&) { ui::ValidateAddressDialog::sweepShowMiningResult("t1DemoTransparentAddressForUiSweep00000"); },
|
|
[](App&) { ui::ValidateAddressDialog::hide(); });
|
|
// A mining (transparent) address selected on Receive: no payment request, no QR — the explainer instead.
|
|
add("receive-mining-address", ui::NavPage::Receive,
|
|
[](App& a) { ui::SweepSelectReceiveAddress(&a, "t1DemoTransparentAddressForUiSweep00000"); },
|
|
[](App& a) { ui::SweepSelectReceiveAddress(&a, nullptr); });
|
|
add("modal-address-label", ui::NavPage::Overview,
|
|
[](App& a) { ui::AddressLabelDialog::show(&a, kDemoZAddr, true); },
|
|
[](App&) { ui::AddressLabelDialog::hide(); });
|
|
// (No modal-daemon-prompt surface: renderDaemonUpdatePrompt is gated behind !capture_mode_,
|
|
// so it can't render during a sweep. Its migration is verified by build + the shared overlay pattern.)
|
|
add("modal-antivirus", ui::NavPage::Mining,
|
|
[](App& a) { a.pending_antivirus_dialog_ = true; },
|
|
[](App& a) { a.pending_antivirus_dialog_ = false; });
|
|
add("modal-switch-stopnode", ui::NavPage::Settings,
|
|
[](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
|
|
[](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
|
|
add("modal-switch-progress", ui::NavPage::Settings,
|
|
[](App& a) { a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::Stopping));
|
|
a.wallet_switch_dialog_open_.store(true); },
|
|
[](App& a) { a.wallet_switch_dialog_open_.store(false);
|
|
a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::None)); });
|
|
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
|
|
add("modal-shield", ui::NavPage::Send,
|
|
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
|
|
[](App&) { ui::ShieldDialog::hide(); });
|
|
add("modal-merge", ui::NavPage::Send,
|
|
[](App&) { ui::ShieldDialog::showMerge(); },
|
|
[](App&) { ui::ShieldDialog::hide(); });
|
|
// z->t transfer so the (converted) deshielding DialogWarningHeader renders.
|
|
add("modal-transfer", ui::NavPage::Overview,
|
|
[](App& a) {
|
|
ui::AddressTransferInfo info;
|
|
info.fromAddr = kDemoZAddr;
|
|
info.toAddr = "t1DemoTransparentReceiveAddress00000";
|
|
info.fromBalance = 12.5; info.toBalance = 3.0;
|
|
info.fromIsZ = true; info.toIsZ = false;
|
|
ui::AddressTransferDialog::show(&a, info);
|
|
},
|
|
[](App&) { ui::AddressTransferDialog::close(); });
|
|
// Distinct from the settings "modal-export-key" (App::renderExportKeyDialog) — this is the
|
|
// per-address KeyExportDialog opened from Overview address rows.
|
|
add("modal-key-export", ui::NavPage::Overview,
|
|
[](App&) { ui::KeyExportDialog::show(kDemoZAddr, ui::KeyExportDialog::KeyType::Private); },
|
|
[](App&) { ui::KeyExportDialog::hide(); });
|
|
// Contacts address-list view modes, each seeded with demo contacts (restored after; no disk write).
|
|
add("contacts-cards", ui::NavPage::Contacts,
|
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(0); },
|
|
[](App& a) { restoreSweepContacts(a); });
|
|
add("contacts-list", ui::NavPage::Contacts,
|
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1); },
|
|
[](App& a) { restoreSweepContacts(a); });
|
|
add("contacts-table", ui::NavPage::Contacts,
|
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(2); },
|
|
[](App& a) { restoreSweepContacts(a); });
|
|
// Revamped edit dialog: live preview + avatar picker, one surface per avatar mode.
|
|
add("contacts-edit-icon", ui::NavPage::Contacts,
|
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
|
|
ui::ContactsSweepOpenEditDialog(1); },
|
|
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
|
add("contacts-edit-badge", ui::NavPage::Contacts,
|
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
|
|
ui::ContactsSweepOpenEditDialog(0); },
|
|
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
|
add("contacts-edit-image", ui::NavPage::Contacts,
|
|
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
|
|
ui::ContactsSweepOpenEditDialog(2); },
|
|
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
|
add("modal-about", ui::NavPage::Overview,
|
|
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
|
add("modal-settings", ui::NavPage::Settings,
|
|
[](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });
|
|
|
|
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
|
|
add("modal-seed-backup", ui::NavPage::Overview,
|
|
[](App& a) {
|
|
a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true;
|
|
a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false;
|
|
a.seed_backup_status_.clear();
|
|
if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic;
|
|
},
|
|
[](App& a) {
|
|
a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false;
|
|
if (!a.seed_backup_phrase_.empty())
|
|
sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size());
|
|
a.seed_backup_phrase_.clear();
|
|
});
|
|
|
|
// Migrate-to-seed — one surface per step (full-node only). Setting the step directly never
|
|
// fires the async create/sweep/adopt (those are button-triggered).
|
|
if (supportsFullNodeLifecycleActions()) {
|
|
auto mig = [&](const char* name, SeedMigrationStep step, std::function<void(App&)> extra) {
|
|
add(name, ui::NavPage::Settings,
|
|
[step, extra](App& a) {
|
|
a.show_seed_migration_ = true; a.seed_migration_step_ = step;
|
|
a.seed_migration_dest_ = kDemoZAddr;
|
|
if (extra) extra(a);
|
|
},
|
|
[](App& a) {
|
|
a.show_seed_migration_ = false;
|
|
if (!a.seed_migration_seed_.empty())
|
|
sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size());
|
|
a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
|
|
});
|
|
};
|
|
// Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
|
|
// (the probe itself is guarded off during capture_mode_).
|
|
mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
|
|
a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
|
|
a.seed_migration_precheck_started_ = true;
|
|
});
|
|
mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
|
|
a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
|
|
a.seed_migration_precheck_started_ = true;
|
|
});
|
|
mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
|
|
a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
|
|
a.seed_migration_precheck_started_ = true;
|
|
});
|
|
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
|
|
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
|
|
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
|
|
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
|
|
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
|
|
[](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
|
|
mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
|
|
a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
|
|
a.seed_migration_legacy_remaining_ = 0.0;
|
|
});
|
|
mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
|
|
mig("modal-migrate-error", SeedMigrationStep::Error,
|
|
[](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });
|
|
|
|
// Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
|
|
// datadir + cache their metadata so the list renders populated.
|
|
add("modal-wallets", ui::NavPage::Settings,
|
|
[](App& a) {
|
|
std::error_code ec;
|
|
const std::string dd = util::Platform::getDragonXDataDir();
|
|
fs::create_directories(dd, ec);
|
|
if (!fs::exists(dd + "/wallet.dat", ec))
|
|
std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
|
|
if (!fs::exists(dd + "/wallet-savings.dat", ec))
|
|
std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
|
|
data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
|
|
e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
|
|
e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
|
|
data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
|
|
e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
|
|
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
|
|
a.walletIndex().upsert(e1);
|
|
a.walletIndex().upsert(e2);
|
|
// An out-of-datadir wallet (in an extra folder) so the Import action shows too.
|
|
const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
|
|
fs::create_directories(extra, ec);
|
|
if (!fs::exists(extra + "/my-wallet.dat", ec))
|
|
std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
|
|
a.walletIndex().addExtraFolder(extra);
|
|
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
|
ui::WalletsDialog::show(&a);
|
|
},
|
|
[](App&) { ui::WalletsDialog::hide(); });
|
|
|
|
// Many wallets — exercises the viewport-cap path: the card can't fit all rows, so the list
|
|
// must scroll internally while the create/scan/footer controls stay pinned (esp. at 150%).
|
|
// Teardown removes the extra files it dropped so the plain modal-wallets surface stays a
|
|
// clean 3-wallet reference (both surfaces share the one throwaway datadir).
|
|
static const char* kManyExtra[] = {"wallet-cold.dat", "wallet-trading.dat", "wallet-mining.dat",
|
|
"wallet-donations.dat", "wallet-2023.dat", "wallet-payroll.dat"};
|
|
add("modal-wallets-many", ui::NavPage::Settings,
|
|
[](App& a) {
|
|
std::error_code ec;
|
|
const std::string dd = util::Platform::getDragonXDataDir();
|
|
fs::create_directories(dd, ec);
|
|
const char* names[] = {"wallet.dat", "wallet-savings.dat", "wallet-cold.dat",
|
|
"wallet-trading.dat", "wallet-mining.dat", "wallet-donations.dat",
|
|
"wallet-2023.dat", "wallet-payroll.dat"};
|
|
for (int i = 0; i < 8; ++i) {
|
|
if (!fs::exists(dd + "/" + names[i], ec))
|
|
std::ofstream(dd + "/" + names[i], std::ios::binary) << std::string(64000 + i * 4096, '\0');
|
|
data::WalletIndexEntry e; e.fileName = names[i]; e.displayName = names[i];
|
|
e.cachedBalance = 5.0 * (i + 1); e.cachedAddressCount = 2 + i;
|
|
e.lastOpenedEpoch = 1720000000 - i * 86400; e.syncedHere = true;
|
|
a.walletIndex().upsert(e);
|
|
}
|
|
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
|
ui::WalletsDialog::show(&a);
|
|
},
|
|
[](App& a) {
|
|
ui::WalletsDialog::hide();
|
|
std::error_code ec;
|
|
const std::string dd = util::Platform::getDragonXDataDir();
|
|
for (const char* n : kManyExtra) fs::remove(dd + "/" + n, ec);
|
|
});
|
|
}
|
|
|
|
// First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).
|
|
if (!isLiteBuild()) {
|
|
auto wiz = [&](const char* name, WizardPhase phase) {
|
|
add(name, ui::NavPage::Overview,
|
|
[phase](App& a) { a.wizard_phase_ = phase; },
|
|
[](App& a) { a.wizard_phase_ = WizardPhase::None; });
|
|
};
|
|
wiz("wizard-appearance", WizardPhase::Appearance);
|
|
wiz("wizard-bootstrap", WizardPhase::BootstrapOffer);
|
|
wiz("wizard-encrypt", WizardPhase::EncryptOffer);
|
|
wiz("wizard-pin", WizardPhase::PinSetup);
|
|
}
|
|
|
|
// App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end).
|
|
add("overlay-lock", ui::NavPage::Overview,
|
|
[](App& a) { a.state_.encrypted = true; a.state_.locked = true; },
|
|
[](App& a) { a.applyHealthyDemoState(); });
|
|
add("overlay-warmup", ui::NavPage::Overview,
|
|
[](App& a) {
|
|
a.state_.warming_up = true;
|
|
a.state_.warmup_status = "Processing blocks…";
|
|
a.state_.warmup_description = "The node is loading the block index.";
|
|
},
|
|
[](App& a) { a.applyHealthyDemoState(); });
|
|
add("overlay-not-ready", ui::NavPage::Overview,
|
|
[](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; },
|
|
[](App& a) { a.applyHealthyDemoState(); });
|
|
|
|
// A mining (transparent) address as the recipient: shown as unpayable, Send disabled.
|
|
add("send-mining-recipient", ui::NavPage::Send,
|
|
[](App& a) { ui::SweepSetSendRecipient(&a, "R9NXAVJezHiBnT3ijTpg3JUZre7PxhJWti"); },
|
|
[](App& a) { ui::SweepSetSendRecipient(&a, nullptr); });
|
|
|
|
// Send-confirm popup (state lives in send_tab statics → driven via a debug hook).
|
|
add("popup-send-confirm", ui::NavPage::Send,
|
|
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
|
|
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
|
|
|
|
// Market portfolio row styles — capture all three so the redesign is reviewable per skin.
|
|
add("market-rows-compact", ui::NavPage::Market,
|
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
|
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
|
add("market-rows-detailed", ui::NavPage::Market,
|
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
|
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
|
add("market-rows-featured", ui::NavPage::Market,
|
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
|
|
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
|
|
|
// Console RPC command-reference popup (fixed-height dialog with a fill-height command list).
|
|
add("modal-console-commands", ui::NavPage::Console,
|
|
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
|
|
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
|
|
|
|
// Debug-options gate: confirmation + warning, with the passphrase re-auth field (encrypted).
|
|
add("modal-debug-gate", ui::NavPage::Settings,
|
|
[](App& a) { a.state_.encrypted = true; ui::SweepOpenDebugGate(true); },
|
|
[](App& a) { ui::SweepOpenDebugGate(false); a.applyHealthyDemoState(); });
|
|
|
|
// Daemon updater — the two-pane version picker (versions left, selected-version detail right).
|
|
// Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
|
|
add("modal-daemon-update", ui::NavPage::Settings,
|
|
[](App& a) {
|
|
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
|
util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
|
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
|
util::DaemonReleaseAsset as;
|
|
as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
|
|
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
|
|
r.assets.push_back(as);
|
|
return r;
|
|
};
|
|
std::vector<util::DaemonRelease> rels;
|
|
rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
|
|
"## What is DragonX?\n\n"
|
|
"DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. "
|
|
"It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n"
|
|
"## Bug Fixes\n"
|
|
"* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n"
|
|
"* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n"
|
|
"## Key Features\n"
|
|
"* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n"
|
|
"* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n"
|
|
"* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n"
|
|
"## Checksums\n| File | SHA-256 |\n"
|
|
"|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
|
|
rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
|
|
"## Notes\n- Stability improvements\n- RPC fixes\n", false));
|
|
rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
|
|
"- First tagged mainnet build\n", false));
|
|
rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
|
|
"Release candidate for the 1.1.0 series. Testing only.\n", true));
|
|
ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
|
|
"v1.0.3-dc45e7d90");
|
|
},
|
|
[](App&) { ui::DaemonUpdateDialog::sweepClose(); });
|
|
|
|
// Miner (xmrig) updater — same two-pane version picker, seeded with fake releases for the sweep.
|
|
add("modal-xmrig-update", ui::NavPage::Mining,
|
|
[](App& a) {
|
|
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
|
util::XmrigRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
|
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
|
util::XmrigReleaseAsset as;
|
|
as.name = std::string("drg-xmrig-") + tag + "-linux-x64.zip";
|
|
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 8000000;
|
|
r.assets.push_back(as);
|
|
return r;
|
|
};
|
|
std::vector<util::XmrigRelease> rels;
|
|
rels.push_back(mk("v6.25.3", "DRG-XMRig v6.25.3", "2026-06-20",
|
|
"## What's new\n- Rebased on upstream XMRig 6.25.3\n- **RandomX** JIT speedups on modern CPUs\n"
|
|
"- Fix `--cpu-priority` parsing on Windows\n\n## Checksums\n| File | SHA-256 |\n"
|
|
"|---|---|\n| drg-xmrig-v6.25.3-linux-x64.zip | `ab12cd34` |\n", false));
|
|
rels.push_back(mk("v6.24.0", "DRG-XMRig v6.24.0", "2026-04-30",
|
|
"## Notes\n- Pool TLS fixes\n- Lower idle CPU\n", false));
|
|
rels.push_back(mk("v6.23.0", "DRG-XMRig v6.23.0", "2026-03-11",
|
|
"- First DRG-XMRig build\n", false));
|
|
rels.push_back(mk("v6.26.0-rc1", "DRG-XMRig v6.26.0-rc1", "2026-07-02",
|
|
"Release candidate. **Testing only.**\n", true));
|
|
ui::XmrigDownloadDialog::sweepSeed(&a, rels, util::XmrigUpdater::State::ReleaseList, "v6.24.0");
|
|
},
|
|
[](App&) { ui::XmrigDownloadDialog::sweepClose(); });
|
|
|
|
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
|
|
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
|
|
add("modal-folder-picker", ui::NavPage::Settings,
|
|
[](App& a) {
|
|
std::error_code ec;
|
|
const std::string demo = util::Platform::getConfigDir() + "picker-demo";
|
|
for (const char* sub : {"Documents", "Downloads", "Backups", "wallet-archive"})
|
|
fs::create_directories(demo + "/" + sub, ec);
|
|
for (const char* f : {"wallet-cold.dat", "wallet-2023.dat"})
|
|
if (!fs::exists(demo + "/" + f, ec))
|
|
std::ofstream(demo + "/" + f, std::ios::binary) << std::string(66000, '\0');
|
|
ui::WalletsDialog::show(&a);
|
|
ui::FolderPicker::open(demo, [](const std::string&) {});
|
|
},
|
|
[](App&) { ui::FolderPicker::close(); ui::WalletsDialog::hide(); });
|
|
}
|
|
|
|
// ── State machine ───────────────────────────────────────────────────────────────────────────
|
|
void App::startSweepImpl(bool full)
|
|
{
|
|
if (screenshot_sweep_active_) return;
|
|
|
|
sweep_skins_.clear();
|
|
for (const auto& sk : ui::schema::SkinManager::instance().available())
|
|
if (sk.valid) sweep_skins_.push_back(sk.id);
|
|
if (sweep_skins_.empty()) return;
|
|
|
|
// Debug Options "Current theme only": sweep just the active skin instead of cycling every theme.
|
|
if (sweep_current_theme_only_)
|
|
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
|
|
|
|
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
|
|
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
|
|
// under review instead of all surfaces x every skin.
|
|
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
|
|
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
|
|
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
|
|
|
|
sweep_full_ = full;
|
|
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
|
buildSweepCatalog();
|
|
if (!sweepOnlyStr.empty()) {
|
|
std::vector<SweepTarget> keep;
|
|
for (const auto& t : sweep_targets_)
|
|
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
|
|
sweep_targets_.swap(keep);
|
|
}
|
|
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
|
|
|
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
|
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
|
|
|
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
|
|
sweep_saved_page_ = current_page_;
|
|
sweep_skin_idx_ = 0; sweep_target_idx_ = 0;
|
|
screenshot_sweep_active_ = true;
|
|
sweep_capture_this_frame_ = false;
|
|
|
|
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
|
|
applySweepTarget();
|
|
ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
|
|
DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
|
|
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
|
|
}
|
|
|
|
void App::startScreenshotSweep() { startSweepImpl(false); }
|
|
void App::startFullUiSweep() { startSweepImpl(true); }
|
|
|
|
void App::applySweepTarget()
|
|
{
|
|
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
|
current_page_ = t.page;
|
|
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
|
|
if (t.setup) t.setup(*this);
|
|
// <dir>/<surface>/<skin>.png — one subfolder per surface, one PNG per theme, overwritten in
|
|
// place next sweep. (writePng in main.cpp creates the parent subfolder.)
|
|
sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string();
|
|
sweep_settle_frames_ = t.settle;
|
|
sweep_capture_this_frame_ = false;
|
|
}
|
|
|
|
void App::updateScreenshotSweep()
|
|
{
|
|
if (!screenshot_sweep_active_) return;
|
|
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
|
current_page_ = t.page; // keep the surface pinned each frame
|
|
page_alpha_ = 1.0f;
|
|
// Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups
|
|
// (must re-fire while open) + to keep the surface state fixed against any refresh.
|
|
if (t.setup) t.setup(*this);
|
|
if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
|
|
else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame
|
|
}
|
|
|
|
void App::onScreenshotCaptured()
|
|
{
|
|
sweep_capture_this_frame_ = false;
|
|
if (!screenshot_sweep_active_) return;
|
|
|
|
{ const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); }
|
|
|
|
// Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is
|
|
// torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal
|
|
// click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps
|
|
// IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on
|
|
// Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so
|
|
// it can't bleed into the next surface's capture or survive past the sweep.
|
|
if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
|
|
ImGui::ClosePopupToLevel(0, false);
|
|
|
|
if (++sweep_target_idx_ >= static_cast<int>(sweep_targets_.size())) {
|
|
sweep_target_idx_ = 0;
|
|
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
|
|
// Done — restore the original skin + page, and (full) the real state.
|
|
const bool wasFull = sweep_full_;
|
|
if (wasFull) writeSweepManifest();
|
|
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
|
|
current_page_ = sweep_saved_page_;
|
|
page_alpha_ = 1.0f;
|
|
if (wasFull) { clearDemoWalletData(); capture_mode_ = false; }
|
|
sweep_full_ = false;
|
|
screenshot_sweep_active_ = false;
|
|
ui::Notifications::instance().success(
|
|
(wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_);
|
|
DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
|
|
return;
|
|
}
|
|
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
|
|
}
|
|
applySweepTarget();
|
|
}
|
|
|
|
void App::writeSweepManifest() const
|
|
{
|
|
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
|
std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc);
|
|
if (!out) return;
|
|
out << "# Full UI sweep\n\n";
|
|
out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n";
|
|
for (const auto& t : sweep_targets_) {
|
|
out << "## " << t.name << " (base: " << sweepPageName(t.page) << ")\n\n";
|
|
for (const auto& skin : sweep_skins_)
|
|
out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n";
|
|
out << "\n";
|
|
}
|
|
}
|
|
|
|
} // namespace dragonx
|