feat(debug): full UI screenshot sweep — captures modals/dialogs/flows/states
The existing sweep only captures tabs. This adds a "Full UI sweep" (DEBUG OPTIONS) that also drives every normally-hidden surface into view and captures it under every skin, so all the UI a normal sweep misses can be reviewed at once. - The capture unit becomes a "surface" (SweepTarget: a base tab, optionally with a setup()/teardown() that forces a modal / multi-step flow / state overlay on top). Tabs are surfaces with a null setup. The existing tab sweep is folded into the same state machine (one code path). All sweep logic + the catalog + demo data move into a new src/app_sweep.cpp. - Runs OFFLINE against injected demo data (installDemoWalletData snapshots then restores the real state_ — WalletState isn't copy-assignable, so a targeted field snapshot is used) and fires NO live op: capture_mode_ guards the seed-backup RPC, auto-lock, and the migration pumps; the async-firing dialogs are neutralized by pre-setting their state (fetch_started_ + demo phrase; seed_migration_step_ set directly). Verified: 0 RPC/secret/send calls. - Catalog (v1): tabs + the bool-flag modals (import/export key, backup, seed-backup, about, settings) + the 6 migrate-to-seed steps + 4 wizard steps + lock/warmup/not-ready overlays + the send-confirm popup (via a new ui::SweepShowSendConfirm debug hook, since its state is send_tab-static). - Overlay/blur surfaces settle 8 frames (blur backdrop freeze); setup re-runs each frame (OpenPopup popups must re-fire). updateScreenshotSweep now runs before the first-run-wizard early-return in render() so the sweep advances while a wizard step is shown. main.cpp drops vsync during a sweep so it isn't throttled to the compositor's unfocused rate. Output: <config>/screenshots-full/<surface>/<skin>.png + index.md. - DRAGONX_FULL_SWEEP=1 runs the sweep headlessly then quits (CI / verification). Verified end-to-end under WSLg: 288 PNGs (9 skins x 32 surfaces), all 1200x720, [Sweep] done, clean quit, no daemon. Byte-identity across runs holds only for static skins on data-free surfaces — the rest vary by animated theme effects / live price / relative timestamps (expected for a review tool, not a bug). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -404,6 +404,7 @@ set(APP_SOURCES
|
|||||||
src/app.cpp
|
src/app.cpp
|
||||||
src/app_network.cpp
|
src/app_network.cpp
|
||||||
src/app_security.cpp
|
src/app_security.cpp
|
||||||
|
src/app_sweep.cpp
|
||||||
src/app_wizard.cpp
|
src/app_wizard.cpp
|
||||||
src/services/network_refresh_service.cpp
|
src/services/network_refresh_service.cpp
|
||||||
src/services/refresh_scheduler.cpp
|
src/services/refresh_scheduler.cpp
|
||||||
|
|||||||
13
src/app.cpp
13
src/app.cpp
@@ -1272,6 +1272,11 @@ void App::handleGlobalShortcuts()
|
|||||||
|
|
||||||
void App::render()
|
void App::render()
|
||||||
{
|
{
|
||||||
|
// Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the
|
||||||
|
// sweep keeps progressing (and re-forces its target's state) even while a wizard step is shown.
|
||||||
|
// (Pins current_page_ too, before the sidebar reads it further down.)
|
||||||
|
updateScreenshotSweep();
|
||||||
|
|
||||||
// First-run wizard gate — blocks all normal UI
|
// First-run wizard gate — blocks all normal UI
|
||||||
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
||||||
renderFirstRunWizard();
|
renderFirstRunWizard();
|
||||||
@@ -1303,9 +1308,6 @@ void App::render()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
|
|
||||||
// before the sidebar reads current_page_ (below) so the forced page is reflected.
|
|
||||||
updateScreenshotSweep();
|
|
||||||
|
|
||||||
// While any full-window BLUR overlay is open: (1) suppress panel theme-effects for the whole
|
// While any full-window BLUR overlay is open: (1) suppress panel theme-effects for the whole
|
||||||
// frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay;
|
// frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay;
|
||||||
@@ -2969,8 +2971,9 @@ void App::renderSeedBackupDialog()
|
|||||||
show_seed_backup_ = false;
|
show_seed_backup_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kick off the async z_exportmnemonic fetch once, and only while unlocked.
|
// Kick off the async z_exportmnemonic fetch once, and only while unlocked. Never during a UI
|
||||||
if (!seed_backup_fetch_started_ && !state_.isLocked()) {
|
// sweep — the sweep pre-sets fetch_started_ + a demo phrase, but guard the RPC too.
|
||||||
|
if (!seed_backup_fetch_started_ && !state_.isLocked() && !capture_mode_) {
|
||||||
seed_backup_fetch_started_ = true;
|
seed_backup_fetch_started_ = true;
|
||||||
seed_backup_loading_ = true;
|
seed_backup_loading_ = true;
|
||||||
seed_backup_no_mnemonic_ = false;
|
seed_backup_no_mnemonic_ = false;
|
||||||
|
|||||||
47
src/app.h
47
src/app.h
@@ -343,6 +343,12 @@ public:
|
|||||||
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
||||||
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
||||||
void startScreenshotSweep();
|
void startScreenshotSweep();
|
||||||
|
// Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
|
||||||
|
// state overlay into view (with injected demo data, offline, firing no live ops) and captures
|
||||||
|
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||||||
|
void startFullUiSweep();
|
||||||
|
std::string screenshotFullDir() const;
|
||||||
|
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||||||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||||
void onScreenshotCaptured();
|
void onScreenshotCaptured();
|
||||||
@@ -756,16 +762,49 @@ private:
|
|||||||
bool screenshot_sweep_active_ = false;
|
bool screenshot_sweep_active_ = false;
|
||||||
bool sweep_capture_this_frame_ = false;
|
bool sweep_capture_this_frame_ = false;
|
||||||
int sweep_skin_idx_ = 0;
|
int sweep_skin_idx_ = 0;
|
||||||
int sweep_page_idx_ = 0;
|
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||||||
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
|
|
||||||
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
||||||
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
|
|
||||||
std::string sweep_dir_; // output folder for this sweep
|
std::string sweep_dir_; // output folder for this sweep
|
||||||
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
||||||
std::string sweep_saved_skin_; // restore on completion
|
std::string sweep_saved_skin_; // restore on completion
|
||||||
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
||||||
void updateScreenshotSweep(); // called at the top of render() while active
|
void updateScreenshotSweep(); // called at the top of render() while active
|
||||||
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
|
void applySweepTarget(); // apply current (skin,page/surface), path, arm settle
|
||||||
|
|
||||||
|
// --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
|
||||||
|
// state forced on top). A tab is a surface with a null setup. ---
|
||||||
|
struct SweepTarget {
|
||||||
|
std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview"
|
||||||
|
ui::NavPage page = ui::NavPage::Overview; // base tab under the surface
|
||||||
|
std::function<void(App&)> setup; // reveal the surface (null = plain tab)
|
||||||
|
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
|
||||||
|
int settle = 4;// blur overlays override to 8
|
||||||
|
};
|
||||||
|
std::vector<SweepTarget> sweep_targets_;
|
||||||
|
int sweep_target_idx_ = 0;
|
||||||
|
bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
|
||||||
|
// capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
|
||||||
|
// RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
|
||||||
|
bool capture_mode_ = false;
|
||||||
|
void startSweepImpl(bool full);
|
||||||
|
void buildSweepCatalog();
|
||||||
|
void installDemoWalletData();
|
||||||
|
void clearDemoWalletData();
|
||||||
|
void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values
|
||||||
|
void writeSweepManifest() const;
|
||||||
|
// Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
|
||||||
|
// plain struct because WalletState has reference-alias members and isn't copy-assignable.
|
||||||
|
struct SweepStateSnapshot {
|
||||||
|
bool valid = false;
|
||||||
|
bool connected=false, warming_up=false, daemon_initializing=false;
|
||||||
|
bool encrypted=false, locked=false, encryption_state_known=false;
|
||||||
|
std::string warmup_status, warmup_description;
|
||||||
|
SyncInfo sync;
|
||||||
|
double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
|
||||||
|
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
||||||
|
std::vector<TransactionInfo> transactions;
|
||||||
|
double market_price_usd=0;
|
||||||
|
} sweep_state_snapshot_;
|
||||||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||||||
|
|||||||
@@ -759,109 +759,7 @@ void App::setCurrentPage(ui::NavPage page)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Debug screenshot sweep ──────────────────────────────────────────────────
|
// Screenshot sweep (tab-only + full-UI) + its demo data live in src/app_sweep.cpp.
|
||||||
// Cycle every skin x every build-enabled tab, saving one PNG per (skin,tab). Driven from
|
|
||||||
// App::render() (updateScreenshotSweep pins the page + settles); main.cpp reads the framebuffer
|
|
||||||
// on the settled frame and calls onScreenshotCaptured() to advance.
|
|
||||||
|
|
||||||
// Filesystem-safe one-word page name (tracePageName returns "Overview tab" etc.).
|
|
||||||
static 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::Peers: return "network";
|
|
||||||
case ui::NavPage::Explorer: return "explorer";
|
|
||||||
case ui::NavPage::Settings: return "settings";
|
|
||||||
default: return "page";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string App::screenshotDir() const
|
|
||||||
{
|
|
||||||
return (std::filesystem::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::startScreenshotSweep()
|
|
||||||
{
|
|
||||||
if (screenshot_sweep_active_) return;
|
|
||||||
|
|
||||||
sweep_skins_.clear();
|
|
||||||
for (const auto& s : ui::schema::SkinManager::instance().available())
|
|
||||||
if (s.valid) sweep_skins_.push_back(s.id);
|
|
||||||
sweep_pages_.clear();
|
|
||||||
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
|
|
||||||
ui::NavPage pg = static_cast<ui::NavPage>(p);
|
|
||||||
if (isPageEnabledForBuild(pg)) sweep_pages_.push_back(pg);
|
|
||||||
}
|
|
||||||
if (sweep_skins_.empty() || sweep_pages_.empty()) return;
|
|
||||||
|
|
||||||
// Fixed output folder (organized into per-tab subfolders below); subsequent sweeps overwrite the
|
|
||||||
// existing PNGs in place rather than creating a new folder each time.
|
|
||||||
sweep_dir_ = screenshotDir();
|
|
||||||
std::error_code ec; std::filesystem::create_directories(sweep_dir_, ec);
|
|
||||||
|
|
||||||
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
|
|
||||||
sweep_saved_page_ = current_page_;
|
|
||||||
sweep_skin_idx_ = 0; sweep_page_idx_ = 0;
|
|
||||||
screenshot_sweep_active_ = true;
|
|
||||||
sweep_capture_this_frame_ = false;
|
|
||||||
|
|
||||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
|
|
||||||
applySweepTarget();
|
|
||||||
ui::Notifications::instance().info("Screenshot sweep running…");
|
|
||||||
DEBUG_LOGF("[Sweep] -> %s (%d skins x %d pages)\n",
|
|
||||||
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_pages_.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::applySweepTarget()
|
|
||||||
{
|
|
||||||
current_page_ = sweep_pages_[sweep_page_idx_];
|
|
||||||
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
|
|
||||||
// Organized as <screenshots>/<tab>/<skin>.png — one subfolder per tab, one PNG per theme,
|
|
||||||
// overwritten in place on the next sweep. (writePng in main.cpp creates the parent subfolder.)
|
|
||||||
sweep_current_path_ = (std::filesystem::path(sweep_dir_) / sweepPageName(current_page_)
|
|
||||||
/ (sweep_skins_[sweep_skin_idx_] + ".png")).string();
|
|
||||||
sweep_settle_frames_ = 4; // let the new skin (TOML reload / acrylic recapture) + page settle
|
|
||||||
sweep_capture_this_frame_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::updateScreenshotSweep()
|
|
||||||
{
|
|
||||||
if (!screenshot_sweep_active_) return;
|
|
||||||
current_page_ = sweep_pages_[sweep_page_idx_]; // keep the page pinned each frame
|
|
||||||
page_alpha_ = 1.0f;
|
|
||||||
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;
|
|
||||||
if (++sweep_page_idx_ >= static_cast<int>(sweep_pages_.size())) {
|
|
||||||
sweep_page_idx_ = 0;
|
|
||||||
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
|
|
||||||
// Done — restore the original skin + page.
|
|
||||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
|
|
||||||
current_page_ = sweep_saved_page_;
|
|
||||||
page_alpha_ = 1.0f;
|
|
||||||
screenshot_sweep_active_ = false;
|
|
||||||
ui::Notifications::instance().success("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();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool App::shouldRefreshTransactions() const
|
bool App::shouldRefreshTransactions() const
|
||||||
{
|
{
|
||||||
@@ -2914,6 +2812,7 @@ void App::exportSeedPhrase(std::function<void(bool, bool, const std::string&, co
|
|||||||
// learns whether a mnemonic exists). Legacy (non-mnemonic) wallets are not nagged.
|
// learns whether a mnemonic exists). Legacy (non-mnemonic) wallets are not nagged.
|
||||||
void App::maybeRemindSeedBackup()
|
void App::maybeRemindSeedBackup()
|
||||||
{
|
{
|
||||||
|
if (capture_mode_) return; // no live ops during a UI sweep
|
||||||
if (lite_wallet_) return; // lite has its own seed UX
|
if (lite_wallet_) return; // lite has its own seed UX
|
||||||
if (!settings_ || settings_->getSeedBackupReminded()) return;
|
if (!settings_ || settings_->getSeedBackupReminded()) return;
|
||||||
if (!state_.connected || !state_.encryption_state_known) return;
|
if (!state_.connected || !state_.encryption_state_known) return;
|
||||||
@@ -3063,6 +2962,7 @@ void App::beginSweepToSeedWallet()
|
|||||||
// remainder is left behind (a sweep too big for one transaction).
|
// remainder is left behind (a sweep too big for one transaction).
|
||||||
void App::pollSweepStatus()
|
void App::pollSweepStatus()
|
||||||
{
|
{
|
||||||
|
if (capture_mode_) return; // no live ops during a UI sweep
|
||||||
if (!rpc_ || !worker_ || !state_.connected) return;
|
if (!rpc_ || !worker_ || !state_.connected) return;
|
||||||
if (seed_migration_confirm_in_flight_ || seed_migration_sweep_txid_.empty()) return;
|
if (seed_migration_confirm_in_flight_ || seed_migration_sweep_txid_.empty()) return;
|
||||||
seed_migration_confirm_in_flight_ = true;
|
seed_migration_confirm_in_flight_ = true;
|
||||||
@@ -3172,6 +3072,7 @@ void App::beginAdoptSeedWallet()
|
|||||||
|
|
||||||
void App::pumpSeedMigration()
|
void App::pumpSeedMigration()
|
||||||
{
|
{
|
||||||
|
if (capture_mode_) return; // no live ops during a UI sweep (steps are set directly)
|
||||||
// --- Adopt handoff (Phase 2, background daemon swap) ---
|
// --- Adopt handoff (Phase 2, background daemon swap) ---
|
||||||
{
|
{
|
||||||
bool done = false, ok = false; std::string err;
|
bool done = false, ok = false; std::string err;
|
||||||
|
|||||||
@@ -593,6 +593,7 @@ void App::refreshWalletEncryptionState() {
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
void App::checkAutoLock() {
|
void App::checkAutoLock() {
|
||||||
|
if (capture_mode_) return; // don't auto-lock while a UI sweep forces the encrypted/locked state
|
||||||
if (!state_.isEncrypted() || state_.isLocked()) return;
|
if (!state_.isEncrypted() || state_.isLocked()) return;
|
||||||
|
|
||||||
// Don't auto-lock while mining — mining is a long-running intentional
|
// Don't auto-lock while mining — mining is a long-running intentional
|
||||||
|
|||||||
389
src/app_sweep.cpp
Normal file
389
src/app_sweep.cpp
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
// 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 "ui/schema/skin_manager.h"
|
||||||
|
#include "ui/notifications.h"
|
||||||
|
#include "ui/sidebar.h"
|
||||||
|
#include "ui/windows/send_tab.h"
|
||||||
|
#include "util/platform.h"
|
||||||
|
#include "wallet/wallet_capabilities.h"
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
} // 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.valid = true;
|
||||||
|
|
||||||
|
applyHealthyDemoState();
|
||||||
|
state_.sync.blocks = state_.sync.headers = 3124322;
|
||||||
|
state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
|
||||||
|
|
||||||
|
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
||||||
|
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
||||||
|
state_.market.price_usd = 0.01336200;
|
||||||
|
|
||||||
|
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||||
|
AddressInfo i; i.address = a; i.balance = 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.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"),
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
s = SweepStateSnapshot{}; // invalidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
|
||||||
|
add("modal-export-key", ui::NavPage::Overview,
|
||||||
|
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
||||||
|
add("modal-backup", ui::NavPage::Overview,
|
||||||
|
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
mig("modal-migrate-intro", SeedMigrationStep::Intro, nullptr);
|
||||||
|
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; });
|
||||||
|
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."; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(); });
|
||||||
|
|
||||||
|
// 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); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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;
|
||||||
|
|
||||||
|
sweep_full_ = full;
|
||||||
|
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||||
|
buildSweepCatalog();
|
||||||
|
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); }
|
||||||
|
|
||||||
|
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
|
||||||
25
src/main.cpp
25
src/main.cpp
@@ -1754,6 +1754,16 @@ int main(int argc, char* argv[])
|
|||||||
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often
|
||||||
|
// heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends.
|
||||||
|
{
|
||||||
|
static bool sweepVsyncOff = false;
|
||||||
|
const bool sweeping = app.isScreenshotSweeping();
|
||||||
|
if (sweeping != sweepVsyncOff) {
|
||||||
|
SDL_GL_SetSwapInterval(sweeping ? 0 : 1);
|
||||||
|
sweepVsyncOff = sweeping;
|
||||||
|
}
|
||||||
|
}
|
||||||
SDL_GL_SwapWindow(window);
|
SDL_GL_SwapWindow(window);
|
||||||
#endif
|
#endif
|
||||||
PERF_END("Present", _perfPresent);
|
PERF_END("Present", _perfPresent);
|
||||||
@@ -1761,6 +1771,21 @@ int main(int argc, char* argv[])
|
|||||||
// --- PerfLog: end frame ---
|
// --- PerfLog: end frame ---
|
||||||
dragonx::util::PerfLog::instance().endFrame();
|
dragonx::util::PerfLog::instance().endFrame();
|
||||||
|
|
||||||
|
// Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has
|
||||||
|
// settled, then quits when it finishes. Used to run the sweep without clicking the button.
|
||||||
|
{
|
||||||
|
static const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
|
||||||
|
static int autoFrames = 0;
|
||||||
|
static bool autoStarted = false;
|
||||||
|
if (autoSweep) {
|
||||||
|
if (!autoStarted) {
|
||||||
|
if (++autoFrames >= 90) { app.startFullUiSweep(); autoStarted = true; }
|
||||||
|
} else if (!app.isScreenshotSweeping()) {
|
||||||
|
running = false; // sweep complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Exit when shutdown is complete
|
// Exit when shutdown is complete
|
||||||
if (app.shouldQuit()) {
|
if (app.shouldQuit()) {
|
||||||
running = false;
|
running = false;
|
||||||
|
|||||||
@@ -2510,6 +2510,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button")))
|
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button")))
|
||||||
app->startScreenshotSweep();
|
app->startScreenshotSweep();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
// Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline).
|
||||||
|
if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button")))
|
||||||
|
app->startFullUiSweep();
|
||||||
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
|
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
|
||||||
util::Platform::openFolder(app->screenshotDir());
|
util::Platform::openFolder(app->screenshotDir());
|
||||||
if (chat::hushChatFeatureEnabledAtBuild()) {
|
if (chat::hushChatFeatureEnabledAtBuild()) {
|
||||||
|
|||||||
@@ -659,6 +659,23 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Confirmation dialog
|
// Confirmation dialog
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
void SweepShowSendConfirm(App* app, bool show) {
|
||||||
|
(void)app;
|
||||||
|
if (show) {
|
||||||
|
std::snprintf(s_from_address, sizeof(s_from_address), "%s",
|
||||||
|
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000");
|
||||||
|
std::snprintf(s_to_address, sizeof(s_to_address), "%s",
|
||||||
|
"zs1demopeerreceivingaddressabcdef0000000000000000000000000000000");
|
||||||
|
s_amount = 2.50000000; s_fee = DRAGONX_DEFAULT_FEE;
|
||||||
|
std::snprintf(s_memo, sizeof(s_memo), "%s", "thanks!");
|
||||||
|
s_show_confirm = true; // RenderSendConfirmPopup re-issues OpenPopup while this is set
|
||||||
|
} else {
|
||||||
|
s_show_confirm = false;
|
||||||
|
s_from_address[0] = s_to_address[0] = s_memo[0] = '\0';
|
||||||
|
s_amount = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RenderSendConfirmPopup(App* app) {
|
void RenderSendConfirmPopup(App* app) {
|
||||||
if (!s_show_confirm) return;
|
if (!s_show_confirm) return;
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ void RenderSendTab(App* app);
|
|||||||
*/
|
*/
|
||||||
void RenderSendConfirmPopup(App* app);
|
void RenderSendConfirmPopup(App* app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Debug/screenshot-sweep hook: force the send-confirm popup open with demo values (show=true)
|
||||||
|
* or close it (show=false). The popup's state lives in send_tab file statics, so this hook is
|
||||||
|
* the only way an external driver (the full UI sweep) can reveal it. Fires no send.
|
||||||
|
*/
|
||||||
|
void SweepShowSendConfirm(App* app, bool show);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Pre-fill the "from" address in the send tab
|
* @brief Pre-fill the "from" address in the send tab
|
||||||
* @param address The address to send from
|
* @param address The address to send from
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["stop_external"] = "Stop external daemon";
|
strings_["stop_external"] = "Stop external daemon";
|
||||||
strings_["verbose_logging"] = "Verbose logging";
|
strings_["verbose_logging"] = "Verbose logging";
|
||||||
strings_["screenshot_sweep"] = "Run screenshot sweep";
|
strings_["screenshot_sweep"] = "Run screenshot sweep";
|
||||||
|
strings_["screenshot_sweep_full"] = "Full UI sweep";
|
||||||
strings_["screenshot_open_dir"] = "Open location";
|
strings_["screenshot_open_dir"] = "Open location";
|
||||||
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds.";
|
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds.";
|
||||||
strings_["mine_when_idle"] = "Mine when idle";
|
strings_["mine_when_idle"] = "Mine when idle";
|
||||||
|
|||||||
Reference in New Issue
Block a user