diff --git a/CMakeLists.txt b/CMakeLists.txt index c8af3a5..9f76ac2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -503,6 +503,11 @@ embed_resource( ${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h ui_toml ) +embed_resource( + ${CMAKE_SOURCE_DIR}/res/default_banlist.txt + ${CMAKE_BINARY_DIR}/generated/default_banlist_embedded.h + default_banlist +) # Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds, # following the same pattern as daemon embedding. diff --git a/prebuilt-binaries/xmrig/.gitkeep b/prebuilt-binaries/xmrig/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/res/ObsidianDragon.rc b/res/ObsidianDragon.rc index 4d3a586..8f02ffa 100644 --- a/res/ObsidianDragon.rc +++ b/res/ObsidianDragon.rc @@ -31,11 +31,11 @@ BEGIN BEGIN BLOCK "040904B0" // US-English, Unicode BEGIN - VALUE "CompanyName", "The Hush Developers\0" + VALUE "CompanyName", "DragonX Developers\0" VALUE "FileDescription", "ObsidianDragon Wallet\0" VALUE "FileVersion", "@DRAGONX_VERSION@\0" VALUE "InternalName", "ObsidianDragon\0" - VALUE "LegalCopyright", "Copyright 2024-2026 The Hush Developers. GPLv3.\0" + VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0" VALUE "OriginalFilename", "ObsidianDragon.exe\0" VALUE "ProductName", "ObsidianDragon\0" VALUE "ProductVersion", "@DRAGONX_VERSION@\0" diff --git a/res/default_banlist.txt b/res/default_banlist.txt new file mode 100644 index 0000000..968f281 --- /dev/null +++ b/res/default_banlist.txt @@ -0,0 +1,13 @@ +# Default Ban List — DragonX Wallet +# IPs listed here are banned automatically when the wallet connects. +# One IP or subnet per line. Comments start with #. Blank lines are ignored. +# +# Examples: +# 192.168.1.100 +# 10.0.0.0/8 +# 203.0.113.42 +# +# Rebuild the wallet after editing this file. + +185.159.129.12 +185.228.233.222 \ No newline at end of file diff --git a/res/img/ObsidianDragon.ico b/res/img/ObsidianDragon.ico index 13a0f9c..19c2051 100644 Binary files a/res/img/ObsidianDragon.ico and b/res/img/ObsidianDragon.ico differ diff --git a/res/img/logos/logo_ObsidianDragon.png b/res/img/logos/logo_ObsidianDragon.png index 9935231..8e3e9e9 100644 Binary files a/res/img/logos/logo_ObsidianDragon.png and b/res/img/logos/logo_ObsidianDragon.png differ diff --git a/res/img/logos/logo_ObsidianDragon_dark.png b/res/img/logos/logo_ObsidianDragon_dark.png index 2c4a160..baf82ac 100644 Binary files a/res/img/logos/logo_ObsidianDragon_dark.png and b/res/img/logos/logo_ObsidianDragon_dark.png differ diff --git a/res/img/logos/logo_ObsidianDragon_light.png b/res/img/logos/logo_ObsidianDragon_light.png index 0bb0be0..8e3e9e9 100644 Binary files a/res/img/logos/logo_ObsidianDragon_light.png and b/res/img/logos/logo_ObsidianDragon_light.png differ diff --git a/res/themes/ui.toml b/res/themes/ui.toml index 314a1e5..728da3c 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -779,6 +779,8 @@ pool-url-input = { width = 300.0, height = 28.0 } pool-worker-input = { width = 200.0, height = 28.0 } log-panel-height = { size = 120.0 } log-panel-min = { size = 60.0 } +idle-row-height = { size = 28.0 } +idle-combo-width = { size = 64.0 } [tabs.peers] refresh-button = { size = 110.0 } @@ -1249,8 +1251,8 @@ inset-shadow-fade-ratio = { size = 5.0 } [components.content-area] padding-x = 0.0 padding-y = 0.0 -margin-top = { size = 4.0 } -margin-bottom = { size = -40.0 } +margin-top = { size = 6.0 } +margin-bottom = { size = -39.0 } edge-fade-zone = { size = 0.0 } [components.content-area.window] @@ -1262,10 +1264,10 @@ page-fade-speed = { size = 8.0 } collapse-hysteresis = { size = 60.0 } header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" } coin-icon = { icon = "logos/logo_dragonx_128.png" } -header-title = { font = "subtitle1", size = 14.0, pad-x = 16.0, pad-y = 12.0, logo-gap = 8.0, opacity = 0.7 } +header-title = { font = "subtitle1", size = 14.0, pad-x = 22.0, pad-y = 6.0, logo-gap = 4.0, opacity = 0.7, offset-y = 4.0 } [components.main-window.window] -padding = [12, 38] +padding = [12, 36] [components.shutdown] content-height = { height = 120.0 } @@ -1309,6 +1311,8 @@ rpc-label-width = { size = 85.0 } security-combo-width = { size = 120.0 } port-input-min-width = { size = 60.0 } port-input-width-ratio = { size = 0.4 } +idle-combo-width = { size = 64.0 } +about-logo-size = { size = 64.0 } [components.main-layout] app-bar-height = { size = 64.0 } diff --git a/setup.sh b/setup.sh index d672bbd..6c55dd4 100755 --- a/setup.sh +++ b/setup.sh @@ -416,8 +416,8 @@ clone_dragonx_if_needed() { git clone https://git.dragonx.is/DragonX/dragonx.git "$DRAGONX_SRC" else ok "dragonx source already present" - info "Pulling latest dragonx..." - (cd "$DRAGONX_SRC" && git pull --ff-only 2>/dev/null || true) + info "Switching to dragonx branch and pulling latest..." + (cd "$DRAGONX_SRC" && git checkout dragonx 2>/dev/null && git pull --ff-only 2>/dev/null || true) fi } diff --git a/src/app.cpp b/src/app.cpp index 82bba8b..36859e4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -292,6 +292,9 @@ void App::update() checkAutoLock(); } + // Mine-when-idle check (runs every frame, internally rate-limited by idle detection) + checkIdleMining(); + // P8: Dedup rebuildAddressList — only rebuild once per frame if (address_list_dirty_) { address_list_dirty_ = false; @@ -715,13 +718,14 @@ void App::render() const float brandPadX = hdrF("pad-x", mainPadX / hdp); const float brandPadY = hdrF("pad-y", 8.0f); const float logoGap = hdrF("logo-gap", 8.0f); + const float brandOffY = hdrF("offset-y", 0.0f); const float hdrOpacity = (hdrElem.opacity >= 0.0f) ? hdrElem.opacity : 0.7f; // Logo float logoSize = mainPadTop - brandPadY * 2.0f; // fit within header if (logoSize < 16.0f * hdp) logoSize = 16.0f * hdp; float logoX = winPos.x + brandPadX; - float logoY = winPos.y + brandPadY; + float logoY = winPos.y + brandPadY + brandOffY; if (logo_tex_ != 0) { float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f; float logoW = logoSize * aspect; @@ -740,7 +744,7 @@ void App::render() ? hdrElem.size * hdp : titleFont->LegacySize; if (titleFont) { - float textY = winPos.y + (mainPadTop - titleFontSize) * 0.5f; + float textY = winPos.y + (mainPadTop - titleFontSize) * 0.5f + brandOffY; ImU32 textCol = ui::material::OnSurface(); // Apply header text opacity int a = (int)((float)((textCol >> 24) & 0xFF) * hdrOpacity); @@ -803,7 +807,7 @@ void App::render() // Build sidebar status for badges + footer ui::SidebarStatus sbStatus; sbStatus.peerCount = static_cast(state_.peers.size()); - sbStatus.miningActive = state_.mining.generate; + sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running; // Load logo texture lazily on first frame (or after theme change) // Also reload when dark↔light mode changes so the correct variant shows @@ -1370,8 +1374,9 @@ void App::renderStatusBar() } } - // Mining indicator (if mining) - if (state_.mining.generate) { + // Mining indicator (if mining — solo or pool) + const bool anyMining = state_.mining.generate || state_.pool_mining.xmrig_running; + if (anyMining) { ImGui::SameLine(0, sbSectionGap); ImGui::TextDisabled("|"); ImGui::SameLine(0, sbSeparatorGap); @@ -1379,8 +1384,11 @@ void App::renderStatusBar() ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CONSTRUCTION); ImGui::PopFont(); ImGui::SameLine(0, sbIconTextGap); + double displayHashrate = state_.pool_mining.xmrig_running + ? state_.pool_mining.hashrate_10s + : state_.mining.localHashrate; ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%.1f H/s", - state_.mining.localHashrate); + displayHashrate); } // Decrypt-import background task indicator diff --git a/src/app.h b/src/app.h index a8c51a8..596df25 100644 --- a/src/app.h +++ b/src/app.h @@ -174,6 +174,9 @@ public: void startPoolMining(int threads); void stopPoolMining(); + // Mine-when-idle state query + bool isIdleMiningActive() const { return idle_mining_active_; } + // Peers const std::vector& getPeers() const { return state_.peers; } const std::vector& getBannedPeers() const { return state_.bannedPeers; } @@ -264,6 +267,8 @@ public: // Logo texture accessor (wallet branding icon) ImTextureID getLogoTexture() const { return logo_tex_; } + int getLogoWidth() const { return logo_w_; } + int getLogoHeight() const { return logo_h_; } // Coin logo texture accessor (DragonX currency icon for balance tab) ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; } @@ -564,6 +569,9 @@ private: // Auto-lock on idle std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now(); + // Mine-when-idle: auto-start/stop mining based on system idle state + bool idle_mining_active_ = false; // true when mining was auto-started by idle detection + // Private methods - rendering void renderStatusBar(); void renderAboutDialog(); @@ -582,6 +590,7 @@ private: void tryConnect(); void onConnected(); void onDisconnected(const std::string& reason); + void applyDefaultBanlist(); // Private methods - data refresh void refreshData(); @@ -590,6 +599,7 @@ private: void refreshPrice(); void refreshWalletEncryptionState(); void checkAutoLock(); + void checkIdleMining(); }; } // namespace dragonx diff --git a/src/app_network.cpp b/src/app_network.cpp index 400a00a..fbcbd2c 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -13,6 +13,7 @@ #include "daemon/embedded_daemon.h" #include "daemon/xmrig_manager.h" #include "ui/notifications.h" +#include "default_banlist_embedded.h" #include "util/platform.h" #include "util/perf_log.h" @@ -52,6 +53,7 @@ void App::tryConnect() if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) { connection_status_ = "Waiting for daemon config..."; VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt); + refresh_timer_ = REFRESH_INTERVAL - 1.0f; return; } @@ -63,9 +65,11 @@ void App::tryConnect() if (startEmbeddedDaemon()) { // Will retry connection after daemon starts VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt); + refresh_timer_ = REFRESH_INTERVAL - 1.0f; } else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) { connection_status_ = "Waiting for daemon config..."; VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt); + refresh_timer_ = REFRESH_INTERVAL - 1.0f; } else { VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n", connect_attempt, @@ -148,10 +152,14 @@ void App::tryConnect() state_.connected = false; connection_status_ = "Waiting for dragonxd to start..."; VERBOSE_LOGF("[connect #%d] RPC connection failed — daemon still starting, will retry...\n", attempt); + // Fast retry: force the refresh timer to fire on the next cycle + // instead of waiting the full 5-second REFRESH_INTERVAL. + refresh_timer_ = REFRESH_INTERVAL - 1.0f; } else if (externalDetected) { state_.connected = false; connection_status_ = "Connecting to daemon..."; VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt); + refresh_timer_ = REFRESH_INTERVAL - 1.0f; } else { onDisconnected("Connection failed"); VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt); @@ -286,6 +294,9 @@ void App::onConnected() // Initial data refresh refreshData(); refreshMarketData(); + + // Apply compiled-in default ban list + applyDefaultBanlist(); } void App::onDisconnected(const std::string& reason) @@ -1403,6 +1414,57 @@ void App::clearBans() }); } +void App::applyDefaultBanlist() +{ + if (!state_.connected || !rpc_ || !worker_) return; + + // Parse the embedded default_banlist.txt (compiled from res/default_banlist.txt) + std::string data(reinterpret_cast(embedded::default_banlist_data), + embedded::default_banlist_size); + + std::vector ips; + size_t pos = 0; + while (pos < data.size()) { + size_t eol = data.find('\n', pos); + if (eol == std::string::npos) eol = data.size(); + std::string line = data.substr(pos, eol - pos); + pos = eol + 1; + + // Strip carriage return (Windows line endings) + if (!line.empty() && line.back() == '\r') line.pop_back(); + // Strip leading/trailing whitespace + size_t start = line.find_first_not_of(" \t"); + if (start == std::string::npos) continue; + line = line.substr(start, line.find_last_not_of(" \t") - start + 1); + // Skip empty lines and comments + if (line.empty() || line[0] == '#') continue; + + ips.push_back(line); + } + + if (ips.empty()) return; + + // Apply bans on the worker thread to avoid blocking the UI + worker_->post([this, ips]() -> rpc::RPCWorker::MainCb { + int applied = 0; + for (const auto& ip : ips) { + try { + // 0 = permanent ban (until node restart or manual unban) + // Using a very long duration (10 years) for effectively permanent bans + rpc_->call("setban", {ip, "add", 315360000}); + applied++; + } catch (...) { + // Already banned or invalid — skip silently + } + } + return [applied]() { + if (applied > 0) { + DEBUG_LOGF("[Banlist] Applied %d default bans\n", applied); + } + }; + }); +} + // ============================================================================ // Address Operations // ============================================================================ diff --git a/src/app_security.cpp b/src/app_security.cpp index f5bbaf1..21052af 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -413,6 +413,67 @@ void App::checkAutoLock() { } } +// =========================================================================== +// Mine when idle — auto-start/stop mining based on system idle state +// =========================================================================== + +void App::checkIdleMining() { + if (!settings_ || !settings_->getMineWhenIdle()) { + // Feature disabled — if we previously auto-started, stop now + if (idle_mining_active_) { + idle_mining_active_ = false; + if (settings_ && settings_->getPoolMode()) { + if (xmrig_manager_ && xmrig_manager_->isRunning()) + stopPoolMining(); + } else { + if (state_.mining.generate) + stopMining(); + } + } + return; + } + + int idleSec = util::Platform::getSystemIdleSeconds(); + int delay = settings_->getMineIdleDelay(); + bool isPool = settings_->getPoolMode(); + + // Check if mining is already running (manually started by user) + bool miningActive = isPool + ? (xmrig_manager_ && xmrig_manager_->isRunning()) + : state_.mining.generate; + + if (idleSec >= delay) { + // System is idle — start mining if not already running + if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) { + // For solo mining, need daemon connected and synced + if (!isPool && (!state_.connected || state_.sync.syncing)) return; + + int threads = settings_->getPoolThreads(); + if (threads <= 0) threads = std::max(1, (int)std::thread::hardware_concurrency() / 2); + + idle_mining_active_ = true; + if (isPool) + startPoolMining(threads); + else + startMining(threads); + DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec); + } + } else { + // User is active — stop mining if we auto-started it + if (idle_mining_active_) { + idle_mining_active_ = false; + if (isPool) { + if (xmrig_manager_ && xmrig_manager_->isRunning()) + stopPoolMining(); + } else { + if (state_.mining.generate) + stopMining(); + } + DEBUG_LOGF("[App] Idle mining stopped — user returned\n"); + } + } +} + // =========================================================================== // Restart Setup Wizard (from Settings) // =========================================================================== diff --git a/src/config/settings.cpp b/src/config/settings.cpp index f3529d2..a804f77 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -148,6 +148,8 @@ bool Settings::load(const std::string& path) 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("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get(); + if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].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"]) @@ -231,6 +233,8 @@ bool Settings::save(const std::string& path) j["pool_tls"] = pool_tls_; j["pool_hugepages"] = pool_hugepages_; j["pool_mode"] = pool_mode_; + j["mine_when_idle"] = mine_when_idle_; + j["mine_idle_delay"]= mine_idle_delay_; j["saved_pool_urls"] = json::array(); for (const auto& u : saved_pool_urls_) j["saved_pool_urls"].push_back(u); diff --git a/src/config/settings.h b/src/config/settings.h index 9e223b1..a6454bb 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -208,6 +208,12 @@ public: bool getPoolMode() const { return pool_mode_; } void setPoolMode(bool v) { pool_mode_ = v; } + // Mine when idle (auto-start mining when system is idle) + bool getMineWhenIdle() const { return mine_when_idle_; } + void setMineWhenIdle(bool v) { mine_when_idle_ = v; } + int getMineIdleDelay() const { return mine_idle_delay_; } + void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); } + // Saved pool URLs (user-managed favorites dropdown) const std::vector& getSavedPoolUrls() const { return saved_pool_urls_; } void addSavedPoolUrl(const std::string& url) { @@ -289,6 +295,8 @@ private: bool pool_tls_ = false; bool pool_hugepages_ = true; bool pool_mode_ = false; // false=solo, true=pool + bool mine_when_idle_ = false; // auto-start mining when system idle + int mine_idle_delay_= 120; // seconds of idle before mining starts std::vector saved_pool_urls_; // user-saved pool URL favorites std::vector saved_pool_workers_; // user-saved worker address favorites diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index 17a7896..33a8027 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -87,7 +87,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port, curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers); curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 3L); + curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 1L); // localhost — fails fast if not listening // Test connection with getinfo try { diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index aa08383..1da3607 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -104,6 +104,10 @@ static LowSpecSnapshot s_lowSpecSnap; // Daemon — keep running on close static bool sp_keep_daemon_running = false; static bool sp_stop_external_daemon = false; + +// Mining — mine when idle +static bool sp_mine_when_idle = false; +static int sp_mine_idle_delay = 120; static bool sp_verbose_logging = false; // Debug logging categories @@ -165,6 +169,8 @@ static void loadSettingsPageState(config::Settings* settings) { Layout::setUserFontScale(sp_font_scale); // sync with Layout on load sp_keep_daemon_running = settings->getKeepDaemonRunning(); sp_stop_external_daemon = settings->getStopExternalDaemon(); + sp_mine_when_idle = settings->getMineWhenIdle(); + sp_mine_idle_delay = settings->getMineIdleDelay(); sp_verbose_logging = settings->getVerboseLogging(); sp_debug_categories = settings->getDebugCategories(); sp_debug_cats_dirty = false; @@ -212,6 +218,8 @@ static void saveSettingsPageState(config::Settings* settings) { settings->setFontScale(sp_font_scale); settings->setKeepDaemonRunning(sp_keep_daemon_running); settings->setStopExternalDaemon(sp_stop_external_daemon); + settings->setMineWhenIdle(sp_mine_when_idle); + settings->setMineIdleDelay(sp_mine_idle_delay); settings->setVerboseLogging(sp_verbose_logging); settings->setDebugCategories(sp_debug_categories); @@ -1075,6 +1083,46 @@ void RenderSettingsPage(App* app) { if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } + // Mine when idle — checkbox + delay combo + { + if (ImGui::Checkbox("Mine when idle", &sp_mine_when_idle)) { + saveSettingsPageState(app->settings()); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Automatically start mining when the\nsystem is idle (no keyboard/mouse input)"); + + if (sp_mine_when_idle) { + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "after"); + ImGui::SameLine(0, Layout::spacingSm()); + + struct DelayOption { int seconds; const char* label; }; + static const DelayOption delays[] = { + {30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"} + }; + const char* previewLabel = "2m"; + for (const auto& d : delays) { + if (d.seconds == sp_mine_idle_delay) { previewLabel = d.label; break; } + } + + ImGui::SetNextItemWidth(schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f)); + if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) { + for (const auto& d : delays) { + bool selected = (d.seconds == sp_mine_idle_delay); + if (ImGui::Selectable(d.label, selected)) { + sp_mine_idle_delay = d.seconds; + saveSettingsPageState(app->settings()); + } + if (selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("How long to wait before starting mining"); + } + } + // Bottom row — Keys & Data left-aligned, Setup Wizard right-aligned { const char* r1[] = {"Import Key...", "Export Key...", "Export All...", "Backup...", "Export CSV..."}; @@ -1583,7 +1631,23 @@ void RenderSettingsPage(App* app) { ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); - float contentW = availWidth - pad * 2; + // Logo on the left side of the about card + ImTextureID logoTex = app->getLogoTexture(); + float logoAreaW = 0; + if (logoTex != 0) { + float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f); + float logoH = logoMaxH; + float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f; + float logoW = logoH * aspect; + ImVec2 logoPos = ImGui::GetCursorScreenPos(); + dl->AddImage(logoTex, + ImVec2(logoPos.x, logoPos.y), + ImVec2(logoPos.x + logoW, logoPos.y + logoH)); + logoAreaW = logoW + Layout::spacingLg(); + ImGui::Indent(logoAreaW); + } + + float contentW = availWidth - pad * 2 - logoAreaW; // App name + version on same line ImGui::PushFont(sub1); @@ -1601,7 +1665,7 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushFont(body2); - ImGui::PushTextWrapPos(cardMin.x + availWidth - pad); + ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW); ImGui::TextUnformatted( "A shielded cryptocurrency wallet for DragonX (DRGX), " "built with Dear ImGui for a lightweight, portable experience."); @@ -1611,14 +1675,18 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushFont(capFont); - ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License"); + ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 DragonX Developers | GPLv3 License"); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); - // Buttons — consistent equal-width row + // Buttons — consistent equal-width row (full card width) + if (logoAreaW > 0) { + ImGui::Unindent(logoAreaW); + } { - float aboutBtnW = (contentW - Layout::spacingMd() * 3) / 4.0f; + float fullContentW = availWidth - pad * 2; + float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f; if (TactileButton("Website", ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://dragonx.is"); diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 5034705..aeedb3c 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -1630,12 +1630,39 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc return; } - // Parse command and arguments + // Parse command and arguments (shell-like: handles quotes and JSON brackets) std::vector args; - std::istringstream iss(cmd); - std::string token; - while (iss >> token) { - args.push_back(token); + { + size_t i = 0; + size_t len = cmd.size(); + while (i < len) { + // Skip whitespace + while (i < len && (cmd[i] == ' ' || cmd[i] == '\t')) i++; + if (i >= len) break; + + std::string tok; + if (cmd[i] == '"' || cmd[i] == '\'') { + // Quoted string — collect until matching close quote + char quote = cmd[i++]; + while (i < len && cmd[i] != quote) tok += cmd[i++]; + if (i < len) i++; // skip closing quote + } else if (cmd[i] == '[' || cmd[i] == '{') { + // JSON array/object — collect until matching bracket + char open = cmd[i]; + char close = (open == '[') ? ']' : '}'; + int depth = 0; + while (i < len) { + if (cmd[i] == open) depth++; + else if (cmd[i] == close) depth--; + tok += cmd[i++]; + if (depth == 0) break; + } + } else { + // Unquoted token — collect until whitespace + while (i < len && cmd[i] != ' ' && cmd[i] != '\t') tok += cmd[i++]; + } + if (!tok.empty()) args.push_back(tok); + } } if (args.empty()) return; @@ -1647,24 +1674,31 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc for (size_t i = 1; i < args.size(); i++) { const std::string& arg = args[i]; - // Try to parse as number - try { - if (arg.find('.') != std::string::npos) { - params.push_back(std::stod(arg)); - } else { - // Check for bool - if (arg == "true") { - params.push_back(true); - } else if (arg == "false") { - params.push_back(false); + // Try to parse as JSON first (handles objects, arrays, etc.) + if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) { + auto parsed = nlohmann::json::parse(arg, nullptr, false); + if (!parsed.is_discarded()) { + params.push_back(parsed); + continue; + } + } + + // Try to parse as number or bool + if (arg == "true") { + params.push_back(true); + } else if (arg == "false") { + params.push_back(false); + } else { + try { + if (arg.find('.') != std::string::npos) { + params.push_back(std::stod(arg)); } else { - // Try as integer params.push_back(std::stoll(arg)); } + } catch (...) { + // Keep as string + params.push_back(arg); } - } catch (...) { - // Keep as string - params.push_back(arg); } } @@ -1757,9 +1791,24 @@ void ConsoleTab::addLine(const std::string& line, ImU32 color) lines_.push_back({line, color}); - // Limit buffer size + // Limit buffer size — adjust selection indices when lines are removed + // from the front so the highlight stays on the text the user selected. + int popped = 0; while (lines_.size() > 10000) { lines_.pop_front(); + popped++; + } + if (popped > 0 && has_selection_) { + sel_anchor_.line -= popped; + sel_end_.line -= popped; + if (sel_anchor_.line < 0 && sel_end_.line < 0) { + // Entire selection was in the removed range + has_selection_ = false; + is_selecting_ = false; + } else { + if (sel_anchor_.line < 0) { sel_anchor_.line = 0; sel_anchor_.col = 0; } + if (sel_end_.line < 0) { sel_end_.line = 0; sel_end_.col = 0; } + } } // Track new output while user is scrolled up diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index c465a99..f41e0c0 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -907,7 +907,7 @@ void RenderPeersTab(App* app) effects::ImGuiAcrylic::EndAcrylicPopup(); } - ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowEnd.y)); + ImGui::SetCursorScreenPos(ImVec2(rawRowPosB.x, rowEnd.y)); if (i < state.bannedPeers.size() - 1) { ImVec2 divStart = ImGui::GetCursorScreenPos(); diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 4edba35..197575e 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #ifdef __APPLE__ #include #include @@ -596,5 +597,94 @@ double Platform::getDaemonMemoryUsageMB() #endif } +// ============================================================================ +// System-wide idle time detection +// ============================================================================ + +int Platform::getSystemIdleSeconds() +{ +#ifdef _WIN32 + LASTINPUTINFO lii; + lii.cbSize = sizeof(lii); + if (GetLastInputInfo(&lii)) { + DWORD elapsed = GetTickCount() - lii.dwTime; + return (int)(elapsed / 1000); + } + return 0; +#elif defined(__APPLE__) + // macOS: use IOKit HIDSystem idle time + // Fallback to 0 (feature not supported) to keep it simple + return 0; +#else + // Linux: dynamically load libXss to query X11 screensaver idle time + // This avoids a hard link dependency on X11/Xss libraries. + typedef struct { + int type; + unsigned long serial; + int/*Bool*/ send_event; + void* /*Display**/ display; + unsigned long/*Drawable*/ window; + int state; + int kind; + unsigned long til_or_since; + unsigned long idle; + unsigned long eventMask; + } XScreenSaverInfo; + + // Function pointer types + typedef void* (*XOpenDisplay_t)(const char*); + typedef unsigned long (*XDefaultRootWindow_t)(void*); + typedef XScreenSaverInfo* (*XScreenSaverAllocInfo_t)(void); + typedef int (*XScreenSaverQueryInfo_t)(void*, unsigned long, XScreenSaverInfo*); + typedef int (*XCloseDisplay_t)(void*); + typedef void (*XFree_t)(void*); + + static bool s_tried = false; + static void* s_x11 = nullptr; + static void* s_xss = nullptr; + static XOpenDisplay_t s_XOpenDisplay = nullptr; + static XDefaultRootWindow_t s_XDefaultRootWindow = nullptr; + static XScreenSaverAllocInfo_t s_XScreenSaverAllocInfo = nullptr; + static XScreenSaverQueryInfo_t s_XScreenSaverQueryInfo = nullptr; + static XCloseDisplay_t s_XCloseDisplay = nullptr; + (void)s_XCloseDisplay; // retained for potential cleanup + static XFree_t s_XFree = nullptr; + static void* s_display = nullptr; + + if (!s_tried) { + s_tried = true; + s_x11 = dlopen("libX11.so.6", RTLD_LAZY); + s_xss = dlopen("libXss.so.1", RTLD_LAZY); + if (s_x11 && s_xss) { + s_XOpenDisplay = (XOpenDisplay_t)dlsym(s_x11, "XOpenDisplay"); + s_XDefaultRootWindow = (XDefaultRootWindow_t)dlsym(s_x11, "XDefaultRootWindow"); + s_XCloseDisplay = (XCloseDisplay_t)dlsym(s_x11, "XCloseDisplay"); // NOLINT + s_XFree = (XFree_t)dlsym(s_x11, "XFree"); + s_XScreenSaverAllocInfo = (XScreenSaverAllocInfo_t)dlsym(s_xss, "XScreenSaverAllocInfo"); + s_XScreenSaverQueryInfo = (XScreenSaverQueryInfo_t)dlsym(s_xss, "XScreenSaverQueryInfo"); + + if (s_XOpenDisplay && s_XDefaultRootWindow && s_XScreenSaverAllocInfo && s_XScreenSaverQueryInfo) { + s_display = s_XOpenDisplay(nullptr); + } + } + } + + if (!s_display || !s_XScreenSaverAllocInfo || !s_XScreenSaverQueryInfo || !s_XDefaultRootWindow) { + return 0; + } + + XScreenSaverInfo* info = s_XScreenSaverAllocInfo(); + if (!info) return 0; + + int idleSec = 0; + unsigned long root = s_XDefaultRootWindow(s_display); + if (s_XScreenSaverQueryInfo(s_display, root, info)) { + idleSec = (int)(info->idle / 1000); // idle is in milliseconds + } + if (s_XFree) s_XFree(info); + return idleSec; +#endif +} + } // namespace util } // namespace dragonx diff --git a/src/util/platform.h b/src/util/platform.h index 210bbc7..24ca6d7 100644 --- a/src/util/platform.h +++ b/src/util/platform.h @@ -123,6 +123,14 @@ public: * @return Combined daemon RSS in MB, or 0 if no daemon found */ static double getDaemonMemoryUsageMB(); + + /** + * @brief Get system-wide idle time in seconds + * Uses platform-specific APIs: GetLastInputInfo (Windows), + * XScreenSaverQueryInfo via dlopen (Linux), IOKit (macOS). + * @return Seconds since last user input, or 0 on failure + */ + static int getSystemIdleSeconds(); }; /**