fix: Windows identity, async address creation, mining UI, and chart artifacts
Windows identity: - Add VERSIONINFO resource (.rc) with ObsidianDragon file description - Embed application manifest for DPI awareness and shell identity - Patch libwinpthread/libpthread to remove competing VERSIONINFO - Set AppUserModelID and HWND property store to override Task Manager cache - Link patched pthread libs to eliminate "POSIX WinThreads" description Address creation (+New button): - Move z_getnewaddress/getnewaddress off UI thread to async worker - Inject new address into state immediately for instant UI selection - Trigger background refresh for balance updates Mining tab: - Add pool mining dropdown with saved URLs/workers and bookmarks - Add solo mining log panel from daemon output with chart/log toggle - Fix toggle button cursor (render after InputTextMultiline) - Auto-restart miner on pool config change - Migrate default pool URL to include stratum port Transactions: - Sort pending (0-conf) transactions to top of history - Fall back to timereceived when timestamp is missing Shutdown: - Replace blocking sleep_for calls with 100ms polling loops - Check shutting_down_ flag throughout daemon restart/bootstrap flows - Reduce daemon stop timeout from 30s to 10s Other: - Fix market chart fill artifact (single concave polygon vs per-segment quads) - Add bootstrap checksum verification state display - Rename daemon client identifier to ObsidianDragon
This commit is contained in:
@@ -373,6 +373,7 @@ endif()
|
||||
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
|
||||
if(WIN32)
|
||||
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
|
||||
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
|
||||
# Version numbers for the VERSIONINFO resource block
|
||||
set(DRAGONX_VER_MAJOR 1)
|
||||
set(DRAGONX_VER_MINOR 0)
|
||||
|
||||
18
build.sh
18
build.sh
@@ -412,6 +412,22 @@ build_release_win() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Patch libwinpthread + libpthread to remove VERSIONINFO resources ────
|
||||
# mingw-w64's libwinpthread.a and libpthread.a each ship a version.o
|
||||
# with their own VERSIONINFO ("POSIX WinThreads for Windows") that
|
||||
# collides with ours during .rsrc merge, causing Task Manager to show
|
||||
# the wrong process description.
|
||||
local PATCHED_LIB_DIR="$bd/patched-lib"
|
||||
mkdir -p "$PATCHED_LIB_DIR"
|
||||
for plib in libwinpthread.a libpthread.a; do
|
||||
local SYS_LIB="/usr/x86_64-w64-mingw32/lib/$plib"
|
||||
if [[ -f "$SYS_LIB" ]]; then
|
||||
cp -f "$SYS_LIB" "$PATCHED_LIB_DIR/$plib"
|
||||
x86_64-w64-mingw32-ar d "$PATCHED_LIB_DIR/$plib" version.o 2>/dev/null || true
|
||||
info "Patched $plib (removed version.o VERSIONINFO resource)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Toolchain file ───────────────────────────────────────────────────────
|
||||
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
@@ -423,7 +439,7 @@ set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -L$PATCHED_LIB_DIR -lwinpthread -Wl,--no-whole-archive")
|
||||
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
|
||||
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
|
||||
|
||||
51
res/ObsidianDragon.manifest
Normal file
51
res/ObsidianDragon.manifest
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<!-- Application identity —————————————————————————————— -->
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="DragonX.ObsidianDragon.Wallet"
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="amd64"
|
||||
/>
|
||||
|
||||
<description>ObsidianDragon Wallet</description>
|
||||
|
||||
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 10 / 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -4,6 +4,13 @@
|
||||
// Use numeric ordinal 1 so LoadIcon(hInst, MAKEINTRESOURCE(1)) finds it.
|
||||
1 ICON "@OBSIDIAN_ICO_PATH@"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Application Manifest — declares DPI awareness, common controls v6,
|
||||
// UTF-8 code page, and application identity. Without this, Windows may
|
||||
// fall back to legacy process grouping in Task Manager.
|
||||
// ---------------------------------------------------------------------------
|
||||
1 24 "@OBSIDIAN_MANIFEST_PATH@"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VERSIONINFO — sets the description shown in Task Manager, Explorer
|
||||
// "Details" tab, and other Windows tools. Without this, MinGW-w64
|
||||
|
||||
@@ -766,7 +766,7 @@ control-card-min-height = { size = 60.0 }
|
||||
active-cell-border-thickness = { size = 1.5 }
|
||||
cell-border-thickness = { size = 1.0 }
|
||||
button-icon-y-ratio = { size = 0.42 }
|
||||
button-label-y-ratio = { size = 0.78 }
|
||||
button-label-y-ratio = { size = 0.72 }
|
||||
chart-line-thickness = { size = 1.5 }
|
||||
details-card-min-height = { size = 50.0 }
|
||||
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
|
||||
@@ -808,6 +808,7 @@ dir-pill-padding = { size = 4.0 }
|
||||
dir-pill-y-offset = { size = 1.0 }
|
||||
dir-pill-y-bottom = { size = 3.0 }
|
||||
dir-pill-rounding = { size = 3.0 }
|
||||
seed-badge-padding = { size = 3.0 }
|
||||
tls-badge-min-width = { size = 20.0 }
|
||||
tls-badge-width = { size = 28.0 }
|
||||
tls-badge-rounding = { size = 3.0 }
|
||||
|
||||
21
src/app.cpp
21
src/app.cpp
@@ -382,6 +382,11 @@ void App::update()
|
||||
state_.pool_mining.xmrig_running = false;
|
||||
}
|
||||
|
||||
// Populate solo mining log lines from daemon output
|
||||
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
||||
state_.mining.log_lines = embedded_daemon_->getRecentLines(50);
|
||||
}
|
||||
|
||||
// Check daemon output for rescan progress
|
||||
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
|
||||
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
|
||||
@@ -1935,12 +1940,13 @@ void App::stopEmbeddedDaemon()
|
||||
if (stop_sent) {
|
||||
DEBUG_LOGF("Waiting for daemon to begin shutdown...\n");
|
||||
shutdown_status_ = "Waiting for daemon to begin shutdown...";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
|
||||
// Wait for process to exit; SIGTERM/TerminateProcess as last resort
|
||||
// Wait for process to exit; SIGTERM/TerminateProcess as last resort.
|
||||
// 10 seconds is generous — if the daemon hasn't exited by then it's stuck.
|
||||
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
||||
embedded_daemon_->stop(30000);
|
||||
embedded_daemon_->stop(10000);
|
||||
}
|
||||
|
||||
bool App::isEmbeddedDaemonRunning() const
|
||||
@@ -1972,10 +1978,13 @@ void App::rescanBlockchain()
|
||||
std::thread([this]() {
|
||||
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
|
||||
// Wait for daemon to fully stop
|
||||
DEBUG_LOGF("[App] Waiting for daemon to fully stop...\n");
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
|
||||
// Reset output offset so we parse fresh output for rescan progress
|
||||
daemon_output_offset_ = 0;
|
||||
@@ -2663,8 +2672,10 @@ void App::restartDaemon()
|
||||
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
|
||||
stopEmbeddedDaemon();
|
||||
}
|
||||
if (shutting_down_) { daemon_restarting_ = false; return; }
|
||||
// Brief pause to let the port free up
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
for (int i = 0; i < 5 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
startEmbeddedDaemon();
|
||||
daemon_restarting_ = false;
|
||||
DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n");
|
||||
|
||||
@@ -493,6 +493,7 @@ void App::refreshData()
|
||||
if (tx.contains("category")) info.type = tx["category"].get<std::string>();
|
||||
if (tx.contains("amount")) info.amount = tx["amount"].get<double>();
|
||||
if (tx.contains("time")) info.timestamp = tx["time"].get<int64_t>();
|
||||
else if (tx.contains("timereceived")) info.timestamp = tx["timereceived"].get<int64_t>();
|
||||
if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get<int>();
|
||||
if (tx.contains("address")) info.address = tx["address"].get<std::string>();
|
||||
knownTxids.insert(info.txid);
|
||||
@@ -1408,25 +1409,61 @@ void App::clearBans()
|
||||
|
||||
void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
||||
{
|
||||
if (!state_.connected || !rpc_) return;
|
||||
|
||||
rpc_->z_getNewAddress([this, callback](const json& result) {
|
||||
std::string addr = result.get<std::string>();
|
||||
addresses_dirty_ = true;
|
||||
refreshAddresses();
|
||||
if (callback) callback(addr);
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
|
||||
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
|
||||
std::string addr;
|
||||
try {
|
||||
json result = rpc_->call("z_getnewaddress");
|
||||
addr = result.get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_getnewaddress error: %s\n", e.what());
|
||||
}
|
||||
return [this, callback, addr]() {
|
||||
if (!addr.empty()) {
|
||||
// Inject immediately so UI can select the address next frame
|
||||
AddressInfo info;
|
||||
info.address = addr;
|
||||
info.type = "shielded";
|
||||
info.balance = 0.0;
|
||||
state_.z_addresses.push_back(info);
|
||||
address_list_dirty_ = true;
|
||||
// Also trigger full refresh to get proper balances
|
||||
addresses_dirty_ = true;
|
||||
refreshAddresses();
|
||||
}
|
||||
if (callback) callback(addr);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
||||
{
|
||||
if (!state_.connected || !rpc_) return;
|
||||
|
||||
rpc_->getNewAddress([this, callback](const json& result) {
|
||||
std::string addr = result.get<std::string>();
|
||||
addresses_dirty_ = true;
|
||||
refreshAddresses();
|
||||
if (callback) callback(addr);
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
|
||||
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
|
||||
std::string addr;
|
||||
try {
|
||||
json result = rpc_->call("getnewaddress");
|
||||
addr = result.get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getnewaddress error: %s\n", e.what());
|
||||
}
|
||||
return [this, callback, addr]() {
|
||||
if (!addr.empty()) {
|
||||
// Inject immediately so UI can select the address next frame
|
||||
AddressInfo info;
|
||||
info.address = addr;
|
||||
info.type = "transparent";
|
||||
info.balance = 0.0;
|
||||
state_.t_addresses.push_back(info);
|
||||
address_list_dirty_ = true;
|
||||
// Also trigger full refresh to get proper balances
|
||||
addresses_dirty_ = true;
|
||||
refreshAddresses();
|
||||
}
|
||||
if (callback) callback(addr);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -75,8 +75,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
// Give daemon a moment to shut down, then restart
|
||||
// (do this off the main thread to avoid stalling the UI)
|
||||
std::thread([this]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
for (int i = 0; i < 20 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
// tryConnect will be called by the update loop
|
||||
}).detach();
|
||||
@@ -187,8 +190,11 @@ void App::processDeferredEncryption() {
|
||||
// Restart daemon (it shuts itself down after encryptwallet)
|
||||
if (isUsingEmbeddedDaemon()) {
|
||||
std::thread([this]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
for (int i = 0; i < 20 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
// tryConnect will be called by the update loop
|
||||
}).detach();
|
||||
@@ -1095,7 +1101,8 @@ void App::renderDecryptWalletDialog() {
|
||||
}
|
||||
|
||||
// Wait for daemon to fully stop
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
return [this, exportPath]() {
|
||||
decrypt_step_ = 3;
|
||||
@@ -1127,18 +1134,23 @@ void App::renderDecryptWalletDialog() {
|
||||
decrypt_status_ = "Restarting daemon...";
|
||||
|
||||
auto restartAndImport = [this, exportPath]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
for (int i = 0; i < 20 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
|
||||
if (isUsingEmbeddedDaemon()) {
|
||||
stopEmbeddedDaemon();
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
if (shutting_down_) return;
|
||||
for (int i = 0; i < 10 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
}
|
||||
|
||||
// Wait for daemon to become available
|
||||
int maxWait = 60;
|
||||
bool daemonUp = false;
|
||||
for (int i = 0; i < maxWait; i++) {
|
||||
for (int i = 0; i < maxWait && !shutting_down_; i++) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
try {
|
||||
rpc_->call("getinfo");
|
||||
|
||||
@@ -673,8 +673,13 @@ void App::renderFirstRunWizard() {
|
||||
} else {
|
||||
auto prog = bootstrap_->getProgress();
|
||||
|
||||
const char* statusTitle = (prog.state == util::Bootstrap::State::Downloading)
|
||||
? "Downloading bootstrap..." : "Extracting blockchain data...";
|
||||
const char* statusTitle;
|
||||
if (prog.state == util::Bootstrap::State::Downloading)
|
||||
statusTitle = "Downloading bootstrap...";
|
||||
else if (prog.state == util::Bootstrap::State::Verifying)
|
||||
statusTitle = "Verifying checksums...";
|
||||
else
|
||||
statusTitle = "Extracting blockchain data...";
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
|
||||
cy += bodyFont->LegacySize + 12.0f * dp;
|
||||
|
||||
|
||||
@@ -140,12 +140,24 @@ bool Settings::load(const std::string& path)
|
||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
||||
// Migrate old default pool URL that was missing the stratum port
|
||||
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
||||
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
|
||||
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
||||
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||
saved_pool_urls_.clear();
|
||||
for (const auto& u : j["saved_pool_urls"])
|
||||
if (u.is_string()) saved_pool_urls_.push_back(u.get<std::string>());
|
||||
}
|
||||
if (j.contains("saved_pool_workers") && j["saved_pool_workers"].is_array()) {
|
||||
saved_pool_workers_.clear();
|
||||
for (const auto& w : j["saved_pool_workers"])
|
||||
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
|
||||
}
|
||||
if (j.contains("font_scale") && j["font_scale"].is_number())
|
||||
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
||||
@@ -219,6 +231,12 @@ bool Settings::save(const std::string& path)
|
||||
j["pool_tls"] = pool_tls_;
|
||||
j["pool_hugepages"] = pool_hugepages_;
|
||||
j["pool_mode"] = pool_mode_;
|
||||
j["saved_pool_urls"] = json::array();
|
||||
for (const auto& u : saved_pool_urls_)
|
||||
j["saved_pool_urls"].push_back(u);
|
||||
j["saved_pool_workers"] = json::array();
|
||||
for (const auto& w : saved_pool_workers_)
|
||||
j["saved_pool_workers"].push_back(w);
|
||||
j["font_scale"] = font_scale_;
|
||||
if (window_width_ > 0 && window_height_ > 0) {
|
||||
j["window_width"] = window_width_;
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace config {
|
||||
@@ -206,6 +208,31 @@ public:
|
||||
bool getPoolMode() const { return pool_mode_; }
|
||||
void setPoolMode(bool v) { pool_mode_ = 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) {
|
||||
// Don't add duplicates
|
||||
for (const auto& u : saved_pool_urls_) if (u == url) return;
|
||||
saved_pool_urls_.push_back(url);
|
||||
}
|
||||
void removeSavedPoolUrl(const std::string& url) {
|
||||
saved_pool_urls_.erase(
|
||||
std::remove(saved_pool_urls_.begin(), saved_pool_urls_.end(), url),
|
||||
saved_pool_urls_.end());
|
||||
}
|
||||
|
||||
// Saved pool worker addresses (user-managed favorites dropdown)
|
||||
const std::vector<std::string>& getSavedPoolWorkers() const { return saved_pool_workers_; }
|
||||
void addSavedPoolWorker(const std::string& addr) {
|
||||
for (const auto& a : saved_pool_workers_) if (a == addr) return;
|
||||
saved_pool_workers_.push_back(addr);
|
||||
}
|
||||
void removeSavedPoolWorker(const std::string& addr) {
|
||||
saved_pool_workers_.erase(
|
||||
std::remove(saved_pool_workers_.begin(), saved_pool_workers_.end(), addr),
|
||||
saved_pool_workers_.end());
|
||||
}
|
||||
|
||||
// Font scale (user accessibility setting, 1.0–1.5)
|
||||
float getFontScale() const { return font_scale_; }
|
||||
void setFontScale(float v) { font_scale_ = std::max(1.0f, std::min(1.5f, v)); }
|
||||
@@ -255,13 +282,15 @@ private:
|
||||
std::string selected_pair_ = "DRGX/BTC";
|
||||
|
||||
// Pool mining
|
||||
std::string pool_url_ = "pool.dragonx.is";
|
||||
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||
std::string pool_algo_ = "rx/hush";
|
||||
std::string pool_worker_ = "x";
|
||||
int pool_threads_ = 0;
|
||||
bool pool_tls_ = false;
|
||||
bool pool_hugepages_ = true;
|
||||
bool pool_mode_ = false; // false=solo, true=pool
|
||||
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
|
||||
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||
|
||||
// Font scale (user accessibility, 1.0–3.0; 1.0 = default)
|
||||
float font_scale_ = 1.0f;
|
||||
|
||||
@@ -154,7 +154,7 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
#ifndef _WIN32
|
||||
"-printtoconsole",
|
||||
#endif
|
||||
"-clientname=DragonXImGui",
|
||||
"-clientname=ObsidianDragon",
|
||||
"-ac_name=DRAGONX",
|
||||
"-ac_algo=randomx",
|
||||
"-ac_halving=3500000",
|
||||
|
||||
@@ -101,6 +101,9 @@ struct MiningInfo {
|
||||
// History for chart
|
||||
std::vector<double> hashrate_history; // Last N samples
|
||||
static constexpr int MAX_HISTORY = 300; // 5 minutes at 1s intervals
|
||||
|
||||
// Recent daemon log lines for the mining log panel
|
||||
std::vector<std::string> log_lines;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
75
src/main.cpp
75
src/main.cpp
@@ -46,6 +46,15 @@
|
||||
#include "platform/windows_backdrop.h"
|
||||
#include <windows.h>
|
||||
#include <dwmapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <propkey.h>
|
||||
#include <propsys.h>
|
||||
// SetCurrentProcessExplicitAppUserModelID lives behind NTDDI_WIN7 in
|
||||
// MinGW's shobjidl.h. Rather than forcing the version macro globally,
|
||||
// declare just the one function we need (available on Windows 7+).
|
||||
extern "C" HRESULT __stdcall SetCurrentProcessExplicitAppUserModelID(PCWSTR AppID);
|
||||
// SHGetPropertyStoreForWindow is also behind NTDDI_WIN7 in MinGW headers.
|
||||
extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, void** ppv);
|
||||
// Not defined in older MinGW SDK headers
|
||||
#ifndef WS_EX_NOREDIRECTIONBITMAP
|
||||
#define WS_EX_NOREDIRECTIONBITMAP 0x00200000L
|
||||
@@ -107,6 +116,45 @@ static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
|
||||
} catch (...) {}
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
// Set the window's shell property store so Task Manager, taskbar, and shell
|
||||
// always show "ObsidianDragon" regardless of any cached VERSIONINFO metadata.
|
||||
static void SetWindowIdentity(HWND hwnd)
|
||||
{
|
||||
IPropertyStore* pps = nullptr;
|
||||
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void**)&pps);
|
||||
if (SUCCEEDED(hr) && pps) {
|
||||
// Set AppUserModel.ID on the window (overrides process-level ID for this window)
|
||||
PROPVARIANT pvId;
|
||||
PropVariantInit(&pvId);
|
||||
pvId.vt = VT_LPWSTR;
|
||||
pvId.pwszVal = const_cast<LPWSTR>(L"DragonX.ObsidianDragon.Wallet");
|
||||
pps->SetValue(PKEY_AppUserModel_ID, pvId);
|
||||
// Don't PropVariantClear — the string is a static literal
|
||||
|
||||
// Set RelaunchDisplayNameResource so the shell shows our name
|
||||
PROPVARIANT pvName;
|
||||
PropVariantInit(&pvName);
|
||||
pvName.vt = VT_LPWSTR;
|
||||
pvName.pwszVal = const_cast<LPWSTR>(L"ObsidianDragon");
|
||||
pps->SetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, pvName);
|
||||
|
||||
// Set RelaunchCommand (required alongside RelaunchDisplayNameResource)
|
||||
PROPVARIANT pvCmd;
|
||||
PropVariantInit(&pvCmd);
|
||||
pvCmd.vt = VT_LPWSTR;
|
||||
wchar_t exePath[MAX_PATH] = {};
|
||||
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
|
||||
pvCmd.pwszVal = exePath;
|
||||
pps->SetValue(PKEY_AppUserModel_RelaunchCommand, pvCmd);
|
||||
|
||||
pps->Commit();
|
||||
pps->Release();
|
||||
DEBUG_LOGF("Window property store: identity set to ObsidianDragon\n");
|
||||
} else {
|
||||
DEBUG_LOGF("SHGetPropertyStoreForWindow failed: 0x%08lx\n", (unsigned long)hr);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -353,6 +401,11 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
// Install crash handler for diagnostics
|
||||
SetUnhandledExceptionFilter(CrashHandler);
|
||||
|
||||
// Set the Application User Model ID so Windows Task Manager, taskbar,
|
||||
// and jump lists show "ObsidianDragon" instead of inheriting a
|
||||
// description from the MinGW runtime ("POSIX WinThreads for Windows").
|
||||
SetCurrentProcessExplicitAppUserModelID(L"DragonX.ObsidianDragon.Wallet");
|
||||
#endif
|
||||
|
||||
// Check for payment URI in command line
|
||||
@@ -499,6 +552,10 @@ int main(int argc, char* argv[])
|
||||
UpdateWindow(nativeHwnd);
|
||||
DEBUG_LOGF("Borderless window: native title bar removed\n");
|
||||
|
||||
// Set shell property store on the HWND so Task Manager and the taskbar
|
||||
// always show "ObsidianDragon" (overrides any cached metadata).
|
||||
SetWindowIdentity(nativeHwnd);
|
||||
|
||||
// Initialize DirectX 11 context with DXGI alpha swap chain
|
||||
dragonx::platform::DX11Context dx;
|
||||
if (!dx.init(window)) {
|
||||
@@ -1719,6 +1776,10 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the window immediately so the user perceives the app as closed
|
||||
// while background cleanup (thread joins, RPC disconnect) continues.
|
||||
SDL_HideWindow(window);
|
||||
|
||||
// Final cleanup (daemon already stopped by beginShutdown)
|
||||
app.shutdown();
|
||||
#ifdef DRAGONX_USE_DX11
|
||||
@@ -1727,6 +1788,20 @@ int main(int argc, char* argv[])
|
||||
Shutdown(window, gl_context);
|
||||
#endif
|
||||
|
||||
// Explicitly release the single-instance lock before exit so a new
|
||||
// instance can start immediately.
|
||||
g_single_instance.unlock();
|
||||
|
||||
// Force-terminate the process. All important cleanup (daemon stop,
|
||||
// settings save, RPC disconnect, SDL teardown) has completed above.
|
||||
// On Windows with mingw-w64 POSIX threads, normal CRT cleanup
|
||||
// deadlocks waiting for detached pthreads. On Linux, static
|
||||
// destructors and atexit handlers can also block. _Exit() bypasses
|
||||
// all of that.
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
_Exit(0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -320,15 +320,13 @@ void RenderMarketTab(App* app)
|
||||
points[i] = ImVec2(x, y);
|
||||
}
|
||||
|
||||
// Fill under curve
|
||||
for (size_t i = 0; i + 1 < n; i++) {
|
||||
ImVec2 quad[4] = {
|
||||
points[i],
|
||||
points[i + 1],
|
||||
ImVec2(points[i + 1].x, plotBottom),
|
||||
ImVec2(points[i].x, plotBottom)
|
||||
};
|
||||
dl->AddConvexPolyFilled(quad, 4, fillCol);
|
||||
// Fill under curve (single concave polygon to avoid AA seam artifacts)
|
||||
if (n >= 2) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
dl->PathLineTo(points[i]);
|
||||
dl->PathLineTo(ImVec2(points[n - 1].x, plotBottom));
|
||||
dl->PathLineTo(ImVec2(points[0].x, plotBottom));
|
||||
dl->PathFillConcave(fillCol);
|
||||
}
|
||||
|
||||
// Line
|
||||
|
||||
@@ -40,11 +40,12 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
|
||||
|
||||
// Pool mode state
|
||||
static bool s_pool_mode = false;
|
||||
static char s_pool_url[256] = "pool.dragonx.is";
|
||||
static char s_pool_url[256] = "pool.dragonx.is:3433";
|
||||
static char s_pool_worker[256] = "x";
|
||||
static bool s_pool_settings_dirty = false;
|
||||
static bool s_pool_state_loaded = false;
|
||||
static bool s_show_pool_log = false; // Toggle: false=chart, true=log
|
||||
static bool s_show_solo_log = false; // Toggle: false=chart, true=log (solo mode)
|
||||
|
||||
// Get max threads based on hardware
|
||||
static int GetMaxMiningThreads()
|
||||
@@ -175,12 +176,21 @@ void RenderMiningTab(App* app)
|
||||
app->settings()->setPoolWorker(s_pool_worker);
|
||||
app->settings()->save();
|
||||
s_pool_settings_dirty = false;
|
||||
|
||||
// Auto-restart pool miner if it is currently running so the new
|
||||
// URL / worker address takes effect immediately.
|
||||
if (state.pool_mining.xmrig_running) {
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(s_selected_threads);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine active mining state for UI
|
||||
// Include pool mining running state even when user just switched to solo,
|
||||
// so the button shows STOP/STOPPING while xmrig shuts down.
|
||||
bool isMiningActive = s_pool_mode
|
||||
? state.pool_mining.xmrig_running
|
||||
: mining.generate;
|
||||
: (mining.generate || state.pool_mining.xmrig_running);
|
||||
|
||||
// ================================================================
|
||||
// Proportional section budget — ensures all content fits without
|
||||
@@ -310,20 +320,193 @@ void RenderMiningTab(App* app)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp));
|
||||
|
||||
// Calculate remaining width from inputs start to end of content region
|
||||
float inputFrameH2 = ImGui::GetFrameHeight();
|
||||
float resetBtnW = inputFrameH2; // Square button matching input height
|
||||
float iconBtnW = inputFrameH2;
|
||||
float resetBtnW = iconBtnW;
|
||||
float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
|
||||
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - Layout::spacingSm();
|
||||
// Each input group: [input][▼][bookmark]
|
||||
// Layout: [URL group] [spacing] [Worker group] [spacing] [reset]
|
||||
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
|
||||
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
|
||||
- Layout::spacingSm() - perGroupExtra * 2;
|
||||
float urlW = std::max(60.0f, remainW * 0.30f);
|
||||
float wrkW = std::max(40.0f, remainW * 0.70f);
|
||||
|
||||
// Track positions for popup alignment
|
||||
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||
float urlGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||
float urlGroupW = urlW + perGroupExtra;
|
||||
|
||||
// === Pool URL input ===
|
||||
ImGui::SetNextItemWidth(urlW);
|
||||
if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) {
|
||||
s_pool_settings_dirty = true;
|
||||
}
|
||||
|
||||
// --- URL: Dropdown arrow button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##PoolDropdown", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Saved pools");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
OnSurfaceMedium(), dropIcon);
|
||||
if (btnClk) {
|
||||
ImGui::OpenPopup("##SavedPoolsPopup");
|
||||
}
|
||||
}
|
||||
|
||||
// --- URL: Bookmark button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##SavePoolUrl", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
std::string currentUrl(s_pool_url);
|
||||
bool alreadySaved = false;
|
||||
for (const auto& u : app->settings()->getSavedPoolUrls()) {
|
||||
if (u == currentUrl) { alreadySaved = true; break; }
|
||||
}
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save pool URL");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
||||
if (btnClk && !currentUrl.empty() && !alreadySaved) {
|
||||
app->settings()->addSavedPoolUrl(currentUrl);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
|
||||
// --- URL: Popup positioned below the input group ---
|
||||
// Match popup width to input group; zero horizontal padding so
|
||||
// item highlights are flush with the popup container edges.
|
||||
ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2));
|
||||
ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
if (ImGui::BeginPopup("##SavedPoolsPopup")) {
|
||||
const auto& savedUrls = app->settings()->getSavedPoolUrls();
|
||||
if (savedUrls.empty()) {
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("No saved pools");
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
std::string urlToRemove;
|
||||
float popupInnerW = ImGui::GetContentRegionAvail().x;
|
||||
float xZoneW = ImGui::GetFrameHeight();
|
||||
float textPadX = 8 * dp;
|
||||
ImFont* rowFont = ImGui::GetFont();
|
||||
float rowFontSz = ImGui::GetFontSize();
|
||||
float rowH = ImGui::GetFrameHeight();
|
||||
for (const auto& url : savedUrls) {
|
||||
ImGui::PushID(url.c_str());
|
||||
bool isCurrent = (std::string(s_pool_url) == url);
|
||||
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
|
||||
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
|
||||
bool rowHov = ImGui::IsItemHovered();
|
||||
bool rowClk = ImGui::IsItemClicked();
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW;
|
||||
// Row background — flush with popup edges
|
||||
if (isCurrent)
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Item text with internal padding
|
||||
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
|
||||
pdl->AddText(rowFont, rowFontSz,
|
||||
ImVec2(rowMin.x + textPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), url.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
|
||||
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Remove");
|
||||
} else if (rowHov) {
|
||||
// Show faint X when row is hovered
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), xIcon);
|
||||
}
|
||||
// Always draw icon when hovering X zone
|
||||
if (inXZone) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
Error(), xIcon);
|
||||
}
|
||||
}
|
||||
// Click handling
|
||||
if (rowClk) {
|
||||
if (inXZone) {
|
||||
urlToRemove = url;
|
||||
} else {
|
||||
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
|
||||
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!urlToRemove.empty()) {
|
||||
app->settings()->removeSavedPoolUrl(urlToRemove);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||
float wrkGroupW = wrkW + perGroupExtra;
|
||||
|
||||
ImGui::SetNextItemWidth(wrkW);
|
||||
if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) {
|
||||
s_pool_settings_dirty = true;
|
||||
@@ -332,7 +515,169 @@ void RenderMiningTab(App* app)
|
||||
ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts");
|
||||
}
|
||||
|
||||
// Reset to defaults button (matching input height)
|
||||
// --- Worker: Dropdown arrow button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##WorkerDropdown", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Saved addresses");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
OnSurfaceMedium(), dropIcon);
|
||||
if (btnClk) {
|
||||
ImGui::OpenPopup("##SavedWorkersPopup");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Worker: Bookmark button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##SaveWorkerAddr", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
std::string currentWorker(s_pool_worker);
|
||||
bool alreadySaved = false;
|
||||
for (const auto& w : app->settings()->getSavedPoolWorkers()) {
|
||||
if (w == currentWorker) { alreadySaved = true; break; }
|
||||
}
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save payout address");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
||||
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
|
||||
app->settings()->addSavedPoolWorker(currentWorker);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Worker: Popup positioned below the input group ---
|
||||
// Popup sized to fit full z-addresses without truncation;
|
||||
// zero horizontal padding so item highlights are flush with edges.
|
||||
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
|
||||
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
|
||||
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
if (ImGui::BeginPopup("##SavedWorkersPopup")) {
|
||||
const auto& savedWorkers = app->settings()->getSavedPoolWorkers();
|
||||
if (savedWorkers.empty()) {
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("No saved addresses");
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
std::string addrToRemove;
|
||||
float wPopupInnerW = ImGui::GetContentRegionAvail().x;
|
||||
float wXZoneW = ImGui::GetFrameHeight();
|
||||
float wTextPadX = 8 * dp;
|
||||
ImFont* wRowFont = ImGui::GetFont();
|
||||
float wRowFontSz = ImGui::GetFontSize();
|
||||
float wRowH = ImGui::GetFrameHeight();
|
||||
for (const auto& addr : savedWorkers) {
|
||||
ImGui::PushID(addr.c_str());
|
||||
bool isCurrent = (std::string(s_pool_worker) == addr);
|
||||
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
|
||||
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
|
||||
bool rowHov = ImGui::IsItemHovered();
|
||||
bool rowClk = ImGui::IsItemClicked();
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW;
|
||||
// Row background — flush with popup edges
|
||||
if (isCurrent)
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Full address text with internal padding
|
||||
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
|
||||
pdl->AddText(wRowFont, wRowFontSz,
|
||||
ImVec2(rowMin.x + wTextPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
||||
// Tooltip for long addresses
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetTooltip("%s", addr.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
|
||||
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Remove");
|
||||
} else if (rowHov) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), xIcon);
|
||||
}
|
||||
if (inXZone) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
Error(), xIcon);
|
||||
}
|
||||
}
|
||||
// Click handling
|
||||
if (rowClk) {
|
||||
if (inXZone) {
|
||||
addrToRemove = addr;
|
||||
} else {
|
||||
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!addrToRemove.empty()) {
|
||||
app->settings()->removeSavedPoolWorker(addrToRemove);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup
|
||||
|
||||
// === Reset to defaults button ===
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
@@ -612,9 +957,12 @@ void RenderMiningTab(App* app)
|
||||
bool isToggling = app->isMiningToggleInProgress();
|
||||
// Pool mining connects to an external pool via xmrig — it does not
|
||||
// need the local blockchain synced or even the daemon connected.
|
||||
// If pool mining is still shutting down after switching to solo,
|
||||
// keep the button enabled so user can stop it.
|
||||
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
|
||||
bool disabled = s_pool_mode
|
||||
? (isToggling || poolBlockedBySolo)
|
||||
: (!app->isConnected() || isToggling || isSyncing);
|
||||
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
|
||||
|
||||
// Glass panel background with state-dependent tint
|
||||
GlassPanelSpec btnGlass;
|
||||
@@ -755,7 +1103,11 @@ void RenderMiningTab(App* app)
|
||||
else
|
||||
app->startPoolMining(s_selected_threads);
|
||||
} else {
|
||||
if (mining.generate)
|
||||
// If pool mining is still running (user just switched from pool to solo),
|
||||
// stop pool mining first
|
||||
if (state.pool_mining.xmrig_running)
|
||||
app->stopPoolMining();
|
||||
else if (mining.generate)
|
||||
app->stopMining();
|
||||
else
|
||||
app->startMining(s_selected_threads);
|
||||
@@ -776,8 +1128,12 @@ void RenderMiningTab(App* app)
|
||||
ImU32 greenCol = Success();
|
||||
|
||||
// Determine view mode first
|
||||
bool showLogView = s_pool_mode && s_show_pool_log && !state.pool_mining.log_lines.empty();
|
||||
bool hasLogContent = s_pool_mode && !state.pool_mining.log_lines.empty();
|
||||
bool showLogView = s_pool_mode
|
||||
? (s_show_pool_log && !state.pool_mining.log_lines.empty())
|
||||
: (s_show_solo_log && !mining.log_lines.empty());
|
||||
bool hasLogContent = s_pool_mode
|
||||
? !state.pool_mining.log_lines.empty()
|
||||
: !mining.log_lines.empty();
|
||||
// Use pool hashrate history when in pool mode, solo otherwise
|
||||
const std::vector<double>& chartHistory = s_pool_mode
|
||||
? state.pool_mining.hashrate_history
|
||||
@@ -792,60 +1148,44 @@ void RenderMiningTab(App* app)
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// --- Toggle button in top-right corner (pool mode only) ---
|
||||
if (s_pool_mode && (hasLogContent || hasChartContent)) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* toggleIcon = s_show_pool_log ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
||||
const char* toggleTip = s_show_pool_log ? "Show hashrate chart" : "Show miner log";
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
|
||||
float btnSize = iconSz.y + 8 * dp;
|
||||
float btnX = cardMax.x - pad - btnSize;
|
||||
float btnY = cardMin.y + pad * 0.5f;
|
||||
ImVec2 btnMin(btnX, btnY);
|
||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
||||
|
||||
bool hov = IsRectHovered(btnMin, btnMax);
|
||||
if (hov) {
|
||||
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", toggleTip);
|
||||
}
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||
OnSurfaceMedium(), toggleIcon);
|
||||
if (hov && ImGui::IsMouseClicked(0)) {
|
||||
s_show_pool_log = !s_show_pool_log;
|
||||
}
|
||||
}
|
||||
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
|
||||
|
||||
if (showLogView) {
|
||||
// --- Full-card log view ---
|
||||
// --- Full-card log view (selectable + copyable) ---
|
||||
const std::vector<std::string>& logLines = s_pool_mode
|
||||
? state.pool_mining.log_lines
|
||||
: mining.log_lines;
|
||||
|
||||
// Build a single string buffer for InputTextMultiline
|
||||
static std::string s_log_buf;
|
||||
s_log_buf.clear();
|
||||
for (const auto& line : logLines) {
|
||||
if (!line.empty()) {
|
||||
s_log_buf += line;
|
||||
s_log_buf += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
float logPad = pad * 0.5f;
|
||||
float logW = availWidth - logPad * 2;
|
||||
float logH = totalCardH - logPad * 2;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
|
||||
ImGui::BeginChild("##PoolLogInCard", ImVec2(availWidth - logPad * 2, totalCardH - logPad * 2), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_HorizontalScrollbar);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
|
||||
ImFont* monoFont = Type().body2();
|
||||
ImGui::PushFont(monoFont);
|
||||
for (const auto& line : state.pool_mining.log_lines) {
|
||||
if (!line.empty())
|
||||
ImGui::TextUnformatted(line.c_str());
|
||||
}
|
||||
|
||||
const char* inputId = s_pool_mode ? "##PoolLogText" : "##SoloLogText";
|
||||
ImGui::InputTextMultiline(inputId,
|
||||
const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
|
||||
ImVec2(logW, logH),
|
||||
ImGuiInputTextFlags_ReadOnly);
|
||||
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
// Auto-scroll to bottom only if user is already near the bottom
|
||||
// This allows manual scrolling up to read history
|
||||
float scrollY = ImGui::GetScrollY();
|
||||
float scrollMaxY = ImGui::GetScrollMaxY();
|
||||
if (scrollMaxY <= 0.0f || scrollY >= scrollMaxY - 20.0f * dp)
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
// Reset cursor to end of card after the child window
|
||||
// Reset cursor to end of card after the input
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system
|
||||
} else {
|
||||
@@ -1075,6 +1415,35 @@ void RenderMiningTab(App* app)
|
||||
ImGui::Dummy(ImVec2(availWidth, totalCardH));
|
||||
}
|
||||
|
||||
// --- Toggle button in top-right corner ---
|
||||
// Rendered after content so the Hand cursor takes priority over
|
||||
// the InputTextMultiline text-cursor when hovering the button.
|
||||
if (hasLogContent || hasChartContent) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
||||
const char* toggleTip = showLogFlag ? "Show hashrate chart" : "Show mining log";
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
|
||||
float btnSize = iconSz.y + 8 * dp;
|
||||
float btnX = cardMax.x - pad - btnSize;
|
||||
float btnY = cardMin.y + pad * 0.5f;
|
||||
ImVec2 btnMin(btnX, btnY);
|
||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
||||
|
||||
bool hov = IsRectHovered(btnMin, btnMax);
|
||||
if (hov) {
|
||||
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", toggleTip);
|
||||
}
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||
OnSurfaceMedium(), toggleIcon);
|
||||
if (hov && ImGui::IsMouseClicked(0)) {
|
||||
showLogFlag = !showLogFlag;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
@@ -1106,11 +1475,17 @@ void RenderMiningTab(App* app)
|
||||
double amount;
|
||||
int confirmations;
|
||||
bool mature;
|
||||
std::string txid;
|
||||
bool isPoolPayout;
|
||||
};
|
||||
std::vector<MinedTx> recentMined;
|
||||
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") {
|
||||
bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined");
|
||||
bool isPoolPayout = (tx.type == "receive"
|
||||
&& !tx.memo.empty()
|
||||
&& tx.memo.find("Mining Pool payout") != std::string::npos);
|
||||
if (isSoloMined || isPoolPayout) {
|
||||
double amt = std::abs(tx.amount);
|
||||
minedAllTime += amt;
|
||||
minedAllTimeCount++;
|
||||
@@ -1121,8 +1496,10 @@ void RenderMiningTab(App* app)
|
||||
minedYesterday += amt;
|
||||
minedYesterdayCount++;
|
||||
}
|
||||
if (recentMined.size() < 4) {
|
||||
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100});
|
||||
// Separate solo blocks from pool payouts based on current mode
|
||||
bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined;
|
||||
if (showInCurrentMode && recentMined.size() < 4) {
|
||||
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1164,18 +1541,18 @@ void RenderMiningTab(App* app)
|
||||
};
|
||||
|
||||
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
|
||||
snprintf(subBuf2, sizeof(subBuf2), "(%d blk)", minedTodayCount);
|
||||
snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount);
|
||||
char todayVal[64], todaySub[64];
|
||||
strncpy(todayVal, valBuf, sizeof(todayVal));
|
||||
strncpy(todaySub, subBuf2, sizeof(todaySub));
|
||||
|
||||
char yesterdayVal[64], yesterdaySub[64];
|
||||
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
|
||||
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d blk)", minedYesterdayCount);
|
||||
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount);
|
||||
|
||||
char allVal[64], allSub[64];
|
||||
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
|
||||
snprintf(allSub, sizeof(allSub), "(%d blk)", minedAllTimeCount);
|
||||
snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount);
|
||||
|
||||
char estVal[64];
|
||||
if (estActive)
|
||||
@@ -1500,16 +1877,18 @@ void RenderMiningTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ============================================================
|
||||
// RECENT BLOCKS — last 4 mined blocks
|
||||
// RECENT BLOCKS — last 4 mined blocks (always shown in pool mode)
|
||||
// ============================================================
|
||||
if (!recentMined.empty()) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT BLOCKS");
|
||||
if (!recentMined.empty() || s_pool_mode) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
|
||||
s_pool_mode ? "RECENT POOL PAYOUTS" : "RECENT BLOCKS");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
|
||||
// Size to remaining space — proportional budget ensures fit
|
||||
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
|
||||
float contentH_blocks = rowH_blocks * (float)recentMined.size() + pad * 2.5f;
|
||||
float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size();
|
||||
float contentH_blocks = rowH_blocks * minRows + pad * 2.5f;
|
||||
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
|
||||
|
||||
// Glass panel wrapping the list + scroll-edge mask state
|
||||
@@ -1533,6 +1912,26 @@ void RenderMiningTab(App* app)
|
||||
// Top padding inside glass card
|
||||
ImGui::Dummy(ImVec2(0, pad * 0.5f));
|
||||
|
||||
if (recentMined.empty()) {
|
||||
// Empty state — card is visible but no rows yet
|
||||
float emptyY = ImGui::GetCursorScreenPos().y;
|
||||
float emptyX = ImGui::GetCursorScreenPos().x;
|
||||
float centerX = emptyX + availWidth * 0.5f;
|
||||
ImFont* icoFont = Type().iconMed();
|
||||
const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY;
|
||||
ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon);
|
||||
miningChildDL->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(centerX - iSz.x * 0.5f, emptyY),
|
||||
OnSurfaceDisabled(), emptyIcon);
|
||||
const char* emptyMsg = s_pool_mode
|
||||
? "No pool payouts yet"
|
||||
: "No blocks found yet";
|
||||
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg);
|
||||
miningChildDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), emptyMsg);
|
||||
}
|
||||
|
||||
for (size_t mi = 0; mi < recentMined.size(); mi++) {
|
||||
const auto& mtx = recentMined[mi];
|
||||
|
||||
@@ -1542,10 +1941,14 @@ void RenderMiningTab(App* app)
|
||||
|
||||
// Subtle background on hover (inset from card edges)
|
||||
bool hovered = material::IsRectHovered(rMin, rMax);
|
||||
bool isClickable = !mtx.txid.empty();
|
||||
if (hovered) {
|
||||
dl->AddRectFilled(ImVec2(rMin.x + pad * 0.5f, rMin.y),
|
||||
ImVec2(rMax.x - pad * 0.5f, rMax.y),
|
||||
IM_COL32(255, 255, 255, 8), 3.0f * dp);
|
||||
if (isClickable) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
}
|
||||
|
||||
float rx = rMin.x + pad;
|
||||
@@ -1588,7 +1991,18 @@ void RenderMiningTab(App* app)
|
||||
WithAlpha(Warning(), 200), buf);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(availWidth, rH));
|
||||
// Click to open in block explorer
|
||||
ImGui::SetCursorScreenPos(rMin);
|
||||
char blockBtnId[32];
|
||||
snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi);
|
||||
ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH));
|
||||
if (ImGui::IsItemClicked() && !mtx.txid.empty()) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + mtx.txid;
|
||||
dragonx::util::Platform::openUrl(url);
|
||||
}
|
||||
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
|
||||
ImGui::SetTooltip("Open in explorer");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild(); // ##RecentBlocks
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -45,6 +46,22 @@ static std::string ExtractIP(const std::string& addr)
|
||||
return ip;
|
||||
}
|
||||
|
||||
// Known seed/addnode IPs for the DragonX network.
|
||||
// These are the official seed nodes that the daemon connects to on startup.
|
||||
static bool IsSeedNode(const std::string& addr) {
|
||||
static const std::unordered_set<std::string> seeds = {
|
||||
"176.126.87.241", // embedded daemon -addnode
|
||||
"94.72.112.24", // node1.hush.is
|
||||
"37.60.252.160", // node2.hush.is
|
||||
"176.57.70.185", // node3.hush.is / node6.hush.is
|
||||
"185.213.209.89", // node4.hush.is
|
||||
"137.74.4.198", // node5.hush.is
|
||||
"18.193.113.121", // node7.hush.is
|
||||
"38.60.224.94", // node8.hush.is
|
||||
};
|
||||
return seeds.count(ExtractIP(addr)) > 0;
|
||||
}
|
||||
|
||||
void RenderPeersTab(App* app)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
@@ -149,10 +166,28 @@ void RenderPeersTab(App* app)
|
||||
// Blocks
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks");
|
||||
int blocks = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||
int blocks = state.sync.blocks;
|
||||
if (blocks > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", blocks);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
int blocksLeft = state.sync.headers - blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
if (blocksLeft > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
// Draw block number in normal color
|
||||
char blockStr[32];
|
||||
snprintf(blockStr, sizeof(blockStr), "%d ", blocks);
|
||||
ImVec2 numSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, blockStr);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr);
|
||||
// Draw "(X left)" in warning color
|
||||
char leftStr[32];
|
||||
snprintf(leftStr, sizeof(leftStr), "(%d left)", blocksLeft);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
||||
Warning(), leftStr);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%d", blocks);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
}
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
@@ -680,7 +715,16 @@ void RenderPeersTab(App* app)
|
||||
float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
|
||||
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
|
||||
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy), OnSurface(), peer.addr.c_str());
|
||||
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
|
||||
|
||||
// Seed node icon — rendered right after the IP address
|
||||
if (IsSeedNode(peer.addr)) {
|
||||
ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
|
||||
}
|
||||
|
||||
{
|
||||
const char* dirLabel = peer.inbound ? "In" : "Out";
|
||||
|
||||
@@ -393,9 +393,12 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp descending (same as raw list)
|
||||
// Sort: pending (0-conf) transactions first, then by timestamp descending
|
||||
std::sort(display_txns.begin(), display_txns.end(),
|
||||
[](const DisplayTx& a, const DisplayTx& b) {
|
||||
bool aPending = (a.confirmations == 0);
|
||||
bool bPending = (b.confirmations == 0);
|
||||
if (aPending != bPending) return aPending;
|
||||
return a.timestamp > b.timestamp;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ std::string Bootstrap::computeSHA256(const std::string& filePath) {
|
||||
snprintf(msg, sizeof(msg), "Verifying SHA-256... %.0f%% (%s / %s)",
|
||||
pct, formatSize((double)processed).c_str(),
|
||||
formatSize((double)fileSize).c_str());
|
||||
setProgress(State::Downloading, msg, (double)processed, (double)fileSize);
|
||||
setProgress(State::Verifying, msg, (double)processed, (double)fileSize);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
@@ -643,7 +643,7 @@ std::string Bootstrap::computeMD5(const std::string& filePath) {
|
||||
snprintf(msg, sizeof(msg), "Verifying MD5... %.0f%% (%s / %s)",
|
||||
pct, formatSize((double)processed).c_str(),
|
||||
formatSize((double)fileSize).c_str());
|
||||
setProgress(State::Downloading, msg, (double)processed, (double)fileSize);
|
||||
setProgress(State::Verifying, msg, (double)processed, (double)fileSize);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
@@ -663,7 +663,7 @@ std::string Bootstrap::computeMD5(const std::string& filePath) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& baseUrl) {
|
||||
setProgress(State::Downloading, "Downloading checksums...");
|
||||
setProgress(State::Verifying, "Downloading checksums...");
|
||||
|
||||
std::string sha256Url = baseUrl + "/" + kZipName + ".sha256";
|
||||
std::string md5Url = baseUrl + "/" + kZipName + ".md5";
|
||||
@@ -684,7 +684,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
|
||||
|
||||
// --- SHA-256 ---
|
||||
if (haveSHA256) {
|
||||
setProgress(State::Downloading, "Verifying SHA-256...");
|
||||
setProgress(State::Verifying, "Verifying SHA-256...");
|
||||
std::string expected = parseChecksumFile(sha256Content);
|
||||
std::string actual = computeSHA256(zipPath);
|
||||
|
||||
@@ -712,7 +712,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
|
||||
|
||||
// --- MD5 ---
|
||||
if (haveMD5) {
|
||||
setProgress(State::Downloading, "Verifying MD5...");
|
||||
setProgress(State::Verifying, "Verifying MD5...");
|
||||
std::string expected = parseChecksumFile(md5Content);
|
||||
std::string actual = computeMD5(zipPath);
|
||||
|
||||
@@ -738,7 +738,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
|
||||
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str());
|
||||
}
|
||||
|
||||
setProgress(State::Downloading, "Checksums verified \xe2\x9c\x93");
|
||||
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
enum class State {
|
||||
Idle,
|
||||
Downloading,
|
||||
Verifying,
|
||||
Extracting,
|
||||
Completed,
|
||||
Failed
|
||||
|
||||
Reference in New Issue
Block a user