From 5180f74a6836636d84a448e9a9094d09f644de67 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 01:28:14 -0500 Subject: [PATCH] feat(settings): rebuild Node & Security as one fill-width card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the column/grid/masonry machinery (which kept leaving empty gaps and buried the daemon Advanced actions) with a single full-width card whose controls are arranged horizontally to use the width: - NODE: Data Dir + Wallet Size + Open-folder on one row. - RPC: Host / Port / Username / Password four-across when wide (>=700), wrapping to 2x2 when narrow; auto-detected + plaintext warning kept. - SECURITY: encryption buttons + Auto-lock combo + PIN controls on horizontal rows (factored into renderSecuritySection so lite gets auto-lock+PIN and full-node adds the RPC-backed encrypt/lock/remove block — same gating as before). - DAEMON BINARY: inline Installed/Bundled/status; Check/Refresh/Test row; and the four maintenance actions (Install bundled / Rescan / Delete blockchain / Repair) behind a now-prominent, clearly-clickable "Advanced ▾" toggle instead of the easy-to-miss faint overline — fixing the "daemon options missing" report. Lite keeps its existing wallet-lifecycle/backup/security block, full width. Removed useGrid/stacked/column clips/dividers. Every control + state branch + confirm flag + tooltip preserved (verified). Full-node + lite build clean; ctest green; hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/pages/settings_page.cpp | 935 +++++++++++++++------------------ 1 file changed, 435 insertions(+), 500 deletions(-) diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index c6f6ec3..f825554 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1508,46 +1508,142 @@ void RenderSettingsPage(App* app) { return std::max(minBtnW, maxTextW + btnPad * 2); }; - // --- NODE + SECURITY: 2x2 grid on wide full-node windows, stacked otherwise --- + // --- NODE & SECURITY: one full-width card, controls laid out horizontally --- + // A single vertical flow of sub-sections (Node, RPC, Security, Daemon binary), + // each with its overline header and its controls flowing left-to-right to fill + // the card width. No 2x2 grid, no column clips, no masonry placement. { - // Wide full-node: a 2x2 grid fills the card width with no empty gap — - // TOP-LEFT = NODE/Data, TOP-RIGHT = RPC, - // BOTTOM-LEFT = SECURITY, BOTTOM-RIGHT = DAEMON BINARY head, - // with the Advanced destructive buttons dropping to a full-width strip below. - // Narrow windows (or lite, which has no daemon) fall through to the original - // single full-width column: Node -> RPC -> Security -> Daemon Binary stack. - // `stacked` still drives the existing vertical-flow path below. - float gridBP = S.drawElement("components.settings-page", "node-grid-breakpoint").sizeOr(900.0f); - bool useGrid = app->supportsFullNodeLifecycleActions() && (availWidth >= gridBP); - bool stacked = !useGrid; - float gutter = Layout::spacingLg(); - float leftColW = useGrid ? std::floor((contentW - gutter) * 0.5f) : contentW; - float rightColW = useGrid ? (contentW - gutter - leftColW) : contentW; ImVec2 sectionOrigin = ImGui::GetCursorScreenPos(); - float leftX = sectionOrigin.x; - float rightX = useGrid ? (sectionOrigin.x + leftColW + gutter) : sectionOrigin.x; - // Grid path: NODE (left) and RPC (right) live in separate clips, so their - // bottoms are captured inside the content block below and read out afterward. - float leftBottomGrid = sectionOrigin.y; - float rpcBottomGrid = sectionOrigin.y; + // The lite WALLET block below still positions its rows against a "left" + // column origin/width; in the single-column layout that is simply the full + // content column at the section origin. + const float leftX = sectionOrigin.x; + const float leftColW = contentW; - // ---- LEFT COLUMN: NODE ---- - ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y)); - ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y), - ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true); + ImFont* body2Info = S.resolveFont("body2"); + if (!body2Info) body2Info = Type().body2(); - // Lite has no daemon — this column holds wallet lifecycle/encryption, so label - // it "WALLET" rather than "NODE". - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), - app->isLiteBuild() ? TR("wallet") : TR("node")); - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + // Shared SECURITY controls, rendered full-width with horizontal rows. Used by + // both the lite branch (auto-lock + PIN only) and the full-node branch (which + // also gets the RPC-backed encrypt/change/lock/remove block, includeRpcEncrypt). + // `secX` is the row start X (== content-column left); `secColW` its width. + auto renderSecuritySection = [&](float secX, float secColW, bool includeRpcEncrypt) { + // Encrypt / Change passphrase / Lock / Remove — RPC-backed, full-node only. + // In lite the wallet's encryption controls live in the WALLET block above. + if (includeRpcEncrypt) { + bool isEncrypted = app->state().isEncrypted(); + bool isLocked = app->state().isLocked(); - { - ImFont* body2Info = S.resolveFont("body2"); - if (!body2Info) body2Info = Type().body2(); + float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (secColW - Layout::spacingMd()) * 0.5f); + + ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); + if (!isEncrypted) { + if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button"))) + app->showEncryptDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_encrypt")); + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted")); + } else { + if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button"))) + app->showChangePassphraseDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pass")); + ImGui::SameLine(0, Layout::spacingMd()); + if (isLocked) { + ImGui::PushFont(Type().iconSmall()); + ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK); + ImGui::PopFont(); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%s", TR("settings_locked")); + } else { + if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button"))) + app->lockWallet(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock")); + ImGui::SameLine(0, Layout::spacingSm()); + ImGui::PushFont(Type().iconSmall()); + ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN); + ImGui::PopFont(); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked")); + } + // Remove Encryption button — trails the row. + ImGui::SameLine(0, Layout::spacingMd()); + if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button"))) + app->showDecryptDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt")); + } + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + } + + // Auto-Lock timeout + PIN controls on one horizontal row. + { + float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f); + + int timeout = app->settings()->getAutoLockTimeout(); + const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") }; + int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 }; + int selTimeout = 0; + for (int i = 0; i < 6; i++) { + if (timeoutValues[i] == timeout) { selTimeout = i; break; } + } + ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); + ImGui::AlignTextToFramePadding(); + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock")); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::PushItemWidth(comboW); + if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) { + app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]); + app->settings()->save(); + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock")); + ImGui::PopItemWidth(); + + // PIN unlock controls, trailing the auto-lock combo on the same row. + bool isEncryptedPIN = app->state().isEncrypted(); + if (isEncryptedPIN) { + bool hasPIN = app->hasPinVault(); + float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (secColW - Layout::spacingSm()) * 0.5f); + + ImGui::SameLine(0, Layout::spacingMd()); + if (!hasPIN) { + if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) + app->showPinSetupDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_set_pin")); + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin")); + } else { + if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) + app->showPinChangeDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pin")); + ImGui::SameLine(0, Layout::spacingSm()); + if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) + app->showPinRemoveDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_pin")); + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::PushFont(Type().iconSmall()); + ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD); + ImGui::PopFont(); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active")); + } + } else { + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin")); + } + } + }; + + if (app->isLiteBuild()) { + // ============================ LITE ============================ + // Lite has no daemon: this card holds wallet lifecycle/encryption + // (the "WALLET" block) followed by the shared SECURITY block, both + // rendered full-width in a single column exactly as before. + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + + { ImGui::PushFont(body2Info); - - if (app->isLiteBuild()) { + { float liteLabelW = std::min(leftColW * 0.35f, 132.0f); float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm()); @@ -1889,40 +1985,65 @@ void RenderSettingsPage(App* app) { sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); } - } else { + } // lite WALLET block + ImGui::PopFont(); + } // lite WALLET push-font scope - float rpcLblW = std::max( - S.drawElement("components.settings-page", "rpc-label-min-width").size, - std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs)); + // ---- SECURITY (lite): auto-lock + PIN only ---- + // The wallet's passphrase encryption controls live in the WALLET block + // above (lite backend), so this section shows only the shared auto-lock + // timeout + PIN controls. + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/false); - std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat"; - uint64_t wallet_size = util::Platform::getFileSize(wallet_path); + // Advance to the true bottom of the single column. + ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y)); - // Data Dir + Wallet Size — same line with "Label: value" format - ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - ImGui::AlignTextToFramePadding(); + } else { + // ========================= FULL NODE ========================= + // One full-width column; each sub-section's controls flow left-to-right. + ImGui::PushFont(body2Info); + + const float spMd = Layout::spacingMd(); + + // -------------------- NODE / DATA -------------------- + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { - // Measure the wallet size string first - std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); - std::string dirPath = util::Platform::getDragonXDataDir(); + std::string dirPath = util::Platform::getDragonXDataDir(); + std::string walletPath = dirPath + "wallet.dat"; + uint64_t wallet_size = util::Platform::getFileSize(walletPath); + std::string size_str = (wallet_size > 0) + ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); - // Wallet Size now lives on its own row below (so the grid's left cell - // has no mid-row gap), so the path gets the full column width. - float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x + Layout::spacingXs(); - float availForPath = leftColW - dataDirLabelW; + // Right-aligned "Open data folder" button trailing the row; the path + // gets whatever width is left after the data-dir label, wallet-size + // label+value, and the trailing button. + ImFont* nodeBtnFont = S.resolveFont("button"); + float openBtnW = ImGui::CalcTextSize(TR("settings_open_data_dir")).x + + ImGui::GetStyle().FramePadding.x * 2.0f + btnPad; + float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x; + float sizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x; + float sizeValueW = ImGui::CalcTextSize(size_str.c_str()).x; + // Space reserved to the right of the path for the size block + button. + float trailW = sizeLabelW + Layout::spacingXs() + sizeValueW + spMd + openBtnW; + float availForPath = contentW - dataDirLabelW - Layout::spacingXs() - spMd - trailW; + if (availForPath < 60.0f) availForPath = contentW - dataDirLabelW - Layout::spacingXs(); - ImGui::TextUnformatted(TR("settings_data_dir")); - ImGui::SameLine(0, Layout::spacingXs()); - ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str()); ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary()); ImVec4 linkHoverCol = linkCol; linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(TR("settings_data_dir")); + ImGui::SameLine(0, Layout::spacingXs()); + ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str()); bool hovered = false; + ImGui::AlignTextToFramePadding(); if (pathSize.x > availForPath && availForPath > 0) { float scale = availForPath / pathSize.x; float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f); ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize); - ImGui::AlignTextToFramePadding(); ImGui::TextColored(linkCol, "%s", dirPath.c_str()); hovered = ImGui::IsItemHovered(); if (hovered) { @@ -1947,474 +2068,288 @@ void RenderSettingsPage(App* app) { if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath); - // Wallet Size on its own row (no SameLine) so the left grid cell has - // no mid-row gap; reads fine full-width too. - ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + // Wallet Size label + value, trailing the path on the same row. + ImGui::SameLine(0, spMd); + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("settings_wallet_size_label")); ImGui::SameLine(0, Layout::spacingXs()); - if (wallet_size > 0) { + ImGui::AlignTextToFramePadding(); + if (wallet_size > 0) ImGui::TextUnformatted(size_str.c_str()); - } else { + else ImGui::TextDisabled("%s", TR("settings_not_found")); + + // Open-data-folder button: right-aligned when there is room, else trailing. + float rowY = ImGui::GetCursorScreenPos().y; + float rightEdge = sectionOrigin.x + contentW; + float actualOpenW = ImGui::CalcTextSize(TR("settings_open_data_dir")).x + + ImGui::GetStyle().FramePadding.x * 2.0f; + float openX = rightEdge - actualOpenW; + float afterSizeX = ImGui::GetItemRectMax().x + spMd; + ImGui::SameLine(0, spMd); + if (openX > afterSizeX) + ImGui::SetCursorScreenPos(ImVec2(openX, rowY)); + if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), nodeBtnFont)) { + util::Platform::openFolder(dirPath); + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir")); + } + + // -------------------- RPC -------------------- + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("rpc_connection")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + { + // Four fields (Host, Port, Username, Password) across one row when the + // card is wide enough; otherwise 2x2. Each field is "label [input]". + const char* hostLbl = TR("rpc_host"); + const char* portLbl = TR("rpc_port"); + const char* userLbl = TR("rpc_user"); + const char* passLbl = TR("rpc_pass"); + float labelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(portLbl).x + + ImGui::CalcTextSize(userLbl).x + ImGui::CalcTextSize(passLbl).x; + + const bool fourAcross = contentW >= 700.0f; + auto field = [&](const char* label, const char* id, char* buf, size_t bufSz, + float inputW, bool password) { + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(label); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::SetNextItemWidth(inputW); + ImGui::InputText(id, buf, bufSz, + password ? ImGuiInputTextFlags_Password : 0); + }; + + if (fourAcross) { + // fieldW = (contentW - labels - per-field label gaps - 3 inter-field gaps) / 4 + float inputTotal = contentW - labelsW - Layout::spacingXs() * 4 - spMd * 3; + float inputW = std::max(60.0f, std::floor(inputTotal / 4.0f)); + field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host")); + ImGui::SameLine(0, spMd); + field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port")); + ImGui::SameLine(0, spMd); + field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); + ImGui::SameLine(0, spMd); + field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass")); + } else { + // 2x2: two fields per row. + float halfLabelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(userLbl).x; + float inputW = std::max(60.0f, std::floor( + (contentW - halfLabelsW - Layout::spacingXs() * 2 - spMd) / 2.0f)); + field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host")); + ImGui::SameLine(0, spMd); + field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); + + field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port")); + ImGui::SameLine(0, spMd); + field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass")); + } + + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("settings_auto_detected")); + if (s_settingsState.rpc_plaintext_remote) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); + ImGui::PushTextWrapPos(sectionOrigin.x + contentW); + ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS."); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); } } - // Explicit button to open the wallet + blockchain data folder in the OS file manager. - ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) { - util::Platform::openFolder(util::Platform::getDragonXDataDir()); - } - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir")); - - // When gridded, RPC moves into the TOP-RIGHT cell: close the LEFT clip here - // and open the RIGHT cell rebased to rightX/rightColW. When stacked, RPC - // stays inline in the left column exactly as before. - if (useGrid) { - ImGui::PopFont(); // body2 re-pushed below for the right cell - leftBottomGrid = ImGui::GetCursorScreenPos().y; - ImGui::PopClipRect(); - float rpcTopY = sectionOrigin.y; - ImGui::SetCursorScreenPos(ImVec2(rightX, rpcTopY)); - ImGui::PushClipRect(ImVec2(rightX, rpcTopY), - ImVec2(rightX + rightColW, rpcTopY + 9999), true); - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("rpc_connection")); - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImGui::PushFont(body2Info); - } - - // RPC cell geometry: rebased to the right cell when gridded, else the left. - const float rpcColX = useGrid ? rightX : leftX; - const float rpcColW = useGrid ? rightColW : leftColW; - + // -------------------- SECURITY -------------------- + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/true); - // RPC connection — two columns: (Host | Username) and (Port | Password) - float rpcHalfW = (rpcColW - Layout::spacingMd()) * 0.5f; - float rpcHalfLblW = std::min(rpcLblW, rpcHalfW * 0.4f); - float rpcHalfInputW = std::max(40.0f, rpcHalfW - rpcHalfLblW); - float rpcRightColX = rpcColX + rpcHalfW + Layout::spacingMd(); + // -------------------- DAEMON BINARY -------------------- + if (app->supportsFullNodeLifecycleActions()) { + ImFont* dbBtnFont = S.resolveFont("button"); + if (!s_settingsState.daemon_info_loaded) { + s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); + s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo(); + s_settingsState.daemon_info_loaded = true; + } + const auto& inst = s_settingsState.installed_daemon; + const auto& bun = s_settingsState.bundled_daemon; - // Row 1: RPC Host + Username - float row1Y = ImGui::GetCursorScreenPos().y; - ImGui::SetCursorScreenPos(ImVec2(rpcColX, row1Y)); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(TR("rpc_host")); - ImGui::SameLine(rpcColX - sectionOrigin.x + rpcHalfLblW); - ImGui::SetNextItemWidth(rpcHalfInputW); - ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host)); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host")); + auto fmtDate = [](std::int64_t epoch) -> std::string { + if (epoch <= 0) return "—"; + std::time_t t = static_cast(epoch); + std::tm tmv{}; +#ifdef _WIN32 + localtime_s(&tmv, &t); +#else + localtime_r(&t, &tmv); +#endif + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv); + return std::string(buf); + }; - float afterRow1Y = ImGui::GetCursorScreenPos().y; - ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y)); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(TR("rpc_user")); - ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW); - ImGui::SetNextItemWidth(rpcHalfInputW); - ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user)); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); + const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()); - ImGui::SetCursorScreenPos(ImVec2(rpcColX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y))); + ImGui::Dummy(ImVec2(0, Layout::spacingLg())); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - // Row 2: Port + Password - float row2Y = ImGui::GetCursorScreenPos().y; - ImGui::SetCursorScreenPos(ImVec2(rpcColX, row2Y)); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(TR("rpc_port")); - ImGui::SameLine(rpcColX - sectionOrigin.x + rpcHalfLblW); - ImGui::SetNextItemWidth(rpcHalfInputW); - ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port)); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port")); + // Inline single line: Installed ( · ) · Bundled () · + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(TR("daemon_installed")); + ImGui::SameLine(0, Layout::spacingXs()); + if (inst.exists) { + ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str()); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(dbDim, "(%s · %s)", + util::Platform::formatFileSize(inst.size).c_str(), + fmtDate(inst.modifiedEpoch).c_str()); + } else { + ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed")); + } + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(dbDim, "·"); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(TR("daemon_bundled")); + ImGui::SameLine(0, Layout::spacingXs()); + if (bun.available) { + ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str()); + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(dbDim, "(%s)", util::Platform::formatFileSize(bun.size).c_str()); + } else { + ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled")); + } + if (bun.available) { + const bool sameSize = inst.exists && inst.size == bun.size; + ImGui::SameLine(0, Layout::spacingXs()); + ImGui::TextColored(dbDim, "·"); + ImGui::SameLine(0, Layout::spacingXs()); + if (!inst.exists) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing")); + else if (sameSize) + ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match")); + else + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ")); + } - float afterRow2Y = ImGui::GetCursorScreenPos().y; - ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y)); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(TR("rpc_pass")); - ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW); - ImGui::SetNextItemWidth(rpcHalfInputW); - ImGui::InputText("##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), - ImGuiInputTextFlags_Password); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass")); + // In-app node update: download + verify the latest dragonxd from the project Gitea. + // Refresh the cached daemon info once an install has completed. + if (ui::DaemonUpdateDialog::consumeInstalled()) + s_settingsState.daemon_info_loaded = false; - ImGui::SetCursorScreenPos(ImVec2(rpcColX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y))); + // Common action row: Check for updates | Refresh | Test connection. + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) { + ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check")); + ImGui::SameLine(0, spMd); + if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.daemon_info_loaded = false; + } + ImGui::SameLine(0, spMd); + ImGui::BeginDisabled(!app->isConnected()); + if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) { + if (app->rpc() && app->rpc()->isConnected() && app->worker()) { + app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb { + try { + rpc::RPCClient::TraceScope trace("Settings / Test connection"); + rpc->call("getinfo"); + return []() { Notifications::instance().success("RPC connection OK"); }; + } catch (const std::exception& e) { + std::string err = e.what(); + return [err]() { Notifications::instance().error("RPC error: " + err); }; + } + }); + } else { + Notifications::instance().warning("Not connected to daemon"); + } + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); + ImGui::EndDisabled(); - ImGui::SetCursorScreenPos(ImVec2(rpcColX, ImGui::GetCursorScreenPos().y)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), - TR("settings_auto_detected")); - if (s_settingsState.rpc_plaintext_remote) { - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); - ImGui::PushTextWrapPos(rpcColX + rpcColW); - ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS."); - ImGui::PopTextWrapPos(); - ImGui::PopStyleColor(); - } + // The FOUR destructive maintenance actions, in one horizontal row prefixed + // by a visible "Advanced" toggle. Every BeginDisabled/EndDisabled predicate + // is preserved exactly. + auto renderDaemonAdvancedButtons = [&]() { + ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); + if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_reinstall_daemon = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); + ImGui::EndDisabled(); + ImGui::SameLine(0, spMd); + ImGui::BeginDisabled(!app->isConnected()); + if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_rescan = true; + // Re-probe the available block range each time the dialog is opened. + s_settingsState.rescan_height_detecting = false; + s_settingsState.rescan_height_detected = false; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan")); + ImGui::EndDisabled(); + ImGui::SameLine(0, spMd); + ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); + if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_delete_blockchain = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); + ImGui::SameLine(0, spMd); + if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_repair_wallet = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); + ImGui::EndDisabled(); + }; - if (useGrid) { - rpcBottomGrid = ImGui::GetCursorScreenPos().y; - } + // Advanced — a clearly-clickable toggle (label + arrow) on its own line, + // controlling the rare + destructive daemon maintenance actions. When + // expanded the four buttons render on one horizontal row: inline with the + // toggle when there is room, otherwise wrapped to the next full-width row. + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + { + char advBuf[64]; + const char* advArrow = s_settingsState.node_advanced_expanded + ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; + snprintf(advBuf, sizeof(advBuf), "%s %s##NodeAdvancedToggle", + TR("advanced"), advArrow); + // Width of the four Advanced buttons + gaps, to decide inline vs. wrap. + float advBtnsW = spMd * 3; + const char* advLabels[] = { TR("daemon_install_bundled"), TR("rescan"), + TR("delete_blockchain"), TR("repair_wallet") }; + for (const char* l : advLabels) + advBtnsW += ImGui::CalcTextSize(l).x + ImGui::GetStyle().FramePadding.x * 2.0f; + float advToggleW = ImGui::CalcTextSize(advBuf).x + + ImGui::GetStyle().FramePadding.x * 2.0f; + if (TactileButton(advBuf, ImVec2(0, 0), dbBtnFont)) { + s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded; + } + if (s_settingsState.node_advanced_expanded) { + const bool inline_ = (advToggleW + spMd + advBtnsW) <= contentW; + if (inline_) { + ImGui::SameLine(0, spMd); + } else { + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y)); + } + renderDaemonAdvancedButtons(); + } + } } ImGui::PopFont(); + + // Advance to the true bottom of the single column. + ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y)); } - - float leftBottom = useGrid ? leftBottomGrid : ImGui::GetCursorScreenPos().y; - float rpcBottom = useGrid ? rpcBottomGrid : leftBottom; - ImGui::PopClipRect(); - - // Grid: the two top cells share a row bottom; the bottom cells hang below it. - float row1Bottom = std::max(leftBottom, rpcBottom); - - // ---- SECURITY ---- - // Grid: BOTTOM-LEFT cell, under NODE. Stacked: flows below the left column. - // Rebased to the LEFT column (X=leftX, width=leftColW) when gridded. - const float secX = useGrid ? leftX : rightX; - const float secColW = useGrid ? leftColW : rightColW; - float rightTopY = useGrid ? (row1Bottom + Layout::spacingMd()) - : (stacked ? (leftBottom + Layout::spacingMd()) : sectionOrigin.y); - ImGui::SetCursorScreenPos(ImVec2(secX, rightTopY)); - ImGui::PushClipRect(ImVec2(secX, rightTopY), - ImVec2(secX + secColW, rightTopY + 9999), true); - - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security")); - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - - { - // Encrypt / Change passphrase / Lock / Remove — RPC-backed, so full-node - // only. In lite the wallet's encryption controls live in the NODE column - // above (via the lite backend), so this duplicate is hidden. - if (!app->isLiteBuild()) { - bool isEncrypted = app->state().isEncrypted(); - bool isLocked = app->state().isLocked(); - - float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (secColW - Layout::spacingMd()) * 0.5f); - - ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); - if (!isEncrypted) { - if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button"))) - app->showEncryptDialog(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_encrypt")); - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted")); - } else { - if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button"))) - app->showChangePassphraseDialog(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pass")); - ImGui::SameLine(0, Layout::spacingMd()); - if (isLocked) { - ImGui::PushFont(Type().iconSmall()); - ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK); - ImGui::PopFont(); - ImGui::SameLine(0, Layout::spacingXs()); - ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%s", TR("settings_locked")); - } else { - if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button"))) - app->lockWallet(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock")); - ImGui::SameLine(0, Layout::spacingSm()); - ImGui::PushFont(Type().iconSmall()); - ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN); - ImGui::PopFont(); - ImGui::SameLine(0, Layout::spacingXs()); - ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked")); - } - // Remove Encryption button - ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y + Layout::spacingXs())); - if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button"))) - app->showDecryptDialog(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt")); - } - - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - } // !isLiteBuild — RPC-backed encrypt/change/lock/remove block - - // Auto-Lock timeout - { - float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f); - - int timeout = app->settings()->getAutoLockTimeout(); - const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") }; - int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 }; - int selTimeout = 0; - for (int i = 0; i < 6; i++) { - if (timeoutValues[i] == timeout) { selTimeout = i; break; } - } - ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); - Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock")); - ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); - ImGui::PushItemWidth(comboW); - if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) { - app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]); - app->settings()->save(); - } - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock")); - ImGui::PopItemWidth(); - } - - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - - // PIN unlock section - bool isEncryptedPIN = app->state().isEncrypted(); - if (isEncryptedPIN) { - bool hasPIN = app->hasPinVault(); - float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (secColW - Layout::spacingSm()) * 0.5f); - - ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); - if (!hasPIN) { - if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) - app->showPinSetupDialog(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_set_pin")); - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin")); - } else { - if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) - app->showPinChangeDialog(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_change_pin")); - ImGui::SameLine(0, Layout::spacingSm()); - if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) - app->showPinRemoveDialog(); - if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_pin")); - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::PushFont(Type().iconSmall()); - ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD); - ImGui::PopFont(); - ImGui::SameLine(0, Layout::spacingXs()); - ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active")); - } - } else { - ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); - ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin")); - } - } - - float rightBottom = ImGui::GetCursorScreenPos().y; - float secBottom = rightBottom; - ImGui::PopClipRect(); - - // ---- DAEMON BINARY ---- - // Grid: BOTTOM-RIGHT cell (under RPC), sharing the bottom-row top with SECURITY. - // Stacked (narrow): spans full width below everything. (The old masonry pick — - // dropping it into the shorter column — only applied to the abandoned 2-col path.) - float dbBottom = 0.0f; - float gridMaxBottom = row1Bottom; // grid path: set in the daemon block below - if (app->supportsFullNodeLifecycleActions()) { - const bool dbFull = useGrid ? false : stacked; - const bool dbLeftCol = leftBottom <= rightBottom; - const float dbColW = useGrid ? rightColW - : (dbFull ? contentW : (dbLeftCol ? leftColW : rightColW)); - const float dbTopY = useGrid ? (row1Bottom + Layout::spacingMd()) - : (dbFull ? std::max(leftBottom, rightBottom) - : (dbLeftCol ? leftBottom : rightBottom)); - const float dbLeftX = useGrid ? rightX - : (dbFull ? sectionOrigin.x : (dbLeftCol ? leftX : rightX)); - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbTopY)); - ImGui::PushClipRect(ImVec2(dbLeftX, dbTopY), - ImVec2(dbLeftX + dbColW, dbTopY + 9999.0f), true); - - ImFont* dbBtnFont = S.resolveFont("button"); - if (!s_settingsState.daemon_info_loaded) { - s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); - s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo(); - s_settingsState.daemon_info_loaded = true; - } - const auto& inst = s_settingsState.installed_daemon; - const auto& bun = s_settingsState.bundled_daemon; - - auto fmtDate = [](std::int64_t epoch) -> std::string { - if (epoch <= 0) return "—"; - std::time_t t = static_cast(epoch); - std::tm tmv{}; -#ifdef _WIN32 - localtime_s(&tmv, &t); -#else - localtime_r(&t, &tmv); -#endif - char buf[32]; - std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv); - return std::string(buf); - }; - - const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()); - - ImGui::Dummy(ImVec2(0, Layout::spacingLg())); - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - - // Installed | Bundled side by side across the full container width. - const float dbStartY = ImGui::GetCursorScreenPos().y; - const float dbLineH = ImGui::GetTextLineHeightWithSpacing(); - const float dbCol2X = dbLeftX + std::min(dbColW * (useGrid ? 0.5f : 0.4f), useGrid ? 200.0f : 340.0f); - - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY)); - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_installed")); - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH)); - if (inst.exists) { - ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str()); - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH * 2)); - ImGui::TextColored(dbDim, "%s · %s", - util::Platform::formatFileSize(inst.size).c_str(), - fmtDate(inst.modifiedEpoch).c_str()); - } else { - ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed")); - } - - ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY)); - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_bundled")); - ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY + dbLineH)); - if (bun.available) { - ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str()); - ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY + dbLineH * 2)); - ImGui::TextColored(dbDim, "%s", util::Platform::formatFileSize(bun.size).c_str()); - } else { - ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled")); - } - - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH * 3 + Layout::spacingXs())); - if (bun.available) { - const bool sameSize = inst.exists && inst.size == bun.size; - if (!inst.exists) - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing")); - else if (sameSize) - ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match")); - else - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ")); - } - - // In-app node update: download + verify the latest dragonxd from the project Gitea. - // Refresh the cached daemon info once an install has completed. - if (ui::DaemonUpdateDialog::consumeInstalled()) - s_settingsState.daemon_info_loaded = false; - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) { - ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check")); - - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - // Common actions, always visible: Refresh | Test connection. - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y)); - if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) { - s_settingsState.daemon_info_loaded = false; - } - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::BeginDisabled(!app->isConnected()); - if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) { - if (app->rpc() && app->rpc()->isConnected() && app->worker()) { - app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb { - try { - rpc::RPCClient::TraceScope trace("Settings / Test connection"); - rpc->call("getinfo"); - return []() { Notifications::instance().success("RPC connection OK"); }; - } catch (const std::exception& e) { - std::string err = e.what(); - return [err]() { Notifications::instance().error("RPC error: " + err); }; - } - }); - } else { - Notifications::instance().warning("Not connected to daemon"); - } - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); - ImGui::EndDisabled(); - - // The FOUR destructive Advanced buttons, factored so they can render inline - // (stacked) or as a full-width strip (grid). btnW==0 → auto width. Every - // BeginDisabled/EndDisabled predicate is preserved exactly. - auto renderDaemonAdvancedButtons = [&](float btnW) { - ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); - if (TactileButton(TR("daemon_install_bundled"), ImVec2(btnW, 0), dbBtnFont)) { - s_settingsState.confirm_reinstall_daemon = true; - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); - ImGui::EndDisabled(); - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::BeginDisabled(!app->isConnected()); - if (TactileButton(TR("rescan"), ImVec2(btnW, 0), dbBtnFont)) { - s_settingsState.confirm_rescan = true; - // Re-probe the available block range each time the dialog is opened. - s_settingsState.rescan_height_detecting = false; - s_settingsState.rescan_height_detected = false; - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan")); - ImGui::EndDisabled(); - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); - if (TactileButton(TR("delete_blockchain"), ImVec2(btnW, 0), dbBtnFont)) { - s_settingsState.confirm_delete_blockchain = true; - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); - ImGui::SameLine(0, Layout::spacingMd()); - if (TactileButton(TR("repair_wallet"), ImVec2(btnW, 0), dbBtnFont)) { - s_settingsState.confirm_repair_wallet = true; - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); - ImGui::EndDisabled(); - }; - - // Advanced — rare + destructive daemon actions behind a collapsible header - // (reuses the DEBUG LOGGING expander idiom). - ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - { - ImVec2 advPos = ImGui::GetCursorScreenPos(); - const char* advArrow = s_settingsState.node_advanced_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); - if (ImGui::Button("##NodeAdvancedToggle", ImVec2(dbColW, ImGui::GetFrameHeight()))) { - s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded; - } - ImGui::PopStyleColor(3); - ImFont* ovFont = Type().overline(); - float textY = advPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f; - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(advPos.x, textY), OnSurfaceMedium(), TR("advanced")); - ImFont* iconFont = Type().iconSmall(); - if (!iconFont) iconFont = ovFont; - float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, advArrow).x; - dl->AddText(iconFont, iconFont->LegacySize, ImVec2(advPos.x + dbColW - arrowW, textY), OnSurfaceMedium(), advArrow); - } - // Stacked: render the Advanced buttons inline under the header. Grid: they - // drop to a full-width strip below the grid (rendered after the grid closes). - if (!useGrid && s_settingsState.node_advanced_expanded) { - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y)); - renderDaemonAdvancedButtons(0.0f); - } - - dbBottom = ImGui::GetCursorScreenPos().y; - ImGui::PopClipRect(); - if (!useGrid) { - if (dbFull || dbLeftCol) leftBottom = std::max(leftBottom, dbBottom); - if (dbFull || !dbLeftCol) rightBottom = std::max(rightBottom, dbBottom); - } - - // Grid finalization: the Advanced buttons drop to a full-width strip below - // the 2x2 grid, then thin dividers separate the four cells. Done inside the - // daemon block so renderDaemonAdvancedButtons (which captures bun/dbBtnFont) - // is still in scope. useGrid implies this block ran, so this covers it. - if (useGrid) { - float gridBottom = std::max(secBottom, dbBottom); - float advBottom = gridBottom; - if (s_settingsState.node_advanced_expanded) { - float advTopY = gridBottom + gutter; - ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, advTopY)); - float advBtnW = std::floor((contentW - 3 * Layout::spacingMd()) / 4.0f); - renderDaemonAdvancedButtons(advBtnW); - advBottom = ImGui::GetCursorScreenPos().y; - } - gridMaxBottom = std::max(gridBottom, advBottom); - - // Thin, low-alpha dividers separating the four grid cells. - const ImU32 divCol = OnSurfaceDisabled(); - float vDivX = leftX + leftColW + gutter * 0.5f; - dl->AddLine(ImVec2(vDivX, sectionOrigin.y), ImVec2(vDivX, gridBottom), divCol); - dl->AddLine(ImVec2(sectionOrigin.x, row1Bottom), - ImVec2(sectionOrigin.x + contentW, row1Bottom), divCol); - } - } - - // Advance the card cursor past the taller cell/column. - float maxBottom = useGrid ? gridMaxBottom : std::max(leftBottom, rightBottom); - ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom)); } ImGui::Dummy(ImVec2(0, bottomPad));