daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup

- Add startup binary version checking for dragonxd/xmrig
- Display daemon version in UI
- Add idle mining thread count adjustment
- Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard
- Add paste button to import private key dialog with address validation
- Add z-address generation UI feedback (loading indicator)
- Add option to delete blockchain data while preserving wallet.dat
- Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus)
- Fix Windows RPC auth: trim \r from config values, add .cookie fallback
- Fix connection status message during block index loading
- Improve application shutdown to prevent lingering background process
This commit is contained in:
dan_s
2026-03-17 14:57:12 -05:00
parent f0c87e4092
commit 4a841fd032
27 changed files with 897 additions and 2050 deletions

View File

@@ -1,173 +0,0 @@
# Copilot Instructions — DragonX ImGui Wallet
## UI Layout: All values in `ui.toml`
**Every UI layout constant must be defined in `res/themes/ui.toml` and read at runtime via the schema API.** Never hardcode pixel sizes, ratios, rounding values, thicknesses, or spacing constants directly in C++ source files. This is critical for maintainability, theming support, and hot-reload.
### Schema API reference
The singleton is accessed via `schema::UI()` (header: `#include "../schema/ui_schema.h"`).
| Method | Returns | Use for |
|---|---|---|
| `drawElement(section, name)` | `DrawElementStyle` | Custom DrawList layout values (`.size`, `.height`, `.thickness`, `.radius`, `.opacity`) |
| `button(section, name)` | `ButtonStyle` | Button width/height/font |
| `input(section, name)` | `InputStyle` | Input field dimensions |
| `label(section, name)` | `LabelStyle` | Label styling |
| `table(section, name)` | `TableStyle` | Table layout |
| `window(section, name)` | `WindowStyle` | Window/dialog dimensions |
| `combo(section, name)` | `ComboStyle` | Combo box styling |
| `slider(section, name)` | `SliderStyle` | Slider styling |
| `checkbox(section, name)` | `CheckboxStyle` | Checkbox styling |
| `separator(section, name)` | `SeparatorStyle` | Separator/divider styling |
### Section naming convention
Sections use dot-separated paths matching the file/feature:
- `tabs.send`, `tabs.receive`, `tabs.transactions`, `tabs.mining`, `tabs.peers`, `tabs.market` — tab-specific values
- `tabs.balance` — balance/home tab
- `components.main-layout`, `components.settings-page` — shared components
- `dialogs.about`, `dialogs.backup`, etc. — dialog-specific values
- `sidebar` — navigation sidebar
### How to add a new layout value
1. **Add the entry to `res/themes/ui.toml`** under the appropriate section:
```toml
[tabs.example]
my-element = {size = 42.0}
```
2. **Read it in C++** instead of using a literal:
```cpp
// WRONG — hardcoded
float myValue = 42.0f;
// CORRECT — schema-driven
float myValue = schema::UI().drawElement("tabs.example", "my-element").size;
```
3. For values used as min/max pairs with scaling:
```cpp
// WRONG
float h = std::max(18.0f, 24.0f * vScale);
// CORRECT
float h = std::max(
schema::UI().drawElement("tabs.example", "row-min-height").size,
schema::UI().drawElement("tabs.example", "row-height").size * vScale
);
```
### What belongs in `ui.toml`
- Pixel sizes (card heights, icon sizes, bar widths/heights)
- Ratios (column width ratios, max-width ratios)
- Rounding values (`FrameRounding`, corner radius)
- Thickness values (accent bars, chart lines, borders)
- Dot/circle radii
- Fade zones, padding constants
- Min/max dimension bounds
- Font selection (via schema font name strings, resolved with `S.resolveFont()`)
- Colors (via `schema::UI().resolveColor()` or color variable references like `"var(--primary)"`)
- Animation durations (transition times, fade durations, pulse speeds)
- Business logic values (fee amounts, ticker strings, buffer sizes, reward amounts)
### What does NOT belong in `ui.toml`
- Spacing that already goes through `Layout::spacing*()` or `spacing::dp()`
### Legacy system: `UILayout`
`UILayout::instance()` is the older layout system still used for fonts, typography, panels, and global spacing. New layout values should use `schema::UI().drawElement()` instead. Do not add new keys to `UILayout`.
### Validation
After editing `ui.toml`, always validate:
```bash
python3 -c "import toml; toml.load('res/themes/ui.toml'); print('Valid TOML')"
```
Or with the C++ toml++ parser (which is what the app uses):
```bash
cd build && make -j$(nproc)
```
### Build
```bash
# Linux
cd build && make -j$(nproc)
# Windows cross-compile
./build.sh --win-release
```
## Plans
When asked to "create a plan", always create a new markdown document in the `docs/` directory with the plan contents.
## Icons: Use Material Design icon font, never Unicode symbols
**Never use raw Unicode symbols or emoji characters** (e.g. ``, ``, ``, `🔍`, `📬`, `⚠️`, ``) for icons in C++ code. Always use the **Material Design Icons font** via the `ICON_MD_*` defines from `#include "../../embedded/IconsMaterialDesign.h"`.
### Icon font API
| Method | Size | Fallback |
|---|---|---|
| `Type().iconSmall()` | 14px | Body2 |
| `Type().iconMed()` | 18px | Body1 |
| `Type().iconLarge()` | 24px | H5 |
| `Type().iconXL()` | 40px | H3 |
### Correct usage
```cpp
#include "../../embedded/IconsMaterialDesign.h"
// WRONG — raw Unicode symbol
itemSpec.leadingIcon = "↙";
// CORRECT — Material Design icon codepoint
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
// WRONG — emoji for search
searchSpec.leadingIcon = "🔍";
// CORRECT — Material Design icon
searchSpec.leadingIcon = ICON_MD_SEARCH;
// For rendering with icon font directly:
ImGui::PushFont(Type().iconSmall());
ImGui::TextUnformatted(ICON_MD_ARROW_DOWNWARD);
ImGui::PopFont();
```
### Why
Raw Unicode symbols and emoji render inconsistently across platforms and may not be present in the loaded text fonts. The Material Design icon font is always loaded and provides consistent, scalable glyphs on both Linux and Windows.
### Audit for Unicode symbols
Before completing any task that touches UI code, search for and replace any raw Unicode symbols that may have been introduced. Common symbols to look for:
| Unicode | Replacement |
|---------|-------------|
| `` `` | `ICON_MD_PLAY_ARROW` |
| `` `` `` `` | `ICON_MD_STOP` or `ICON_MD_SQUARE` |
| `` `` `` `` | `ICON_MD_FIBER_MANUAL_RECORD` or `ICON_MD_CIRCLE` |
| `` `` `` `` | `ICON_MD_ARROW_UPWARD`, `_DOWNWARD`, `_BACK`, `_FORWARD` |
| `` `` `` `` | `ICON_MD_CALL_RECEIVED`, `_MADE`, etc. |
| `` `` | `ICON_MD_CHECK` |
| `` `` `` | `ICON_MD_CLOSE` |
| `` `⚠️` | `ICON_MD_WARNING` |
| `` `` | `ICON_MD_INFO` |
| `🔍` | `ICON_MD_SEARCH` |
| `📋` | `ICON_MD_CONTENT_COPY` or `ICON_MD_DESCRIPTION` |
| `🛡` `🛡️` | `ICON_MD_SHIELD` |
| `` | `ICON_MD_HOURGLASS_EMPTY` |
| `🔄` `` `` | `ICON_MD_SYNC` or `ICON_MD_REFRESH` |
| `` `⚙️` | `ICON_MD_SETTINGS` |
| `🔒` | `ICON_MD_LOCK` |
| `` `` | `ICON_MD_STAR` or `ICON_MD_STAR_BORDER` |

5
.gitignore vendored
View File

@@ -33,4 +33,7 @@ imgui.ini
*.bak*
*.params
asmap.dat
/external/xmrig-hac
/external/xmrig-hac
/memory
/todo.md
/.github/

View File

@@ -617,6 +617,10 @@ HDR
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
# Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ────────────────────────────

View File

@@ -104,6 +104,12 @@ bool App::init()
if (!settings_->load()) {
DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
}
// On upgrade (version mismatch), re-save to persist new defaults + current version
if (settings_->needsUpgradeSave()) {
DEBUG_LOGF("[INFO] Wallet upgraded — re-saving settings with new defaults\n");
settings_->save();
settings_->clearUpgradeSave();
}
// Apply verbose logging preference from saved settings
util::Logger::instance().setVerbose(settings_->getVerboseLogging());
@@ -137,6 +143,27 @@ bool App::init()
{
std::string schemaPath = util::Platform::getExecutableDirectory() + "/res/themes/ui.toml";
bool loaded = false;
#if HAS_EMBEDDED_UI_TOML
// If on-disk ui.toml exists but differs in size from the embedded
// version, a newer wallet binary is running against stale theme
// files. Overwrite the on-disk copy so layout matches the binary.
if (std::filesystem::exists(schemaPath)) {
std::error_code ec;
auto diskSize = std::filesystem::file_size(schemaPath, ec);
if (!ec && diskSize != static_cast<std::uintmax_t>(embedded::ui_toml_size)) {
DEBUG_LOGF("[INFO] ui.toml on disk (%ju bytes) differs from embedded (%zu bytes) — updating\n",
(uintmax_t)diskSize, embedded::ui_toml_size);
std::ofstream ofs(schemaPath, std::ios::binary | std::ios::trunc);
if (ofs.is_open()) {
ofs.write(reinterpret_cast<const char*>(embedded::ui_toml_data),
embedded::ui_toml_size);
ofs.close();
}
}
}
#endif
if (std::filesystem::exists(schemaPath)) {
loaded = ui::schema::UISchema::instance().loadFromFile(schemaPath);
}
@@ -1548,6 +1575,9 @@ void App::renderImportKeyDialog()
return it != dlg.extraFloats.end() ? it->second : fb;
};
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
return;
}
@@ -1561,6 +1591,50 @@ void App::renderImportKeyDialog()
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
// Paste & Clear buttons
if (ui::material::StyledButton(TR("paste"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
snprintf(import_key_input_, sizeof(import_key_input_), "%s", clipboard);
// Trim whitespace
std::string trimmed(import_key_input_);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
}
}
ImGui::SameLine();
if (ui::material::StyledButton(TR("clear"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
memset(import_key_input_, 0, sizeof(import_key_input_));
}
// Key validation indicator
if (import_key_input_[0] != '\0') {
std::string k(import_key_input_);
bool isZKey = (k.substr(0, 20) == "secret-extended-key-") ||
(k.length() >= 2 && k[0] == 'S' && k[1] == 'K');
bool isTKey = (k.length() >= 51 && k.length() <= 52 &&
(k[0] == '5' || k[0] == 'K' || k[0] == 'L' || k[0] == 'U'));
if (isZKey || isTKey) {
ImGui::PushFont(ui::material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s",
isZKey ? "Shielded spending key" : "Transparent private key");
} else {
ImGui::PushFont(ui::material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Unrecognized key format");
}
}
ImGui::Spacing();
if (!import_status_.empty()) {
@@ -1574,8 +1648,6 @@ void App::renderImportKeyDialog()
ImGui::Spacing();
ImGui::Separator();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string key(import_key_input_);
if (!key.empty()) {
@@ -2037,6 +2109,57 @@ void App::rescanBlockchain()
}).detach();
}
void App::deleteBlockchainData()
{
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
ui::Notifications::instance().warning(
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory.");
return;
}
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
std::thread([this]() {
DEBUG_LOGF("[App] Stopping daemon for blockchain deletion...\n");
stopEmbeddedDaemon();
if (shutting_down_) return;
for (int i = 0; i < 30 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
namespace fs = std::filesystem;
std::string dataDir = util::Platform::getDragonXDataDir();
// Directories to remove
const char* dirs[] = { "blocks", "chainstate", "database", "notarizations" };
// Files to remove
const char* files[] = { "peers.dat", "fee_estimates.dat", "banlist.dat",
"db.log", ".lock" };
int removed = 0;
std::error_code ec;
for (auto d : dirs) {
fs::path p = fs::path(dataDir) / d;
if (fs::exists(p, ec)) {
auto n = fs::remove_all(p, ec);
if (!ec) { removed += (int)n; DEBUG_LOGF("[App] Removed %s (%d entries)\n", d, (int)n); }
else { DEBUG_LOGF("[App] Failed to remove %s: %s\n", d, ec.message().c_str()); }
}
}
for (auto f : files) {
fs::path p = fs::path(dataDir) / f;
if (fs::remove(p, ec)) { removed++; DEBUG_LOGF("[App] Removed %s\n", f); }
}
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", removed);
daemon_output_offset_ = 0;
startEmbeddedDaemon();
}).detach();
}
double App::getDaemonMemoryUsageMB() const
{
// If we have an embedded daemon with a tracked process handle, use it

View File

@@ -253,6 +253,7 @@ public:
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
@@ -571,6 +572,7 @@ private:
// Mine-when-idle: auto-start/stop mining based on system idle state
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
// Private methods - rendering
void renderStatusBar();

View File

@@ -138,6 +138,37 @@ void App::tryConnect()
// endlessly — tell the user what's wrong.
bool authFailure = (connectErr.find("401") != std::string::npos);
if (authFailure) {
// Try .cookie auth as fallback — the daemon may have
// generated a .cookie file instead of using DRAGONX.conf credentials
std::string dataDir = rpc::Connection::getDefaultDataDir();
std::string cookieUser, cookiePass;
if (rpc::Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) {
VERBOSE_LOGF("[connect #%d] HTTP 401 — retrying with .cookie auth from %s\n",
attempt, dataDir.c_str());
worker_->post([this, config, cookieUser, cookiePass, attempt]() -> rpc::RPCWorker::MainCb {
auto cookieConfig = config;
cookieConfig.rpcuser = cookieUser;
cookieConfig.rpcpassword = cookiePass;
bool ok = rpc_->connect(cookieConfig.host, cookieConfig.port, cookieConfig.rpcuser, cookieConfig.rpcpassword);
return [this, cookieConfig, ok, attempt]() {
connection_in_progress_ = false;
if (ok) {
VERBOSE_LOGF("[connect #%d] Connected via .cookie auth\n", attempt);
saved_config_ = cookieConfig;
onConnected();
} else {
state_.connected = false;
connection_status_ = "Auth failed — check rpcuser/rpcpassword";
VERBOSE_LOGF("[connect #%d] .cookie auth also failed\n", attempt);
ui::Notifications::instance().error(
"RPC authentication failed (HTTP 401). "
"The rpcuser/rpcpassword in DRAGONX.conf don't match the running daemon. "
"Restart the daemon or correct the credentials.");
}
};
});
return; // async retry in progress
}
state_.connected = false;
std::string confPath = rpc::Connection::getDefaultConfPath();
connection_status_ = "Auth failed — check rpcuser/rpcpassword";
@@ -160,6 +191,17 @@ void App::tryConnect()
connection_status_ = "Connecting to daemon...";
VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt);
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else if (connectErr.find("Loading") != std::string::npos ||
connectErr.find("Verifying") != std::string::npos ||
connectErr.find("Activating") != std::string::npos ||
connectErr.find("Rewinding") != std::string::npos ||
connectErr.find("Rescanning") != std::string::npos ||
connectErr.find("Pruning") != std::string::npos) {
// Daemon is reachable but still in warmup (Loading block index, etc.)
state_.connected = false;
connection_status_ = connectErr;
VERBOSE_LOGF("[connect #%d] Daemon warmup: %s\n", attempt, connectErr.c_str());
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else {
onDisconnected("Connection failed");
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
@@ -230,12 +272,18 @@ void App::onConnected()
state_.protocol_version = info["protocolversion"].get<int>();
if (info.contains("p2pport"))
state_.p2p_port = info["p2pport"].get<int>();
if (info.contains("longestchain"))
state_.longestchain = info["longestchain"].get<int>();
if (info.contains("longestchain")) {
int lc = info["longestchain"].get<int>();
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
if (lc > 0) state_.longestchain = lc;
}
if (info.contains("notarized"))
state_.notarized = info["notarized"].get<int>();
if (info.contains("blocks"))
state_.sync.blocks = info["blocks"].get<int>();
// longestchain can lag behind blocks when peer data is stale
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
state_.longestchain = state_.sync.blocks;
} catch (const std::exception& e) {
DEBUG_LOGF("[onConnected] getinfo callback error: %s\n", e.what());
}
@@ -742,8 +790,14 @@ void App::refreshData()
state_.sync.headers = blockInfo["headers"].get<int>();
if (blockInfo.contains("verificationprogress"))
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
if (blockInfo.contains("longestchain"))
state_.longestchain = blockInfo["longestchain"].get<int>();
if (blockInfo.contains("longestchain")) {
int lc = blockInfo["longestchain"].get<int>();
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
if (lc > 0) state_.longestchain = lc;
}
// longestchain can lag behind blocks when peer data is stale
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
state_.longestchain = state_.sync.blocks;
// Use longestchain (actual network tip) for sync check when available,
// since headers can be inflated by misbehaving peers.
if (state_.longestchain > 0)
@@ -891,8 +945,14 @@ void App::refreshBalance()
state_.sync.headers = blockInfo["headers"].get<int>();
if (blockInfo.contains("verificationprogress"))
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
if (blockInfo.contains("longestchain"))
state_.longestchain = blockInfo["longestchain"].get<int>();
if (blockInfo.contains("longestchain")) {
int lc = blockInfo["longestchain"].get<int>();
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
if (lc > 0) state_.longestchain = lc;
}
// longestchain can lag behind blocks when peer data is stale
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
state_.longestchain = state_.sync.blocks;
if (state_.longestchain > 0)
state_.sync.syncing = (state_.sync.blocks < state_.longestchain - 2);
else
@@ -1358,7 +1418,7 @@ void App::startPoolMining(int threads)
if (cfg.wallet_address.empty()) {
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
ui::Notifications::instance().error("No wallet address available for pool mining");
ui::Notifications::instance().error("No wallet address available — generate a Z address in the Receive tab");
return;
}

View File

@@ -415,6 +415,9 @@ void App::checkAutoLock() {
// ===========================================================================
// Mine when idle — auto-start/stop mining based on system idle state
// Supports two modes:
// 1. Start/Stop mode (default): start mining when idle, stop when active
// 2. Thread scaling mode: mining stays running, thread count changes
// ===========================================================================
void App::checkIdleMining() {
@@ -422,6 +425,7 @@ void App::checkIdleMining() {
// Feature disabled — if we previously auto-started, stop now
if (idle_mining_active_) {
idle_mining_active_ = false;
idle_scaled_to_idle_ = false;
if (settings_ && settings_->getPoolMode()) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
@@ -430,46 +434,89 @@ void App::checkIdleMining() {
stopMining();
}
}
// Reset scaling state when feature is off
if (idle_scaled_to_idle_) idle_scaled_to_idle_ = false;
return;
}
int idleSec = util::Platform::getSystemIdleSeconds();
int delay = settings_->getMineIdleDelay();
bool isPool = settings_->getPoolMode();
bool threadScaling = settings_->getIdleThreadScaling();
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
// Check if mining is already running (manually started by user)
bool miningActive = isPool
? (xmrig_manager_ && xmrig_manager_->isRunning())
: state_.mining.generate;
if (idleSec >= delay) {
// System is idle — start mining if not already running
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
// For solo mining, need daemon connected and synced
if (!isPool && (!state_.connected || state_.sync.syncing)) return;
if (threadScaling) {
// --- Thread scaling mode ---
// Mining must already be running (started by user). We just adjust threads.
if (!miningActive || mining_toggle_in_progress_.load()) return;
int threads = settings_->getPoolThreads();
if (threads <= 0) threads = std::max(1, (int)std::thread::hardware_concurrency() / 2);
int activeThreads = settings_->getIdleThreadsActive();
int idleThreads = settings_->getIdleThreadsIdle();
// Resolve auto values: active defaults to half, idle defaults to all
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
if (idleThreads <= 0) idleThreads = maxThreads;
idle_mining_active_ = true;
if (isPool)
startPoolMining(threads);
else
startMining(threads);
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
if (idleSec >= delay) {
// System is idle — scale up to idle thread count
if (!idle_scaled_to_idle_) {
idle_scaled_to_idle_ = true;
if (isPool) {
stopPoolMining();
startPoolMining(idleThreads);
} else {
startMining(idleThreads);
}
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
}
} else {
// User is active — scale down to active thread count
if (idle_scaled_to_idle_) {
idle_scaled_to_idle_ = false;
if (isPool) {
stopPoolMining();
startPoolMining(activeThreads);
} else {
startMining(activeThreads);
}
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads);
}
}
} else {
// User is active — stop mining if we auto-started it
if (idle_mining_active_) {
idle_mining_active_ = false;
if (isPool) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
} else {
if (state_.mining.generate)
stopMining();
// --- Start/Stop mode (original behavior) ---
if (idleSec >= delay) {
// System is idle — start mining if not already running
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
// For solo mining, need daemon connected and synced
if (!isPool && (!state_.connected || state_.sync.syncing)) return;
int threads = settings_->getPoolThreads();
if (threads <= 0) threads = std::max(1, maxThreads / 2);
idle_mining_active_ = true;
if (isPool)
startPoolMining(threads);
else
startMining(threads);
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
}
} else {
// User is active — stop mining if we auto-started it
if (idle_mining_active_) {
idle_mining_active_ = false;
if (isPool) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
} else {
if (state_.mining.generate)
stopMining();
}
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
}
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
}
}
}

View File

@@ -766,6 +766,15 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap retry");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
@@ -894,7 +903,7 @@ void App::renderFirstRunWizard() {
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
const char* twText = "Only use bootstrap.dragonx.is. Using files from untrusted sources could compromise your node.";
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
float twWrap = contentW - iw - 4.0f * dp;
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
@@ -903,18 +912,29 @@ void App::renderFirstRunWizard() {
// Buttons (only when focused)
if (isFocused) {
float dlBtnW = 180.0f * dp;
float dlBtnW = 150.0f * dp;
float mirrorW = 150.0f * dp;
float skipW2 = 80.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = dlBtnW + 12.0f * dp + skipW2;
float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f;
// --- Download button (main / Cloudflare) ---
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
@@ -923,7 +943,35 @@ void App::renderFirstRunWizard() {
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 12.0f * dp, cy));
// --- Mirror Download button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;

View File

@@ -150,6 +150,9 @@ bool Settings::load(const std::string& path)
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"])
@@ -166,7 +169,16 @@ bool Settings::load(const std::string& path)
window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>();
// Version tracking — detect upgrades so we can re-save with new defaults
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
DRAGONX_VERSION);
needs_upgrade_save_ = true;
}
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
@@ -235,6 +247,9 @@ bool Settings::save(const std::string& path)
j["pool_mode"] = pool_mode_;
j["mine_when_idle"] = mine_when_idle_;
j["mine_idle_delay"]= mine_idle_delay_;
j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_;
j["idle_threads_idle"] = idle_threads_idle_;
j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u);
@@ -242,6 +257,7 @@ bool Settings::save(const std::string& path)
for (const auto& w : saved_pool_workers_)
j["saved_pool_workers"].push_back(w);
j["font_scale"] = font_scale_;
j["settings_version"] = std::string(DRAGONX_VERSION);
if (window_width_ > 0 && window_height_ > 0) {
j["window_width"] = window_width_;
j["window_height"] = window_height_;

View File

@@ -214,6 +214,14 @@ public:
int getMineIdleDelay() const { return mine_idle_delay_; }
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
// Idle thread scaling — scale thread count instead of start/stop
bool getIdleThreadScaling() const { return idle_thread_scaling_; }
void setIdleThreadScaling(bool v) { idle_thread_scaling_ = v; }
int getIdleThreadsActive() const { return idle_threads_active_; }
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
int getIdleThreadsIdle() const { return idle_threads_idle_; }
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
// Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
void addSavedPoolUrl(const std::string& url) {
@@ -248,6 +256,10 @@ public:
int getWindowHeight() const { return window_height_; }
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
// Returns true once after an upgrade (version mismatch detected on load)
bool needsUpgradeSave() const { return needs_upgrade_save_; }
void clearUpgradeSave() { needs_upgrade_save_ = false; }
private:
std::string settings_path_;
@@ -290,13 +302,16 @@ private:
// Pool mining
std::string pool_url_ = "pool.dragonx.is:3433";
std::string pool_algo_ = "rx/hush";
std::string pool_worker_ = "x";
std::string pool_worker_ = "";
int pool_threads_ = 0;
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
int idle_threads_active_ = 0; // threads when user active (0 = auto)
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
@@ -306,6 +321,10 @@ private:
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
int window_width_ = 0;
int window_height_ = 0;
// Wallet version that last wrote this settings file (for upgrade detection)
std::string settings_version_;
bool needs_upgrade_save_ = false; // true when version changed
};
} // namespace config

View File

@@ -1800,6 +1800,17 @@ int main(int argc, char* argv[])
// while background cleanup (thread joins, RPC disconnect) continues.
SDL_HideWindow(window);
// Watchdog: if cleanup takes too long the process lingers without a
// window, showing up as a "Background Service" in Task Manager.
// Force-exit after 3 seconds — all critical state (settings, daemon
// stop) was handled in beginShutdown().
std::thread([]() {
std::this_thread::sleep_for(std::chrono::seconds(3));
fflush(stdout);
fflush(stderr);
_Exit(0);
}).detach();
// Final cleanup (daemon already stopped by beginShutdown)
app.shutdown();
#ifdef DRAGONX_USE_DX11

View File

@@ -144,6 +144,37 @@ int extractBundledThemes(const std::string& destDir)
return count;
}
int updateBundledThemes(const std::string& dir)
{
namespace fs = std::filesystem;
int count = 0;
const auto* themes = getEmbeddedThemes();
if (!themes || !themes->data) return 0;
if (!fs::exists(dir)) return 0;
for (const auto* t = themes; t->data != nullptr; ++t) {
fs::path dest = fs::path(dir) / t->filename;
if (!fs::exists(dest)) {
// New theme not yet on disk — extract it
} else {
std::error_code ec;
auto diskSize = fs::file_size(dest, ec);
if (!ec && diskSize == static_cast<std::uintmax_t>(t->size))
continue; // up to date
}
std::ofstream f(dest, std::ios::binary);
if (f.is_open()) {
f.write(reinterpret_cast<const char*>(t->data), t->size);
f.close();
DEBUG_LOGF("[INFO] EmbeddedResources: Updated stale theme: %s (%zu bytes)\n",
t->filename, t->size);
count++;
}
}
return count;
}
std::string getParamsDirectory()
{
#ifdef _WIN32
@@ -169,6 +200,9 @@ std::string getParamsDirectory()
#endif
}
// Forward declaration — defined below extractResource()
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath);
bool needsParamsExtraction()
{
if (!hasEmbeddedResources()) {
@@ -177,21 +211,46 @@ bool needsParamsExtraction()
// Check daemon directory (dragonx/) — the only extraction target
std::string daemonDir = getDaemonDirectory();
std::string spendPath = daemonDir +
#ifdef _WIN32
"\\sapling-spend.params";
const char pathSep = '\\';
#else
"/sapling-spend.params";
#endif
std::string outputPath = daemonDir +
#ifdef _WIN32
"\\sapling-output.params";
#else
"/sapling-output.params";
const char pathSep = '/';
#endif
std::string spendPath = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
std::string outputPath = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
// Check if both params exist in daemon directory
return !std::filesystem::exists(spendPath) || !std::filesystem::exists(outputPath);
// Check if params are missing or stale (size mismatch → updated in newer build)
const auto* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
const auto* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
// Also check if daemon binaries need updating
#ifdef HAS_EMBEDDED_DAEMON
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
if (daemonRes && resourceNeedsUpdate(daemonRes, daemonPath)) return true;
#endif
#ifdef HAS_EMBEDDED_XMRIG
const auto* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
std::string xmrigPath = daemonDir + pathSep + RESOURCE_XMRIG;
if (xmrigRes && resourceNeedsUpdate(xmrigRes, xmrigPath)) return true;
#endif
return false;
}
// Check if an on-disk file is missing or differs in size from the embedded resource.
// A size mismatch means the binary was updated in a newer wallet build.
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath)
{
if (!res || !res->data || res->size == 0) return false;
if (!std::filesystem::exists(destPath)) return true;
std::error_code ec;
auto diskSize = std::filesystem::file_size(destPath, ec);
if (ec) return true; // can't stat → re-extract
return diskSize != static_cast<std::uintmax_t>(res->size);
}
static bool extractResource(const EmbeddedResource* res, const std::string& destPath)
@@ -251,7 +310,7 @@ bool extractEmbeddedResources()
const EmbeddedResource* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
if (spendRes) {
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
if (!std::filesystem::exists(dest)) {
if (resourceNeedsUpdate(spendRes, dest)) {
DEBUG_LOGF("[INFO] Extracting sapling-spend.params (%zu MB)...\n", spendRes->size / (1024*1024));
if (!extractResource(spendRes, dest)) {
success = false;
@@ -262,7 +321,7 @@ bool extractEmbeddedResources()
const EmbeddedResource* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
if (outputRes) {
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
if (!std::filesystem::exists(dest)) {
if (resourceNeedsUpdate(outputRes, dest)) {
DEBUG_LOGF("[INFO] Extracting sapling-output.params (%zu MB)...\n", outputRes->size / (1024*1024));
if (!extractResource(outputRes, dest)) {
success = false;
@@ -274,7 +333,7 @@ bool extractEmbeddedResources()
const EmbeddedResource* asmapRes = getEmbeddedResource(RESOURCE_ASMAP);
if (asmapRes) {
std::string dest = daemonDir + pathSep + RESOURCE_ASMAP;
if (!std::filesystem::exists(dest)) {
if (resourceNeedsUpdate(asmapRes, dest)) {
DEBUG_LOGF("[INFO] Extracting asmap.dat...\n");
if (!extractResource(asmapRes, dest)) {
success = false;
@@ -291,33 +350,48 @@ bool extractEmbeddedResources()
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
if (daemonRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting dragonxd.exe (%zu MB)...\n", daemonRes->size / (1024*1024));
if (resourceNeedsUpdate(daemonRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
if (!extractResource(daemonRes, dest)) {
success = false;
}
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
}
}
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
if (cliRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting dragonx-cli.exe (%zu MB)...\n", cliRes->size / (1024*1024));
if (resourceNeedsUpdate(cliRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
if (!extractResource(cliRes, dest)) {
success = false;
}
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
}
}
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
if (txRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting dragonx-tx.exe (%zu MB)...\n", txRes->size / (1024*1024));
if (resourceNeedsUpdate(txRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
if (!extractResource(txRes, dest)) {
success = false;
}
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
}
}
#endif
@@ -326,11 +400,16 @@ bool extractEmbeddedResources()
const EmbeddedResource* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
if (xmrigRes) {
std::string dest = daemonDir + pathSep + RESOURCE_XMRIG;
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting xmrig.exe (%zu MB)...\n", xmrigRes->size / (1024*1024));
if (resourceNeedsUpdate(xmrigRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale xmrig (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting xmrig (%zu MB)...\n", xmrigRes->size / (1024*1024));
if (!extractResource(xmrigRes, dest)) {
success = false;
}
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
}
}
#endif
@@ -360,7 +439,8 @@ bool needsDaemonExtraction()
#else
std::string daemonPath = daemonDir + "/dragonxd";
#endif
return !std::filesystem::exists(daemonPath);
const auto* res = getEmbeddedResource(RESOURCE_DRAGONXD);
return resourceNeedsUpdate(res, daemonPath);
#else
return false;
#endif
@@ -426,7 +506,8 @@ bool needsXmrigExtraction()
#else
std::string xmrigPath = daemonDir + "/xmrig";
#endif
return !std::filesystem::exists(xmrigPath);
const auto* res = getEmbeddedResource(RESOURCE_XMRIG);
return resourceNeedsUpdate(res, xmrigPath);
#else
return false;
#endif

View File

@@ -55,6 +55,11 @@ const EmbeddedTheme* getEmbeddedThemes();
// Returns number of themes extracted
int extractBundledThemes(const std::string& destDir);
// Update stale bundled theme files in the given directory.
// Compares on-disk file size to embedded size; overwrites on mismatch.
// Returns number of themes updated.
int updateBundledThemes(const std::string& dir);
// Check if daemon needs to be extracted
bool needsDaemonExtraction();

View File

@@ -118,13 +118,16 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
std::string key = line.substr(0, eq_pos);
std::string value = line.substr(eq_pos + 1);
// Trim whitespace
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
// Trim whitespace (including \r from Windows line endings)
while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r')) {
key.pop_back();
}
while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) {
value.erase(0, 1);
}
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || value.back() == '\r')) {
value.pop_back();
}
// Map to config
if (key == "rpcuser") {
@@ -172,6 +175,16 @@ ConnectionConfig Connection::autoDetectConfig()
}
}
// If rpcpassword is empty, the daemon may be using .cookie auth
if (config.rpcpassword.empty()) {
std::string cookieUser, cookiePass;
if (readAuthCookie(data_dir, cookieUser, cookiePass)) {
config.rpcuser = cookieUser;
config.rpcpassword = cookiePass;
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
}
}
// Set defaults for missing values
if (config.host.empty()) {
config.host = DRAGONX_DEFAULT_RPC_HOST;
@@ -319,5 +332,40 @@ bool Connection::ensureEncryptionEnabled(const std::string& confPath)
return true;
}
bool Connection::readAuthCookie(const std::string& dataDir, std::string& user, std::string& password)
{
if (dataDir.empty()) return false;
#ifdef _WIN32
std::string cookiePath = dataDir + "\\.cookie";
#else
std::string cookiePath = dataDir + "/.cookie";
#endif
std::ifstream file(cookiePath);
if (!file.is_open()) return false;
std::string cookie;
std::getline(file, cookie);
file.close();
// Cookie format: __cookie__:base64encodedpassword
size_t colonPos = cookie.find(':');
if (colonPos == std::string::npos || colonPos == 0) return false;
user = cookie.substr(0, colonPos);
password = cookie.substr(colonPos + 1);
// Trim \r if present (Windows line endings)
while (!password.empty() && (password.back() == '\r' || password.back() == '\n')) {
password.pop_back();
}
if (user.empty() || password.empty()) return false;
DEBUG_LOGF("Read auth cookie from: %s (user=%s)\n", cookiePath.c_str(), user.c_str());
return true;
}
} // namespace rpc
} // namespace dragonx

View File

@@ -87,6 +87,15 @@ public:
*/
static bool ensureEncryptionEnabled(const std::string& confPath);
/**
* @brief Try to read .cookie auth file from the data directory
* @param dataDir Path to the daemon data directory
* @param user Output: cookie username (__cookie__)
* @param password Output: cookie password
* @return true if cookie file was read successfully
*/
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
private:
};

View File

@@ -100,7 +100,20 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
}
} catch (const std::exception& e) {
last_connect_error_ = e.what();
DEBUG_LOGF("Connection failed: %s\n", e.what());
// Daemon warmup messages (Loading block index, Verifying blocks, etc.)
// are normal startup progress — don't label them "Connection failed".
std::string msg = e.what();
bool isWarmup = (msg.find("Loading") != std::string::npos ||
msg.find("Verifying") != std::string::npos ||
msg.find("Activating") != std::string::npos ||
msg.find("Rewinding") != std::string::npos ||
msg.find("Rescanning") != std::string::npos ||
msg.find("Pruning") != std::string::npos);
if (isWarmup) {
DEBUG_LOGF("Daemon starting: %s\n", msg.c_str());
} else {
DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
}
}
connected_ = false;

View File

@@ -116,6 +116,9 @@ static bool sp_stop_external_daemon = false;
// Mining — mine when idle
static bool sp_mine_when_idle = false;
static int sp_mine_idle_delay = 120;
static bool sp_idle_thread_scaling = false;
static int sp_idle_threads_active = 0;
static int sp_idle_threads_idle = 0;
static bool sp_verbose_logging = false;
// Debug logging categories
@@ -125,6 +128,7 @@ static bool sp_debug_expanded = false; // collapsible card state
static bool sp_effects_expanded = false; // "Advanced Effects..." toggle
static bool sp_tools_expanded = false; // "Tools & Actions..." toggle
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
static bool sp_confirm_delete_blockchain = false; // confirmation dialog for deleting blockchain data
// (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -181,6 +185,9 @@ static void loadSettingsPageState(config::Settings* settings) {
sp_stop_external_daemon = settings->getStopExternalDaemon();
sp_mine_when_idle = settings->getMineWhenIdle();
sp_mine_idle_delay = settings->getMineIdleDelay();
sp_idle_thread_scaling = settings->getIdleThreadScaling();
sp_idle_threads_active = settings->getIdleThreadsActive();
sp_idle_threads_idle = settings->getIdleThreadsIdle();
sp_verbose_logging = settings->getVerboseLogging();
sp_debug_categories = settings->getDebugCategories();
sp_debug_cats_dirty = false;
@@ -230,6 +237,9 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->setStopExternalDaemon(sp_stop_external_daemon);
settings->setMineWhenIdle(sp_mine_when_idle);
settings->setMineIdleDelay(sp_mine_idle_delay);
settings->setIdleThreadScaling(sp_idle_thread_scaling);
settings->setIdleThreadsActive(sp_idle_threads_active);
settings->setIdleThreadsIdle(sp_idle_threads_idle);
settings->setVerboseLogging(sp_verbose_logging);
settings->setDebugCategories(sp_debug_categories);
@@ -1485,6 +1495,15 @@ void RenderSettingsPage(App* app) {
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
// Delete blockchain button (always available when using embedded daemon)
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
sp_confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
ImGui::EndDisabled();
}
ImGui::PopFont();
@@ -1737,6 +1756,20 @@ void RenderSettingsPage(App* app) {
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
ImGui::PopFont();
// Daemon version
{
const auto& st = app->state();
if (st.daemon_version > 0) {
int dmaj = st.daemon_version / 1000000;
int dmin = (st.daemon_version / 10000) % 100;
int dpat = (st.daemon_version / 100) % 100;
ImGui::PushFont(body2);
snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf);
ImGui::PopFont();
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(body2);
@@ -2009,6 +2042,39 @@ void RenderSettingsPage(App* app) {
}
}
// Confirmation dialog for deleting blockchain data
if (sp_confirm_delete_blockchain) {
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &sp_confirm_delete_blockchain, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
sp_confirm_delete_blockchain = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
app->deleteBlockchainData();
sp_confirm_delete_blockchain = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
}
} // namespace ui

View File

@@ -1,848 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "settings_page.h"
#include "../../app.h"
#include "../../version.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../rpc/rpc_client.h"
#include "../theme.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../schema/skin_manager.h"
#include "../notifications.h"
#include "../effects/imgui_acrylic.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../windows/validate_address_dialog.h"
#include "../windows/address_book_dialog.h"
#include "../windows/shield_dialog.h"
#include "../windows/request_payment_dialog.h"
#include "../windows/block_info_dialog.h"
#include "../windows/export_all_keys_dialog.h"
#include "../windows/export_transactions_dialog.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <vector>
#include <filesystem>
#include <algorithm>
#include <cmath>
namespace dragonx {
namespace ui {
using namespace material;
// ============================================================================
// Settings state — loaded from config::Settings on first render
// ============================================================================
static bool sp_initialized = false;
static int sp_language_index = 0;
static bool sp_save_ztxs = true;
static bool sp_allow_custom_fees = false;
static bool sp_auto_shield = false;
static bool sp_fetch_prices = true;
static bool sp_use_tor = false;
static char sp_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
static char sp_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
static char sp_rpc_user[64] = "";
static char sp_rpc_password[64] = "";
static char sp_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
static char sp_addr_explorer[256] = "https://explorer.dragonx.is/address/";
// Acrylic settings
static bool sp_acrylic_enabled = true;
static int sp_acrylic_quality = 2;
static float sp_blur_multiplier = 1.0f;
static bool sp_reduced_transparency = false;
static void loadSettingsPageState(config::Settings* settings) {
if (!settings) return;
sp_save_ztxs = settings->getSaveZtxs();
sp_allow_custom_fees = settings->getAllowCustomFees();
sp_auto_shield = settings->getAutoShield();
sp_fetch_prices = settings->getFetchPrices();
sp_use_tor = settings->getUseTor();
strncpy(sp_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(sp_tx_explorer) - 1);
strncpy(sp_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(sp_addr_explorer) - 1);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::string current_lang = settings->getLanguage();
if (current_lang.empty()) current_lang = "en";
sp_language_index = 0;
int idx = 0;
for (const auto& lang : languages) {
if (lang.first == current_lang) {
sp_language_index = idx;
break;
}
idx++;
}
sp_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
sp_acrylic_quality = static_cast<int>(effects::ImGuiAcrylic::GetQuality());
sp_blur_multiplier = effects::ImGuiAcrylic::GetBlurMultiplier();
sp_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
sp_initialized = true;
}
static void saveSettingsPageState(config::Settings* settings) {
if (!settings) return;
settings->setTheme(settings->getSkinId());
settings->setSaveZtxs(sp_save_ztxs);
settings->setAllowCustomFees(sp_allow_custom_fees);
settings->setAutoShield(sp_auto_shield);
settings->setFetchPrices(sp_fetch_prices);
settings->setUseTor(sp_use_tor);
settings->setTxExplorerUrl(sp_tx_explorer);
settings->setAddressExplorerUrl(sp_addr_explorer);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
auto it = languages.begin();
std::advance(it, sp_language_index);
if (it != languages.end()) {
settings->setLanguage(it->first);
}
settings->save();
}
// ============================================================================
// Settings Page Renderer
// ============================================================================
void RenderSettingsPage(App* app) {
// Load settings state on first render
if (!sp_initialized && app->settings()) {
loadSettingsPageState(app->settings());
}
auto& S = schema::UI();
// Responsive layout — matches other tabs
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
float availWidth = contentAvail.x;
float hs = Layout::hScale(availWidth);
float vs = Layout::vScale(contentAvail.y);
float pad = Layout::cardInnerPadding();
float gap = Layout::cardGap();
float glassRound = Layout::glassRounding();
(void)vs;
char buf[256];
// Label column position — adaptive to width
float labelW = std::max(100.0f, 120.0f * hs);
// Input field width — fill remaining space in card
float inputW = std::max(180.0f, availWidth - labelW - pad * 3);
// Scrollable content area — NoBackground matches other tabs
ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false,
ImGuiWindowFlags_NoBackground);
// Get draw list AFTER BeginChild so we draw on the child window's list
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
ImFont* sub1 = Type().subtitle1();
// ====================================================================
// GENERAL — Appearance & Preferences card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "APPEARANCE");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Measure content height for card
// We'll use ImGui cursor-based layout inside the card
ImVec2 cardMin = ImGui::GetCursorScreenPos();
// Use a child window inside the glass panel for layout
// First draw the glass panel, then place content
// We need to estimate height — use a generous estimate and clip
float rowH = body2->LegacySize + Layout::spacingSm();
float sectionGap = Layout::spacingMd();
float cardH = pad // top pad
+ rowH // Theme
+ rowH // Language
+ sectionGap
+ rowH * 5 // Visual effects (acrylic + quality + blur + reduce + gap)
+ pad; // bottom pad
if (!sp_acrylic_enabled) cardH -= rowH * 2;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// --- Theme row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Theme");
ImGui::SameLine(labelW);
auto& skinMgr = schema::SkinManager::instance();
const auto& skins = skinMgr.available();
std::string active_preview = "DragonX";
bool active_is_custom = false;
for (const auto& skin : skins) {
if (skin.id == skinMgr.activeSkinId()) {
active_preview = skin.name;
active_is_custom = !skin.bundled;
break;
}
}
float refreshBtnW = 80.0f;
ImGui::SetNextItemWidth(inputW - refreshBtnW - Layout::spacingSm());
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
ImGui::TextDisabled("Built-in");
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (!skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) ImGui::SetItemDefaultFocus();
}
bool has_custom = false;
for (const auto& skin : skins) {
if (!skin.bundled) { has_custom = true; break; }
}
if (has_custom) {
ImGui::Spacing();
ImGui::TextDisabled("Custom");
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string lbl = skin.name + " (invalid)";
ImGui::Selectable(lbl.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", skin.validationError.c_str());
} else {
std::string lbl = skin.name;
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
if (ImGui::Selectable(lbl.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) ImGui::SetItemDefaultFocus();
}
}
}
ImGui::EndCombo();
}
if (active_is_custom) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Custom theme active");
}
ImGui::SameLine();
if (TactileButton("Refresh", ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
schema::SkinManager::instance().refresh();
Notifications::instance().info("Theme list refreshed");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// --- Language row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Language");
ImGui::SameLine(labelW);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::vector<const char*> lang_names;
lang_names.reserve(languages.size());
for (const auto& lang : languages) {
lang_names.push_back(lang.second.c_str());
}
ImGui::SetNextItemWidth(inputW);
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
static_cast<int>(lang_names.size()))) {
auto it = languages.begin();
std::advance(it, sp_language_index);
i18n.loadLanguage(it->first);
}
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Visual Effects subsection ---
dl->AddText(ovFont, ovFont->LegacySize, ImGui::GetCursorScreenPos(), OnSurfaceMedium(), "VISUAL EFFECTS");
ImGui::Dummy(ImVec2(0, ovFont->LegacySize + Layout::spacingXs()));
{
// Two-column: left = acrylic toggle + reduce toggle, right = quality + blur
float colW = (availWidth - pad * 2 - Layout::spacingLg()) * 0.5f;
if (ImGui::Checkbox("Acrylic effects", &sp_acrylic_enabled)) {
effects::ImGuiAcrylic::SetEnabled(sp_acrylic_enabled);
}
if (sp_acrylic_enabled) {
ImGui::SameLine(labelW + colW + Layout::spacingLg());
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Quality");
ImGui::PopFont();
ImGui::SameLine();
const char* quality_levels[] = { "Off", "Low", "Medium", "High" };
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
if (ImGui::Combo("##AcrylicQuality", &sp_acrylic_quality, quality_levels,
IM_ARRAYSIZE(quality_levels))) {
effects::ImGuiAcrylic::SetQuality(
static_cast<effects::AcrylicQuality>(sp_acrylic_quality));
}
}
if (ImGui::Checkbox("Reduce transparency", &sp_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(sp_reduced_transparency);
}
if (sp_acrylic_enabled) {
ImGui::SameLine(labelW + colW + Layout::spacingLg());
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Blur");
ImGui::PopFont();
ImGui::SameLine();
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
if (ImGui::SliderFloat("##BlurAmount", &sp_blur_multiplier, 0.5f, 2.0f, "%.1fx")) {
effects::ImGuiAcrylic::SetBlurMultiplier(sp_blur_multiplier);
}
}
}
// Recalculate actual card bottom from cursor
ImVec2 cardEnd = ImGui::GetCursorScreenPos();
float actualH = (cardEnd.y - cardMin.y) + pad;
if (actualH != cardH) {
// Redraw glass panel with correct height
cardMax.y = cardMin.y + actualH;
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// PRIVACY & OPTIONS — Two cards side by side
// ====================================================================
{
float colW = (availWidth - gap) * 0.5f;
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
// --- Privacy card (left) ---
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PRIVACY");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
ImGui::Checkbox("Save shielded tx history", &sp_save_ztxs);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Checkbox("Auto-shield transparent funds", &sp_auto_shield);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Checkbox("Use Tor for connections", &sp_use_tor);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// --- Options card (right) ---
{
float rightX = rowOrigin.x + colW + gap;
// Position cursor at the same Y as privacy label
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "OPTIONS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
ImGui::Checkbox("Allow custom transaction fees", &sp_allow_custom_fees);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Checkbox("Fetch price data from CoinGecko", &sp_fetch_prices);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// Advance past the side-by-side row
// Find the maximum bottom
float rowBottom = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// EXPLORER URLS + SAVE — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK EXPLORER & SETTINGS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingSm();
float cardH = pad + rowH * 2 + Layout::spacingSm()
+ body2->LegacySize + Layout::spacingMd() // save/reset row
+ pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// Transaction URL
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Transaction URL");
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(inputW);
ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer));
ImGui::PopFont();
}
// Address URL
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Address URL");
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(inputW);
ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Save / Reset — right-aligned
{
float saveBtnW = 120.0f;
float resetBtnW = 140.0f;
float btnGap = Layout::spacingSm();
if (TactileButton("Save Settings", ImVec2(saveBtnW, 0), S.resolveFont("button"))) {
saveSettingsPageState(app->settings());
Notifications::instance().success("Settings saved");
}
ImGui::SameLine(0, btnGap);
if (TactileButton("Reset to Defaults", ImVec2(resetBtnW, 0), S.resolveFont("button"))) {
if (app->settings()) {
loadSettingsPageState(app->settings());
Notifications::instance().info("Settings reloaded from disk");
}
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// KEYS & BACKUP — card with two rows
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "KEYS & BACKUP");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + btnRowH * 2 + Layout::spacingSm() + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// Keys row — spread buttons across width
{
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
if (TactileButton("Import Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
app->showImportKeyDialog();
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Export Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
app->showExportKeyDialog();
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Export All Keys...", ImVec2(btnW, 0), S.resolveFont("button"))) {
ExportAllKeysDialog::show();
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Backup row
{
float btnW = (availWidth - pad * 2 - Layout::spacingSm()) / 2.0f;
if (TactileButton("Backup wallet.dat...", ImVec2(btnW, 0), S.resolveFont("button"))) {
app->showBackupDialog();
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Export Transactions CSV...", ImVec2(btnW, 0), S.resolveFont("button"))) {
ExportTransactionsDialog::show();
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// WALLET — Two cards side by side: Tools | Maintenance
// ====================================================================
{
float colW = (availWidth - gap) * 0.5f;
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
float btnH = std::max(28.0f, 34.0f * vs);
// --- Wallet Tools card (left) ---
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET TOOLS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
float innerBtnW = colW - pad * 2;
if (TactileButton("Address Book...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
AddressBookDialog::show();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Validate Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
ValidateAddressDialog::show();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Request Payment...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
RequestPaymentDialog::show();
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// --- Shielding & Maintenance card (right) ---
{
float rightX = rowOrigin.x + colW + gap;
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SHIELDING & MAINTENANCE");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
float innerBtnW = colW - pad * 2;
if (TactileButton("Shield Mining Rewards...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Merge to Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("Rescan Blockchain", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
if (app->rpc() && app->rpc()->isConnected()) {
app->rpc()->rescanBlockchain(0, [](bool success, const nlohmann::json&) {
if (success)
Notifications::instance().success("Blockchain rescan started");
else
Notifications::instance().error("Failed to start rescan");
});
} else {
Notifications::instance().warning("Not connected to daemon");
}
}
ImGui::EndDisabled();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// Advance past sidebar row
float rowBottom = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// WALLET INFO — Small card with file path + clear history
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET INFO");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingSm();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + rowH * 2 + btnRowH + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Location");
ImGui::SameLine(labelW);
ImGui::TextUnformatted(wallet_path.c_str());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("File size");
ImGui::SameLine(labelW);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::TextUnformatted(size_str.c_str());
} else {
ImGui::TextDisabled("Not found");
}
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Clear Z-Transaction History", ImVec2(0, 0), S.resolveFont("button"))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file))
Notifications::instance().success("Z-transaction history cleared");
else
Notifications::instance().info("No history file found");
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// NODE / RPC — card with two-column inputs
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NODE / RPC CONNECTION");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingSm();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + rowH * 2 + Layout::spacingSm() + rowH * 2 + Layout::spacingSm()
+ capFont->LegacySize + Layout::spacingSm()
+ btnRowH + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// Two-column: Host+Port on one line, User+Pass on next
float halfInput = (availWidth - pad * 2 - labelW * 2 - Layout::spacingLg()) * 0.5f;
float rpcLabelW = std::max(70.0f, 85.0f * hs);
ImGui::PushFont(body2);
// Row 1: Host + Port
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Host");
ImGui::SameLine(rpcLabelW);
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Port");
ImGui::SameLine();
ImGui::SetNextItemWidth(std::max(60.0f, halfInput * 0.4f));
ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Row 2: Username + Password
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Username");
ImGui::SameLine(rpcLabelW);
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Password");
ImGui::SameLine();
ImGui::SetNextItemWidth(halfInput);
ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password),
ImGuiInputTextFlags_Password);
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
"Connection settings are usually auto-detected from DRAGONX.conf");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (TactileButton("Test Connection", ImVec2(0, 0), S.resolveFont("button"))) {
if (app->rpc() && app->rpc()->isConnected()) {
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
(void)result;
if (error.empty())
Notifications::instance().success("RPC connection OK");
else
Notifications::instance().error("RPC error: " + error);
});
} else {
Notifications::instance().warning("Not connected to daemon");
}
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Block Info...", ImVec2(0, 0), S.resolveFont("button"))) {
BlockInfoDialog::show(app->getBlockHeight());
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// ABOUT — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ABOUT");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingXs();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + sub1->LegacySize + rowH * 2 + Layout::spacingSm()
+ body2->LegacySize * 2 + Layout::spacingSm()
+ capFont->LegacySize * 2 + Layout::spacingMd()
+ btnRowH + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// App name + version on same line
ImGui::PushFont(sub1);
ImGui::TextUnformatted(DRAGONX_APP_NAME);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingLg());
ImGui::PushFont(body2);
snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION);
ImGui::TextUnformatted(buf);
ImGui::SameLine(0, Layout::spacingLg());
snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION);
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(body2);
ImGui::PushTextWrapPos(cardMax.x - pad);
ImGui::TextUnformatted(
"A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::PopTextWrapPos();
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(capFont);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License");
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Buttons — spread across width
{
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
if (TactileButton("Website", ImVec2(btnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://dragonx.is");
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Report Bug", ImVec2(btnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://git.hush.is/hush/SilentDragonX/issues");
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Block Explorer", ImVec2(btnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://explorer.dragonx.is");
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
ImGui::EndChild(); // ##SettingsPageScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -43,6 +43,11 @@ std::string SkinManager::getBundledSkinsDirectory() {
fs::path themes_dir = exe_dir / "res" / "themes";
if (fs::exists(themes_dir)) {
// Update any stale overlay themes from embedded versions
int updated = resources::updateBundledThemes(themes_dir.string());
if (updated > 0)
DEBUG_LOGF("[SkinManager] Updated %d stale theme(s) in %s\n",
updated, themes_dir.string().c_str());
return themes_dir.string();
}

View File

@@ -142,6 +142,7 @@ static constexpr int s_legacyLayoutCount = 10;
static std::vector<BalanceLayoutEntry> s_balanceLayouts;
static std::string s_defaultLayoutId = "classic";
static bool s_layoutConfigLoaded = false;
static bool s_generating_z_address = false;
static void LoadBalanceLayoutConfig()
{
@@ -803,8 +804,16 @@ static void RenderBalanceClassic(App* app)
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(addrSyncing);
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}
@@ -1420,9 +1429,18 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(sharedAddrSyncing);
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}

View File

@@ -147,7 +147,14 @@ void ImportKeyDialog::render(App* app)
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1);
std::string trimmed(clipboard);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
}
}
@@ -156,6 +163,43 @@ void ImportKeyDialog::render(App* app)
s_key_input[0] = '\0';
}
// Key validation indicator
if (s_key_input[0] != '\0') {
auto keys = splitKeys(s_key_input);
int zCount = 0, tCount = 0, unknownCount = 0;
for (const auto& key : keys) {
std::string kt = detectKeyType(key);
if (kt == "z-spending") zCount++;
else if (kt == "t-privkey") tCount++;
else unknownCount++;
}
if (zCount > 0 || tCount > 0) {
ImGui::PushFont(material::Type().iconSmall());
material::Type().textColored(material::TypeStyle::Caption, material::Success(), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 2.0f);
char validBuf[128];
if (zCount > 0 && tCount > 0)
snprintf(validBuf, sizeof(validBuf), "%d shielded, %d transparent key(s)", zCount, tCount);
else if (zCount > 0)
snprintf(validBuf, sizeof(validBuf), "%d shielded key(s)", zCount);
else
snprintf(validBuf, sizeof(validBuf), "%d transparent key(s)", tCount);
material::Type().textColored(material::TypeStyle::Caption, material::Success(), validBuf);
if (unknownCount > 0) {
ImGui::SameLine();
snprintf(validBuf, sizeof(validBuf), "(%d unrecognized)", unknownCount);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), validBuf);
}
} else if (unknownCount > 0) {
ImGui::PushFont(material::Type().iconSmall());
material::Type().textColored(material::TypeStyle::Caption, material::Error(), ICON_MD_ERROR);
ImGui::PopFont();
ImGui::SameLine(0, 2.0f);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
}
}
ImGui::Spacing();
// Rescan options

View File

@@ -156,7 +156,9 @@ void RenderMiningTab(App* app)
s_pool_state_loaded = true;
}
// Default pool worker to user's first shielded address once addresses are available
// Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
// is prompted to generate one before mining.
{
static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker);
@@ -169,18 +171,14 @@ void RenderMiningTab(App* app)
break;
}
}
if (defaultAddr.empty()) {
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
} else {
// No z-address yet — clear the placeholder "x" so field shows empty
s_pool_worker[0] = '\0';
s_pool_settings_dirty = true;
}
s_pool_worker_defaulted = true;
}
@@ -536,7 +534,12 @@ void RenderMiningTab(App* app)
s_pool_settings_dirty = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
std::string currentWorkerStr(s_pool_worker);
if (currentWorkerStr.empty()) {
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
} else {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
}
}
// --- Worker: Dropdown arrow button ---
@@ -739,7 +742,8 @@ void RenderMiningTab(App* app)
if (btnClk) {
strncpy(s_pool_url, "pool.dragonx.is", sizeof(s_pool_url) - 1);
// Default to user's first shielded address for pool payouts
// Default to user's first shielded (z) address for pool payouts.
// Leave blank if no z-address exists yet.
std::string defaultAddr;
for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) {
@@ -747,15 +751,6 @@ void RenderMiningTab(App* app)
break;
}
}
if (defaultAddr.empty()) {
// Fallback to transparent if no shielded available
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
@@ -840,6 +835,7 @@ void RenderMiningTab(App* app)
float idleRightEdge = cardMax.x - pad;
{
bool idleOn = app->settings()->getMineWhenIdle();
bool threadScaling = app->settings()->getIdleThreadScaling();
ImFont* icoFont = Type().iconSmall();
const char* idleIcon = ICON_MD_SCHEDULE;
float icoH = icoFont->LegacySize;
@@ -875,8 +871,40 @@ void RenderMiningTab(App* app)
idleRightEdge = btnX - 4.0f * dp;
// Idle delay combo (to the left of the icon when enabled)
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
if (idleOn) {
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
float sBtnX = idleRightEdge - btnSz;
float sBtnY = btnY;
if (threadScaling) {
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
sIcoCol, scaleIcon);
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setIdleThreadScaling(!threadScaling);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", threadScaling
? TR("mining_idle_scale_on_tooltip")
: TR("mining_idle_scale_off_tooltip"));
}
idleRightEdge = sBtnX - 4.0f * dp;
}
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
if (idleOn && !threadScaling) {
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
@@ -907,6 +935,111 @@ void RenderMiningTab(App* app)
idleRightEdge = comboX - 4.0f * dp;
}
// Thread scaling controls: idle delay + active threads / idle threads combos
if (idleOn && threadScaling) {
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
// Idle delay combo
{
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
// Idle threads combo (threads when system is idle)
{
int curVal = app->settings()->getIdleThreadsIdle();
if (curVal <= 0) curVal = hwThreads;
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsIdle(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
// Separator arrow icon
{
const char* arrowIcon = ICON_MD_ARROW_BACK;
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
float arrX = idleRightEdge - arrSz.x;
float arrY = curY + (headerH - arrSz.y) * 0.5f;
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
idleRightEdge = arrX - 4.0f * dp;
}
// Active threads combo (threads when user is active)
{
int curVal = app->settings()->getIdleThreadsActive();
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsActive(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
}
ImGui::SetCursorScreenPos(savedCur);
}

View File

@@ -65,6 +65,7 @@ static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static std::string s_pending_select_address;
static bool s_generating_address = false;
// ============================================================================
// Helpers
@@ -302,10 +303,18 @@ static void RenderAddressDropdown(App* app, float width) {
// New address button on same line
ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
if (s_generating_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
s_generating_address = true;
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_shielded"));
else {
@@ -315,6 +324,7 @@ static void RenderAddressDropdown(App* app, float width) {
});
} else {
app->createNewTAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_transparent"));
else {

View File

@@ -1,934 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Layout G: QR-Centered Hero
// - QR code dominates center as hero element
// - Address info wraps around the QR
// - Payment request section below QR
// - Horizontal address strip at bottom for fast switching
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../version.h"
#include "../../wallet_state.h"
#include "../../ui/widgets/qr_code.h"
#include "../sidebar.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../notifications.h"
#include "imgui.h"
#include <string>
#include <algorithm>
#include <cmath>
#include <map>
namespace dragonx {
namespace ui {
using namespace material;
// ============================================================================
// State
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[256] = "";
static std::string s_cached_qr_data;
static uintptr_t s_qr_texture = 0;
static bool s_payment_request_open = false;
// Track newly created addresses for NEW badge
static std::map<std::string, double> s_new_address_timestamps;
static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static char s_label_edit_buf[64] = "";
// Address type filter
static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T
// ============================================================================
// Helpers
// ============================================================================
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 35) {
if (addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
static void OpenExplorerURL(const std::string& address) {
std::string url = "https://explorer.dragonx.com/address/" + address;
#ifdef _WIN32
std::string cmd = "start \"\" \"" + url + "\"";
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
#else
std::string cmd = "xdg-open \"" + url + "\"";
#endif
system(cmd.c_str());
}
// ============================================================================
// Sync banner
// ============================================================================
static void RenderSyncBanner(const WalletState& state) {
if (!state.sync.syncing || state.sync.isSynced()) return;
float syncPct = (state.sync.headers > 0)
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf),
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.4f, 0.0f, 0.15f));
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, 28),
false, ImGuiWindowFlags_NoScrollbar);
ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), 6));
Type().textColored(TypeStyle::Caption, Warning(), syncBuf);
ImGui::EndChild();
ImGui::PopStyleColor();
}
// ============================================================================
// Track new addresses (detect creations)
// ============================================================================
static void TrackNewAddresses(const WalletState& state) {
if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) {
for (const auto& a : state.addresses) {
if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) {
s_new_address_timestamps[a.address] = ImGui::GetTime();
}
}
} else if (s_prev_address_count == 0) {
for (const auto& a : state.addresses) {
s_new_address_timestamps[a.address] = 0.0;
}
}
s_prev_address_count = state.addresses.size();
}
// ============================================================================
// Build sorted address groups
// ============================================================================
struct AddressGroups {
std::vector<int> shielded;
std::vector<int> transparent;
};
static AddressGroups BuildSortedAddressGroups(const WalletState& state) {
AddressGroups groups;
for (int i = 0; i < (int)state.addresses.size(); i++) {
if (state.addresses[i].type == "shielded")
groups.shielded.push_back(i);
else
groups.transparent.push_back(i);
}
std::sort(groups.shielded.begin(), groups.shielded.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
std::sort(groups.transparent.begin(), groups.transparent.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
return groups;
}
// ============================================================================
// QR Hero — the centerpiece of Layout G
// ============================================================================
static void RenderQRHero(App* app, ImDrawList* dl, const AddressInfo& addr,
float width, float qrSize,
const std::string& qr_data,
const GlassPanelSpec& glassSpec,
const WalletState& state,
ImFont* sub1, ImFont* /*body2*/, ImFont* capFont) {
char buf[128];
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
const char* typeBadge = isZ ? "Shielded" : "Transparent";
float qrPadding = Layout::spacingLg();
float totalQrSize = qrSize + qrPadding * 2;
float heroH = totalQrSize + 80.0f; // QR + info below
ImVec2 heroMin = ImGui::GetCursorScreenPos();
ImVec2 heroMax(heroMin.x + width, heroMin.y + heroH);
GlassPanelSpec heroGlass = glassSpec;
heroGlass.fillAlpha = 16;
heroGlass.borderAlpha = 35;
DrawGlassPanel(dl, heroMin, heroMax, heroGlass);
// --- Address info bar above QR ---
float infoBarH = 32.0f;
float cx = heroMin.x + Layout::spacingLg();
float cy = heroMin.y + Layout::spacingSm();
// Type badge circle + label
dl->AddCircleFilled(ImVec2(cx + 8, cy + 10), 8.0f, IM_COL32(255, 255, 255, 20));
const char* typeChar = isZ ? "Z" : "T";
ImVec2 tcSz = sub1->CalcTextSizeA(sub1->LegacySize, 100, 0, typeChar);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(cx + 8 - tcSz.x * 0.5f, cy + 10 - tcSz.y * 0.5f),
typeCol, typeChar);
// Education tooltip on badge
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::InvisibleButton("##TypeBadgeHero", ImVec2(22, 22));
if (ImGui::IsItemHovered()) {
if (isZ) {
ImGui::SetTooltip(
"Shielded Address (Z)\n"
"- Full transaction privacy\n"
"- Encrypted sender, receiver, amount\n"
"- Supports encrypted memos\n"
"- Recommended for privacy");
} else {
ImGui::SetTooltip(
"Transparent Address (T)\n"
"- Publicly visible on blockchain\n"
"- Similar to Bitcoin addresses\n"
"- No memo support\n"
"- Use Z addresses for privacy");
}
}
// Type label text
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 24, cy + 4), typeCol, typeBadge);
// Balance right-aligned
snprintf(buf, sizeof(buf), "%.8f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
float balX = heroMax.x - balSz.x - Layout::spacingLg();
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(balX, cy + 2), typeCol, buf);
// USD value
if (state.market.price_usd > 0 && addr.balance > 0) {
double usd = addr.balance * state.market.price_usd;
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(heroMax.x - usdSz.x - Layout::spacingLg(), cy + sub1->LegacySize + 4),
OnSurfaceDisabled(), buf);
}
// --- QR Code centered ---
float qrOffset = (width - totalQrSize) * 0.5f;
ImVec2 qrPanelMin(heroMin.x + qrOffset, heroMin.y + infoBarH + Layout::spacingSm());
ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize);
// Subtle inner panel for QR
GlassPanelSpec qrGlass;
qrGlass.rounding = glassSpec.rounding * 0.75f;
qrGlass.fillAlpha = 12;
qrGlass.borderAlpha = 25;
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
if (s_qr_texture) {
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - 50,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), "QR unavailable");
}
// Click QR to copy
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy %s",
s_request_amount > 0 ? "payment URI" : "address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? "Payment URI copied to clipboard"
: "Address copied to clipboard");
}
// --- Address strip below QR ---
float addrStripY = qrPanelMax.y + Layout::spacingMd();
float addrStripX = heroMin.x + Layout::spacingLg();
float addrStripW = width - Layout::spacingXxl();
// Full address (word-wrapped)
ImVec2 fullAddrPos(addrStripX, addrStripY);
float wrapWidth = addrStripW;
ImVec2 addrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
wrapWidth, addr.address.c_str());
dl->AddText(capFont, capFont->LegacySize, fullAddrPos,
OnSurface(), addr.address.c_str(), nullptr, wrapWidth);
// Address click-to-copy overlay
ImGui::SetCursorScreenPos(fullAddrPos);
ImGui::InvisibleButton("##addrCopyHero", ImVec2(wrapWidth, addrSz.y));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Action buttons row
float btnRowY = addrStripY + addrSz.y + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(addrStripX, btnRowY));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
{
// Copy — primary (uses global glass style)
if (TactileSmallButton("Copy Address##hero", schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
ImGui::SameLine();
// Explorer
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileSmallButton("Explorer##hero", schema::UI().resolveFont("button"))) {
OpenExplorerURL(addr.address);
}
ImGui::PopStyleColor(3);
// Send From
if (addr.balance > 0) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
if (TactileSmallButton("Send \xe2\x86\x97##hero", schema::UI().resolveFont("button"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::PopStyleColor(3);
}
// Label editor (inline)
ImGui::SameLine(0, Layout::spacingXl());
auto lblIt = s_address_labels.find(addr.address);
std::string currentLabel = (lblIt != s_address_labels.end()) ? lblIt->second : "";
snprintf(s_label_edit_buf, sizeof(s_label_edit_buf), "%s", currentLabel.c_str());
ImGui::SetNextItemWidth(std::min(200.0f, addrStripW * 0.3f));
if (ImGui::InputTextWithHint("##LabelHero", "Add label...",
s_label_edit_buf, sizeof(s_label_edit_buf))) {
s_address_labels[addr.address] = std::string(s_label_edit_buf);
}
}
ImGui::PopStyleVar();
// Update hero height based on actual content
float actualBottom = btnRowY + 24;
heroH = actualBottom - heroMin.y + Layout::spacingMd();
heroMax.y = heroMin.y + heroH;
ImGui::SetCursorScreenPos(ImVec2(heroMin.x, heroMax.y));
ImGui::Dummy(ImVec2(width, 0));
}
// ============================================================================
// Payment request section (below QR hero)
// ============================================================================
static void RenderPaymentRequest(ImDrawList* dl, const AddressInfo& addr,
float innerW, const GlassPanelSpec& glassSpec,
const char* suffix) {
auto& S = schema::UI();
const float kLabelPos = S.label("tabs.receive", "label-column").position;
bool hasMemo = (addr.type == "shielded");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Compute card height
float prCardH = 16.0f + 24.0f + 8.0f + 12.0f;
if (hasMemo) prCardH += 24.0f;
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImFont* capF = Type().caption();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
innerW - 24, s_cached_qr_data.c_str());
prCardH += uriSz.y + 8.0f;
}
if (s_request_amount > 0) prCardH += 32.0f;
if (s_request_amount > 0 || s_request_memo[0]) prCardH += 4.0f;
ImVec2 prMin = ImGui::GetCursorScreenPos();
ImVec2 prMax(prMin.x + innerW, prMin.y + prCardH);
DrawGlassPanel(dl, prMin, prMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(prMin.x + Layout::spacingLg(), prMin.y + Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, 0));
ImGui::Text("Amount:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(std::max(S.input("tabs.receive", "amount-input").width, innerW * 0.4f));
char amtId[32];
snprintf(amtId, sizeof(amtId), "##RequestAmount%s", suffix);
ImGui::InputDouble(amtId, &s_request_amount, 0.01, 1.0, "%.8f");
ImGui::SameLine();
ImGui::Text("%s", DRAGONX_TICKER);
if (hasMemo) {
ImGui::Text("Memo:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(innerW - kLabelPos - Layout::spacingXxl());
char memoId[32];
snprintf(memoId, sizeof(memoId), "##RequestMemo%s", suffix);
ImGui::InputText(memoId, s_request_memo, sizeof(s_request_memo));
}
// Live URI preview
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImGui::Spacing();
ImFont* capF = Type().caption();
ImVec2 uriPos = ImGui::GetCursorScreenPos();
float uriWrapW = innerW - Layout::spacingXxl();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
uriWrapW, s_cached_qr_data.c_str());
dl->AddText(capF, capF->LegacySize, uriPos,
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
ImGui::Dummy(ImVec2(uriWrapW, uriSz.y + Layout::spacingSm()));
}
ImGui::Spacing();
if (s_request_amount > 0) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
char copyUriId[64];
snprintf(copyUriId, sizeof(copyUriId), "Copy Payment URI%s", suffix);
if (TactileButton(copyUriId, ImVec2(innerW - Layout::spacingXxl(), 0), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_cached_qr_data.c_str());
Notifications::instance().info("Payment URI copied to clipboard");
}
ImGui::PopStyleVar();
// Share as text
char shareId[32];
snprintf(shareId, sizeof(shareId), "Share as Text%s", suffix);
if (TactileSmallButton(shareId, S.resolveFont("button"))) {
char shareBuf[1024];
snprintf(shareBuf, sizeof(shareBuf),
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
s_request_amount, DRAGONX_TICKER,
addr.address.c_str(), s_cached_qr_data.c_str());
ImGui::SetClipboardText(shareBuf);
Notifications::instance().info("Payment request copied to clipboard");
}
}
if (s_request_amount > 0 || s_request_memo[0]) {
ImGui::SameLine();
char clearId[32];
snprintf(clearId, sizeof(clearId), "Clear%s", suffix);
if (TactileSmallButton(clearId, S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_memo[0] = '\0';
}
}
ImGui::SetCursorScreenPos(ImVec2(prMin.x, prMax.y));
ImGui::Dummy(ImVec2(innerW, 0));
}
// ============================================================================
// Recent received transactions for selected address
// ============================================================================
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& addr,
const WalletState& state, float width,
ImFont* capFont) {
char buf[128];
int recvCount = 0;
for (const auto& tx : state.transactions) {
if (tx.address == addr.address && tx.type == "receive") recvCount++;
}
if (recvCount == 0) return;
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
snprintf(buf, sizeof(buf), "RECENT RECEIVED (%d)", std::min(recvCount, 3));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
int shown = 0;
for (const auto& tx : state.transactions) {
if (tx.address != addr.address || tx.type != "receive") continue;
if (shown >= 3) break;
ImVec2 rMin = ImGui::GetCursorScreenPos();
float rH = 22.0f;
ImVec2 rMax(rMin.x + width, rMin.y + rH);
GlassPanelSpec rsGlass;
rsGlass.rounding = Layout::glassRounding() * 0.5f;
rsGlass.fillAlpha = 8;
DrawGlassPanel(dl, rMin, rMax, rsGlass);
float rx = rMin.x + Layout::spacingMd();
float ry = rMin.y + (rH - capFont->LegacySize) * 0.5f;
// Arrow indicator
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx, ry),
Success(), "\xe2\x86\x90");
snprintf(buf, sizeof(buf), "+%.8f %s %s %s",
tx.amount, DRAGONX_TICKER,
tx.getTimeString().c_str(),
tx.confirmations < 1 ? "(unconfirmed)" : "");
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + 16, ry),
tx.confirmations >= 1 ? Success() : Warning(), buf);
ImGui::Dummy(ImVec2(width, rH));
ImGui::Dummy(ImVec2(0, 2));
shown++;
}
}
// ============================================================================
// Horizontal Address Strip — bottom switching bar (Layout G signature)
// ============================================================================
static void RenderAddressStrip(App* app, ImDrawList* dl, const WalletState& state,
float width, float hs,
ImFont* /*sub1*/, ImFont* capFont) {
char buf[128];
// Header row with filter and + New button
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR ADDRESSES");
float btnW = std::max(70.0f, 85.0f * hs);
float comboW = std::max(48.0f, 58.0f * hs);
ImGui::SameLine(width - btnW - comboW - Layout::spacingMd());
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
const char* types[] = { "All", "Z", "T" };
ImGui::SetNextItemWidth(comboW);
ImGui::Combo("##AddrTypeStrip", &s_addr_type_filter, types, 3);
ImGui::SameLine();
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("+ New##strip", ImVec2(btnW, 0), schema::UI().resolveFont("button"))) {
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new shielded address");
else
Notifications::instance().success("New shielded address created");
});
} else {
app->createNewTAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new transparent address");
else
Notifications::instance().success("New transparent address created");
});
}
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (!app->isConnected()) {
Type().textColored(TypeStyle::Caption, Warning(), "Waiting for connection...");
return;
}
if (state.addresses.empty()) {
// Loading skeleton
ImVec2 skelPos = ImGui::GetCursorScreenPos();
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
for (int sk = 0; sk < 3; sk++) {
dl->AddRectFilled(
ImVec2(skelPos.x + sk * (130 + 8), skelPos.y),
ImVec2(skelPos.x + sk * (130 + 8) + 120, skelPos.y + 56),
skelCol, 6.0f);
}
ImGui::Dummy(ImVec2(width, 60));
return;
}
TrackNewAddresses(state);
AddressGroups groups = BuildSortedAddressGroups(state);
// Build filtered list
std::vector<int> filteredIdxs;
if (s_addr_type_filter != 2)
for (int idx : groups.shielded) filteredIdxs.push_back(idx);
if (s_addr_type_filter != 1)
for (int idx : groups.transparent) filteredIdxs.push_back(idx);
// Horizontal scrolling strip
float cardW = std::max(140.0f, std::min(200.0f, width * 0.22f));
float cardH = std::max(52.0f, 64.0f * hs);
float stripH = cardH + 8;
ImGui::BeginChild("##AddrStrip", ImVec2(width, stripH), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoBackground);
ImDrawList* sdl = ImGui::GetWindowDrawList();
for (size_t fi = 0; fi < filteredIdxs.size(); fi++) {
int i = filteredIdxs[fi];
const auto& addr = state.addresses[i];
bool isCurrent = (i == s_selected_address_idx);
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
bool hasBalance = addr.balance > 0;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
// Card background
GlassPanelSpec cardGlass;
cardGlass.rounding = Layout::glassRounding() * 0.75f;
cardGlass.fillAlpha = isCurrent ? 28 : 14;
cardGlass.borderAlpha = isCurrent ? 50 : 25;
DrawGlassPanel(sdl, cardMin, cardMax, cardGlass);
// Selected indicator — top accent bar
if (isCurrent) {
sdl->AddRectFilled(cardMin, ImVec2(cardMax.x, cardMin.y + 3), Primary(),
cardGlass.rounding);
}
float ix = cardMin.x + Layout::spacingMd();
float iy = cardMin.y + Layout::spacingSm() + (isCurrent ? 4 : 0);
// Type dot
sdl->AddCircleFilled(ImVec2(ix + 4, iy + 6), 3.5f, typeCol);
// Address label or truncated address
auto lblIt = s_address_labels.find(addr.address);
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
size_t addrTruncLen = static_cast<size_t>(std::max(8.0f, (cardW - 30) / 9.0f));
if (hasLabel) {
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? PrimaryLight() : OnSurfaceMedium(),
lblIt->second.c_str());
std::string shortAddr = TruncateAddress(addr.address, std::max((size_t)6, addrTruncLen / 2));
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy + capFont->LegacySize + 2),
OnSurfaceDisabled(), shortAddr.c_str());
} else {
std::string dispAddr = TruncateAddress(addr.address, addrTruncLen);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? OnSurface() : OnSurfaceDisabled(),
dispAddr.c_str());
}
// Balance
snprintf(buf, sizeof(buf), "%.4f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float balY = cardMax.y - balSz.y - Layout::spacingSm();
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, balY),
hasBalance ? typeCol : OnSurfaceDisabled(), buf);
// NEW badge
double now = ImGui::GetTime();
auto newIt = s_new_address_timestamps.find(addr.address);
if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) {
double age = now - newIt->second;
if (age < 10.0) {
float alpha = (float)std::max(0.0, 1.0 - age / 10.0);
int a = (int)(alpha * 220);
ImVec2 badgePos(cardMax.x - 32, cardMin.y + 4);
sdl->AddRectFilled(badgePos, ImVec2(badgePos.x + 28, badgePos.y + 14),
IM_COL32(77, 204, 255, a / 4), 3.0f);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(badgePos.x + 4, badgePos.y + 1),
IM_COL32(77, 204, 255, a), "NEW");
}
}
// Click interaction
ImGui::SetCursorScreenPos(cardMin);
ImGui::PushID(i);
ImGui::InvisibleButton("##addrCard", ImVec2(cardW, cardH));
if (ImGui::IsItemHovered()) {
if (!isCurrent)
sdl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 10),
cardGlass.rounding);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s\nBalance: %.8f %s%s\nDouble-click to copy | Right-click for options",
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? " (selected)" : "");
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
s_selected_address_idx = i;
s_cached_qr_data.clear();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Context menu
if (ImGui::BeginPopupContextItem("##addrStripCtx")) {
if (ImGui::MenuItem("Copy Address")) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
if (ImGui::MenuItem("View on Explorer")) {
OpenExplorerURL(addr.address);
}
if (addr.balance > 0) {
if (ImGui::MenuItem("Send From This Address")) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
}
ImGui::EndPopup();
}
ImGui::PopID();
ImGui::SameLine(0, Layout::spacingSm());
}
// Total balance at end of strip
{
double totalBal = 0;
for (const auto& a : state.addresses) totalBal += a.balance;
ImVec2 totPos = ImGui::GetCursorScreenPos();
float totCardW = std::max(100.0f, cardW * 0.6f);
ImVec2 totMax(totPos.x + totCardW, totPos.y + cardH);
GlassPanelSpec totGlass;
totGlass.rounding = Layout::glassRounding() * 0.75f;
totGlass.fillAlpha = 8;
totGlass.borderAlpha = 15;
DrawGlassPanel(sdl, totPos, totMax, totGlass);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(), totPos.y + Layout::spacingSm()),
OnSurfaceMedium(), "TOTAL");
snprintf(buf, sizeof(buf), "%.8f", totalBal);
ImVec2 totSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm()),
OnSurface(), buf);
snprintf(buf, sizeof(buf), "%s", DRAGONX_TICKER);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm() - capFont->LegacySize - 2),
OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(totCardW, cardH));
}
// Keyboard navigation
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
int next = s_selected_address_idx + 1;
if (next < (int)state.addresses.size()) {
s_selected_address_idx = next;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
int prev = s_selected_address_idx - 1;
if (prev >= 0) {
s_selected_address_idx = prev;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
}
}
ImGui::EndChild(); // ##AddrStrip
}
// ============================================================================
// MAIN: RenderReceiveTab — Layout G: QR-Centered Hero
// ============================================================================
void RenderReceiveTab(App* app)
{
const auto& state = app->getWalletState();
RenderSyncBanner(state);
ImVec2 recvAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##ReceiveScroll", recvAvail, false, ImGuiWindowFlags_NoBackground);
float hs = Layout::hScale(recvAvail.x);
float vScale = Layout::vScale(recvAvail.y);
float glassRound = Layout::glassRounding();
float availWidth = ImGui::GetContentRegionAvail().x;
float contentWidth = std::min(availWidth * 0.92f, 1200.0f * hs);
float offsetX = (availWidth - contentWidth) * 0.5f;
if (offsetX > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
float sectionGap = Layout::spacingXl() * vScale;
ImGui::BeginGroup();
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2();
// Auto-select first address
if (!state.addresses.empty() &&
(s_selected_address_idx < 0 ||
s_selected_address_idx >= (int)state.addresses.size())) {
s_selected_address_idx = 0;
}
const AddressInfo* selected = nullptr;
if (s_selected_address_idx >= 0 &&
s_selected_address_idx < (int)state.addresses.size()) {
selected = &state.addresses[s_selected_address_idx];
}
// Generate QR data
std::string qr_data;
if (selected) {
qr_data = selected->address;
if (s_request_amount > 0) {
qr_data = std::string("dragonx:") + selected->address +
"?amount=" + std::to_string(s_request_amount);
if (s_request_memo[0] && selected->type == "shielded") {
qr_data += "&memo=" + std::string(s_request_memo);
}
}
if (qr_data != s_cached_qr_data) {
if (s_qr_texture) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
int w, h;
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
s_cached_qr_data = qr_data;
}
}
// ================================================================
// Not connected / empty state
// ================================================================
if (!app->isConnected()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 120.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
OnSurfaceDisabled(), "Waiting for daemon connection...");
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + 8),
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
if (state.addresses.empty()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 100.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
ImVec2(emptyMin.x + contentWidth * 0.6f, emptyMin.y + Layout::spacingLg() + 16),
skelCol, 4.0f);
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + 24),
ImVec2(emptyMin.x + contentWidth * 0.4f, emptyMin.y + Layout::spacingLg() + 36),
skelCol, 4.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - 24),
OnSurfaceDisabled(), "Loading addresses...");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
// ================================================================
// QR HERO — dominates center (Layout G signature)
// ================================================================
if (selected) {
// Calculate QR size based on available space
float maxQrForWidth = std::min(contentWidth * 0.6f, 400.0f);
float maxQrForHeight = std::min(recvAvail.y * 0.45f, 400.0f);
float qrSize = std::max(140.0f, std::min(maxQrForWidth, maxQrForHeight));
// Center the hero horizontally
float heroW = std::min(contentWidth, 700.0f * hs);
float heroOffsetX = (contentWidth - heroW) * 0.5f;
if (heroOffsetX > 4) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + heroOffsetX);
}
RenderQRHero(app, dl, *selected, heroW, qrSize, qr_data,
glassSpec, state, sub1, body2, capFont);
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- PAYMENT REQUEST (collapsible on narrow) ----
constexpr float kTwoColumnThreshold = 800.0f;
bool isNarrow = contentWidth < kTwoColumnThreshold;
if (isNarrow) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1, 1, 1, 0.05f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1, 1, 1, 0.08f));
ImGui::PushFont(Type().overline());
s_payment_request_open = ImGui::CollapsingHeader(
"PAYMENT REQUEST (OPTIONAL)",
s_payment_request_open ? ImGuiTreeNodeFlags_DefaultOpen : 0);
ImGui::PopFont();
ImGui::PopStyleColor(3);
if (s_payment_request_open) {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
} else {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- RECENT RECEIVED ----
{
float rcvW = std::min(contentWidth, 600.0f * hs);
float rcvOffX = (contentWidth - rcvW) * 0.5f;
if (rcvOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rcvOffX);
RenderRecentReceived(dl, *selected, state, rcvW, capFont);
}
ImGui::Dummy(ImVec2(0, sectionGap));
}
// ================================================================
// ADDRESS STRIP — horizontal switching bar at bottom
// ================================================================
RenderAddressStrip(app, dl, state, contentWidth, hs, sub1, capFont);
ImGui::EndGroup();
ImGui::EndChild(); // ##ReceiveScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -3,6 +3,7 @@
// Released under the GPLv3
#include "bootstrap.h"
#include "../daemon/embedded_daemon.h"
#include <curl/curl.h>
#include <miniz.h>
@@ -135,11 +136,33 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
worker_running_ = false;
return;
}
// Step 3: Clean old chain data
// Step 3: Ensure daemon is fully stopped before touching chain data
{
setProgress(State::Extracting, "Waiting for daemon to stop...");
int waited = 0;
while (daemon::EmbeddedDaemon::isRpcPortInUse() && waited < 60 && !cancel_requested_) {
std::this_thread::sleep_for(std::chrono::seconds(1));
waited++;
if (waited % 5 == 0)
DEBUG_LOGF("[Bootstrap] Still waiting for daemon to stop... (%ds)\n", waited);
}
if (cancel_requested_) {
setProgress(State::Failed, "Cancelled while waiting for daemon");
worker_running_ = false;
return;
}
if (daemon::EmbeddedDaemon::isRpcPortInUse()) {
setProgress(State::Failed, "Daemon is still running — stop it before using bootstrap");
worker_running_ = false;
return;
}
}
// Step 4: Clean old chain data
setProgress(State::Extracting, "Removing old chain data...");
cleanChainData(dataDir);
// Step 4: Extract (skipping wallet.dat)
// Step 5: Extract (skipping wallet.dat)
if (!extract(zipPath, dataDir)) {
if (cancel_requested_)
setProgress(State::Failed, "Extraction cancelled");

View File

@@ -51,6 +51,7 @@ public:
/// Base URL for bootstrap downloads (zip + checksum files).
static constexpr const char* kBaseUrl = "https://bootstrap.dragonx.is";
static constexpr const char* kMirrorUrl = "https://bootstrap2.dragonx.is";
static constexpr const char* kZipName = "DRAGONX.zip";
/// Start the bootstrap process on a background thread.

View File

@@ -297,7 +297,7 @@ void I18n::loadBuiltinEnglish()
strings_["tt_noise"] = "Grain texture intensity (0%% = off, 100%% = maximum)";
strings_["tt_ui_opacity"] = "Card and sidebar opacity (100%% = fully opaque, lower = more see-through)";
strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)";
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x).";
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x). Hotkey: Alt + Scroll Wheel";
strings_["tt_custom_theme"] = "Custom theme active";
strings_["tt_address_book"] = "Manage saved addresses for quick sending";
strings_["tt_validate"] = "Check if a DragonX address is valid";
@@ -321,6 +321,12 @@ void I18n::loadBuiltinEnglish()
strings_["tt_rpc_pass"] = "RPC authentication password";
strings_["tt_test_conn"] = "Verify the RPC connection to the daemon";
strings_["tt_rescan"] = "Rescan the blockchain for missing transactions";
strings_["tt_delete_blockchain"] = "Delete all blockchain data and start a fresh sync. Your wallet.dat and config are preserved.";
strings_["delete_blockchain"] = "Delete Blockchain";
strings_["delete_blockchain_confirm"] = "Delete & Resync";
strings_["confirm_delete_blockchain_title"] = "Delete Blockchain Data";
strings_["confirm_delete_blockchain_msg"] = "This will stop the daemon, delete all blockchain data (blocks, chainstate, peers), and start a fresh sync from scratch. This can take several hours to complete.";
strings_["confirm_delete_blockchain_safe"] = "Your wallet.dat, config, and transaction history are safe and will not be deleted.";
strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase";
strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
strings_["tt_lock"] = "Lock the wallet immediately";
@@ -718,6 +724,7 @@ void I18n::loadBuiltinEnglish()
strings_["console_daemon_error"] = "Daemon error!";
strings_["console_daemon_started"] = "Daemon started";
strings_["console_daemon_stopped"] = "Daemon stopped";
strings_["daemon_version"] = "Daemon";
strings_["console_disconnected"] = "Disconnected from daemon";
strings_["console_errors"] = "Errors";
strings_["console_filter_hint"] = "Filter output...";
@@ -836,6 +843,10 @@ void I18n::loadBuiltinEnglish()
strings_["mining_filter_tip_solo"] = "Show solo earnings only";
strings_["mining_idle_off_tooltip"] = "Enable idle mining";
strings_["mining_idle_on_tooltip"] = "Disable idle mining";
strings_["mining_idle_scale_on_tooltip"] = "Thread scaling: ON\nClick to switch to start/stop mode";
strings_["mining_idle_scale_off_tooltip"] = "Start/stop mode: ON\nClick to switch to thread scaling mode";
strings_["mining_idle_threads_active_tooltip"] = "Threads when user is active";
strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle";
strings_["mining_local_hashrate"] = "Local Hashrate";
strings_["mining_mine"] = "Mine";
strings_["mining_mining_addr"] = "Mining Addr";
@@ -847,6 +858,7 @@ void I18n::loadBuiltinEnglish()
strings_["mining_open_in_explorer"] = "Open in explorer";
strings_["mining_payout_address"] = "Payout Address";
strings_["mining_payout_tooltip"] = "Address to receive mining rewards";
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
strings_["mining_pool"] = "Pool";
strings_["mining_pool_hashrate"] = "Pool Hashrate";
strings_["mining_pool_url"] = "Pool URL";
@@ -927,6 +939,7 @@ void I18n::loadBuiltinEnglish()
// --- Receive Tab ---
strings_["click_copy_address"] = "Click to copy address";
strings_["click_copy_uri"] = "Click to copy URI";
strings_["generating"] = "Generating";
strings_["failed_create_shielded"] = "Failed to create shielded address";
strings_["failed_create_transparent"] = "Failed to create transparent address";
strings_["new_shielded_created"] = "New shielded address created";