fix: Windows identity, async address creation, mining UI, and chart artifacts
Windows identity: - Add VERSIONINFO resource (.rc) with ObsidianDragon file description - Embed application manifest for DPI awareness and shell identity - Patch libwinpthread/libpthread to remove competing VERSIONINFO - Set AppUserModelID and HWND property store to override Task Manager cache - Link patched pthread libs to eliminate "POSIX WinThreads" description Address creation (+New button): - Move z_getnewaddress/getnewaddress off UI thread to async worker - Inject new address into state immediately for instant UI selection - Trigger background refresh for balance updates Mining tab: - Add pool mining dropdown with saved URLs/workers and bookmarks - Add solo mining log panel from daemon output with chart/log toggle - Fix toggle button cursor (render after InputTextMultiline) - Auto-restart miner on pool config change - Migrate default pool URL to include stratum port Transactions: - Sort pending (0-conf) transactions to top of history - Fall back to timereceived when timestamp is missing Shutdown: - Replace blocking sleep_for calls with 100ms polling loops - Check shutting_down_ flag throughout daemon restart/bootstrap flows - Reduce daemon stop timeout from 30s to 10s Other: - Fix market chart fill artifact (single concave polygon vs per-segment quads) - Add bootstrap checksum verification state display - Rename daemon client identifier to ObsidianDragon
This commit is contained in:
@@ -320,15 +320,13 @@ void RenderMarketTab(App* app)
|
||||
points[i] = ImVec2(x, y);
|
||||
}
|
||||
|
||||
// Fill under curve
|
||||
for (size_t i = 0; i + 1 < n; i++) {
|
||||
ImVec2 quad[4] = {
|
||||
points[i],
|
||||
points[i + 1],
|
||||
ImVec2(points[i + 1].x, plotBottom),
|
||||
ImVec2(points[i].x, plotBottom)
|
||||
};
|
||||
dl->AddConvexPolyFilled(quad, 4, fillCol);
|
||||
// Fill under curve (single concave polygon to avoid AA seam artifacts)
|
||||
if (n >= 2) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
dl->PathLineTo(points[i]);
|
||||
dl->PathLineTo(ImVec2(points[n - 1].x, plotBottom));
|
||||
dl->PathLineTo(ImVec2(points[0].x, plotBottom));
|
||||
dl->PathFillConcave(fillCol);
|
||||
}
|
||||
|
||||
// Line
|
||||
|
||||
@@ -40,11 +40,12 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
|
||||
|
||||
// Pool mode state
|
||||
static bool s_pool_mode = false;
|
||||
static char s_pool_url[256] = "pool.dragonx.is";
|
||||
static char s_pool_url[256] = "pool.dragonx.is:3433";
|
||||
static char s_pool_worker[256] = "x";
|
||||
static bool s_pool_settings_dirty = false;
|
||||
static bool s_pool_state_loaded = false;
|
||||
static bool s_show_pool_log = false; // Toggle: false=chart, true=log
|
||||
static bool s_show_solo_log = false; // Toggle: false=chart, true=log (solo mode)
|
||||
|
||||
// Get max threads based on hardware
|
||||
static int GetMaxMiningThreads()
|
||||
@@ -175,12 +176,21 @@ void RenderMiningTab(App* app)
|
||||
app->settings()->setPoolWorker(s_pool_worker);
|
||||
app->settings()->save();
|
||||
s_pool_settings_dirty = false;
|
||||
|
||||
// Auto-restart pool miner if it is currently running so the new
|
||||
// URL / worker address takes effect immediately.
|
||||
if (state.pool_mining.xmrig_running) {
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(s_selected_threads);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine active mining state for UI
|
||||
// Include pool mining running state even when user just switched to solo,
|
||||
// so the button shows STOP/STOPPING while xmrig shuts down.
|
||||
bool isMiningActive = s_pool_mode
|
||||
? state.pool_mining.xmrig_running
|
||||
: mining.generate;
|
||||
: (mining.generate || state.pool_mining.xmrig_running);
|
||||
|
||||
// ================================================================
|
||||
// Proportional section budget — ensures all content fits without
|
||||
@@ -310,20 +320,193 @@ void RenderMiningTab(App* app)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp));
|
||||
|
||||
// Calculate remaining width from inputs start to end of content region
|
||||
float inputFrameH2 = ImGui::GetFrameHeight();
|
||||
float resetBtnW = inputFrameH2; // Square button matching input height
|
||||
float iconBtnW = inputFrameH2;
|
||||
float resetBtnW = iconBtnW;
|
||||
float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
|
||||
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - Layout::spacingSm();
|
||||
// Each input group: [input][▼][bookmark]
|
||||
// Layout: [URL group] [spacing] [Worker group] [spacing] [reset]
|
||||
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
|
||||
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
|
||||
- Layout::spacingSm() - perGroupExtra * 2;
|
||||
float urlW = std::max(60.0f, remainW * 0.30f);
|
||||
float wrkW = std::max(40.0f, remainW * 0.70f);
|
||||
|
||||
// Track positions for popup alignment
|
||||
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||
float urlGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||
float urlGroupW = urlW + perGroupExtra;
|
||||
|
||||
// === Pool URL input ===
|
||||
ImGui::SetNextItemWidth(urlW);
|
||||
if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) {
|
||||
s_pool_settings_dirty = true;
|
||||
}
|
||||
|
||||
// --- URL: Dropdown arrow button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##PoolDropdown", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Saved pools");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
OnSurfaceMedium(), dropIcon);
|
||||
if (btnClk) {
|
||||
ImGui::OpenPopup("##SavedPoolsPopup");
|
||||
}
|
||||
}
|
||||
|
||||
// --- URL: Bookmark button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##SavePoolUrl", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
std::string currentUrl(s_pool_url);
|
||||
bool alreadySaved = false;
|
||||
for (const auto& u : app->settings()->getSavedPoolUrls()) {
|
||||
if (u == currentUrl) { alreadySaved = true; break; }
|
||||
}
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save pool URL");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
||||
if (btnClk && !currentUrl.empty() && !alreadySaved) {
|
||||
app->settings()->addSavedPoolUrl(currentUrl);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
|
||||
// --- URL: Popup positioned below the input group ---
|
||||
// Match popup width to input group; zero horizontal padding so
|
||||
// item highlights are flush with the popup container edges.
|
||||
ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2));
|
||||
ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
if (ImGui::BeginPopup("##SavedPoolsPopup")) {
|
||||
const auto& savedUrls = app->settings()->getSavedPoolUrls();
|
||||
if (savedUrls.empty()) {
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("No saved pools");
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
std::string urlToRemove;
|
||||
float popupInnerW = ImGui::GetContentRegionAvail().x;
|
||||
float xZoneW = ImGui::GetFrameHeight();
|
||||
float textPadX = 8 * dp;
|
||||
ImFont* rowFont = ImGui::GetFont();
|
||||
float rowFontSz = ImGui::GetFontSize();
|
||||
float rowH = ImGui::GetFrameHeight();
|
||||
for (const auto& url : savedUrls) {
|
||||
ImGui::PushID(url.c_str());
|
||||
bool isCurrent = (std::string(s_pool_url) == url);
|
||||
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
|
||||
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
|
||||
bool rowHov = ImGui::IsItemHovered();
|
||||
bool rowClk = ImGui::IsItemClicked();
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW;
|
||||
// Row background — flush with popup edges
|
||||
if (isCurrent)
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Item text with internal padding
|
||||
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
|
||||
pdl->AddText(rowFont, rowFontSz,
|
||||
ImVec2(rowMin.x + textPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), url.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
|
||||
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Remove");
|
||||
} else if (rowHov) {
|
||||
// Show faint X when row is hovered
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), xIcon);
|
||||
}
|
||||
// Always draw icon when hovering X zone
|
||||
if (inXZone) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
Error(), xIcon);
|
||||
}
|
||||
}
|
||||
// Click handling
|
||||
if (rowClk) {
|
||||
if (inXZone) {
|
||||
urlToRemove = url;
|
||||
} else {
|
||||
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
|
||||
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!urlToRemove.empty()) {
|
||||
app->settings()->removeSavedPoolUrl(urlToRemove);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||
float wrkGroupW = wrkW + perGroupExtra;
|
||||
|
||||
ImGui::SetNextItemWidth(wrkW);
|
||||
if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) {
|
||||
s_pool_settings_dirty = true;
|
||||
@@ -332,7 +515,169 @@ void RenderMiningTab(App* app)
|
||||
ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts");
|
||||
}
|
||||
|
||||
// Reset to defaults button (matching input height)
|
||||
// --- Worker: Dropdown arrow button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##WorkerDropdown", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Saved addresses");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
OnSurfaceMedium(), dropIcon);
|
||||
if (btnClk) {
|
||||
ImGui::OpenPopup("##SavedWorkersPopup");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Worker: Bookmark button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||
ImGui::InvisibleButton("##SaveWorkerAddr", btnSize);
|
||||
bool btnHov = ImGui::IsItemHovered();
|
||||
bool btnClk = ImGui::IsItemClicked();
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
std::string currentWorker(s_pool_worker);
|
||||
bool alreadySaved = false;
|
||||
for (const auto& w : app->settings()->getSavedPoolWorkers()) {
|
||||
if (w == currentWorker) { alreadySaved = true; break; }
|
||||
}
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save payout address");
|
||||
}
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
||||
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
||||
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
|
||||
app->settings()->addSavedPoolWorker(currentWorker);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Worker: Popup positioned below the input group ---
|
||||
// Popup sized to fit full z-addresses without truncation;
|
||||
// zero horizontal padding so item highlights are flush with edges.
|
||||
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
|
||||
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
|
||||
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
if (ImGui::BeginPopup("##SavedWorkersPopup")) {
|
||||
const auto& savedWorkers = app->settings()->getSavedPoolWorkers();
|
||||
if (savedWorkers.empty()) {
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("No saved addresses");
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(8 * dp);
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
std::string addrToRemove;
|
||||
float wPopupInnerW = ImGui::GetContentRegionAvail().x;
|
||||
float wXZoneW = ImGui::GetFrameHeight();
|
||||
float wTextPadX = 8 * dp;
|
||||
ImFont* wRowFont = ImGui::GetFont();
|
||||
float wRowFontSz = ImGui::GetFontSize();
|
||||
float wRowH = ImGui::GetFrameHeight();
|
||||
for (const auto& addr : savedWorkers) {
|
||||
ImGui::PushID(addr.c_str());
|
||||
bool isCurrent = (std::string(s_pool_worker) == addr);
|
||||
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
|
||||
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
|
||||
bool rowHov = ImGui::IsItemHovered();
|
||||
bool rowClk = ImGui::IsItemClicked();
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW;
|
||||
// Row background — flush with popup edges
|
||||
if (isCurrent)
|
||||
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||
if (rowHov && !inXZone)
|
||||
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||
// Full address text with internal padding
|
||||
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
|
||||
pdl->AddText(wRowFont, wRowFontSz,
|
||||
ImVec2(rowMin.x + wTextPadX, textY),
|
||||
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
||||
// Tooltip for long addresses
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetTooltip("%s", addr.c_str());
|
||||
// X button — flush with right edge, icon centered
|
||||
{
|
||||
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
|
||||
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||
if (inXZone) {
|
||||
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("Remove");
|
||||
} else if (rowHov) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), xIcon);
|
||||
}
|
||||
if (inXZone) {
|
||||
ImFont* icoF = Type().iconSmall();
|
||||
const char* xIcon = ICON_MD_CLOSE;
|
||||
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||
pdl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||
Error(), xIcon);
|
||||
}
|
||||
}
|
||||
// Click handling
|
||||
if (rowClk) {
|
||||
if (inXZone) {
|
||||
addrToRemove = addr;
|
||||
} else {
|
||||
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
if (rowHov && !inXZone)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!addrToRemove.empty()) {
|
||||
app->settings()->removeSavedPoolWorker(addrToRemove);
|
||||
app->settings()->save();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup
|
||||
|
||||
// === Reset to defaults button ===
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
{
|
||||
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||
@@ -612,9 +957,12 @@ void RenderMiningTab(App* app)
|
||||
bool isToggling = app->isMiningToggleInProgress();
|
||||
// Pool mining connects to an external pool via xmrig — it does not
|
||||
// need the local blockchain synced or even the daemon connected.
|
||||
// If pool mining is still shutting down after switching to solo,
|
||||
// keep the button enabled so user can stop it.
|
||||
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
|
||||
bool disabled = s_pool_mode
|
||||
? (isToggling || poolBlockedBySolo)
|
||||
: (!app->isConnected() || isToggling || isSyncing);
|
||||
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
|
||||
|
||||
// Glass panel background with state-dependent tint
|
||||
GlassPanelSpec btnGlass;
|
||||
@@ -755,7 +1103,11 @@ void RenderMiningTab(App* app)
|
||||
else
|
||||
app->startPoolMining(s_selected_threads);
|
||||
} else {
|
||||
if (mining.generate)
|
||||
// If pool mining is still running (user just switched from pool to solo),
|
||||
// stop pool mining first
|
||||
if (state.pool_mining.xmrig_running)
|
||||
app->stopPoolMining();
|
||||
else if (mining.generate)
|
||||
app->stopMining();
|
||||
else
|
||||
app->startMining(s_selected_threads);
|
||||
@@ -776,8 +1128,12 @@ void RenderMiningTab(App* app)
|
||||
ImU32 greenCol = Success();
|
||||
|
||||
// Determine view mode first
|
||||
bool showLogView = s_pool_mode && s_show_pool_log && !state.pool_mining.log_lines.empty();
|
||||
bool hasLogContent = s_pool_mode && !state.pool_mining.log_lines.empty();
|
||||
bool showLogView = s_pool_mode
|
||||
? (s_show_pool_log && !state.pool_mining.log_lines.empty())
|
||||
: (s_show_solo_log && !mining.log_lines.empty());
|
||||
bool hasLogContent = s_pool_mode
|
||||
? !state.pool_mining.log_lines.empty()
|
||||
: !mining.log_lines.empty();
|
||||
// Use pool hashrate history when in pool mode, solo otherwise
|
||||
const std::vector<double>& chartHistory = s_pool_mode
|
||||
? state.pool_mining.hashrate_history
|
||||
@@ -792,60 +1148,44 @@ void RenderMiningTab(App* app)
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||
|
||||
// --- Toggle button in top-right corner (pool mode only) ---
|
||||
if (s_pool_mode && (hasLogContent || hasChartContent)) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* toggleIcon = s_show_pool_log ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
||||
const char* toggleTip = s_show_pool_log ? "Show hashrate chart" : "Show miner log";
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
|
||||
float btnSize = iconSz.y + 8 * dp;
|
||||
float btnX = cardMax.x - pad - btnSize;
|
||||
float btnY = cardMin.y + pad * 0.5f;
|
||||
ImVec2 btnMin(btnX, btnY);
|
||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
||||
|
||||
bool hov = IsRectHovered(btnMin, btnMax);
|
||||
if (hov) {
|
||||
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", toggleTip);
|
||||
}
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||
OnSurfaceMedium(), toggleIcon);
|
||||
if (hov && ImGui::IsMouseClicked(0)) {
|
||||
s_show_pool_log = !s_show_pool_log;
|
||||
}
|
||||
}
|
||||
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
|
||||
|
||||
if (showLogView) {
|
||||
// --- Full-card log view ---
|
||||
// --- Full-card log view (selectable + copyable) ---
|
||||
const std::vector<std::string>& logLines = s_pool_mode
|
||||
? state.pool_mining.log_lines
|
||||
: mining.log_lines;
|
||||
|
||||
// Build a single string buffer for InputTextMultiline
|
||||
static std::string s_log_buf;
|
||||
s_log_buf.clear();
|
||||
for (const auto& line : logLines) {
|
||||
if (!line.empty()) {
|
||||
s_log_buf += line;
|
||||
s_log_buf += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
float logPad = pad * 0.5f;
|
||||
float logW = availWidth - logPad * 2;
|
||||
float logH = totalCardH - logPad * 2;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
|
||||
ImGui::BeginChild("##PoolLogInCard", ImVec2(availWidth - logPad * 2, totalCardH - logPad * 2), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_HorizontalScrollbar);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
|
||||
ImFont* monoFont = Type().body2();
|
||||
ImGui::PushFont(monoFont);
|
||||
for (const auto& line : state.pool_mining.log_lines) {
|
||||
if (!line.empty())
|
||||
ImGui::TextUnformatted(line.c_str());
|
||||
}
|
||||
|
||||
const char* inputId = s_pool_mode ? "##PoolLogText" : "##SoloLogText";
|
||||
ImGui::InputTextMultiline(inputId,
|
||||
const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
|
||||
ImVec2(logW, logH),
|
||||
ImGuiInputTextFlags_ReadOnly);
|
||||
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
// Auto-scroll to bottom only if user is already near the bottom
|
||||
// This allows manual scrolling up to read history
|
||||
float scrollY = ImGui::GetScrollY();
|
||||
float scrollMaxY = ImGui::GetScrollMaxY();
|
||||
if (scrollMaxY <= 0.0f || scrollY >= scrollMaxY - 20.0f * dp)
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
// Reset cursor to end of card after the child window
|
||||
// Reset cursor to end of card after the input
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system
|
||||
} else {
|
||||
@@ -1075,6 +1415,35 @@ void RenderMiningTab(App* app)
|
||||
ImGui::Dummy(ImVec2(availWidth, totalCardH));
|
||||
}
|
||||
|
||||
// --- Toggle button in top-right corner ---
|
||||
// Rendered after content so the Hand cursor takes priority over
|
||||
// the InputTextMultiline text-cursor when hovering the button.
|
||||
if (hasLogContent || hasChartContent) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
||||
const char* toggleTip = showLogFlag ? "Show hashrate chart" : "Show mining log";
|
||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
|
||||
float btnSize = iconSz.y + 8 * dp;
|
||||
float btnX = cardMax.x - pad - btnSize;
|
||||
float btnY = cardMin.y + pad * 0.5f;
|
||||
ImVec2 btnMin(btnX, btnY);
|
||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
||||
|
||||
bool hov = IsRectHovered(btnMin, btnMax);
|
||||
if (hov) {
|
||||
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", toggleTip);
|
||||
}
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||
OnSurfaceMedium(), toggleIcon);
|
||||
if (hov && ImGui::IsMouseClicked(0)) {
|
||||
showLogFlag = !showLogFlag;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
@@ -1106,11 +1475,17 @@ void RenderMiningTab(App* app)
|
||||
double amount;
|
||||
int confirmations;
|
||||
bool mature;
|
||||
std::string txid;
|
||||
bool isPoolPayout;
|
||||
};
|
||||
std::vector<MinedTx> recentMined;
|
||||
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") {
|
||||
bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined");
|
||||
bool isPoolPayout = (tx.type == "receive"
|
||||
&& !tx.memo.empty()
|
||||
&& tx.memo.find("Mining Pool payout") != std::string::npos);
|
||||
if (isSoloMined || isPoolPayout) {
|
||||
double amt = std::abs(tx.amount);
|
||||
minedAllTime += amt;
|
||||
minedAllTimeCount++;
|
||||
@@ -1121,8 +1496,10 @@ void RenderMiningTab(App* app)
|
||||
minedYesterday += amt;
|
||||
minedYesterdayCount++;
|
||||
}
|
||||
if (recentMined.size() < 4) {
|
||||
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100});
|
||||
// Separate solo blocks from pool payouts based on current mode
|
||||
bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined;
|
||||
if (showInCurrentMode && recentMined.size() < 4) {
|
||||
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1164,18 +1541,18 @@ void RenderMiningTab(App* app)
|
||||
};
|
||||
|
||||
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
|
||||
snprintf(subBuf2, sizeof(subBuf2), "(%d blk)", minedTodayCount);
|
||||
snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount);
|
||||
char todayVal[64], todaySub[64];
|
||||
strncpy(todayVal, valBuf, sizeof(todayVal));
|
||||
strncpy(todaySub, subBuf2, sizeof(todaySub));
|
||||
|
||||
char yesterdayVal[64], yesterdaySub[64];
|
||||
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
|
||||
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d blk)", minedYesterdayCount);
|
||||
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount);
|
||||
|
||||
char allVal[64], allSub[64];
|
||||
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
|
||||
snprintf(allSub, sizeof(allSub), "(%d blk)", minedAllTimeCount);
|
||||
snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount);
|
||||
|
||||
char estVal[64];
|
||||
if (estActive)
|
||||
@@ -1500,16 +1877,18 @@ void RenderMiningTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ============================================================
|
||||
// RECENT BLOCKS — last 4 mined blocks
|
||||
// RECENT BLOCKS — last 4 mined blocks (always shown in pool mode)
|
||||
// ============================================================
|
||||
if (!recentMined.empty()) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT BLOCKS");
|
||||
if (!recentMined.empty() || s_pool_mode) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
|
||||
s_pool_mode ? "RECENT POOL PAYOUTS" : "RECENT BLOCKS");
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
|
||||
// Size to remaining space — proportional budget ensures fit
|
||||
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
|
||||
float contentH_blocks = rowH_blocks * (float)recentMined.size() + pad * 2.5f;
|
||||
float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size();
|
||||
float contentH_blocks = rowH_blocks * minRows + pad * 2.5f;
|
||||
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
|
||||
|
||||
// Glass panel wrapping the list + scroll-edge mask state
|
||||
@@ -1533,6 +1912,26 @@ void RenderMiningTab(App* app)
|
||||
// Top padding inside glass card
|
||||
ImGui::Dummy(ImVec2(0, pad * 0.5f));
|
||||
|
||||
if (recentMined.empty()) {
|
||||
// Empty state — card is visible but no rows yet
|
||||
float emptyY = ImGui::GetCursorScreenPos().y;
|
||||
float emptyX = ImGui::GetCursorScreenPos().x;
|
||||
float centerX = emptyX + availWidth * 0.5f;
|
||||
ImFont* icoFont = Type().iconMed();
|
||||
const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY;
|
||||
ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon);
|
||||
miningChildDL->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(centerX - iSz.x * 0.5f, emptyY),
|
||||
OnSurfaceDisabled(), emptyIcon);
|
||||
const char* emptyMsg = s_pool_mode
|
||||
? "No pool payouts yet"
|
||||
: "No blocks found yet";
|
||||
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg);
|
||||
miningChildDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), emptyMsg);
|
||||
}
|
||||
|
||||
for (size_t mi = 0; mi < recentMined.size(); mi++) {
|
||||
const auto& mtx = recentMined[mi];
|
||||
|
||||
@@ -1542,10 +1941,14 @@ void RenderMiningTab(App* app)
|
||||
|
||||
// Subtle background on hover (inset from card edges)
|
||||
bool hovered = material::IsRectHovered(rMin, rMax);
|
||||
bool isClickable = !mtx.txid.empty();
|
||||
if (hovered) {
|
||||
dl->AddRectFilled(ImVec2(rMin.x + pad * 0.5f, rMin.y),
|
||||
ImVec2(rMax.x - pad * 0.5f, rMax.y),
|
||||
IM_COL32(255, 255, 255, 8), 3.0f * dp);
|
||||
if (isClickable) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
}
|
||||
|
||||
float rx = rMin.x + pad;
|
||||
@@ -1588,7 +1991,18 @@ void RenderMiningTab(App* app)
|
||||
WithAlpha(Warning(), 200), buf);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(availWidth, rH));
|
||||
// Click to open in block explorer
|
||||
ImGui::SetCursorScreenPos(rMin);
|
||||
char blockBtnId[32];
|
||||
snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi);
|
||||
ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH));
|
||||
if (ImGui::IsItemClicked() && !mtx.txid.empty()) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + mtx.txid;
|
||||
dragonx::util::Platform::openUrl(url);
|
||||
}
|
||||
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
|
||||
ImGui::SetTooltip("Open in explorer");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild(); // ##RecentBlocks
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -45,6 +46,22 @@ static std::string ExtractIP(const std::string& addr)
|
||||
return ip;
|
||||
}
|
||||
|
||||
// Known seed/addnode IPs for the DragonX network.
|
||||
// These are the official seed nodes that the daemon connects to on startup.
|
||||
static bool IsSeedNode(const std::string& addr) {
|
||||
static const std::unordered_set<std::string> seeds = {
|
||||
"176.126.87.241", // embedded daemon -addnode
|
||||
"94.72.112.24", // node1.hush.is
|
||||
"37.60.252.160", // node2.hush.is
|
||||
"176.57.70.185", // node3.hush.is / node6.hush.is
|
||||
"185.213.209.89", // node4.hush.is
|
||||
"137.74.4.198", // node5.hush.is
|
||||
"18.193.113.121", // node7.hush.is
|
||||
"38.60.224.94", // node8.hush.is
|
||||
};
|
||||
return seeds.count(ExtractIP(addr)) > 0;
|
||||
}
|
||||
|
||||
void RenderPeersTab(App* app)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
@@ -149,10 +166,28 @@ void RenderPeersTab(App* app)
|
||||
// Blocks
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks");
|
||||
int blocks = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||
int blocks = state.sync.blocks;
|
||||
if (blocks > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", blocks);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
int blocksLeft = state.sync.headers - blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
if (blocksLeft > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
// Draw block number in normal color
|
||||
char blockStr[32];
|
||||
snprintf(blockStr, sizeof(blockStr), "%d ", blocks);
|
||||
ImVec2 numSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, blockStr);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr);
|
||||
// Draw "(X left)" in warning color
|
||||
char leftStr[32];
|
||||
snprintf(leftStr, sizeof(leftStr), "(%d left)", blocksLeft);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
||||
Warning(), leftStr);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%d", blocks);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
}
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
@@ -680,7 +715,16 @@ void RenderPeersTab(App* app)
|
||||
float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
|
||||
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
|
||||
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy), OnSurface(), peer.addr.c_str());
|
||||
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
|
||||
|
||||
// Seed node icon — rendered right after the IP address
|
||||
if (IsSeedNode(peer.addr)) {
|
||||
ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
|
||||
}
|
||||
|
||||
{
|
||||
const char* dirLabel = peer.inbound ? "In" : "Out";
|
||||
|
||||
@@ -393,9 +393,12 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp descending (same as raw list)
|
||||
// Sort: pending (0-conf) transactions first, then by timestamp descending
|
||||
std::sort(display_txns.begin(), display_txns.end(),
|
||||
[](const DisplayTx& a, const DisplayTx& b) {
|
||||
bool aPending = (a.confirmations == 0);
|
||||
bool bPending = (b.confirmations == 0);
|
||||
if (aPending != bPending) return aPending;
|
||||
return a.timestamp > b.timestamp;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user