diff --git a/CMakeLists.txt b/CMakeLists.txt index aa170b0..c8af3a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/build.sh b/build.sh index e1a571a..9dafad7 100755 --- a/build.sh +++ b/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" < + + + + + + ObsidianDragon Wallet + + + + + + + + + + + + true/pm + PerMonitorV2,PerMonitor + UTF-8 + + + + + + + + + + + + + + + + + + diff --git a/res/ObsidianDragon.rc b/res/ObsidianDragon.rc index e7670e2..4d3a586 100644 --- a/res/ObsidianDragon.rc +++ b/res/ObsidianDragon.rc @@ -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 diff --git a/res/themes/ui.toml b/res/themes/ui.toml index 7386959..314a1e5 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -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 } diff --git a/src/app.cpp b/src/app.cpp index 3c2ba6a..82bba8b 100644 --- a/src/app.cpp +++ b/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"); diff --git a/src/app_network.cpp b/src/app_network.cpp index b4d19dd..400a00a 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -493,6 +493,7 @@ void App::refreshData() if (tx.contains("category")) info.type = tx["category"].get(); if (tx.contains("amount")) info.amount = tx["amount"].get(); if (tx.contains("time")) info.timestamp = tx["time"].get(); + else if (tx.contains("timereceived")) info.timestamp = tx["timereceived"].get(); if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get(); if (tx.contains("address")) info.address = tx["address"].get(); knownTxids.insert(info.txid); @@ -1408,25 +1409,61 @@ void App::clearBans() void App::createNewZAddress(std::function callback) { - if (!state_.connected || !rpc_) return; - - rpc_->z_getNewAddress([this, callback](const json& result) { - std::string addr = result.get(); - 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(); + } 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 callback) { - if (!state_.connected || !rpc_) return; - - rpc_->getNewAddress([this, callback](const json& result) { - std::string addr = result.get(); - 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(); + } 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); + }; }); } diff --git a/src/app_security.cpp b/src/app_security.cpp index d4f9a8a..f5bbaf1 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -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"); diff --git a/src/app_wizard.cpp b/src/app_wizard.cpp index e376426..748e6f1 100644 --- a/src/app_wizard.cpp +++ b/src/app_wizard.cpp @@ -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; diff --git a/src/config/settings.cpp b/src/config/settings.cpp index a265d9e..f3529d2 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -140,12 +140,24 @@ bool Settings::load(const std::string& path) if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get(); if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get(); if (j.contains("pool_url")) pool_url_ = j["pool_url"].get(); + // 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(); if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get(); if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get(); if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get(); if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get(); if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get(); + 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()); + } + 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()); + } if (j.contains("font_scale") && j["font_scale"].is_number()) font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get())); 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_; diff --git a/src/config/settings.h b/src/config/settings.h index 60141d5..9e223b1 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -4,8 +4,10 @@ #pragma once +#include #include #include +#include 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& 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& 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 saved_pool_urls_; // user-saved pool URL favorites + std::vector saved_pool_workers_; // user-saved worker address favorites // Font scale (user accessibility, 1.0–3.0; 1.0 = default) float font_scale_ = 1.0f; diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index b9eba0e..e85401a 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -154,7 +154,7 @@ std::vector EmbeddedDaemon::getChainParams() #ifndef _WIN32 "-printtoconsole", #endif - "-clientname=DragonXImGui", + "-clientname=ObsidianDragon", "-ac_name=DRAGONX", "-ac_algo=randomx", "-ac_halving=3500000", diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 9ce25da..73903d5 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -101,6 +101,9 @@ struct MiningInfo { // History for chart std::vector 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 log_lines; }; /** diff --git a/src/main.cpp b/src/main.cpp index f8cf9ea..cdac886 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,15 @@ #include "platform/windows_backdrop.h" #include #include +#include +#include +#include +// 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(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(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; } diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 4835d93..7f2f271 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -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 diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index b2da8bd..c25c380 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -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& 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& 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(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 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 diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index c063422..c465a99 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -21,6 +21,7 @@ #include #include #include +#include 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 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"; diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index 8449697..437f479 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -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; }); } diff --git a/src/util/bootstrap.cpp b/src/util/bootstrap.cpp index e695eeb..e1e28f2 100644 --- a/src/util/bootstrap.cpp +++ b/src/util/bootstrap.cpp @@ -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; } diff --git a/src/util/bootstrap.h b/src/util/bootstrap.h index 7b6a944..aa18948 100644 --- a/src/util/bootstrap.h +++ b/src/util/bootstrap.h @@ -27,6 +27,7 @@ public: enum class State { Idle, Downloading, + Verifying, Extracting, Completed, Failed