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

@@ -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";