The daemon's RSS was read on the UI thread: a scan of every /proc entry on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll probed every 1-5 s on every tab, and the Mining tab's memory bar probed again every 1.5 s. util::BackgroundSampler now runs the probe on one lazily started thread and caches the result. getDaemonMemoryUsageMB() returns the last sample and asks for a new one at most every 3 s, and only while the Mining tab memory bar or the Peers tab Memory cell is drawn: nothing on other tabs or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a pid snapshot (on Windows the value read from the owned process handle), so nothing off-thread touches daemon state. The mining poll does no I/O any more; it carries the last sample. Both shutdown paths stop the sampler. Fixed along the way: an unreadable /proc made the directory iterator throw on the UI thread; the scan now just stops. User-visible: - the Peers tab Memory cell keeps updating during sync and shows an external daemon's memory (it showed the mining poll's value, which pauses during sync and covers only the embedded daemon); - the Mining tab memory bar refreshes every ~3 s (was 1.5 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
942 lines
49 KiB
C++
942 lines
49 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "peers_tab.h"
|
|
#include "../../app.h"
|
|
#include "../../data/wallet_state.h"
|
|
#include "../../util/i18n.h"
|
|
#include "mining_tab_helpers.h"
|
|
#include "../theme.h"
|
|
#include "../effects/imgui_acrylic.h"
|
|
#include "../effects/low_spec.h"
|
|
#include "../schema/ui_schema.h"
|
|
#include "../material/type.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "../material/colors.h"
|
|
#include "../layout.h"
|
|
#include "../notifications.h"
|
|
#include "../../embedded/IconsMaterialDesign.h"
|
|
#include "imgui.h"
|
|
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <unordered_set>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
using namespace material;
|
|
|
|
// Track selected peer for ban action
|
|
static int s_selected_peer_idx = -1;
|
|
static int s_selected_banned_idx = -1;
|
|
|
|
// Helper: Extract IP without port
|
|
static std::string ExtractIP(const std::string& addr)
|
|
{
|
|
if (addr.empty()) return addr;
|
|
std::string ip = addr;
|
|
if (ip[0] == '[') {
|
|
auto pos = ip.rfind("]:");
|
|
if (pos != std::string::npos) ip = ip.substr(1, pos - 1);
|
|
} else {
|
|
auto pos = ip.rfind(':');
|
|
if (pos != std::string::npos) ip = ip.substr(0, pos);
|
|
}
|
|
return ip;
|
|
}
|
|
|
|
// Known seed/addnode IPs for the DragonX network.
|
|
// These are the official seed nodes that the daemon connects to on startup.
|
|
static bool IsSeedNode(const std::string& addr) {
|
|
static const std::unordered_set<std::string> seeds = {
|
|
"176.126.87.241", // embedded daemon -addnode
|
|
"94.72.112.24", // node1.hush.is
|
|
"37.60.252.160", // node2.hush.is
|
|
"176.57.70.185", // node3.hush.is / node6.hush.is
|
|
"185.213.209.89", // node4.hush.is
|
|
"137.74.4.198", // node5.hush.is
|
|
"18.193.113.121", // node7.hush.is
|
|
"38.60.224.94", // node8.hush.is
|
|
};
|
|
return seeds.count(ExtractIP(addr)) > 0;
|
|
}
|
|
|
|
// Draw one label/value stat cell in an info card. The caption label sits on the first line
|
|
// (OnSurfaceMedium); the value sits below it (valueFont) — or an em-dash "—" in the disabled
|
|
// color when the value is null/empty. Matches the hand-positioned blocks it replaces exactly:
|
|
// value baseline is ry + capFont->LegacySize + spacingXs().
|
|
static void drawStatCell(ImDrawList* dl, float cx, float ry, ImFont* capFont, ImFont* valueFont,
|
|
const char* label, const char* valueText, ImU32 valueCol)
|
|
{
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), label);
|
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
if (valueText && valueText[0]) {
|
|
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), valueCol, valueText);
|
|
} else {
|
|
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
}
|
|
}
|
|
|
|
// Draw a subtle horizontal divider between info-card rows, inset to the card's rounded corners.
|
|
static void drawCardDivider(ImDrawList* dl, float leftX, float rightX, float y, float rounding, float dp)
|
|
{
|
|
dl->AddLine(ImVec2(leftX + rounding * 0.5f, y),
|
|
ImVec2(rightX - rounding * 0.5f, y),
|
|
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
|
}
|
|
|
|
void RenderPeersTab(App* app)
|
|
{
|
|
auto& S = schema::UI();
|
|
auto peerTable = S.table("tabs.peers", "peer-table");
|
|
auto bannedTable = S.table("tabs.peers", "banned-table");
|
|
const auto& state = app->getWalletState();
|
|
|
|
// Scrollable child to contain all content within available space
|
|
ImVec2 peersAvail = ImGui::GetContentRegionAvail();
|
|
// NoScrollWithMouse: the inner ##PeersList owns wheel scrolling (via ApplySmoothScroll). Without
|
|
// this, a wheel over the list would scroll BOTH the list (smooth-scroll) and this outer container
|
|
// (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
|
|
// Safe because the peer panel is sized to fill the remaining height, so this outer never overflows.
|
|
ImGui::BeginChild("##PeersScroll", peersAvail, false,
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
|
|
// Responsive: scale factors per frame
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
float hs = Layout::hScale(availWidth);
|
|
float pad = Layout::cardInnerPadding();
|
|
float gap = Layout::cardGap();
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
GlassPanelSpec glassSpec;
|
|
glassSpec.rounding = Layout::glassRounding();
|
|
ImFont* ovFont = Type().overline();
|
|
ImFont* capFont = Type().caption();
|
|
ImFont* sub1 = Type().subtitle1();
|
|
ImFont* body2 = Type().body2();
|
|
|
|
char buf[128];
|
|
|
|
// ================================================================
|
|
// BLOCKCHAIN & PEERS CARDS — Side by side
|
|
// ================================================================
|
|
float infoCardsH = 0;
|
|
{
|
|
const auto& mining = state.mining;
|
|
|
|
// Compute peer stats
|
|
int totalPeers = (int)state.peers.size();
|
|
int inboundCount = 0;
|
|
int outboundCount = 0;
|
|
double totalPing = 0;
|
|
int64_t totalBytesSent = 0, totalBytesRecv = 0;
|
|
int tlsCount = 0;
|
|
for (const auto& p : state.peers) {
|
|
if (p.inbound) inboundCount++;
|
|
else outboundCount++;
|
|
totalPing += p.pingtime;
|
|
totalBytesSent += p.bytessent;
|
|
totalBytesRecv += p.bytesrecv;
|
|
if (!p.tls_cipher.empty()) tlsCount++;
|
|
}
|
|
double avgPing = totalPeers > 0 ? (totalPing / totalPeers) * 1000.0 : 0;
|
|
|
|
// Format bytes helper
|
|
auto fmtBytes = [](int64_t bytes) -> std::string {
|
|
char b[32];
|
|
if (bytes >= 1073741824LL)
|
|
snprintf(b, sizeof(b), "%.1f GB", bytes / 1073741824.0);
|
|
else if (bytes >= 1048576LL)
|
|
snprintf(b, sizeof(b), "%.1f MB", bytes / 1048576.0);
|
|
else if (bytes >= 1024LL)
|
|
snprintf(b, sizeof(b), "%.0f KB", bytes / 1024.0);
|
|
else
|
|
snprintf(b, sizeof(b), "%lld B", (long long)bytes);
|
|
return b;
|
|
};
|
|
|
|
// Blockchain card: 5 rows, Peers card: 4 rows (2 cols per row)
|
|
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
|
|
float headerH = ovFont->LegacySize + Layout::spacingSm();
|
|
float dividerH = 1.0f * Layout::dpiScale();
|
|
// Use 5 rows for blockchain card (peers card will have empty space at bottom)
|
|
float cardInnerH = pad * 0.5f + headerH + rowH * 5 + (Layout::spacingSm() + dividerH) * 4 + pad * 0.5f;
|
|
infoCardsH = cardInnerH;
|
|
|
|
float cardW = (availWidth - gap) * 0.5f;
|
|
ImVec2 basePos = ImGui::GetCursorScreenPos();
|
|
float dp = Layout::dpiScale();
|
|
|
|
// ================================================================
|
|
// BLOCKCHAIN CARD (left)
|
|
// ================================================================
|
|
{
|
|
ImVec2 cardMin = basePos;
|
|
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
// Card header
|
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
|
|
|
|
float colW = (cardW - pad * 2) / 2.0f;
|
|
float leftX = cardMin.x + pad;
|
|
float rightX = cardMin.x + pad + colW;
|
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
|
|
|
// Advance ry past a row and draw a divider before the next one. Keeps the row
|
|
// stepping + divider in one place (was a per-row copy + a duplicate divider lambda).
|
|
auto stepRow = [&]() {
|
|
ry += rowH + Layout::spacingSm() * 0.5f;
|
|
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
|
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
};
|
|
|
|
// Row 1: Blocks | Longest Chain
|
|
{
|
|
// Blocks — bespoke: block number + "(X left)" in a second color
|
|
float cx = leftX;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
|
int blocks = state.sync.blocks;
|
|
if (blocks > 0) {
|
|
int chainTip = state.longestchain > 0 ? state.longestchain : state.sync.headers;
|
|
int blocksLeft = chainTip - blocks;
|
|
if (blocksLeft < 0) blocksLeft = 0;
|
|
if (blocksLeft > 0) {
|
|
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), TR("peers_blocks_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");
|
|
}
|
|
|
|
// Longest Chain
|
|
if (state.longestchain > 0) {
|
|
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
|
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
|
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), buf, chainCol);
|
|
} else {
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), nullptr, OnSurface());
|
|
}
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 2: Hashrate | Difficulty
|
|
{
|
|
std::string hr = mining.networkHashrate > 0 ? FormatHashrate(mining.networkHashrate) : std::string();
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_hashrate"),
|
|
hr.empty() ? nullptr : hr.c_str(), Success());
|
|
|
|
if (mining.difficulty > 0) snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("difficulty"),
|
|
mining.difficulty > 0 ? buf : nullptr, OnSurface());
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 3: Notarized | Protocol
|
|
{
|
|
if (state.notarized > 0) snprintf(buf, sizeof(buf), "%d", state.notarized);
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_notarized"),
|
|
state.notarized > 0 ? buf : nullptr, OnSurface());
|
|
|
|
if (state.protocol_version > 0) snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_protocol"),
|
|
state.protocol_version > 0 ? buf : nullptr, OnSurface());
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 4: Version | Memory
|
|
{
|
|
if (state.daemon_version > 0) {
|
|
int major = state.daemon_version / 1000000;
|
|
int minor = (state.daemon_version / 10000) % 100;
|
|
int patch = (state.daemon_version / 100) % 100;
|
|
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
|
|
}
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_version"),
|
|
state.daemon_version > 0 ? buf : nullptr, OnSurface());
|
|
|
|
// The cached off-thread sample (refreshed every ~3 s while this tab or the Mining tab asks). Also
|
|
// keeps updating during sync and shows an external daemon's memory (process scan).
|
|
double memMb = app->getDaemonMemoryUsageMB();
|
|
if (memMb > 0) {
|
|
if (memMb >= 1024.0)
|
|
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
|
else
|
|
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
|
|
}
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_memory"),
|
|
memMb > 0 ? buf : nullptr, OnSurface());
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 5: Longest Chain | Best Block
|
|
{
|
|
// Longest Chain
|
|
if (state.longestchain > 0) {
|
|
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
|
// Color green if local matches longest, warning if behind
|
|
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
|
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), buf, chainCol);
|
|
} else {
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), nullptr, OnSurface());
|
|
}
|
|
|
|
// Best Block — bespoke: truncated hash with click-to-copy + hover underline
|
|
float cx = rightX;
|
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
|
if (!state.sync.best_blockhash.empty()) {
|
|
// Truncate hash to fit: first 6 + "..." + last 6
|
|
std::string hash = state.sync.best_blockhash;
|
|
std::string truncHash;
|
|
if (hash.length() > 15) {
|
|
truncHash = hash.substr(0, 6) + "..." + hash.substr(hash.length() - 6);
|
|
} else {
|
|
truncHash = hash;
|
|
}
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), truncHash.c_str());
|
|
|
|
// Click to copy full hash
|
|
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
|
|
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
|
|
std::string copyTip = std::string(TR("peers_click_copy")) + " " + hash;
|
|
if (material::CopyableTextOverlay("##BestBlockCopy", ImVec2(cx, valY), hashSz,
|
|
sub1->LegacySize, hash.c_str(), copyTip.c_str(),
|
|
ImVec2(Layout::spacingSm(), 2 * dp))) {
|
|
ui::Notifications::instance().info(TR("peers_hash_copied"));
|
|
}
|
|
ImGui::SetCursorScreenPos(savedCursor);
|
|
} else {
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// PEERS CARD (right)
|
|
// ================================================================
|
|
{
|
|
ImVec2 cardMin(basePos.x + cardW + gap, basePos.y);
|
|
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
// Card header
|
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
|
|
|
|
float colW = (cardW - pad * 2) / 2.0f;
|
|
float leftX = cardMin.x + pad;
|
|
float rightX = cardMin.x + pad + colW;
|
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
|
|
|
// Advance ry past a row and draw a divider before the next one (see blockchain card).
|
|
auto stepRow = [&]() {
|
|
ry += rowH + Layout::spacingSm() * 0.5f;
|
|
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
|
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
};
|
|
|
|
// Row 1: Connected | In/Out
|
|
{
|
|
snprintf(buf, sizeof(buf), "%d", totalPeers);
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_connected"), buf, OnSurface());
|
|
|
|
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_in_out"), buf, OnSurface());
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 2: TLS | Avg Ping
|
|
{
|
|
// TLS — bespoke: appends a check icon when every peer is TLS
|
|
float cx = leftX;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
|
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
if (totalPeers > 0) {
|
|
snprintf(buf, sizeof(buf), "%d / %d", tlsCount, totalPeers);
|
|
ImU32 tlsCol = (tlsCount == totalPeers) ? Success() : OnSurface();
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), tlsCol, buf);
|
|
if (tlsCount == totalPeers) {
|
|
ImFont* iconFont = Type().iconSmall();
|
|
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
|
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
|
|
}
|
|
} else {
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
}
|
|
|
|
// Avg Ping — color by latency
|
|
ImU32 pingCol;
|
|
if (avgPing < 100) pingCol = Success();
|
|
else if (avgPing < 500) pingCol = Warning();
|
|
else pingCol = Error();
|
|
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_avg_ping"), buf, pingCol);
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 3: Received | Sent
|
|
{
|
|
std::string recvStr = fmtBytes(totalBytesRecv);
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_received"), recvStr.c_str(), OnSurface());
|
|
|
|
std::string sentStr = fmtBytes(totalBytesSent);
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_sent"), sentStr.c_str(), OnSurface());
|
|
}
|
|
|
|
stepRow();
|
|
|
|
// Row 4: P2P Port | Banned
|
|
{
|
|
if (state.p2p_port > 0) snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_p2p_port"),
|
|
state.p2p_port > 0 ? buf : nullptr, OnSurface());
|
|
|
|
size_t bannedCount = state.bannedPeers.size();
|
|
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
|
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
|
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_banned"), buf, bannedCol);
|
|
}
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(basePos);
|
|
ImGui::Dummy(ImVec2(availWidth, infoCardsH));
|
|
ImGui::Dummy(ImVec2(0, gap));
|
|
}
|
|
|
|
// ================================================================
|
|
// Compute remaining space for peer list + footer
|
|
// ================================================================
|
|
float footerH = ImGui::GetFrameHeight() + Layout::spacingSm();
|
|
float toggleH = body2->LegacySize + Layout::spacingMd() * 2 + Layout::spacingSm();
|
|
float remainForPeers = std::max(60.0f, peersAvail.y - (ImGui::GetCursorScreenPos().y - ImGui::GetWindowPos().y) - footerH - Layout::spacingSm());
|
|
float peerPanelHeight = remainForPeers - toggleH;
|
|
peerPanelHeight = std::max(S.drawElement("tabs.peers", "peer-panel-min-height").size, peerPanelHeight);
|
|
|
|
// ================================================================
|
|
// PEERS — Single glass card with Connected / Banned toggle
|
|
// ================================================================
|
|
static bool s_show_banned = false;
|
|
{
|
|
// Toggle header: "Connected (N)" / "Banned (N)"
|
|
float toggleY = ImGui::GetCursorScreenPos().y;
|
|
{
|
|
char connLabel[64], banLabel[64];
|
|
// Format strings use %d — cast size_t down to int (a 64-bit size_t passed to %d
|
|
// is a format/type mismatch / UB; peer counts comfortably fit in int).
|
|
snprintf(connLabel, sizeof(connLabel), TR("peers_connected_count"), (int)state.peers.size());
|
|
snprintf(banLabel, sizeof(banLabel), TR("peers_banned_count"), (int)state.bannedPeers.size());
|
|
|
|
ImVec2 connSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, connLabel);
|
|
ImVec2 banSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, banLabel);
|
|
|
|
float tabGap = Layout::spacingXl();
|
|
float tabStartX = ImGui::GetCursorScreenPos().x + pad;
|
|
float dp = Layout::dpiScale();
|
|
|
|
// Connected tab
|
|
ImVec2 connPos(tabStartX, toggleY);
|
|
ImU32 connCol = s_show_banned ? OnSurfaceDisabled() : OnSurface();
|
|
dl->AddText(body2, body2->LegacySize, ImVec2(connPos.x, connPos.y + Layout::spacingMd() * 0.5f), connCol, connLabel);
|
|
if (!s_show_banned) {
|
|
float underY = connPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp;
|
|
dl->AddRectFilled(ImVec2(connPos.x, underY), ImVec2(connPos.x + connSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp);
|
|
}
|
|
ImGui::SetCursorScreenPos(connPos);
|
|
if (ImGui::InvisibleButton("##tabConn", ImVec2(connSz.x, toggleH))) {
|
|
s_show_banned = false;
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
// Banned tab
|
|
float banX = tabStartX + connSz.x + tabGap;
|
|
ImVec2 banPos(banX, toggleY);
|
|
ImU32 banCol = s_show_banned ? OnSurface() : OnSurfaceDisabled();
|
|
dl->AddText(body2, body2->LegacySize, ImVec2(banPos.x, banPos.y + Layout::spacingMd() * 0.5f), banCol, banLabel);
|
|
if (s_show_banned) {
|
|
float underY = banPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp;
|
|
dl->AddRectFilled(ImVec2(banPos.x, underY), ImVec2(banPos.x + banSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp);
|
|
}
|
|
ImGui::SetCursorScreenPos(banPos);
|
|
if (ImGui::InvisibleButton("##tabBan", ImVec2(banSz.x, toggleH))) {
|
|
s_show_banned = true;
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
// Refresh button — top-right, glass panel style (similar to mining button)
|
|
{
|
|
bool isRefreshing = app->isPeerRefreshInProgress();
|
|
ImFont* iconFont = Type().iconMed();
|
|
float iconSz = iconFont->LegacySize;
|
|
const char* label = isRefreshing ? TR("peers_refreshing") : TR("peers_refresh");
|
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
|
// Use the wider label so button doesn't resize between states
|
|
ImVec2 altSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0,
|
|
isRefreshing ? TR("peers_refresh") : TR("peers_refreshing"));
|
|
float maxLblW = std::max(lblSz.x, altSz.x);
|
|
float padH = Layout::spacingSm();
|
|
float padV = Layout::spacingSm();
|
|
float btnW = padH + iconSz + Layout::spacingXs() + maxLblW + padH;
|
|
float btnH = std::max(iconSz, lblSz.y) + padV * 2;
|
|
float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm();
|
|
float btnY = toggleY + (toggleH - btnH) * 0.5f;
|
|
ImVec2 bMin(btnX, btnY);
|
|
ImVec2 bMax(btnX + btnW, btnY + btnH);
|
|
|
|
bool btnHovered = material::IsRectHovered(bMin, bMax);
|
|
|
|
// Glass panel background
|
|
GlassPanelSpec btnGlass;
|
|
btnGlass.rounding = Layout::glassRounding();
|
|
if (isRefreshing) {
|
|
float pulse = effects::isLowSpecMode()
|
|
? 0.5f
|
|
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0);
|
|
btnGlass.fillAlpha = (int)(15 + 25 * pulse);
|
|
} else {
|
|
btnGlass.fillAlpha = btnHovered ? 30 : 18;
|
|
}
|
|
DrawGlassPanel(dl, bMin, bMax, btnGlass);
|
|
|
|
// Hover highlight
|
|
if (btnHovered && !isRefreshing) {
|
|
dl->AddRectFilled(bMin, bMax, WithAlpha(Primary(), 20), btnGlass.rounding);
|
|
}
|
|
|
|
// Icon: spinner while refreshing, refresh icon otherwise
|
|
float cx = bMin.x + padH + iconSz * 0.5f;
|
|
float cy = bMin.y + btnH * 0.5f;
|
|
|
|
if (isRefreshing) {
|
|
// Spinning arc spinner (same style as mining toggle)
|
|
float spinnerR = iconSz * 0.5f;
|
|
float thickness = std::max(1.5f, spinnerR * 0.18f);
|
|
float time = (float)ImGui::GetTime();
|
|
|
|
// Track circle (faint)
|
|
dl->AddCircle(ImVec2(cx, cy), spinnerR, WithAlpha(Primary(), 40), 0, thickness);
|
|
|
|
// Animated arc
|
|
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f);
|
|
float cycleTime = fmodf(time, 1.333f);
|
|
float arcLength = (cycleTime < 0.666f)
|
|
? (cycleTime / 0.666f) * 0.75f + 0.1f
|
|
: ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
|
|
float startAngle = rotation - IM_PI * 0.5f;
|
|
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
|
|
int segments = (int)(32 * arcLength) + 1;
|
|
float angleStep = (endAngle - startAngle) / segments;
|
|
ImU32 arcCol = Primary();
|
|
for (int si = 0; si < segments; si++) {
|
|
float a1 = startAngle + angleStep * si;
|
|
float a2 = startAngle + angleStep * (si + 1);
|
|
ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR);
|
|
ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR);
|
|
dl->AddLine(p1, p2, arcCol, thickness);
|
|
}
|
|
} else {
|
|
// Static refresh icon
|
|
ImU32 iconCol = btnHovered ? OnSurface() : OnSurfaceMedium();
|
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
|
|
dl->AddText(iconFont, iconFont->LegacySize,
|
|
ImVec2(cx - iSz.x * 0.5f, cy - iSz.y * 0.5f),
|
|
iconCol, ICON_MD_REFRESH);
|
|
}
|
|
|
|
// Label to the right of icon
|
|
{
|
|
ImU32 lblCol;
|
|
if (isRefreshing) {
|
|
float pulse = effects::isLowSpecMode()
|
|
? 0.7f
|
|
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
|
|
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
|
|
} else {
|
|
lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160);
|
|
}
|
|
float labelAreaX = bMin.x + padH + iconSz + Layout::spacingXs();
|
|
float lblX = labelAreaX + (maxLblW - lblSz.x) * 0.5f;
|
|
float lblY = cy - lblSz.y * 0.5f;
|
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
|
|
}
|
|
|
|
// Invisible button for click handling
|
|
ImGui::SetCursorScreenPos(bMin);
|
|
ImGui::PushID("##peersRefresh");
|
|
if (ImGui::InvisibleButton("##btn", ImVec2(btnW, btnH))) {
|
|
if (!isRefreshing) {
|
|
// Peers only: refreshNow() also forced a full wallet re-scan (every z-address)
|
|
// for a button that says "Refresh peer list".
|
|
app->refreshPeerInfo();
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
if (!isRefreshing)
|
|
material::Tooltip("%s", TR("peers_refresh_tooltip"));
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x, toggleY + toggleH));
|
|
}
|
|
|
|
// Glass panel background
|
|
ImVec2 panelMin = ImGui::GetCursorScreenPos();
|
|
ImVec2 panelMax(panelMin.x + availWidth, panelMin.y + peerPanelHeight);
|
|
DrawGlassPanel(dl, panelMin, panelMax, glassSpec);
|
|
|
|
// Scroll-edge mask state
|
|
float listScrollY = 0.0f, listScrollMaxY = 0.0f;
|
|
int listParentVtx = dl->VtxBuffer.Size;
|
|
|
|
ImGui::BeginChild("##PeersList", ImVec2(availWidth, peerPanelHeight), false,
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
|
ApplySmoothScroll();
|
|
ImDrawList* listChildDL = ImGui::GetWindowDrawList();
|
|
int listChildVtx = listChildDL->VtxBuffer.Size;
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
if (!s_show_banned) {
|
|
// ---- Connected Peers ----
|
|
if (!app->isConnected()) {
|
|
material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
|
|
} else if (state.peers.empty()) {
|
|
material::DrawEmptyState(ICON_MD_WIFI_TETHERING, TR("peers_no_connected"));
|
|
} else {
|
|
const float dp = ui::Layout::dpiScale();
|
|
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
|
|
float rowInset = Layout::spacingLg();
|
|
float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2;
|
|
listScrollY = ImGui::GetScrollY();
|
|
listScrollMaxY = ImGui::GetScrollMaxY();
|
|
|
|
for (size_t i = 0; i < state.peers.size(); i++) {
|
|
const auto& peer = state.peers[i];
|
|
bool is_selected = (s_selected_peer_idx == static_cast<int>(i));
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
ImVec2 rawRowPos = ImGui::GetCursorScreenPos();
|
|
ImVec2 rowPos(rawRowPos.x + rowInset, rawRowPos.y);
|
|
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
|
|
|
if (is_selected) {
|
|
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
|
|
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size * dp);
|
|
}
|
|
|
|
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
|
if (hovered && !is_selected) {
|
|
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
}
|
|
|
|
float cx = rowPos.x + pad;
|
|
float cy = rowPos.y + Layout::spacingSm();
|
|
|
|
double ping_ms = peer.pingtime * 1000.0;
|
|
ImU32 dotCol;
|
|
if (ping_ms < 100) dotCol = Success();
|
|
else if (ping_ms < 500) dotCol = Warning();
|
|
else dotCol = Error();
|
|
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 * dp, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
|
|
|
|
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
|
|
// Reserve the line-1 trailing zone (ping + direction pill live on the far right,
|
|
// the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port
|
|
// addr must clip before it so it can't run under those. Mirror the ping formula
|
|
// conservatively and leave a gap.
|
|
float line1RightLimit = rowPos.x + innerW - Layout::spacingXl() * 3 -
|
|
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "0000ms").x -
|
|
Layout::spacingSm();
|
|
dl->PushClipRect(ImVec2(addrX, rowPos.y), ImVec2(line1RightLimit, rowEnd.y), true);
|
|
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
|
|
dl->PopClipRect();
|
|
|
|
// Seed node icon — rendered right after the IP address, but never past the
|
|
// reserved trailing zone (a long addr would otherwise push it into the ping text).
|
|
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;
|
|
float seedIconX = std::min(addrX + addrSz.x + Layout::spacingSm(),
|
|
line1RightLimit - iconFont->LegacySize);
|
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(seedIconX, iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
|
|
}
|
|
|
|
{
|
|
const char* dirLabel = peer.inbound ? TR("peers_dir_in") : TR("peers_dir_out");
|
|
ImU32 dirBg = peer.inbound ? WithAlpha(Success(), 30) : WithAlpha(Secondary(), 30);
|
|
ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
|
|
ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
|
|
float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl();
|
|
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
|
|
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
|
|
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
|
|
}
|
|
|
|
{
|
|
char pingBuf[32];
|
|
snprintf(pingBuf, sizeof(pingBuf), "%.0fms", ping_ms);
|
|
ImVec2 pingSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pingBuf);
|
|
float pingX = rowPos.x + innerW - pingSz.x - Layout::spacingXl() * 3;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(pingX, cy + 2), dotCol, pingBuf);
|
|
}
|
|
|
|
float cy2 = cy + body2->LegacySize + Layout::spacingXs();
|
|
float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
|
|
|
|
// Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long
|
|
// remote subver cannot push the TLS badge / ban-score off the row. Order right→left:
|
|
// ban-score pinned far right, then the TLS/no-TLS badge, then the clipped subver text.
|
|
float line2RightLimit = rowPos.x + innerW - Layout::spacingLg();
|
|
float banScoreLeftX = line2RightLimit;
|
|
char banBuf[16];
|
|
ImU32 banCol = 0;
|
|
bool haveBanScore = peer.banscore > 0;
|
|
if (haveBanScore) {
|
|
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
|
|
banCol = peer.banscore > 50 ? Error() : Warning();
|
|
float banW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf).x;
|
|
banScoreLeftX = line2RightLimit - banW;
|
|
// The TLS/no-TLS badge sits to the LEFT of the ban score with a gap.
|
|
line2RightLimit = banScoreLeftX - Layout::spacingLg();
|
|
}
|
|
|
|
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
|
|
float noTlsW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("peers_no_tls")).x;
|
|
float badgeW = peer.tls_cipher.empty() ? noTlsW : tlsBadgeW;
|
|
// Hard right boundary for the subver text column, leaving room for the badge.
|
|
float subverMaxX = line2RightLimit - badgeW - Layout::spacingSm();
|
|
if (subverMaxX < subverX) subverMaxX = subverX;
|
|
|
|
float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x;
|
|
dl->PushClipRect(ImVec2(subverX, rowPos.y), ImVec2(subverMaxX, rowEnd.y), true);
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(subverX, cy2),
|
|
OnSurfaceDisabled(), peer.subver.c_str());
|
|
dl->PopClipRect();
|
|
|
|
// Pin the badge just after the (clipped) subver, but never past the reserved zone.
|
|
float badgeX = std::min(subverX + verW + Layout::spacingSm(), subverMaxX + Layout::spacingSm());
|
|
if (!peer.tls_cipher.empty()) {
|
|
ImU32 tlsBg = WithAlpha(Success(), 25);
|
|
ImU32 tlsFg = WithAlpha(Success(), 200);
|
|
ImVec2 tlsMin(badgeX, cy2);
|
|
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
|
|
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
|
|
} else {
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
|
|
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
|
}
|
|
|
|
if (haveBanScore) {
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(banScoreLeftX, cy2), banCol, banBuf);
|
|
}
|
|
|
|
ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH));
|
|
if (ImGui::IsItemClicked(0)) {
|
|
s_selected_peer_idx = static_cast<int>(i);
|
|
}
|
|
|
|
const auto& acrylicTheme = GetCurrentAcrylicTheme();
|
|
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme.menu)) {
|
|
ImGui::Text(TR("peers_peer_label"), peer.addr.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem(TR("copy_address"))) {
|
|
ImGui::SetClipboardText(peer.addr.c_str());
|
|
}
|
|
if (ImGui::MenuItem(TR("peers_copy_ip"))) {
|
|
ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem(TR("peers_ban_24h"))) {
|
|
app->banPeer(ExtractIP(peer.addr), 86400);
|
|
}
|
|
effects::ImGuiAcrylic::EndAcrylicPopup();
|
|
}
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
material::BeginTooltip();
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(8, 3));
|
|
if (ImGui::BeginTable("##PeerTT", 2, ImGuiTableFlags_SizingFixedFit)) {
|
|
auto TTRow = [](const char* label, const char* value) {
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextDisabled("%s", label);
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%s", value);
|
|
};
|
|
char ttBuf[128];
|
|
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.id);
|
|
TTRow(TR("peers_tt_id"), ttBuf);
|
|
TTRow(TR("peers_tt_services"), peer.services.c_str());
|
|
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.startingheight);
|
|
TTRow(TR("peers_tt_start_height"), ttBuf);
|
|
// %lld + (long long) — %ld truncates int64_t on Windows (LLP64).
|
|
snprintf(ttBuf, sizeof(ttBuf), "%lld bytes", (long long)peer.bytessent);
|
|
TTRow(TR("peers_tt_sent"), ttBuf);
|
|
snprintf(ttBuf, sizeof(ttBuf), "%lld bytes", (long long)peer.bytesrecv);
|
|
TTRow(TR("peers_tt_received"), ttBuf);
|
|
snprintf(ttBuf, sizeof(ttBuf), "%d / %d", peer.synced_headers, peer.synced_blocks);
|
|
TTRow(TR("peers_tt_synced"), ttBuf);
|
|
if (!peer.tls_cipher.empty())
|
|
TTRow(TR("peers_tt_tls_cipher"), peer.tls_cipher.c_str());
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
material::EndTooltip();
|
|
}
|
|
|
|
if (i < state.peers.size() - 1) {
|
|
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
|
dl->AddLine(ImVec2(divStart.x + pad + 18 * dp, divStart.y),
|
|
ImVec2(divStart.x + innerW - pad, divStart.y),
|
|
IM_COL32(255, 255, 255, 15));
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
} else {
|
|
// ---- Banned Peers ----
|
|
if (!app->isConnected()) {
|
|
material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
|
|
} else if (state.bannedPeers.empty()) {
|
|
material::DrawEmptyState(ICON_MD_BLOCK, TR("peers_no_banned"));
|
|
} else {
|
|
const float dp = ui::Layout::dpiScale();
|
|
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp;
|
|
float rowInsetB = pad;
|
|
float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2;
|
|
listScrollY = ImGui::GetScrollY();
|
|
listScrollMaxY = ImGui::GetScrollMaxY();
|
|
|
|
for (size_t i = 0; i < state.bannedPeers.size(); i++) {
|
|
const auto& banned = state.bannedPeers[i];
|
|
bool is_selected = (s_selected_banned_idx == static_cast<int>(i));
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
ImVec2 rawRowPosB = ImGui::GetCursorScreenPos();
|
|
ImVec2 rowPos(rawRowPosB.x + rowInsetB, rawRowPosB.y);
|
|
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
|
|
|
if (is_selected) {
|
|
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
|
|
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size * dp);
|
|
}
|
|
|
|
if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) {
|
|
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
|
|
}
|
|
|
|
float cx = rowPos.x + pad;
|
|
float cy = rowPos.y + Layout::spacingXs();
|
|
|
|
float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs;
|
|
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size * dp, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200));
|
|
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size * dp, cy),
|
|
OnSurfaceDisabled(), banned.address.c_str());
|
|
|
|
std::string banUntil = banned.getBannedUntilString();
|
|
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banUntil.c_str());
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingXl() * 5, cy),
|
|
OnSurfaceDisabled(), banUntil.c_str());
|
|
|
|
float btnX = rowPos.x + innerW - Layout::spacingXl() * S.drawElement("tabs.peers", "unban-btn-right-offset-multiplier").size;
|
|
ImGui::SetCursorScreenPos(ImVec2(btnX, cy - 1));
|
|
if (TactileSmallButton(TR("peers_unban"), S.resolveFont("button"))) {
|
|
app->unbanPeer(banned.address);
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(rowPos);
|
|
ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size * dp, rowH));
|
|
if (ImGui::IsItemClicked(0)) {
|
|
s_selected_banned_idx = static_cast<int>(i);
|
|
}
|
|
|
|
const auto& acrylicTheme2 = GetCurrentAcrylicTheme();
|
|
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
|
|
if (ImGui::MenuItem(TR("copy_address"))) {
|
|
ImGui::SetClipboardText(banned.address.c_str());
|
|
}
|
|
if (ImGui::MenuItem(TR("peers_unban"))) {
|
|
app->unbanPeer(banned.address);
|
|
}
|
|
effects::ImGuiAcrylic::EndAcrylicPopup();
|
|
}
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(rawRowPosB.x, rowEnd.y));
|
|
|
|
if (i < state.bannedPeers.size() - 1) {
|
|
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
|
dl->AddLine(ImVec2(divStart.x + pad + 8 * dp, divStart.y),
|
|
ImVec2(divStart.x + innerW - pad, divStart.y),
|
|
IM_COL32(255, 255, 255, 15));
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
ImGui::EndChild();
|
|
|
|
// CSS-style clipping mask
|
|
{
|
|
float fadeFont = s_show_banned ? capFont->LegacySize : body2->LegacySize;
|
|
float fadeZone = std::min(fadeFont * 3.0f, peerPanelHeight * 0.18f);
|
|
ApplyScrollEdgeMask(dl, listParentVtx, listChildDL, listChildVtx,
|
|
panelMin.y, panelMax.y, fadeZone, listScrollY, listScrollMaxY);
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
}
|
|
|
|
// ================================================================
|
|
// Footer — Clear Bans (material styled)
|
|
// ================================================================
|
|
if (s_show_banned && !state.bannedPeers.empty()) {
|
|
ImGui::BeginDisabled(!app->isConnected());
|
|
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button"))) {
|
|
app->clearBans();
|
|
}
|
|
ImGui::EndDisabled();
|
|
}
|
|
|
|
ImGui::EndChild(); // ##PeersScroll
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|