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:
127
src/app.cpp
127
src/app.cpp
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
11
src/main.cpp
11
src/main.cpp
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user