Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include "../../app.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -121,33 +122,15 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
|
||||
// Links
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://dragonx.is");
|
||||
#elif __APPLE__
|
||||
system("open https://dragonx.is");
|
||||
#else
|
||||
system("xdg-open https://dragonx.is &");
|
||||
#endif
|
||||
util::Platform::openUrl("https://dragonx.is");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
#elif __APPLE__
|
||||
system("open https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
#else
|
||||
system("xdg-open https://git.dragonx.is/dragonx/ObsidianDragon &");
|
||||
#endif
|
||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://explorer.dragonx.is");
|
||||
#elif __APPLE__
|
||||
system("open https://explorer.dragonx.is");
|
||||
#else
|
||||
system("xdg-open https://explorer.dragonx.is &");
|
||||
#endif
|
||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace dragonx {
|
||||
@@ -35,6 +36,12 @@ static data::AddressBook& getAddressBook() {
|
||||
return *s_address_book;
|
||||
}
|
||||
|
||||
static void copyEditField(char* dest, size_t destSize, const std::string& source) {
|
||||
if (destSize == 0) return;
|
||||
std::strncpy(dest, source.c_str(), destSize - 1);
|
||||
dest[destSize - 1] = '\0';
|
||||
}
|
||||
|
||||
void AddressBookDialog::show()
|
||||
{
|
||||
s_open = true;
|
||||
@@ -66,15 +73,101 @@ void AddressBookDialog::render(App* app)
|
||||
auto notesInput = S.input("dialogs.address-book", "notes-input");
|
||||
auto actionBtn = S.button("dialogs.address-book", "action-button");
|
||||
|
||||
auto clearEditFields = []() {
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
s_edit_notes[0] = '\0';
|
||||
};
|
||||
|
||||
auto loadEditFields = [](const data::AddressBookEntry& entry) {
|
||||
copyEditField(s_edit_label, sizeof(s_edit_label), entry.label);
|
||||
copyEditField(s_edit_address, sizeof(s_edit_address), entry.address);
|
||||
copyEditField(s_edit_notes, sizeof(s_edit_notes), entry.notes);
|
||||
};
|
||||
|
||||
auto renderEntryDialog = [&]() {
|
||||
bool isEdit = s_show_edit_dialog;
|
||||
bool* open = isEdit ? &s_show_edit_dialog : &s_show_add_dialog;
|
||||
if (!*open) return;
|
||||
|
||||
const char* title = isEdit ? TR("address_book_edit") : TR("address_book_add");
|
||||
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
|
||||
float dialogW = std::max(Layout::kDialogMinWidth(), Layout::kDialogDefaultWidth());
|
||||
float formW = addrInput.width > 0 ? addrInput.width : Layout::kDialogFormWidth();
|
||||
float actionW = actionBtn.width > 0 ? actionBtn.width : Layout::kDialogActionWidth();
|
||||
float actionGap = actionBtn.gap > 0 ? actionBtn.gap : Layout::kDialogActionGap();
|
||||
float notesH = notesInput.height > 0 ? notesInput.height : 60.0f;
|
||||
|
||||
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
|
||||
Layout::kDialogCompactBottomRatio(), id)) {
|
||||
ImGui::Text("%s", TR("label"));
|
||||
ImGui::SetNextItemWidth(formW);
|
||||
ImGui::InputText(isEdit ? "##EditLabel" : "##AddLabel", s_edit_label, sizeof(s_edit_label));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
ImGui::SetNextItemWidth(formW);
|
||||
ImGui::InputText(isEdit ? "##EditAddress" : "##AddAddress", s_edit_address, sizeof(s_edit_address));
|
||||
if (!isEdit) {
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) copyEditField(s_edit_address, sizeof(s_edit_address), clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("notes_optional"));
|
||||
ImGui::SetNextItemWidth(formW);
|
||||
ImGui::InputTextMultiline(isEdit ? "##EditNotes" : "##AddNotes",
|
||||
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
|
||||
|
||||
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
|
||||
float totalActionsW = actionW * 2.0f + actionGap;
|
||||
material::BeginOverlayDialogFooter(totalActionsW);
|
||||
|
||||
if (!canSubmit) ImGui::BeginDisabled();
|
||||
|
||||
const char* primaryLabel = isEdit ? TR("save") : TR("add");
|
||||
if (material::StyledButton(primaryLabel, ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
||||
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
|
||||
if (isEdit) {
|
||||
if (getAddressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
s_show_edit_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_update_failed"));
|
||||
}
|
||||
} else {
|
||||
if (getAddressBook().addEntry(entry)) {
|
||||
Notifications::instance().success(TR("address_book_added"));
|
||||
s_show_add_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!canSubmit) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine(0, actionGap);
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
||||
*open = false;
|
||||
}
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
};
|
||||
|
||||
if (material::BeginOverlayDialog(TR("address_book_title"), &s_open, win.width, 0.94f)) {
|
||||
auto& book = getAddressBook();
|
||||
|
||||
// Toolbar
|
||||
if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_add_dialog = true;
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
s_edit_notes[0] = '\0';
|
||||
clearEditFields();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -86,9 +179,7 @@ void AddressBookDialog::render(App* app)
|
||||
if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
const auto& entry = book.entries()[s_selected_index];
|
||||
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1);
|
||||
strncpy(s_edit_address, entry.address.c_str(), sizeof(s_edit_address) - 1);
|
||||
strncpy(s_edit_notes, entry.notes.c_str(), sizeof(s_edit_notes) - 1);
|
||||
loadEditFields(entry);
|
||||
s_show_edit_dialog = true;
|
||||
}
|
||||
}
|
||||
@@ -153,9 +244,7 @@ void AddressBookDialog::render(App* app)
|
||||
// Double-click to edit
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
s_selected_index = static_cast<int>(i);
|
||||
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1);
|
||||
strncpy(s_edit_address, entry.address.c_str(), sizeof(s_edit_address) - 1);
|
||||
strncpy(s_edit_notes, entry.notes.c_str(), sizeof(s_edit_notes) - 1);
|
||||
loadEditFields(entry);
|
||||
s_show_edit_dialog = true;
|
||||
}
|
||||
|
||||
@@ -186,123 +275,8 @@ void AddressBookDialog::render(App* app)
|
||||
ImGui::TextDisabled(TR("address_book_count"), book.size());
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Add dialog
|
||||
if (s_show_add_dialog) {
|
||||
ImGui::OpenPopup("Add Address");
|
||||
}
|
||||
|
||||
// Re-use center for sub-dialogs
|
||||
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
if (ImGui::BeginPopupModal("Add Address", &s_show_add_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
material::Type().text(material::TypeStyle::H6, TR("address_book_add"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::Text("%s", TR("label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##AddLabel", s_edit_label, sizeof(s_edit_label));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##AddAddress", s_edit_address, sizeof(s_edit_address));
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) {
|
||||
strncpy(s_edit_address, clipboard, sizeof(s_edit_address) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("notes_optional"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputTextMultiline("##AddNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
bool can_add = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
|
||||
if (!can_add) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton(TR("add"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
|
||||
if (getAddressBook().addEntry(entry)) {
|
||||
Notifications::instance().success(TR("address_book_added"));
|
||||
s_show_add_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!can_add) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_add_dialog = false;
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Edit dialog
|
||||
if (s_show_edit_dialog) {
|
||||
ImGui::OpenPopup("Edit Address");
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
if (ImGui::BeginPopupModal("Edit Address", &s_show_edit_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
material::Type().text(material::TypeStyle::H6, TR("address_book_edit"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
ImGui::Text("%s", TR("label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##EditLabel", s_edit_label, sizeof(s_edit_label));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("address_label"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputText("##EditAddress", s_edit_address, sizeof(s_edit_address));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("notes_optional"));
|
||||
ImGui::SetNextItemWidth(addrInput.width);
|
||||
ImGui::InputTextMultiline("##EditNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
bool can_save = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
|
||||
if (!can_save) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton(TR("save"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
|
||||
if (getAddressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
s_show_edit_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_update_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!can_save) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_edit_dialog = false;
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
renderEntryDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/typography.h"
|
||||
#include "../material/project_icons.h"
|
||||
#include "../theme.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
@@ -22,8 +21,6 @@ namespace ui {
|
||||
|
||||
class AddressLabelDialog {
|
||||
public:
|
||||
static constexpr const char* kPickaxeGlyph = "\xEE\x80\x81";
|
||||
|
||||
static bool drawIconByName(ImDrawList* dl,
|
||||
const std::string& name,
|
||||
ImVec2 center,
|
||||
@@ -31,23 +28,7 @@ public:
|
||||
ImU32 color,
|
||||
ImFont* iconFont,
|
||||
float iconFontSize) {
|
||||
if (name == "pickaxe") {
|
||||
ImFont* pickaxeFont = material::Typography::instance().pickaxeFontForSize(iconFontSize);
|
||||
if (!pickaxeFont) return false;
|
||||
|
||||
ImVec2 iSz = pickaxeFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, kPickaxeGlyph);
|
||||
dl->AddText(pickaxeFont, iconFontSize,
|
||||
ImVec2(center.x - iSz.x * 0.5f, center.y - iSz.y * 0.5f), color, kPickaxeGlyph);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* glyph = iconGlyphForName(name);
|
||||
if (!glyph || !iconFont) return false;
|
||||
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, glyph);
|
||||
dl->AddText(iconFont, iconFontSize,
|
||||
ImVec2(center.x - iSz.x * 0.5f, center.y - iSz.y * 0.5f), color, glyph);
|
||||
return true;
|
||||
return material::project_icons::drawByName(dl, name, center, color, iconFont, iconFontSize);
|
||||
}
|
||||
|
||||
static void show(App* app, const std::string& address, bool isZ) {
|
||||
@@ -64,8 +45,8 @@ public:
|
||||
s_label[sizeof(s_label) - 1] = '\0';
|
||||
|
||||
std::string existingIcon = app->getAddressIcon(address);
|
||||
for (int i = 0; i < kIconCount; ++i) {
|
||||
if (kIconNames[i] == existingIcon) {
|
||||
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
|
||||
if (material::project_icons::walletIconName(i) == existingIcon) {
|
||||
s_selectedIcon = i;
|
||||
break;
|
||||
}
|
||||
@@ -134,13 +115,14 @@ public:
|
||||
|
||||
// Build filtered index list
|
||||
std::vector<int> visible;
|
||||
visible.reserve(kIconCount);
|
||||
visible.reserve(material::project_icons::walletIconCount());
|
||||
{
|
||||
// Simple case-insensitive substring match on icon name
|
||||
std::string needle(s_iconSearch);
|
||||
for (char& c : needle) c = (char)std::tolower((unsigned char)c);
|
||||
for (int i = 0; i < kIconCount; ++i) {
|
||||
if (needle.empty() || std::strstr(kIconNames[i], needle.c_str()) != nullptr)
|
||||
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
|
||||
const char* iconName = material::project_icons::walletIconName(i);
|
||||
if (needle.empty() || std::strstr(iconName, needle.c_str()) != nullptr)
|
||||
visible.push_back(i);
|
||||
}
|
||||
}
|
||||
@@ -162,6 +144,7 @@ public:
|
||||
int col = 0;
|
||||
for (int vi = 0; vi < (int)visible.size(); ++vi) {
|
||||
int i = visible[vi];
|
||||
const char* iconName = material::project_icons::walletIconName(i);
|
||||
if (col != 0) ImGui::SameLine(0, 4.0f * dp);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 mn = pos;
|
||||
@@ -179,7 +162,7 @@ public:
|
||||
|
||||
// Icon centered in cell
|
||||
drawIconByName(dl,
|
||||
kIconNames[i],
|
||||
iconName,
|
||||
ImVec2(mn.x + cellSz * 0.5f, mn.y + cellSz * 0.5f),
|
||||
iconFsz,
|
||||
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||
@@ -189,7 +172,7 @@ public:
|
||||
ImGui::PushID(i);
|
||||
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
||||
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
||||
if (hov) ImGui::SetTooltip("%s", kIconNames[i]);
|
||||
if (hov) ImGui::SetTooltip("%s", iconName);
|
||||
ImGui::PopID();
|
||||
|
||||
col = (col + 1) % cols;
|
||||
@@ -239,7 +222,7 @@ public:
|
||||
// Apply changes
|
||||
s_app->setAddressLabel(s_address, s_label);
|
||||
if (s_selectedIcon >= 0)
|
||||
s_app->setAddressIcon(s_address, kIconNames[s_selectedIcon]);
|
||||
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(s_selectedIcon));
|
||||
else
|
||||
s_app->setAddressIcon(s_address, "");
|
||||
s_open = false;
|
||||
@@ -260,123 +243,10 @@ private:
|
||||
static inline int s_selectedIcon = -1;
|
||||
static inline char s_iconSearch[64] = {};
|
||||
|
||||
// Icon palette — wallet-relevant Material Design icons
|
||||
static inline const char* kIconNames[] = {
|
||||
// Finance / Crypto
|
||||
"savings", "account_balance", "account_balance_wallet", "wallet",
|
||||
"payments", "credit_card", "local_atm", "diamond",
|
||||
"attach_money", "currency_bitcoin", "currency_exchange", "balance",
|
||||
"calculate", "trending_up", "euro", "leaderboard",
|
||||
"paid", "sell", "receipt", "percent",
|
||||
"price_change", "price_check", "toll", "money",
|
||||
// Charts / Analytics
|
||||
"show_chart", "candlestick_chart", "bar_chart", "pie_chart",
|
||||
"area_chart", "stacked_bar_chart", "waterfall_chart", "scatter_plot",
|
||||
"query_stats", "speed", "donut_large",
|
||||
// Mining / Tools
|
||||
"pickaxe",
|
||||
"hardware", "construction", "handyman", "build",
|
||||
"carpenter", "plumbing", "home_repair_service", "precision_manufacturing",
|
||||
"factory", "warehouse", "inventory", "recycling",
|
||||
"oil_barrel", "offline_bolt", "thunderstorm", "terminal",
|
||||
"storage", "memory", "developer_board",
|
||||
// Security / Auth
|
||||
"shield", "security", "lock", "swap_horiz",
|
||||
"verified", "verified_user", "key", "badge",
|
||||
// Commerce / Business
|
||||
"store", "storefront", "shopping_bag", "business",
|
||||
"work", "real_estate_agent", "gavel", "local_shipping",
|
||||
// Home / Property
|
||||
"home", "apartment", "cottage", "landscape",
|
||||
// People / Identity
|
||||
"account_circle", "face", "manage_accounts", "groups", "mood",
|
||||
// Travel / Transport
|
||||
"rocket_launch", "flight", "directions_car", "travel_explore",
|
||||
"explore", "location_on", "map", "luggage", "anchor",
|
||||
// Nature / Outdoors
|
||||
"public", "language", "forest", "park",
|
||||
"water_drop", "beach_access", "energy_savings_leaf", "solar_power",
|
||||
// Social / Lifestyle
|
||||
"favorite", "star", "celebration", "casino",
|
||||
"auto_awesome", "emoji_events", "military_tech", "flag",
|
||||
// Tech / Science
|
||||
"bolt", "tungsten", "lightbulb", "insights",
|
||||
"hub", "token", "electric_bolt", "science", "biotech",
|
||||
// Organisation
|
||||
"category", "label", "school", "local_hospital", "local_florist",
|
||||
// Food / Drink
|
||||
"coffee", "restaurant", "wine_bar", "liquor",
|
||||
"outdoor_grill", "nightlife", "sports_bar",
|
||||
// Recreation / Health
|
||||
"pets", "fitness_center", "spa", "self_improvement",
|
||||
"psychology", "sports_soccer", "sports_esports",
|
||||
"hiking", "palette", "museum", "church", "surfing",
|
||||
// Community
|
||||
"redeem", "handshake", "healing", "volunteer",
|
||||
"stadium", "temple_buddhist", "theater_comedy", "watch",
|
||||
};
|
||||
static inline const char* kIconGlyphs[] = {
|
||||
// Finance / Crypto
|
||||
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_ACCOUNT_BALANCE_WALLET, ICON_MD_WALLET,
|
||||
ICON_MD_PAYMENTS, ICON_MD_CREDIT_CARD, ICON_MD_LOCAL_ATM, ICON_MD_DIAMOND,
|
||||
ICON_MD_ATTACH_MONEY, ICON_MD_CURRENCY_BITCOIN, ICON_MD_CURRENCY_EXCHANGE, ICON_MD_BALANCE,
|
||||
ICON_MD_CALCULATE, ICON_MD_TRENDING_UP, ICON_MD_EURO, ICON_MD_LEADERBOARD,
|
||||
ICON_MD_PAID, ICON_MD_SELL, ICON_MD_RECEIPT, ICON_MD_PERCENT,
|
||||
ICON_MD_PRICE_CHANGE, ICON_MD_PRICE_CHECK, ICON_MD_TOLL, ICON_MD_MONEY,
|
||||
// Charts / Analytics
|
||||
ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART, ICON_MD_BAR_CHART, ICON_MD_PIE_CHART,
|
||||
ICON_MD_AREA_CHART, ICON_MD_STACKED_BAR_CHART, ICON_MD_WATERFALL_CHART, ICON_MD_SCATTER_PLOT,
|
||||
ICON_MD_QUERY_STATS, ICON_MD_SPEED, ICON_MD_DONUT_LARGE,
|
||||
// Mining / Tools
|
||||
nullptr,
|
||||
ICON_MD_HARDWARE, ICON_MD_CONSTRUCTION, ICON_MD_HANDYMAN, ICON_MD_BUILD,
|
||||
ICON_MD_CARPENTER, ICON_MD_PLUMBING, ICON_MD_HOME_REPAIR_SERVICE, ICON_MD_PRECISION_MANUFACTURING,
|
||||
ICON_MD_FACTORY, ICON_MD_WAREHOUSE, ICON_MD_INVENTORY, ICON_MD_RECYCLING,
|
||||
ICON_MD_OIL_BARREL, ICON_MD_OFFLINE_BOLT, ICON_MD_THUNDERSTORM, ICON_MD_TERMINAL,
|
||||
ICON_MD_STORAGE, ICON_MD_MEMORY, ICON_MD_DEVELOPER_BOARD,
|
||||
// Security / Auth
|
||||
ICON_MD_SHIELD, ICON_MD_SECURITY, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
|
||||
ICON_MD_VERIFIED, ICON_MD_VERIFIED_USER, ICON_MD_KEY, ICON_MD_BADGE,
|
||||
// Commerce / Business
|
||||
ICON_MD_STORE, ICON_MD_STOREFRONT, ICON_MD_SHOPPING_BAG, ICON_MD_BUSINESS,
|
||||
ICON_MD_WORK, ICON_MD_REAL_ESTATE_AGENT, ICON_MD_GAVEL, ICON_MD_LOCAL_SHIPPING,
|
||||
// Home / Property
|
||||
ICON_MD_HOME, ICON_MD_APARTMENT, ICON_MD_COTTAGE, ICON_MD_LANDSCAPE,
|
||||
// People / Identity
|
||||
ICON_MD_ACCOUNT_CIRCLE, ICON_MD_FACE, ICON_MD_MANAGE_ACCOUNTS, ICON_MD_GROUPS, ICON_MD_MOOD,
|
||||
// Travel / Transport
|
||||
ICON_MD_ROCKET_LAUNCH, ICON_MD_FLIGHT, ICON_MD_DIRECTIONS_CAR, ICON_MD_TRAVEL_EXPLORE,
|
||||
ICON_MD_EXPLORE, ICON_MD_LOCATION_ON, ICON_MD_MAP, ICON_MD_LUGGAGE, ICON_MD_ANCHOR,
|
||||
// Nature / Outdoors
|
||||
ICON_MD_PUBLIC, ICON_MD_LANGUAGE, ICON_MD_FOREST, ICON_MD_PARK,
|
||||
ICON_MD_WATER_DROP, ICON_MD_BEACH_ACCESS, ICON_MD_ENERGY_SAVINGS_LEAF, ICON_MD_SOLAR_POWER,
|
||||
// Social / Lifestyle
|
||||
ICON_MD_FAVORITE, ICON_MD_STAR, ICON_MD_CELEBRATION, ICON_MD_CASINO,
|
||||
ICON_MD_AUTO_AWESOME, ICON_MD_EMOJI_EVENTS, ICON_MD_MILITARY_TECH, ICON_MD_FLAG,
|
||||
// Tech / Science
|
||||
ICON_MD_BOLT, ICON_MD_TUNGSTEN, ICON_MD_LIGHTBULB, ICON_MD_INSIGHTS,
|
||||
ICON_MD_HUB, ICON_MD_TOKEN, ICON_MD_ELECTRIC_BOLT, ICON_MD_SCIENCE, ICON_MD_BIOTECH,
|
||||
// Organisation
|
||||
ICON_MD_CATEGORY, ICON_MD_LABEL, ICON_MD_SCHOOL, ICON_MD_LOCAL_HOSPITAL, ICON_MD_LOCAL_FLORIST,
|
||||
// Food / Drink
|
||||
ICON_MD_LOCAL_CAFE, ICON_MD_RESTAURANT, ICON_MD_WINE_BAR, ICON_MD_LIQUOR,
|
||||
ICON_MD_OUTDOOR_GRILL, ICON_MD_NIGHTLIFE, ICON_MD_SPORTS_BAR,
|
||||
// Recreation / Health
|
||||
ICON_MD_PETS, ICON_MD_FITNESS_CENTER, ICON_MD_SPA, ICON_MD_SELF_IMPROVEMENT,
|
||||
ICON_MD_PSYCHOLOGY, ICON_MD_SPORTS_SOCCER, ICON_MD_SPORTS_ESPORTS,
|
||||
ICON_MD_HIKING, ICON_MD_PALETTE, ICON_MD_MUSEUM, ICON_MD_CHURCH, ICON_MD_SURFING,
|
||||
// Community
|
||||
ICON_MD_REDEEM, ICON_MD_HANDSHAKE, ICON_MD_HEALING, ICON_MD_VOLUNTEER_ACTIVISM,
|
||||
ICON_MD_STADIUM, ICON_MD_TEMPLE_BUDDHIST, ICON_MD_THEATER_COMEDY, ICON_MD_WATCH,
|
||||
};
|
||||
static constexpr int kIconCount = static_cast<int>(std::size(kIconNames));
|
||||
|
||||
public:
|
||||
// Expose for the address list to look up icon glyphs by name
|
||||
static const char* iconGlyphForName(const std::string& name) {
|
||||
for (int i = 0; i < kIconCount; ++i)
|
||||
if (kIconNames[i] == name) return kIconGlyphs[i];
|
||||
return nullptr;
|
||||
return material::project_icons::glyphForName(name);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
106
src/ui/windows/balance_address_list.cpp
Normal file
106
src/ui/windows/balance_address_list.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "balance_address_list.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace {
|
||||
|
||||
bool containsIgnoreCase(const std::string& haystack, const std::string& needle)
|
||||
{
|
||||
if (needle.empty()) return true;
|
||||
auto it = std::search(haystack.begin(), haystack.end(),
|
||||
needle.begin(), needle.end(),
|
||||
[](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
return it != haystack.end();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AddressListMatchesFilter(const AddressListInput& input, const std::string& filter)
|
||||
{
|
||||
if (!input.info) return false;
|
||||
return containsIgnoreCase(input.info->address, filter) ||
|
||||
containsIgnoreCase(input.info->label, filter) ||
|
||||
containsIgnoreCase(input.label, filter);
|
||||
}
|
||||
|
||||
std::vector<AddressListRow> BuildAddressListRows(const std::vector<AddressListInput>& inputs,
|
||||
const std::string& filter,
|
||||
bool hideZeroBalances,
|
||||
bool showHidden)
|
||||
{
|
||||
std::vector<AddressListRow> rows;
|
||||
rows.reserve(inputs.size());
|
||||
for (const auto& input : inputs) {
|
||||
if (!input.info) continue;
|
||||
if (!AddressListMatchesFilter(input, filter)) continue;
|
||||
if (input.hidden && !showHidden) continue;
|
||||
if (hideZeroBalances && input.info->balance < 1e-9 && !input.hidden && !input.favorite) continue;
|
||||
rows.push_back({input.info, input.isZ, input.hidden, input.favorite,
|
||||
input.label, input.icon, input.sortOrder});
|
||||
}
|
||||
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const AddressListRow& a, const AddressListRow& b) {
|
||||
bool aHasOrder = a.sortOrder >= 0;
|
||||
bool bHasOrder = b.sortOrder >= 0;
|
||||
if (aHasOrder && bHasOrder) return a.sortOrder < b.sortOrder;
|
||||
if (aHasOrder != bHasOrder) return aHasOrder > bHasOrder;
|
||||
if (a.favorite != b.favorite) return a.favorite > b.favorite;
|
||||
if (a.isZ != b.isZ) return a.isZ > b.isZ;
|
||||
return a.info->balance > b.info->balance;
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
float rowY,
|
||||
float rowWidth,
|
||||
float rowHeight,
|
||||
float rowPadLeft,
|
||||
float rowIconSize,
|
||||
float spacingMd,
|
||||
float spacingSm,
|
||||
float spacingXs)
|
||||
{
|
||||
AddressRowLayout layout;
|
||||
layout.contentStartX = rowX + rowPadLeft;
|
||||
layout.contentStartY = rowY + spacingMd;
|
||||
layout.buttonSize = rowHeight - spacingMd * 2.0f;
|
||||
|
||||
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
|
||||
const float rightEdge = rowX + rowWidth;
|
||||
const float favoriteX = rightEdge - layout.buttonSize - spacingXs;
|
||||
const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
|
||||
|
||||
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};
|
||||
layout.visibilityButton = {visibilityX, buttonY, layout.buttonSize, layout.buttonSize};
|
||||
layout.contentRight = visibilityX - spacingSm;
|
||||
layout.labelX = layout.contentStartX + rowIconSize * 2.0f + spacingMd;
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::string FormatAddressUsdValue(double balance, double priceUsd)
|
||||
{
|
||||
if (priceUsd <= 0.0 || balance <= 0.0) return {};
|
||||
|
||||
char buffer[32];
|
||||
const double usdValue = balance * priceUsd;
|
||||
if (usdValue >= 1.0) {
|
||||
std::snprintf(buffer, sizeof(buffer), "$%.2f", usdValue);
|
||||
} else if (usdValue >= 0.01) {
|
||||
std::snprintf(buffer, sizeof(buffer), "$%.4f", usdValue);
|
||||
} else {
|
||||
std::snprintf(buffer, sizeof(buffer), "$%.6f", usdValue);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
65
src/ui/windows/balance_address_list.h
Normal file
65
src/ui/windows/balance_address_list.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/wallet_state.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct AddressListInput {
|
||||
const AddressInfo* info = nullptr;
|
||||
bool isZ = false;
|
||||
bool hidden = false;
|
||||
bool favorite = false;
|
||||
std::string label;
|
||||
std::string icon;
|
||||
int sortOrder = -1;
|
||||
};
|
||||
|
||||
struct AddressListRow {
|
||||
const AddressInfo* info = nullptr;
|
||||
bool isZ = false;
|
||||
bool hidden = false;
|
||||
bool favorite = false;
|
||||
std::string label;
|
||||
std::string icon;
|
||||
int sortOrder = -1;
|
||||
};
|
||||
|
||||
struct AddressRowRect {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
};
|
||||
|
||||
struct AddressRowLayout {
|
||||
float contentStartX = 0.0f;
|
||||
float contentStartY = 0.0f;
|
||||
float labelX = 0.0f;
|
||||
float contentRight = 0.0f;
|
||||
float buttonSize = 0.0f;
|
||||
AddressRowRect favoriteButton;
|
||||
AddressRowRect visibilityButton;
|
||||
};
|
||||
|
||||
bool AddressListMatchesFilter(const AddressListInput& input, const std::string& filter);
|
||||
std::vector<AddressListRow> BuildAddressListRows(const std::vector<AddressListInput>& inputs,
|
||||
const std::string& filter,
|
||||
bool hideZeroBalances,
|
||||
bool showHidden);
|
||||
AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
float rowY,
|
||||
float rowWidth,
|
||||
float rowHeight,
|
||||
float rowPadLeft,
|
||||
float rowIconSize,
|
||||
float spacingMd,
|
||||
float spacingSm,
|
||||
float spacingXs);
|
||||
std::string FormatAddressUsdValue(double balance, double priceUsd);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
52
src/ui/windows/balance_recent_tx.cpp
Normal file
52
src/ui/windows/balance_recent_tx.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "balance_recent_tx.h"
|
||||
#include "../../config/version.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
std::string truncateRecentAddress(const std::string& address, int maxLen)
|
||||
{
|
||||
if (address.length() <= static_cast<size_t>(maxLen)) return address;
|
||||
int half = (maxLen - 3) / 2;
|
||||
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
||||
}
|
||||
|
||||
std::string formatRecentAmount(const std::string& type, double amount)
|
||||
{
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%s%.4f %s",
|
||||
type == "send" ? "-" : "+",
|
||||
std::abs(amount), DRAGONX_TICKER);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
std::string recentTimeAgo(int64_t timestamp)
|
||||
{
|
||||
if (timestamp <= 0) return "";
|
||||
int64_t now = static_cast<int64_t>(std::time(nullptr));
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
||||
return std::to_string(diff / 86400) + "d ago";
|
||||
}
|
||||
}
|
||||
|
||||
RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLen)
|
||||
{
|
||||
return {
|
||||
tx.getTypeDisplay(),
|
||||
truncateRecentAddress(tx.address, addressMaxLen),
|
||||
formatRecentAmount(tx.type, tx.amount),
|
||||
recentTimeAgo(tx.timestamp)
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
20
src/ui/windows/balance_recent_tx.h
Normal file
20
src/ui/windows/balance_recent_tx.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../data/wallet_state.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct RecentTxDisplay {
|
||||
std::string typeText;
|
||||
std::string addressText;
|
||||
std::string amountText;
|
||||
std::string timeText;
|
||||
};
|
||||
|
||||
RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLen);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -3,6 +3,9 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "balance_tab.h"
|
||||
#include "balance_address_list.h"
|
||||
#include "balance_tab_helpers.h"
|
||||
#include "balance_recent_tx.h"
|
||||
#include "key_export_dialog.h"
|
||||
#include "qr_popup_dialog.h"
|
||||
#include "address_label_dialog.h"
|
||||
@@ -32,92 +35,12 @@
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
|
||||
static std::string TrId(const char* tr_key, const char* id) {
|
||||
std::string s = TR(tr_key);
|
||||
s += "##";
|
||||
s += id;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Case-insensitive substring search
|
||||
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
|
||||
if (search.empty()) return true;
|
||||
std::string s = str, q = search;
|
||||
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
|
||||
std::transform(q.begin(), q.end(), q.begin(), ::tolower);
|
||||
return s.find(q) != std::string::npos;
|
||||
}
|
||||
|
||||
// Relative time string ("2m ago", "3h ago", etc.)
|
||||
static std::string timeAgo(int64_t timestamp) {
|
||||
if (timestamp <= 0) return "";
|
||||
int64_t now = (int64_t)std::time(nullptr);
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
||||
return std::to_string(diff / 86400) + "d ago";
|
||||
}
|
||||
|
||||
// Draw a small transaction-type icon (send=up, receive=down, mined=construction)
|
||||
static void DrawTxIcon(ImDrawList* dl, const std::string& type,
|
||||
float cx, float cy, float /*s*/, ImU32 col)
|
||||
{
|
||||
using namespace material;
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* icon;
|
||||
if (type == "send") {
|
||||
icon = ICON_MD_CALL_MADE;
|
||||
} else if (type == "receive") {
|
||||
icon = ICON_MD_CALL_RECEIVED;
|
||||
} else {
|
||||
icon = ICON_MD_CONSTRUCTION;
|
||||
}
|
||||
ImVec2 sz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
|
||||
}
|
||||
|
||||
// Animated balance state — lerps smoothly toward target
|
||||
static double s_dispTotal = 0.0;
|
||||
static double s_dispShielded = 0.0;
|
||||
static double s_dispTransparent = 0.0;
|
||||
static double s_dispUnconfirmed = 0.0;
|
||||
|
||||
// Helper to truncate address for display
|
||||
static std::string truncateAddress(const std::string& addr, int maxLen = 32) {
|
||||
if (addr.length() <= static_cast<size_t>(maxLen)) return addr;
|
||||
int half = (maxLen - 3) / 2;
|
||||
return addr.substr(0, half) + "..." + addr.substr(addr.length() - half);
|
||||
}
|
||||
|
||||
// Helper to draw a sparkline polyline within a bounding box
|
||||
static void DrawSparkline(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax,
|
||||
const std::vector<double>& data, ImU32 color,
|
||||
float thickness = 1.5f)
|
||||
{
|
||||
if (data.size() < 2) return;
|
||||
double lo = *std::min_element(data.begin(), data.end());
|
||||
double hi = *std::max_element(data.begin(), data.end());
|
||||
double range = hi - lo;
|
||||
if (range < 1e-12) range = 1.0;
|
||||
|
||||
float w = pMax.x - pMin.x;
|
||||
float h = pMax.y - pMin.y;
|
||||
int n = (int)data.size();
|
||||
|
||||
std::vector<ImVec2> pts;
|
||||
pts.reserve(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float x = pMin.x + (float)i / (float)(n - 1) * w;
|
||||
float y = pMax.y - (float)((data[i] - lo) / range) * h;
|
||||
pts.push_back(ImVec2(x, y));
|
||||
}
|
||||
dl->AddPolyline(pts.data(), n, color, ImDrawFlags_None, thickness);
|
||||
}
|
||||
|
||||
// Forward declarations for all layout functions
|
||||
static void RenderBalanceClassic(App* app);
|
||||
static void RenderBalanceDonut(App* app);
|
||||
@@ -1207,51 +1130,37 @@ static void RenderBalanceClassic(App* app)
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
|
||||
// Icon
|
||||
ImU32 iconCol;
|
||||
if (tx.type == "send")
|
||||
iconCol = Error();
|
||||
else if (tx.type == "receive")
|
||||
iconCol = Success();
|
||||
else
|
||||
iconCol = Warning();
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, iconCol);
|
||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
||||
|
||||
// Type label
|
||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp),
|
||||
OnSurfaceMedium(), tx.getTypeDisplay().c_str());
|
||||
OnSurfaceMedium(), display.typeText.c_str());
|
||||
|
||||
// Address (truncated)
|
||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
std::string trAddr = truncateAddress(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), trAddr.c_str());
|
||||
OnSurfaceDisabled(), display.addressText.c_str());
|
||||
|
||||
// Amount (right-aligned area)
|
||||
char amtBuf[32];
|
||||
snprintf(amtBuf, sizeof(amtBuf), "%s%.4f %s",
|
||||
tx.type == "send" ? "-" : "+",
|
||||
std::abs(tx.amount), DRAGONX_TICKER);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, amtBuf);
|
||||
capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
||||
tx.type == "send" ? Error()
|
||||
: Success(),
|
||||
amtBuf);
|
||||
recentTxAmountColor(tx.type),
|
||||
display.amountText.c_str());
|
||||
|
||||
// Time ago
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, ago.c_str());
|
||||
capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
OnSurfaceDisabled(), display.timeText.c_str());
|
||||
|
||||
// Clickable row — hover highlight + navigate to History
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
@@ -1380,45 +1289,22 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
static float s_copiedTime = 0.0f;
|
||||
|
||||
// ---- Build and filter address rows ----
|
||||
struct AddrRow {
|
||||
const AddressInfo* info;
|
||||
bool isZ, hidden, favorite;
|
||||
std::string label, icon;
|
||||
int sortOrder;
|
||||
};
|
||||
std::vector<AddrRow> rows;
|
||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
std::vector<AddressListInput> rowInputs;
|
||||
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
|
||||
auto addRows = [&](const auto& addrs, bool isZ) {
|
||||
for (const auto& a : addrs) {
|
||||
std::string filter(addr_search);
|
||||
std::string addrLabel = app->getAddressLabel(a.address);
|
||||
if (!containsIgnoreCase(a.address, filter) &&
|
||||
!containsIgnoreCase(a.label, filter) &&
|
||||
!containsIgnoreCase(addrLabel, filter)) continue;
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
if (isHidden && !s_showHidden) continue;
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
|
||||
rows.push_back({&a, isZ, isHidden, isFav,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
}
|
||||
};
|
||||
addRows(state.z_addresses, true);
|
||||
addRows(state.t_addresses, false);
|
||||
|
||||
// Sort: custom order (if any) → favorites → Z first → balance desc
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const AddrRow& a, const AddrRow& b) -> bool {
|
||||
bool aHasOrder = a.sortOrder >= 0;
|
||||
bool bHasOrder = b.sortOrder >= 0;
|
||||
if (aHasOrder && bHasOrder) return a.sortOrder < b.sortOrder;
|
||||
if (aHasOrder != bHasOrder) return aHasOrder > bHasOrder;
|
||||
if (a.favorite != b.favorite) return a.favorite > b.favorite;
|
||||
if (a.isZ != b.isZ) return a.isZ > b.isZ;
|
||||
return a.info->balance > b.info->balance;
|
||||
});
|
||||
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
|
||||
|
||||
// ---- Toolbar: search + checkboxes + create buttons ----
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
@@ -1677,24 +1563,21 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
|
||||
float cx = rowPos.x + rowPadLeft;
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
auto rowLayout = ComputeAddressRowLayout(
|
||||
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
|
||||
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
|
||||
float cx = rowLayout.contentStartX;
|
||||
float cy = rowLayout.contentStartY;
|
||||
|
||||
// ---- Button zone (right edge): [eye] [star] ----
|
||||
float btnH = rowH - Layout::spacingMd() * 2.0f;
|
||||
float btnW = btnH;
|
||||
float btnGap = Layout::spacingSm();
|
||||
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
|
||||
float rightEdge = rowPos.x + innerW;
|
||||
float starX = rightEdge - btnW - Layout::spacingXs();
|
||||
float eyeX = starX - btnGap - btnW;
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
|
||||
if (!isDragged) {
|
||||
// Star button
|
||||
{
|
||||
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
|
||||
const auto& starRect = rowLayout.favoriteButton;
|
||||
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
|
||||
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
|
||||
@@ -1703,7 +1586,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
||||
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
|
||||
ImVec2(starRect.x + (starRect.width - iSz.x) * 0.5f,
|
||||
starRect.y + (starRect.height - iSz.y) * 0.5f), starCol, starIcon);
|
||||
if (bHov && mouseClicked) {
|
||||
if (row.favorite) app->unfavoriteAddress(addr.address);
|
||||
else app->favoriteAddress(addr.address);
|
||||
@@ -1715,10 +1599,11 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
// Eye button (zero balance or hidden)
|
||||
bool showEye = true;
|
||||
// Always reserve space for both buttons so content doesn't shift
|
||||
float contentRight = eyeX - Layout::spacingSm();
|
||||
float contentRight = rowLayout.contentRight;
|
||||
|
||||
if (showEye) {
|
||||
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
|
||||
const auto& eyeRect = rowLayout.visibilityButton;
|
||||
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
|
||||
@@ -1727,7 +1612,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
||||
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
|
||||
ImVec2(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
|
||||
eyeRect.y + (eyeRect.height - iSz.y) * 0.5f), iconCol, hideIcon);
|
||||
if (bHov && mouseClicked) {
|
||||
if (row.hidden) app->unhideAddress(addr.address);
|
||||
else app->hideAddress(addr.address);
|
||||
@@ -1756,7 +1642,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
}
|
||||
|
||||
// ---- Type label (first line) ----
|
||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingMd();
|
||||
float labelX = rowLayout.labelX;
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
@@ -1805,18 +1691,13 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
|
||||
// ---- USD value (second line, right-aligned) ----
|
||||
{
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0 && addr.balance > 0.0) {
|
||||
char usdBuf[32];
|
||||
double usdVal = addr.balance * priceUsd;
|
||||
if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
|
||||
else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
|
||||
std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
|
||||
if (!usdText.empty()) {
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(contentRight - usdSz.x,
|
||||
cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usdBuf);
|
||||
OnSurfaceDisabled(), usdText.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2006,39 +1887,30 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
|
||||
const auto& tx = txs[i];
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
ImU32 iconCol;
|
||||
if (tx.type == "send") iconCol = Error();
|
||||
else if (tx.type == "receive") iconCol = Success();
|
||||
else iconCol = Warning();
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, iconCol);
|
||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
||||
|
||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), tx.getTypeDisplay().c_str());
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
||||
|
||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
std::string trAddr = truncateAddress(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), trAddr.c_str());
|
||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
||||
|
||||
char amtBuf[32];
|
||||
snprintf(amtBuf, sizeof(amtBuf), "%s%.4f %s",
|
||||
tx.type == "send" ? "-" : "+",
|
||||
std::abs(tx.amount), DRAGONX_TICKER);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, amtBuf);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(
|
||||
S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
||||
tx.type == "send" ? Error() : Success(), amtBuf);
|
||||
recentTxAmountColor(tx.type), display.amountText.c_str());
|
||||
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, ago.c_str());
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
OnSurfaceDisabled(), display.timeText.c_str());
|
||||
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
|
||||
117
src/ui/windows/balance_tab_helpers.cpp
Normal file
117
src/ui/windows/balance_tab_helpers.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "balance_tab_helpers.h"
|
||||
|
||||
#include "../../config/version.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/type.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
std::string TrId(const char* trKey, const char* id)
|
||||
{
|
||||
std::string value = TR(trKey);
|
||||
value += "##";
|
||||
value += id;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool containsIgnoreCase(const std::string& value, const std::string& search)
|
||||
{
|
||||
if (search.empty()) return true;
|
||||
std::string haystack = value;
|
||||
std::string needle = search;
|
||||
std::transform(haystack.begin(), haystack.end(), haystack.begin(), ::tolower);
|
||||
std::transform(needle.begin(), needle.end(), needle.begin(), ::tolower);
|
||||
return haystack.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
std::string timeAgo(int64_t timestamp)
|
||||
{
|
||||
if (timestamp <= 0) return "";
|
||||
int64_t now = static_cast<int64_t>(std::time(nullptr));
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
||||
return std::to_string(diff / 86400) + "d ago";
|
||||
}
|
||||
|
||||
std::string truncateAddress(const std::string& address, int maxLen)
|
||||
{
|
||||
if (address.length() <= static_cast<size_t>(maxLen)) return address;
|
||||
int half = (maxLen - 3) / 2;
|
||||
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
||||
}
|
||||
|
||||
ImU32 recentTxIconColor(const std::string& type)
|
||||
{
|
||||
using namespace material;
|
||||
if (type == "send") return Error();
|
||||
if (type == "receive") return Success();
|
||||
return Warning();
|
||||
}
|
||||
|
||||
ImU32 recentTxAmountColor(const std::string& type)
|
||||
{
|
||||
using namespace material;
|
||||
return type == "send" ? Error() : Success();
|
||||
}
|
||||
|
||||
std::string formatRecentTxAmount(const std::string& type, double amount)
|
||||
{
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%s%.4f %s",
|
||||
type == "send" ? "-" : "+",
|
||||
std::abs(amount), DRAGONX_TICKER);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
void DrawTxIcon(ImDrawList* drawList, const std::string& type,
|
||||
float centerX, float centerY, float, ImU32 color)
|
||||
{
|
||||
using namespace material;
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* icon = ICON_MD_CONSTRUCTION;
|
||||
if (type == "send") {
|
||||
icon = ICON_MD_CALL_MADE;
|
||||
} else if (type == "receive") {
|
||||
icon = ICON_MD_CALL_RECEIVED;
|
||||
}
|
||||
ImVec2 size = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
|
||||
drawList->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(centerX - size.x * 0.5f, centerY - size.y * 0.5f), color, icon);
|
||||
}
|
||||
|
||||
void DrawSparkline(ImDrawList* drawList, const ImVec2& min, const ImVec2& max,
|
||||
const std::vector<double>& data, ImU32 color, float thickness)
|
||||
{
|
||||
if (data.size() < 2) return;
|
||||
double low = *std::min_element(data.begin(), data.end());
|
||||
double high = *std::max_element(data.begin(), data.end());
|
||||
double range = high - low;
|
||||
if (range < 1e-12) range = 1.0;
|
||||
|
||||
float width = max.x - min.x;
|
||||
float height = max.y - min.y;
|
||||
int count = static_cast<int>(data.size());
|
||||
|
||||
std::vector<ImVec2> points;
|
||||
points.reserve(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
float x = min.x + static_cast<float>(i) / static_cast<float>(count - 1) * width;
|
||||
float y = max.y - static_cast<float>((data[i] - low) / range) * height;
|
||||
points.push_back(ImVec2(x, y));
|
||||
}
|
||||
drawList->AddPolyline(points.data(), count, color, ImDrawFlags_None, thickness);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
27
src/ui/windows/balance_tab_helpers.h
Normal file
27
src/ui/windows/balance_tab_helpers.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
std::string TrId(const char* trKey, const char* id);
|
||||
bool containsIgnoreCase(const std::string& value, const std::string& search);
|
||||
std::string timeAgo(int64_t timestamp);
|
||||
std::string truncateAddress(const std::string& address, int maxLen = 32);
|
||||
ImU32 recentTxIconColor(const std::string& type);
|
||||
ImU32 recentTxAmountColor(const std::string& type);
|
||||
std::string formatRecentTxAmount(const std::string& type, double amount);
|
||||
|
||||
void DrawTxIcon(ImDrawList* drawList, const std::string& type,
|
||||
float centerX, float centerY, float size, ImU32 color);
|
||||
void DrawSparkline(ImDrawList* drawList, const ImVec2& min, const ImVec2& max,
|
||||
const std::vector<double>& data, ImU32 color,
|
||||
float thickness = 1.5f);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
136
src/ui/windows/console_command_reference.cpp
Normal file
136
src/ui/windows/console_command_reference.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
#include "console_command_reference.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
|
||||
template <size_t N>
|
||||
constexpr int CountOf(const ConsoleCommandEntry (&)[N])
|
||||
{
|
||||
return static_cast<int>(N);
|
||||
}
|
||||
|
||||
const ConsoleCommandEntry kControlCommands[] = {
|
||||
{"help", "List all commands, or get help for a specified command", "[\"command\"]"},
|
||||
{"getinfo", "Get general info about the node", ""},
|
||||
{"stop", "Stop the daemon", ""},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kNetworkCommands[] = {
|
||||
{"getnetworkinfo", "Return P2P network state info", ""},
|
||||
{"getpeerinfo", "Get data about each connected peer", ""},
|
||||
{"getconnectioncount", "Get number of peer connections", ""},
|
||||
{"getnettotals", "Get network traffic statistics", ""},
|
||||
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\""},
|
||||
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]"},
|
||||
{"listbanned", "List all banned IPs/subnets", ""},
|
||||
{"clearbanned", "Clear all banned IPs", ""},
|
||||
{"ping", "Ping all peers to measure round-trip time", ""},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kBlockchainCommands[] = {
|
||||
{"getblockchaininfo", "Get current blockchain state", ""},
|
||||
{"getblockcount", "Get number of blocks in longest chain", ""},
|
||||
{"getbestblockhash", "Get hash of the tip block", ""},
|
||||
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]"},
|
||||
{"getblockhash", "Get block hash at a given height", "height"},
|
||||
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"},
|
||||
{"getdifficulty", "Get proof-of-work difficulty", ""},
|
||||
{"getrawmempool", "Get all txids in mempool", "[verbose]"},
|
||||
{"getmempoolinfo", "Get mempool state info", ""},
|
||||
{"gettxout", "Get details about an unspent output", "\"txid\" n [includemempool]"},
|
||||
{"coinsupply", "Get coin supply information", "[height]"},
|
||||
{"getchaintips", "Get all known chain tips", ""},
|
||||
{"getchaintxstats", "Get chain transaction statistics", "[nblocks] [\"blockhash\"]"},
|
||||
{"verifychain", "Verify the blockchain database", "[checklevel] [numblocks]"},
|
||||
{"kvsearch", "Search the blockchain key-value store", "\"key\""},
|
||||
{"kvupdate", "Update a key-value pair on-chain", "\"key\" \"value\" days"},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kMiningCommands[] = {
|
||||
{"getmininginfo", "Get mining-related information", ""},
|
||||
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]"},
|
||||
{"getgenerate", "Check if the node is mining", ""},
|
||||
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"},
|
||||
{"getblocksubsidy", "Get block reward at a given height", "[height]"},
|
||||
{"getblocktemplate", "Get block template for mining", "[\"jsonrequestobject\"]"},
|
||||
{"submitblock", "Submit a mined block to the network", "\"hexdata\""},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kWalletCommands[] = {
|
||||
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]"},
|
||||
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]"},
|
||||
{"z_getbalances", "Get all balances (transparent + shielded)", ""},
|
||||
{"getnewaddress", "Generate a new transparent address", ""},
|
||||
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]"},
|
||||
{"listaddresses", "List all transparent addresses", ""},
|
||||
{"z_listaddresses", "List all z-addresses", ""},
|
||||
{"sendtoaddress", "Send to a specific address", "\"address\" amount"},
|
||||
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]"},
|
||||
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]"},
|
||||
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]"},
|
||||
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]"},
|
||||
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]"},
|
||||
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"},
|
||||
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"},
|
||||
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"},
|
||||
{"z_listoperationids", "List all async z operation IDs", ""},
|
||||
{"getwalletinfo", "Get wallet state info", ""},
|
||||
{"backupwallet", "Back up wallet to a file", "\"destination\""},
|
||||
{"dumpprivkey", "Dump private key for an address", "\"address\""},
|
||||
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]"},
|
||||
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\""},
|
||||
{"importwallet", "Import wallet from a dump file", "\"filename\""},
|
||||
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\""},
|
||||
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]"},
|
||||
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\""},
|
||||
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]"},
|
||||
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\""},
|
||||
{"signmessage", "Sign a message with an address key", "\"address\" \"message\""},
|
||||
{"settxfee", "Set the transaction fee per kB", "amount"},
|
||||
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout"},
|
||||
{"walletlock", "Lock the wallet", ""},
|
||||
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\""},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kRawTransactionCommands[] = {
|
||||
{"createrawtransaction", "Create a raw transaction spending given inputs", "[{\"txid\":\"...\",\"vout\":n},...] {\"address\":amount,...}"},
|
||||
{"decoderawtransaction", "Decode raw transaction hex string", "\"hexstring\""},
|
||||
{"decodescript", "Decode a hex-encoded script", "\"hex\""},
|
||||
{"getrawtransaction", "Get raw transaction data by txid", "\"txid\" [verbose]"},
|
||||
{"sendrawtransaction", "Submit raw transaction to the network", "\"hexstring\" [allowhighfees]"},
|
||||
{"signrawtransaction", "Sign a raw transaction with private keys", "\"hexstring\""},
|
||||
{"fundrawtransaction", "Add inputs to meet output value", "\"hexstring\""},
|
||||
};
|
||||
|
||||
const ConsoleCommandEntry kUtilityCommands[] = {
|
||||
{"validateaddress", "Validate a transparent address", "\"address\""},
|
||||
{"z_validateaddress", "Validate a z-address", "\"zaddr\""},
|
||||
{"estimatefee", "Estimate fee for a transaction", "nblocks"},
|
||||
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""},
|
||||
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"},
|
||||
{"invalidateblock", "Mark a block as invalid", "\"hash\""},
|
||||
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\""},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<ConsoleCommandCategory>& consoleCommandCategories()
|
||||
{
|
||||
static const std::vector<ConsoleCommandCategory> categories = {
|
||||
{"Control", kControlCommands, CountOf(kControlCommands)},
|
||||
{"Network", kNetworkCommands, CountOf(kNetworkCommands)},
|
||||
{"Blockchain", kBlockchainCommands, CountOf(kBlockchainCommands)},
|
||||
{"Mining", kMiningCommands, CountOf(kMiningCommands)},
|
||||
{"Wallet", kWalletCommands, CountOf(kWalletCommands)},
|
||||
{"Raw Transactions", kRawTransactionCommands, CountOf(kRawTransactionCommands)},
|
||||
{"Utility", kUtilityCommands, CountOf(kUtilityCommands)},
|
||||
};
|
||||
return categories;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
23
src/ui/windows/console_command_reference.h
Normal file
23
src/ui/windows/console_command_reference.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct ConsoleCommandEntry {
|
||||
const char* name;
|
||||
const char* desc;
|
||||
const char* params;
|
||||
};
|
||||
|
||||
struct ConsoleCommandCategory {
|
||||
const char* name;
|
||||
const ConsoleCommandEntry* commands;
|
||||
int count;
|
||||
};
|
||||
|
||||
const std::vector<ConsoleCommandCategory>& consoleCommandCategories();
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
240
src/ui/windows/console_input_model.cpp
Normal file
240
src/ui/windows/console_input_model.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
#include "console_input_model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
const std::vector<std::string>& ConsoleRpcCommandNames()
|
||||
{
|
||||
static const std::vector<std::string> commands = {
|
||||
"help", "getinfo", "stop",
|
||||
"getnetworkinfo", "getpeerinfo", "getconnectioncount",
|
||||
"addnode", "setban", "listbanned", "clearbanned", "ping",
|
||||
"getblockchaininfo", "getblockcount", "getbestblockhash",
|
||||
"getblock", "getblockhash", "getblockheader", "getdifficulty",
|
||||
"getrawmempool", "gettxout", "coinsupply", "getchaintips",
|
||||
"getmininginfo", "setgenerate", "getgenerate",
|
||||
"getnetworkhashps", "getblocksubsidy",
|
||||
"getbalance", "z_gettotalbalance", "z_getbalances",
|
||||
"getnewaddress", "z_getnewaddress",
|
||||
"listaddresses", "z_listaddresses",
|
||||
"sendtoaddress", "z_sendmany",
|
||||
"listtransactions", "listunspent", "z_listunspent",
|
||||
"z_getoperationstatus", "z_getoperationresult",
|
||||
"getwalletinfo", "backupwallet",
|
||||
"dumpprivkey", "importprivkey",
|
||||
"z_exportkey", "z_importkey",
|
||||
"signmessage", "settxfee",
|
||||
"createrawtransaction", "decoderawtransaction",
|
||||
"getrawtransaction", "sendrawtransaction", "signrawtransaction",
|
||||
"validateaddress", "z_validateaddress", "estimatefee",
|
||||
"clear"
|
||||
};
|
||||
return commands;
|
||||
}
|
||||
|
||||
void AppendConsoleHistory(std::vector<std::string>& history,
|
||||
const std::string& command,
|
||||
std::size_t maxEntries)
|
||||
{
|
||||
if (command.empty()) return;
|
||||
if (history.empty() || history.back() != command) {
|
||||
history.push_back(command);
|
||||
while (history.size() > maxEntries) {
|
||||
history.erase(history.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int NavigateConsoleHistoryIndex(int currentIndex, std::size_t historySize, bool up)
|
||||
{
|
||||
if (historySize == 0) return -1;
|
||||
if (up) {
|
||||
if (currentIndex < 0) return static_cast<int>(historySize) - 1;
|
||||
if (currentIndex > 0) return currentIndex - 1;
|
||||
return currentIndex;
|
||||
}
|
||||
if (currentIndex >= 0) {
|
||||
int next = currentIndex + 1;
|
||||
if (next >= static_cast<int>(historySize)) return -1;
|
||||
return next;
|
||||
}
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
std::string ConsoleHistoryEntry(const std::vector<std::string>& history, int historyIndex)
|
||||
{
|
||||
if (historyIndex < 0 || historyIndex >= static_cast<int>(history.size())) return {};
|
||||
return history[static_cast<std::size_t>(historyIndex)];
|
||||
}
|
||||
|
||||
ConsoleCompletionResult CompleteConsoleCommand(const std::string& input)
|
||||
{
|
||||
ConsoleCompletionResult result;
|
||||
if (input.empty()) return result;
|
||||
|
||||
for (const auto& command : ConsoleRpcCommandNames()) {
|
||||
if (command.compare(0, input.size(), input) == 0) {
|
||||
result.matches.push_back(command);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.matches.empty()) return result;
|
||||
result.commonPrefix = result.matches.front();
|
||||
for (std::size_t matchIndex = 1; matchIndex < result.matches.size(); ++matchIndex) {
|
||||
const auto& match = result.matches[matchIndex];
|
||||
std::size_t prefixLength = 0;
|
||||
while (prefixLength < result.commonPrefix.size() &&
|
||||
prefixLength < match.size() &&
|
||||
result.commonPrefix[prefixLength] == match[prefixLength]) {
|
||||
++prefixLength;
|
||||
}
|
||||
result.commonPrefix.resize(prefixLength);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::string>& matches,
|
||||
std::size_t maxLineLength)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
std::string line = " ";
|
||||
for (std::size_t matchIndex = 0; matchIndex < matches.size(); ++matchIndex) {
|
||||
if (matchIndex > 0) line += " ";
|
||||
line += matches[matchIndex];
|
||||
if (line.length() > maxLineLength) {
|
||||
lines.push_back(line);
|
||||
line = " ";
|
||||
}
|
||||
}
|
||||
if (line.length() > 2) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command)
|
||||
{
|
||||
std::vector<std::string> args;
|
||||
std::size_t index = 0;
|
||||
while (index < command.size()) {
|
||||
while (index < command.size() && (command[index] == ' ' || command[index] == '\t')) {
|
||||
++index;
|
||||
}
|
||||
if (index >= command.size()) break;
|
||||
|
||||
std::string token;
|
||||
if (command[index] == '"' || command[index] == '\'') {
|
||||
char quote = command[index++];
|
||||
while (index < command.size() && command[index] != quote) {
|
||||
token += command[index++];
|
||||
}
|
||||
if (index < command.size()) ++index;
|
||||
} else if (command[index] == '[' || command[index] == '{') {
|
||||
char open = command[index];
|
||||
char close = (open == '[') ? ']' : '}';
|
||||
int depth = 0;
|
||||
while (index < command.size()) {
|
||||
if (command[index] == open) ++depth;
|
||||
else if (command[index] == close) --depth;
|
||||
token += command[index++];
|
||||
if (depth == 0) break;
|
||||
}
|
||||
} else {
|
||||
while (index < command.size() && command[index] != ' ' && command[index] != '\t') {
|
||||
token += command[index++];
|
||||
}
|
||||
}
|
||||
if (!token.empty()) args.push_back(token);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
ConsoleRpcCall BuildConsoleRpcCall(const std::string& command)
|
||||
{
|
||||
auto args = ParseConsoleCommandArgs(command);
|
||||
ConsoleRpcCall call;
|
||||
if (args.empty()) return call;
|
||||
|
||||
call.valid = true;
|
||||
call.method = args.front();
|
||||
|
||||
for (std::size_t argIndex = 1; argIndex < args.size(); ++argIndex) {
|
||||
const std::string& arg = args[argIndex];
|
||||
if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) {
|
||||
auto parsed = nlohmann::json::parse(arg, nullptr, false);
|
||||
if (!parsed.is_discarded()) {
|
||||
call.params.push_back(parsed);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (arg == "true") {
|
||||
call.params.push_back(true);
|
||||
} else if (arg == "false") {
|
||||
call.params.push_back(false);
|
||||
} else {
|
||||
try {
|
||||
if (arg.find('.') != std::string::npos) {
|
||||
call.params.push_back(std::stod(arg));
|
||||
} else {
|
||||
call.params.push_back(std::stoll(arg));
|
||||
}
|
||||
} catch (...) {
|
||||
call.params.push_back(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& result,
|
||||
bool isError)
|
||||
{
|
||||
if (isError) {
|
||||
return {{"Error: " + result, ConsoleResultLineRole::Error}};
|
||||
}
|
||||
|
||||
std::vector<ConsoleResultLine> lines;
|
||||
std::string normalized = (result == "null") ? "(no result)" : result;
|
||||
bool isJson = !normalized.empty() && (normalized[0] == '{' || normalized[0] == '[');
|
||||
std::istringstream stream(normalized);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
ConsoleResultLineRole role = ConsoleResultLineRole::Result;
|
||||
if (isJson && !line.empty()) {
|
||||
std::string trimmed = line;
|
||||
std::size_t first = trimmed.find_first_not_of(" \t");
|
||||
if (first != std::string::npos) trimmed = trimmed.substr(first);
|
||||
if (!trimmed.empty()) {
|
||||
unsigned char firstChar = static_cast<unsigned char>(trimmed[0]);
|
||||
if (trimmed[0] == '{' || trimmed[0] == '}' ||
|
||||
trimmed[0] == '[' || trimmed[0] == ']') {
|
||||
role = ConsoleResultLineRole::JsonBrace;
|
||||
} else if (trimmed[0] == '"') {
|
||||
if (trimmed.find("\": ") != std::string::npos ||
|
||||
trimmed.find("\":") != std::string::npos) {
|
||||
role = ConsoleResultLineRole::JsonKey;
|
||||
} else {
|
||||
role = ConsoleResultLineRole::JsonString;
|
||||
}
|
||||
} else if (std::isdigit(firstChar) || trimmed[0] == '-') {
|
||||
role = ConsoleResultLineRole::JsonNumber;
|
||||
} else if (trimmed == "true," || trimmed == "false," ||
|
||||
trimmed == "true" || trimmed == "false" ||
|
||||
trimmed == "null," || trimmed == "null") {
|
||||
role = ConsoleResultLineRole::JsonNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push_back({line, role});
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
54
src/ui/windows/console_input_model.h
Normal file
54
src/ui/windows/console_input_model.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct ConsoleCompletionResult {
|
||||
std::vector<std::string> matches;
|
||||
std::string commonPrefix;
|
||||
};
|
||||
|
||||
struct ConsoleRpcCall {
|
||||
bool valid = false;
|
||||
std::string method;
|
||||
nlohmann::json params = nlohmann::json::array();
|
||||
};
|
||||
|
||||
enum class ConsoleResultLineRole {
|
||||
Result,
|
||||
Error,
|
||||
JsonKey,
|
||||
JsonString,
|
||||
JsonNumber,
|
||||
JsonBrace
|
||||
};
|
||||
|
||||
struct ConsoleResultLine {
|
||||
std::string text;
|
||||
ConsoleResultLineRole role = ConsoleResultLineRole::Result;
|
||||
};
|
||||
|
||||
const std::vector<std::string>& ConsoleRpcCommandNames();
|
||||
void AppendConsoleHistory(std::vector<std::string>& history,
|
||||
const std::string& command,
|
||||
std::size_t maxEntries = 100);
|
||||
int NavigateConsoleHistoryIndex(int currentIndex,
|
||||
std::size_t historySize,
|
||||
bool up);
|
||||
std::string ConsoleHistoryEntry(const std::vector<std::string>& history,
|
||||
int historyIndex);
|
||||
ConsoleCompletionResult CompleteConsoleCommand(const std::string& input);
|
||||
std::vector<std::string> FormatConsoleCompletionLines(const std::vector<std::string>& matches,
|
||||
std::size_t maxLineLength = 60);
|
||||
std::vector<std::string> ParseConsoleCommandArgs(const std::string& command);
|
||||
ConsoleRpcCall BuildConsoleRpcCall(const std::string& command);
|
||||
std::vector<ConsoleResultLine> FormatConsoleRpcResultLines(const std::string& result,
|
||||
bool isError);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
33
src/ui/windows/console_output_model.cpp
Normal file
33
src/ui/windows/console_output_model.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "console_output_model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
std::string lowerCopy(std::string value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
bool consoleLinePassesFilter(const std::string& lineText,
|
||||
ImU32 lineColor,
|
||||
const ConsoleOutputFilter& filter)
|
||||
{
|
||||
if (!filter.daemonMessagesEnabled && lineColor == filter.daemonColor) return false;
|
||||
if (filter.errorsOnly && lineColor != filter.errorColor) return false;
|
||||
if (!filter.text.empty()) {
|
||||
std::string needle = lowerCopy(filter.text);
|
||||
std::string haystack = lowerCopy(lineText);
|
||||
if (haystack.find(needle) == std::string::npos) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
23
src/ui/windows/console_output_model.h
Normal file
23
src/ui/windows/console_output_model.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct ConsoleOutputFilter {
|
||||
std::string text;
|
||||
bool daemonMessagesEnabled = true;
|
||||
bool errorsOnly = false;
|
||||
ImU32 daemonColor = 0;
|
||||
ImU32 errorColor = 0;
|
||||
};
|
||||
|
||||
bool consoleLinePassesFilter(const std::string& lineText,
|
||||
ImU32 lineColor,
|
||||
const ConsoleOutputFilter& filter);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -6,6 +6,10 @@
|
||||
// tab completion, daemon log display, and color-coded output.
|
||||
|
||||
#include "console_tab.h"
|
||||
#include "console_command_reference.h"
|
||||
#include "console_input_model.h"
|
||||
#include "console_output_model.h"
|
||||
#include "console_tab_helpers.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -213,13 +217,18 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Output area (scrollable) — glass panel background
|
||||
float frameH = ImGui::GetFrameHeightWithSpacing();
|
||||
float itemSp = ImGui::GetStyle().ItemSpacing.y;
|
||||
float input_height = (Layout::spacingSm() + itemSp) // Dummy(0,sm) + spacing
|
||||
+ frameH + Layout::spacingSm() + Layout::spacingXs() + schema::UI().drawElement("tabs.console", "input-cursor-offset").size; // input glass panel + cursor offset
|
||||
float outputH = ImGui::GetContentRegionAvail().y - input_height;
|
||||
float availHeight = ImGui::GetContentRegionAvail().y;
|
||||
if (outputH < std::max(schema::UI().drawElement("tabs.console", "output-min-height").size, availHeight * schema::UI().drawElement("tabs.console", "output-min-height-ratio").size)) outputH = std::max(schema::UI().drawElement("tabs.console", "output-min-height").size, availHeight * schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
|
||||
float input_height = ComputeConsoleInputHeight(
|
||||
ImGui::GetFrameHeightWithSpacing(),
|
||||
ImGui::GetStyle().ItemSpacing.y,
|
||||
Layout::spacingSm(),
|
||||
Layout::spacingXs(),
|
||||
schema::UI().drawElement("tabs.console", "input-cursor-offset").size);
|
||||
float outputH = ComputeConsoleOutputHeight(
|
||||
availHeight,
|
||||
input_height,
|
||||
schema::UI().drawElement("tabs.console", "output-min-height").size,
|
||||
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
|
||||
|
||||
ImDrawList* dlOut = ImGui::GetWindowDrawList();
|
||||
ImVec2 outPanelMin = ImGui::GetCursorScreenPos();
|
||||
@@ -600,28 +609,14 @@ void ConsoleTab::renderOutput()
|
||||
output_scroll_y_ = ImGui::GetScrollY();
|
||||
|
||||
// Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
|
||||
std::string filter_str(filter_text_);
|
||||
bool has_text_filter = !filter_str.empty();
|
||||
bool hide_daemon = !s_daemon_messages_enabled;
|
||||
bool errors_only = s_errors_only_enabled;
|
||||
bool has_filter = has_text_filter || hide_daemon || errors_only;
|
||||
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
||||
s_errors_only_enabled, COLOR_DAEMON, COLOR_ERROR};
|
||||
bool has_text_filter = !outputFilter.text.empty();
|
||||
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled || outputFilter.errorsOnly;
|
||||
visible_indices_.clear();
|
||||
if (has_filter) {
|
||||
std::string filter_lower;
|
||||
if (has_text_filter) {
|
||||
filter_lower = filter_str;
|
||||
std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(), ::tolower);
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
|
||||
// Skip daemon lines when daemon toggle is off
|
||||
if (hide_daemon && lines_[i].color == COLOR_DAEMON) continue;
|
||||
// When errors-only is enabled, skip non-error lines
|
||||
if (errors_only && lines_[i].color != COLOR_ERROR) continue;
|
||||
if (has_text_filter) {
|
||||
std::string lower = lines_[i].text;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
||||
if (lower.find(filter_lower) == std::string::npos) continue;
|
||||
}
|
||||
if (!consoleLinePassesFilter(lines_[i].text, lines_[i].color, outputFilter)) continue;
|
||||
visible_indices_.push_back(i);
|
||||
}
|
||||
} else {
|
||||
@@ -636,8 +631,7 @@ void ConsoleTab::renderOutput()
|
||||
// Each segment records which bytes of the source text appear on that visual
|
||||
// row, so hit-testing and selection highlight can map screen positions to
|
||||
// exact character offsets.
|
||||
float wrap_width = ImGui::GetContentRegionAvail().x - padX * 2;
|
||||
if (wrap_width < 50.0f) wrap_width = 50.0f;
|
||||
float wrap_width = ClampConsoleWrapWidth(ImGui::GetContentRegionAvail().x, padX);
|
||||
|
||||
ImFont* font = ImGui::GetFont();
|
||||
float fontSize = ImGui::GetFontSize();
|
||||
@@ -1169,106 +1163,37 @@ void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker)
|
||||
if (console->command_history_.empty()) return 0;
|
||||
|
||||
int prev_index = console->history_index_;
|
||||
|
||||
if (data->EventKey == ImGuiKey_UpArrow) {
|
||||
if (console->history_index_ < 0) {
|
||||
console->history_index_ = static_cast<int>(console->command_history_.size()) - 1;
|
||||
} else if (console->history_index_ > 0) {
|
||||
console->history_index_--;
|
||||
}
|
||||
} else if (data->EventKey == ImGuiKey_DownArrow) {
|
||||
if (console->history_index_ >= 0) {
|
||||
console->history_index_++;
|
||||
if (console->history_index_ >= static_cast<int>(console->command_history_.size())) {
|
||||
console->history_index_ = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
console->history_index_ = NavigateConsoleHistoryIndex(
|
||||
console->history_index_,
|
||||
console->command_history_.size(),
|
||||
data->EventKey == ImGuiKey_UpArrow);
|
||||
|
||||
if (prev_index != console->history_index_) {
|
||||
const char* history_str = (console->history_index_ >= 0)
|
||||
? console->command_history_[console->history_index_].c_str()
|
||||
: "";
|
||||
std::string history = ConsoleHistoryEntry(console->command_history_, console->history_index_);
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, history_str);
|
||||
data->InsertChars(0, history.c_str());
|
||||
}
|
||||
}
|
||||
else if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) {
|
||||
// Tab completion for common RPC commands
|
||||
static const char* commands[] = {
|
||||
// Control
|
||||
"help", "getinfo", "stop",
|
||||
// Network
|
||||
"getnetworkinfo", "getpeerinfo", "getconnectioncount",
|
||||
"addnode", "setban", "listbanned", "clearbanned", "ping",
|
||||
// Blockchain
|
||||
"getblockchaininfo", "getblockcount", "getbestblockhash",
|
||||
"getblock", "getblockhash", "getblockheader", "getdifficulty",
|
||||
"getrawmempool", "gettxout", "coinsupply", "getchaintips",
|
||||
// Mining
|
||||
"getmininginfo", "setgenerate", "getgenerate",
|
||||
"getnetworkhashps", "getblocksubsidy",
|
||||
// Wallet
|
||||
"getbalance", "z_gettotalbalance", "z_getbalances",
|
||||
"getnewaddress", "z_getnewaddress",
|
||||
"listaddresses", "z_listaddresses",
|
||||
"sendtoaddress", "z_sendmany",
|
||||
"listtransactions", "listunspent", "z_listunspent",
|
||||
"z_getoperationstatus", "z_getoperationresult",
|
||||
"getwalletinfo", "backupwallet",
|
||||
"dumpprivkey", "importprivkey",
|
||||
"z_exportkey", "z_importkey",
|
||||
"signmessage", "settxfee",
|
||||
// Raw Transactions
|
||||
"createrawtransaction", "decoderawtransaction",
|
||||
"getrawtransaction", "sendrawtransaction", "signrawtransaction",
|
||||
// Utility
|
||||
"validateaddress", "z_validateaddress", "estimatefee",
|
||||
// Built-in
|
||||
"clear"
|
||||
};
|
||||
|
||||
std::string input(data->Buf);
|
||||
if (!input.empty()) {
|
||||
// Collect all matches
|
||||
std::vector<const char*> matches;
|
||||
for (const char* cmd : commands) {
|
||||
if (strncmp(cmd, input.c_str(), input.length()) == 0) {
|
||||
matches.push_back(cmd);
|
||||
}
|
||||
}
|
||||
auto completion = CompleteConsoleCommand(input);
|
||||
|
||||
if (matches.size() == 1) {
|
||||
if (completion.matches.size() == 1) {
|
||||
// Single match — complete it
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, matches[0]);
|
||||
} else if (matches.size() > 1) {
|
||||
data->InsertChars(0, completion.matches.front().c_str());
|
||||
} else if (completion.matches.size() > 1) {
|
||||
// Multiple matches — show list in console and complete common prefix
|
||||
console->addLine(TR("console_completions"), ConsoleTab::COLOR_INFO);
|
||||
std::string line = " ";
|
||||
for (size_t m = 0; m < matches.size(); m++) {
|
||||
if (m > 0) line += " ";
|
||||
line += matches[m];
|
||||
if (line.length() > 60) {
|
||||
console->addLine(line, ConsoleTab::COLOR_RESULT);
|
||||
line = " ";
|
||||
}
|
||||
}
|
||||
if (line.length() > 2) {
|
||||
for (const auto& line : FormatConsoleCompletionLines(completion.matches)) {
|
||||
console->addLine(line, ConsoleTab::COLOR_RESULT);
|
||||
}
|
||||
|
||||
// Complete to longest common prefix
|
||||
std::string prefix = matches[0];
|
||||
for (size_t m = 1; m < matches.size(); m++) {
|
||||
size_t len = 0;
|
||||
while (len < prefix.length() && len < strlen(matches[m]) &&
|
||||
prefix[len] == matches[m][len]) len++;
|
||||
prefix = prefix.substr(0, len);
|
||||
}
|
||||
if (prefix.length() > input.length()) {
|
||||
if (completion.commonPrefix.length() > input.length()) {
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, prefix.c_str());
|
||||
data->InsertChars(0, completion.commonPrefix.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1312,117 +1237,7 @@ void ConsoleTab::renderCommandsPopup()
|
||||
ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Command entries
|
||||
struct CmdEntry { const char* name; const char* desc; const char* params; };
|
||||
|
||||
static const CmdEntry controlCmds[] = {
|
||||
{"help", "List all commands, or get help for a specified command", "[\"command\"]"},
|
||||
{"getinfo", "Get general info about the node", ""},
|
||||
{"stop", "Stop the daemon", ""},
|
||||
};
|
||||
static const CmdEntry networkCmds[] = {
|
||||
{"getnetworkinfo", "Return P2P network state info", ""},
|
||||
{"getpeerinfo", "Get data about each connected peer", ""},
|
||||
{"getconnectioncount", "Get number of peer connections", ""},
|
||||
{"getnettotals", "Get network traffic statistics", ""},
|
||||
{"addnode", "Add, remove, or connect to a node", "\"node\" \"add|remove|onetry\""},
|
||||
{"setban", "Add or remove an IP/subnet from the ban list", "\"ip\" \"add|remove\" [bantime] [absolute]"},
|
||||
{"listbanned", "List all banned IPs/subnets", ""},
|
||||
{"clearbanned", "Clear all banned IPs", ""},
|
||||
{"ping", "Ping all peers to measure round-trip time", ""},
|
||||
};
|
||||
static const CmdEntry blockchainCmds[] = {
|
||||
{"getblockchaininfo", "Get current blockchain state", ""},
|
||||
{"getblockcount", "Get number of blocks in longest chain", ""},
|
||||
{"getbestblockhash", "Get hash of the tip block", ""},
|
||||
{"getblock", "Get block data for a given hash or height", "\"hash|height\" [verbosity]"},
|
||||
{"getblockhash", "Get block hash at a given height", "height"},
|
||||
{"getblockheader", "Get block header for a given hash", "\"hash\" [verbose]"},
|
||||
{"getdifficulty", "Get proof-of-work difficulty", ""},
|
||||
{"getrawmempool", "Get all txids in mempool", "[verbose]"},
|
||||
{"getmempoolinfo", "Get mempool state info", ""},
|
||||
{"gettxout", "Get details about an unspent output", "\"txid\" n [includemempool]"},
|
||||
{"coinsupply", "Get coin supply information", "[height]"},
|
||||
{"getchaintips", "Get all known chain tips", ""},
|
||||
{"getchaintxstats", "Get chain transaction statistics", "[nblocks] [\"blockhash\"]"},
|
||||
{"verifychain", "Verify the blockchain database", "[checklevel] [numblocks]"},
|
||||
{"kvsearch", "Search the blockchain key-value store", "\"key\""},
|
||||
{"kvupdate", "Update a key-value pair on-chain", "\"key\" \"value\" days"},
|
||||
};
|
||||
static const CmdEntry miningCmds[] = {
|
||||
{"getmininginfo", "Get mining-related information", ""},
|
||||
{"setgenerate", "Turn mining on or off (true/false [threads])", "generate [genproclimit]"},
|
||||
{"getgenerate", "Check if the node is mining", ""},
|
||||
{"getnetworkhashps", "Get estimated network hash rate", "[blocks] [height]"},
|
||||
{"getblocksubsidy", "Get block reward at a given height", "[height]"},
|
||||
{"getblocktemplate", "Get block template for mining", "[\"jsonrequestobject\"]"},
|
||||
{"submitblock", "Submit a mined block to the network", "\"hexdata\""},
|
||||
};
|
||||
static const CmdEntry walletCmds[] = {
|
||||
{"getbalance", "Get wallet transparent balance", "[\"account\"] [minconf]"},
|
||||
{"z_gettotalbalance", "Get total transparent + shielded balance", "[minconf]"},
|
||||
{"z_getbalances", "Get all balances (transparent + shielded)", ""},
|
||||
{"getnewaddress", "Generate a new transparent address", ""},
|
||||
{"z_getnewaddress", "Generate a new shielded address", "[\"type\"]"},
|
||||
{"listaddresses", "List all transparent addresses", ""},
|
||||
{"z_listaddresses", "List all z-addresses", ""},
|
||||
{"sendtoaddress", "Send to a specific address", "\"address\" amount"},
|
||||
{"z_sendmany", "Send to multiple z/t-addresses with shielded support", "\"fromaddress\" [{\"address\":\"...\",\"amount\":...}]"},
|
||||
{"z_shieldcoinbase", "Shield transparent coinbase funds to a z-address", "\"fromaddress\" \"tozaddress\" [fee] [limit]"},
|
||||
{"z_mergetoaddress", "Merge multiple UTXOs/notes to one address", "[\"fromaddress\",...] \"toaddress\" [fee] [limit]"},
|
||||
{"listtransactions", "List recent wallet transactions", "[\"account\"] [count] [from]"},
|
||||
{"listunspent", "List unspent transaction outputs", "[minconf] [maxconf]"},
|
||||
{"z_listunspent", "List unspent shielded notes", "[minconf] [maxconf]"},
|
||||
{"z_getoperationstatus", "Get status of async z operations", "[\"operationid\",...]"},
|
||||
{"z_getoperationresult", "Get result of completed z operations", "[\"operationid\",...]"},
|
||||
{"z_listoperationids", "List all async z operation IDs", ""},
|
||||
{"getwalletinfo", "Get wallet state info", ""},
|
||||
{"backupwallet", "Back up wallet to a file", "\"destination\""},
|
||||
{"dumpprivkey", "Dump private key for an address", "\"address\""},
|
||||
{"importprivkey", "Import a private key into the wallet", "\"privkey\" [\"label\"] [rescan]"},
|
||||
{"dumpwallet", "Dump all wallet keys to a file", "\"filename\""},
|
||||
{"importwallet", "Import wallet from a dump file", "\"filename\""},
|
||||
{"z_exportkey", "Export spending key for a z-address", "\"zaddr\""},
|
||||
{"z_importkey", "Import a z-address spending key", "\"zkey\" [rescan] [startheight]"},
|
||||
{"z_exportviewingkey", "Export viewing key for a z-address", "\"zaddr\""},
|
||||
{"z_importviewingkey", "Import a z-address viewing key", "\"vkey\" [rescan] [startheight]"},
|
||||
{"z_exportwallet", "Export all wallet keys (including z-keys) to file", "\"filename\""},
|
||||
{"signmessage", "Sign a message with an address key", "\"address\" \"message\""},
|
||||
{"settxfee", "Set the transaction fee per kB", "amount"},
|
||||
{"walletpassphrase", "Unlock the wallet with passphrase", "\"passphrase\" timeout"},
|
||||
{"walletlock", "Lock the wallet", ""},
|
||||
{"encryptwallet", "Encrypt the wallet with a passphrase", "\"passphrase\""},
|
||||
};
|
||||
static const CmdEntry rawTxCmds[] = {
|
||||
{"createrawtransaction", "Create a raw transaction spending given inputs", "[{\"txid\":\"...\",\"vout\":n},...] {\"address\":amount,...}"},
|
||||
{"decoderawtransaction", "Decode raw transaction hex string", "\"hexstring\""},
|
||||
{"decodescript", "Decode a hex-encoded script", "\"hex\""},
|
||||
{"getrawtransaction", "Get raw transaction data by txid", "\"txid\" [verbose]"},
|
||||
{"sendrawtransaction", "Submit raw transaction to the network", "\"hexstring\" [allowhighfees]"},
|
||||
{"signrawtransaction", "Sign a raw transaction with private keys", "\"hexstring\""},
|
||||
{"fundrawtransaction", "Add inputs to meet output value", "\"hexstring\""},
|
||||
};
|
||||
static const CmdEntry utilCmds[] = {
|
||||
{"validateaddress", "Validate a transparent address", "\"address\""},
|
||||
{"z_validateaddress", "Validate a z-address", "\"zaddr\""},
|
||||
{"estimatefee", "Estimate fee for a transaction", "nblocks"},
|
||||
{"verifymessage", "Verify a signed message", "\"address\" \"signature\" \"message\""},
|
||||
{"createmultisig", "Create a multisig address", "nrequired [\"key\",...]"},
|
||||
{"invalidateblock", "Mark a block as invalid", "\"hash\""},
|
||||
{"reconsiderblock", "Reconsider a previously invalidated block", "\"hash\""},
|
||||
};
|
||||
|
||||
struct CmdCategory { const char* name; const CmdEntry* commands; int count; };
|
||||
|
||||
static const CmdCategory categories[] = {
|
||||
{"Control", controlCmds, IM_ARRAYSIZE(controlCmds)},
|
||||
{"Network", networkCmds, IM_ARRAYSIZE(networkCmds)},
|
||||
{"Blockchain", blockchainCmds, IM_ARRAYSIZE(blockchainCmds)},
|
||||
{"Mining", miningCmds, IM_ARRAYSIZE(miningCmds)},
|
||||
{"Wallet", walletCmds, IM_ARRAYSIZE(walletCmds)},
|
||||
{"Raw Transactions", rawTxCmds, IM_ARRAYSIZE(rawTxCmds)},
|
||||
{"Utility", utilCmds, IM_ARRAYSIZE(utilCmds)},
|
||||
};
|
||||
const auto& categories = consoleCommandCategories();
|
||||
|
||||
std::string filter(cmdFilter);
|
||||
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
||||
@@ -1602,12 +1417,7 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
{
|
||||
using namespace material;
|
||||
// Add to history (avoid duplicates)
|
||||
if (command_history_.empty() || command_history_.back() != cmd) {
|
||||
command_history_.push_back(cmd);
|
||||
if (command_history_.size() > 100) {
|
||||
command_history_.erase(command_history_.begin());
|
||||
}
|
||||
}
|
||||
AppendConsoleHistory(command_history_, cmd, 100);
|
||||
history_index_ = -1;
|
||||
|
||||
// Echo command
|
||||
@@ -1645,77 +1455,11 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse command and arguments (shell-like: handles quotes and JSON brackets)
|
||||
std::vector<std::string> args;
|
||||
{
|
||||
size_t i = 0;
|
||||
size_t len = cmd.size();
|
||||
while (i < len) {
|
||||
// Skip whitespace
|
||||
while (i < len && (cmd[i] == ' ' || cmd[i] == '\t')) i++;
|
||||
if (i >= len) break;
|
||||
auto call = BuildConsoleRpcCall(cmd);
|
||||
if (!call.valid) return;
|
||||
|
||||
std::string tok;
|
||||
if (cmd[i] == '"' || cmd[i] == '\'') {
|
||||
// Quoted string — collect until matching close quote
|
||||
char quote = cmd[i++];
|
||||
while (i < len && cmd[i] != quote) tok += cmd[i++];
|
||||
if (i < len) i++; // skip closing quote
|
||||
} else if (cmd[i] == '[' || cmd[i] == '{') {
|
||||
// JSON array/object — collect until matching bracket
|
||||
char open = cmd[i];
|
||||
char close = (open == '[') ? ']' : '}';
|
||||
int depth = 0;
|
||||
while (i < len) {
|
||||
if (cmd[i] == open) depth++;
|
||||
else if (cmd[i] == close) depth--;
|
||||
tok += cmd[i++];
|
||||
if (depth == 0) break;
|
||||
}
|
||||
} else {
|
||||
// Unquoted token — collect until whitespace
|
||||
while (i < len && cmd[i] != ' ' && cmd[i] != '\t') tok += cmd[i++];
|
||||
}
|
||||
if (!tok.empty()) args.push_back(tok);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.empty()) return;
|
||||
|
||||
std::string method = args[0];
|
||||
nlohmann::json params = nlohmann::json::array();
|
||||
|
||||
// Convert remaining args to JSON params
|
||||
for (size_t i = 1; i < args.size(); i++) {
|
||||
const std::string& arg = args[i];
|
||||
|
||||
// Try to parse as JSON first (handles objects, arrays, etc.)
|
||||
if (!arg.empty() && (arg[0] == '{' || arg[0] == '[')) {
|
||||
auto parsed = nlohmann::json::parse(arg, nullptr, false);
|
||||
if (!parsed.is_discarded()) {
|
||||
params.push_back(parsed);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to parse as number or bool
|
||||
if (arg == "true") {
|
||||
params.push_back(true);
|
||||
} else if (arg == "false") {
|
||||
params.push_back(false);
|
||||
} else {
|
||||
try {
|
||||
if (arg.find('.') != std::string::npos) {
|
||||
params.push_back(std::stod(arg));
|
||||
} else {
|
||||
params.push_back(std::stoll(arg));
|
||||
}
|
||||
} catch (...) {
|
||||
// Keep as string
|
||||
params.push_back(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string method = call.method;
|
||||
nlohmann::json params = call.params;
|
||||
|
||||
// Execute RPC call on worker thread to avoid blocking UI
|
||||
if (worker) {
|
||||
@@ -1726,9 +1470,6 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
bool is_error = false;
|
||||
try {
|
||||
result_str = rpc->callRaw(method, params);
|
||||
if (result_str == "null") {
|
||||
result_str = "(no result)";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
result_str = e.what();
|
||||
is_error = true;
|
||||
@@ -1736,51 +1477,22 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
return [result_str, is_error, self]() {
|
||||
// Process results on main thread where ImGui colors are available
|
||||
using namespace material;
|
||||
if (is_error) {
|
||||
self->addLine("Error: " + result_str, COLOR_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_json = false;
|
||||
if (!result_str.empty()) {
|
||||
char first = result_str[0];
|
||||
is_json = (first == '{' || first == '[');
|
||||
}
|
||||
ImU32 json_key_col = WithAlpha(Secondary(), 255);
|
||||
ImU32 json_str_col = WithAlpha(Success(), 255);
|
||||
ImU32 json_num_col = WithAlpha(Warning(), 255);
|
||||
ImU32 json_brace_col = IM_COL32(200, 200, 200, 150);
|
||||
|
||||
std::istringstream stream(result_str);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
if (is_json && !line.empty()) {
|
||||
std::string trimmed = line;
|
||||
size_t first = trimmed.find_first_not_of(" \t");
|
||||
if (first != std::string::npos) trimmed = trimmed.substr(first);
|
||||
|
||||
ImU32 lineCol = COLOR_RESULT;
|
||||
if (trimmed[0] == '{' || trimmed[0] == '}' ||
|
||||
trimmed[0] == '[' || trimmed[0] == ']') {
|
||||
lineCol = json_brace_col;
|
||||
} else if (trimmed[0] == '\"') {
|
||||
size_t colon = trimmed.find("\": ");
|
||||
if (colon != std::string::npos || trimmed.find("\":") != std::string::npos) {
|
||||
lineCol = json_key_col;
|
||||
} else {
|
||||
lineCol = json_str_col;
|
||||
}
|
||||
} else if (std::isdigit(trimmed[0]) || trimmed[0] == '-') {
|
||||
lineCol = json_num_col;
|
||||
} else if (trimmed == "true," || trimmed == "false," ||
|
||||
trimmed == "true" || trimmed == "false" ||
|
||||
trimmed == "null," || trimmed == "null") {
|
||||
lineCol = json_num_col;
|
||||
}
|
||||
self->addLine(line, lineCol);
|
||||
} else {
|
||||
self->addLine(line, COLOR_RESULT);
|
||||
for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, is_error)) {
|
||||
ImU32 lineCol = COLOR_RESULT;
|
||||
switch (resultLine.role) {
|
||||
case ConsoleResultLineRole::Error: lineCol = COLOR_ERROR; break;
|
||||
case ConsoleResultLineRole::JsonKey: lineCol = json_key_col; break;
|
||||
case ConsoleResultLineRole::JsonString: lineCol = json_str_col; break;
|
||||
case ConsoleResultLineRole::JsonNumber: lineCol = json_num_col; break;
|
||||
case ConsoleResultLineRole::JsonBrace: lineCol = json_brace_col; break;
|
||||
case ConsoleResultLineRole::Result: break;
|
||||
}
|
||||
self->addLine(resultLine.text, lineCol);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1788,14 +1500,13 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
|
||||
// Fallback: synchronous execution if no worker available
|
||||
try {
|
||||
std::string result_str = rpc->callRaw(method, params);
|
||||
if (result_str == "null") result_str = "(no result)";
|
||||
std::istringstream stream(result_str);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
addLine(line, COLOR_RESULT);
|
||||
for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, false)) {
|
||||
addLine(resultLine.text, COLOR_RESULT);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
addLine("Error: " + std::string(e.what()), COLOR_ERROR);
|
||||
for (const auto& resultLine : FormatConsoleRpcResultLines(e.what(), true)) {
|
||||
addLine(resultLine.text, COLOR_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
src/ui/windows/console_tab_helpers.cpp
Normal file
31
src/ui/windows/console_tab_helpers.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "console_tab_helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
float ComputeConsoleInputHeight(float frameHeightWithSpacing,
|
||||
float itemSpacingY,
|
||||
float spacingSm,
|
||||
float spacingXs,
|
||||
float cursorOffset)
|
||||
{
|
||||
return spacingSm + itemSpacingY + frameHeightWithSpacing + spacingSm + spacingXs + cursorOffset;
|
||||
}
|
||||
|
||||
float ComputeConsoleOutputHeight(float availableHeight,
|
||||
float inputHeight,
|
||||
float minHeight,
|
||||
float minHeightRatio)
|
||||
{
|
||||
return std::max(availableHeight - inputHeight, std::max(minHeight, availableHeight * minHeightRatio));
|
||||
}
|
||||
|
||||
float ClampConsoleWrapWidth(float contentWidth, float paddingX)
|
||||
{
|
||||
return std::max(50.0f, contentWidth - paddingX * 2.0f);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
18
src/ui/windows/console_tab_helpers.h
Normal file
18
src/ui/windows/console_tab_helpers.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
float ComputeConsoleInputHeight(float frameHeightWithSpacing,
|
||||
float itemSpacingY,
|
||||
float spacingSm,
|
||||
float spacingXs,
|
||||
float cursorOffset);
|
||||
float ComputeConsoleOutputHeight(float availableHeight,
|
||||
float inputHeight,
|
||||
float minHeight,
|
||||
float minHeightRatio);
|
||||
float ClampConsoleWrapWidth(float contentWidth, float paddingX);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -267,13 +267,12 @@ static void fetchBlockDetailByHash(App* app, const std::string& hash) {
|
||||
});
|
||||
}
|
||||
|
||||
static void fetchRecentBlocks(App* app, int currentHeight, int count = 10) {
|
||||
static bool fetchRecentBlocks(App* app, int currentHeight, int count = 10) {
|
||||
auto* worker = app->worker();
|
||||
auto* rpc = app->rpc();
|
||||
if (!worker || !rpc || s_pending_block_fetches > 0) return;
|
||||
if (!worker || !rpc || s_pending_block_fetches > 0) return false;
|
||||
|
||||
s_recent_blocks.clear();
|
||||
s_recent_blocks.resize(count);
|
||||
if (s_recent_blocks.empty()) s_recent_blocks.resize(count);
|
||||
s_pending_block_fetches = 1; // single batched fetch
|
||||
|
||||
worker->post([rpc, currentHeight, count]() -> rpc::RPCWorker::MainCb {
|
||||
@@ -295,11 +294,19 @@ static void fetchRecentBlocks(App* app, int currentHeight, int count = 10) {
|
||||
bs.tx_count = static_cast<int>(result["tx"].size());
|
||||
} catch (...) {}
|
||||
}
|
||||
return [results]() {
|
||||
s_recent_blocks = results;
|
||||
return [results = std::move(results)]() mutable {
|
||||
bool gotAny = false;
|
||||
for (const auto& block : results) {
|
||||
if (block.height > 0) {
|
||||
gotAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gotAny) s_recent_blocks = std::move(results);
|
||||
s_pending_block_fetches = 0;
|
||||
};
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
static void fetchMempoolInfo(App* app) {
|
||||
@@ -567,10 +574,12 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
if (bs.height > 0) blocks.push_back(&bs);
|
||||
}
|
||||
|
||||
// Fixed card height — content scrolls inside
|
||||
// Stretch card to fill the remaining tab height; rows scroll inside.
|
||||
float maxRows = 10.0f;
|
||||
float contentH = capFont->LegacySize + Layout::spacingXs() + rowH * maxRows;
|
||||
float tableH = headerH + contentH + pad;
|
||||
float minTableH = headerH + contentH + pad;
|
||||
float remainingH = ImGui::GetContentRegionAvail().y;
|
||||
float tableH = std::max(minTableH, remainingH - Layout::spacingSm());
|
||||
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + tableH);
|
||||
@@ -1024,12 +1033,15 @@ void RenderExplorerTab(App* app)
|
||||
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// Auto-refresh recent blocks when chain height changes
|
||||
if (state.sync.blocks > 0 && state.sync.blocks != s_last_known_height) {
|
||||
s_last_known_height = state.sync.blocks;
|
||||
// Auto-refresh recent blocks when chain height changes, but avoid
|
||||
// starting expensive block fetches while the user is viewing details.
|
||||
if (state.sync.blocks > 0 && state.sync.blocks != s_last_known_height &&
|
||||
!s_show_detail_modal && !s_detail_loading && !s_tx_loading) {
|
||||
if (rpc && rpc->isConnected()) {
|
||||
fetchRecentBlocks(app, state.sync.blocks);
|
||||
fetchMempoolInfo(app);
|
||||
if (fetchRecentBlocks(app, state.sync.blocks)) {
|
||||
s_last_known_height = state.sync.blocks;
|
||||
fetchMempoolInfo(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -306,7 +306,20 @@ void RenderMarketTab(App* app)
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
} else {
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), TR("market_price_unavailable"));
|
||||
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), status);
|
||||
if (!market.price_loading && !market.price_error.empty()) {
|
||||
std::string errorText = market.price_error;
|
||||
float maxErrorW = cardMax.x - cx - Layout::spacingLg();
|
||||
while (errorText.size() > 4 &&
|
||||
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, errorText.c_str()).x > maxErrorW) {
|
||||
errorText.pop_back();
|
||||
}
|
||||
if (errorText.size() < market.price_error.size()) errorText += "...";
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
|
||||
Warning(), errorText.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||
|
||||
211
src/ui/windows/mining_benchmark.cpp
Normal file
211
src/ui/windows/mining_benchmark.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include "mining_benchmark.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
void ThreadBenchmark::reset()
|
||||
{
|
||||
phase = Phase::Idle;
|
||||
candidates.clear();
|
||||
current_index = 0;
|
||||
results.clear();
|
||||
phase_timer = 0.0f;
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
measure_sum = 0.0;
|
||||
measure_samples = 0;
|
||||
optimal_threads = 0;
|
||||
optimal_hashrate = 0.0;
|
||||
was_pool_running = false;
|
||||
prev_threads = 0;
|
||||
total_warmup_secs = 0.0f;
|
||||
}
|
||||
|
||||
void ThreadBenchmark::buildCandidates(int maxThreads)
|
||||
{
|
||||
candidates.clear();
|
||||
int start = std::max(1, maxThreads / 2);
|
||||
for (int threads = start; threads <= maxThreads; ++threads) {
|
||||
candidates.push_back(threads);
|
||||
}
|
||||
}
|
||||
|
||||
float ThreadBenchmark::avgWarmupSecs() const
|
||||
{
|
||||
if (current_index > 0) {
|
||||
return total_warmup_secs / static_cast<float>(current_index);
|
||||
}
|
||||
return (MIN_WARMUP_SECS + MAX_WARMUP_SECS) * 0.5f;
|
||||
}
|
||||
|
||||
float ThreadBenchmark::perTestSecs() const
|
||||
{
|
||||
return avgWarmupSecs() + MEASURE_SECS;
|
||||
}
|
||||
|
||||
float ThreadBenchmark::totalEstimatedSecs() const
|
||||
{
|
||||
int count = static_cast<int>(candidates.size());
|
||||
if (count <= 0) return 0.0f;
|
||||
float completedTime = total_warmup_secs
|
||||
+ static_cast<float>(current_index) * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
int remaining = count - current_index;
|
||||
float remainingTime = static_cast<float>(remaining) * (avgWarmupSecs() + MEASURE_SECS)
|
||||
+ static_cast<float>(std::max(0, remaining - 1)) * COOLDOWN_SECS;
|
||||
return completedTime + remainingTime;
|
||||
}
|
||||
|
||||
float ThreadBenchmark::elapsedSecs() const
|
||||
{
|
||||
float completed = total_warmup_secs
|
||||
+ static_cast<float>(current_index) * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
return completed + phase_timer;
|
||||
}
|
||||
|
||||
float ThreadBenchmark::progress() const
|
||||
{
|
||||
float total = totalEstimatedSecs();
|
||||
return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f;
|
||||
}
|
||||
|
||||
void ThreadBenchmark::resetStabilityTracking()
|
||||
{
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
}
|
||||
|
||||
bool ThreadBenchmark::active() const
|
||||
{
|
||||
return phase != Phase::Idle && phase != Phase::Done;
|
||||
}
|
||||
|
||||
ThreadBenchmarkUpdate AdvanceThreadBenchmark(ThreadBenchmark& benchmark,
|
||||
float deltaSeconds,
|
||||
double poolHashrate10s)
|
||||
{
|
||||
ThreadBenchmarkUpdate update;
|
||||
if (!benchmark.active()) return update;
|
||||
|
||||
benchmark.phase_timer += deltaSeconds;
|
||||
|
||||
switch (benchmark.phase) {
|
||||
case ThreadBenchmark::Phase::Starting:
|
||||
if (benchmark.current_index < static_cast<int>(benchmark.candidates.size())) {
|
||||
int threads = benchmark.candidates[benchmark.current_index];
|
||||
update.stopPoolMining = true;
|
||||
update.startPoolMining = true;
|
||||
update.startThreads = threads;
|
||||
benchmark.phase = ThreadBenchmark::Phase::WarmingUp;
|
||||
benchmark.phase_timer = 0.0f;
|
||||
benchmark.resetStabilityTracking();
|
||||
benchmark.measure_sum = 0.0;
|
||||
benchmark.measure_samples = 0;
|
||||
} else {
|
||||
benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::WarmingUp: {
|
||||
bool pastMin = benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS;
|
||||
bool pastMax = benchmark.phase_timer >= ThreadBenchmark::MAX_WARMUP_SECS;
|
||||
|
||||
if (poolHashrate10s > 0.0) {
|
||||
benchmark.window_sum += poolHashrate10s;
|
||||
benchmark.window_samples++;
|
||||
}
|
||||
benchmark.window_timer += deltaSeconds;
|
||||
|
||||
bool stable = false;
|
||||
if (pastMin && benchmark.window_timer >= ThreadBenchmark::STABILITY_WINDOW_SECS &&
|
||||
benchmark.window_samples > 0) {
|
||||
double currentAverage = benchmark.window_sum / benchmark.window_samples;
|
||||
if (benchmark.prev_window_avg > 0.0) {
|
||||
double change = std::abs(currentAverage - benchmark.prev_window_avg)
|
||||
/ benchmark.prev_window_avg;
|
||||
if (change < ThreadBenchmark::STABILITY_THRESHOLD)
|
||||
benchmark.consecutive_stable++;
|
||||
else
|
||||
benchmark.consecutive_stable = 0;
|
||||
if (benchmark.consecutive_stable >= ThreadBenchmark::STABLE_WINDOWS_NEEDED)
|
||||
stable = true;
|
||||
}
|
||||
benchmark.prev_window_avg = currentAverage;
|
||||
benchmark.window_sum = 0.0;
|
||||
benchmark.window_samples = 0;
|
||||
benchmark.window_timer = 0.0f;
|
||||
}
|
||||
|
||||
if (stable || pastMax) {
|
||||
benchmark.total_warmup_secs += benchmark.phase_timer;
|
||||
benchmark.phase = ThreadBenchmark::Phase::Measuring;
|
||||
benchmark.phase_timer = 0.0f;
|
||||
benchmark.measure_sum = 0.0;
|
||||
benchmark.measure_samples = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ThreadBenchmark::Phase::Measuring:
|
||||
if (poolHashrate10s > 0.0) {
|
||||
benchmark.measure_sum += poolHashrate10s;
|
||||
benchmark.measure_samples++;
|
||||
}
|
||||
if (benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) {
|
||||
int threads = benchmark.candidates[benchmark.current_index];
|
||||
double average = (benchmark.measure_samples > 0)
|
||||
? benchmark.measure_sum / benchmark.measure_samples
|
||||
: 0.0;
|
||||
benchmark.results.push_back({threads, average});
|
||||
if (average > benchmark.optimal_hashrate) {
|
||||
benchmark.optimal_hashrate = average;
|
||||
benchmark.optimal_threads = threads;
|
||||
}
|
||||
benchmark.phase = ThreadBenchmark::Phase::Advancing;
|
||||
benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::Advancing:
|
||||
update.stopPoolMining = true;
|
||||
benchmark.current_index++;
|
||||
if (benchmark.current_index < static_cast<int>(benchmark.candidates.size())) {
|
||||
benchmark.phase = ThreadBenchmark::Phase::CoolingDown;
|
||||
benchmark.phase_timer = 0.0f;
|
||||
} else {
|
||||
benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
if (benchmark.optimal_threads > 0) {
|
||||
update.saveOptimalThreads = true;
|
||||
update.optimalThreads = benchmark.optimal_threads;
|
||||
if (benchmark.was_pool_running) {
|
||||
update.startPoolMining = true;
|
||||
update.startThreads = benchmark.optimal_threads;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::CoolingDown:
|
||||
if (benchmark.phase_timer >= ThreadBenchmark::COOLDOWN_SECS) {
|
||||
benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||
benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
66
src/ui/windows/mining_benchmark.h
Normal file
66
src/ui/windows/mining_benchmark.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct ThreadBenchmark {
|
||||
enum class Phase { Idle, Starting, WarmingUp, Measuring, Advancing, CoolingDown, Done };
|
||||
|
||||
struct Result {
|
||||
int threads = 0;
|
||||
double hashrate = 0.0;
|
||||
};
|
||||
|
||||
static constexpr float MIN_WARMUP_SECS = 90.0f;
|
||||
static constexpr float MAX_WARMUP_SECS = 300.0f;
|
||||
static constexpr float MEASURE_SECS = 30.0f;
|
||||
static constexpr float COOLDOWN_SECS = 5.0f;
|
||||
static constexpr float STABILITY_WINDOW_SECS = 10.0f;
|
||||
static constexpr float STABILITY_THRESHOLD = 0.05f;
|
||||
static constexpr int STABLE_WINDOWS_NEEDED = 3;
|
||||
|
||||
Phase phase = Phase::Idle;
|
||||
std::vector<int> candidates;
|
||||
int current_index = 0;
|
||||
std::vector<Result> results;
|
||||
float phase_timer = 0.0f;
|
||||
double prev_window_avg = 0.0;
|
||||
double window_sum = 0.0;
|
||||
int window_samples = 0;
|
||||
float window_timer = 0.0f;
|
||||
int consecutive_stable = 0;
|
||||
double measure_sum = 0.0;
|
||||
int measure_samples = 0;
|
||||
int optimal_threads = 0;
|
||||
double optimal_hashrate = 0.0;
|
||||
bool was_pool_running = false;
|
||||
int prev_threads = 0;
|
||||
float total_warmup_secs = 0.0f;
|
||||
|
||||
void reset();
|
||||
void buildCandidates(int maxThreads);
|
||||
float avgWarmupSecs() const;
|
||||
float perTestSecs() const;
|
||||
float totalEstimatedSecs() const;
|
||||
float elapsedSecs() const;
|
||||
float progress() const;
|
||||
void resetStabilityTracking();
|
||||
bool active() const;
|
||||
};
|
||||
|
||||
struct ThreadBenchmarkUpdate {
|
||||
bool stopPoolMining = false;
|
||||
bool startPoolMining = false;
|
||||
int startThreads = 0;
|
||||
bool saveOptimalThreads = false;
|
||||
int optimalThreads = 0;
|
||||
};
|
||||
|
||||
ThreadBenchmarkUpdate AdvanceThreadBenchmark(ThreadBenchmark& benchmark,
|
||||
float deltaSeconds,
|
||||
double poolHashrate10s);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
36
src/ui/windows/mining_pool_panel.cpp
Normal file
36
src/ui/windows/mining_pool_panel.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "mining_pool_panel.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
bool shouldDefaultPoolWorker(const std::string& currentWorker, bool alreadyDefaulted)
|
||||
{
|
||||
return !alreadyDefaulted && (currentWorker.empty() || currentWorker == "x");
|
||||
}
|
||||
|
||||
std::string defaultPoolWorkerAddress(const std::vector<AddressInfo>& addresses)
|
||||
{
|
||||
for (const auto& addr : addresses) {
|
||||
if (addr.type == "shielded" && !addr.address.empty()) {
|
||||
return addr.address;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool miningValueAlreadySaved(const std::vector<std::string>& savedValues,
|
||||
const std::string& value)
|
||||
{
|
||||
if (value.empty()) return false;
|
||||
return std::find(savedValues.begin(), savedValues.end(), value) != savedValues.end();
|
||||
}
|
||||
|
||||
const char* defaultPoolUrl()
|
||||
{
|
||||
return "pool.dragonx.is:3433";
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
18
src/ui/windows/mining_pool_panel.h
Normal file
18
src/ui/windows/mining_pool_panel.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../data/wallet_state.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
bool shouldDefaultPoolWorker(const std::string& currentWorker, bool alreadyDefaulted);
|
||||
std::string defaultPoolWorkerAddress(const std::vector<AddressInfo>& addresses);
|
||||
bool miningValueAlreadySaved(const std::vector<std::string>& savedValues,
|
||||
const std::string& value);
|
||||
const char* defaultPoolUrl();
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -3,6 +3,9 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "mining_tab.h"
|
||||
#include "mining_benchmark.h"
|
||||
#include "mining_tab_helpers.h"
|
||||
#include "mining_pool_panel.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../config/version.h"
|
||||
@@ -43,130 +46,10 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
|
||||
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
|
||||
static int s_earnings_filter = 0;
|
||||
|
||||
// Thread benchmark state
|
||||
struct ThreadBenchmark {
|
||||
enum class Phase { Idle, Starting, WarmingUp, Measuring, Advancing, CoolingDown, Done };
|
||||
Phase phase = Phase::Idle;
|
||||
|
||||
std::vector<int> candidates;
|
||||
int current_index = 0;
|
||||
|
||||
struct Result {
|
||||
int threads;
|
||||
double hashrate;
|
||||
};
|
||||
std::vector<Result> results;
|
||||
|
||||
float phase_timer = 0.0f;
|
||||
|
||||
// Warmup: wait at least MIN then check for hashrate stability; cap at MAX.
|
||||
// Laptops need 90s+ for thermal throttling to fully manifest.
|
||||
static constexpr float MIN_WARMUP_SECS = 90.0f;
|
||||
static constexpr float MAX_WARMUP_SECS = 300.0f;
|
||||
static constexpr float MEASURE_SECS = 30.0f;
|
||||
static constexpr float COOLDOWN_SECS = 5.0f;
|
||||
|
||||
// Stability detection — compare rolling 10s hashrate windows.
|
||||
// Require STABLE_WINDOWS_NEEDED consecutive stable readings.
|
||||
static constexpr float STABILITY_WINDOW_SECS = 10.0f;
|
||||
static constexpr float STABILITY_THRESHOLD = 0.05f; // 5% change → stable
|
||||
static constexpr int STABLE_WINDOWS_NEEDED = 3;
|
||||
double prev_window_avg = 0.0;
|
||||
double window_sum = 0.0;
|
||||
int window_samples = 0;
|
||||
float window_timer = 0.0f;
|
||||
int consecutive_stable = 0; // count of consecutive stable windows
|
||||
|
||||
// Measurement: average-based (sustained performance, not peak burst)
|
||||
double measure_sum = 0.0;
|
||||
int measure_samples = 0;
|
||||
|
||||
int optimal_threads = 0;
|
||||
double optimal_hashrate = 0.0;
|
||||
bool was_pool_running = false;
|
||||
int prev_threads = 0;
|
||||
|
||||
// Track actual warmup durations for better time estimates
|
||||
float total_warmup_secs = 0.0f;
|
||||
|
||||
void reset() {
|
||||
phase = Phase::Idle;
|
||||
candidates.clear();
|
||||
current_index = 0;
|
||||
results.clear();
|
||||
phase_timer = 0.0f;
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
measure_sum = 0.0;
|
||||
measure_samples = 0;
|
||||
optimal_threads = 0;
|
||||
optimal_hashrate = 0.0;
|
||||
was_pool_running = false;
|
||||
prev_threads = 0;
|
||||
total_warmup_secs = 0.0f;
|
||||
}
|
||||
|
||||
void buildCandidates(int max_threads) {
|
||||
candidates.clear();
|
||||
// Start at half the cores — lower counts are rarely optimal and
|
||||
// testing them first would waste time warming up the CPU before
|
||||
// reaching the thread counts that actually matter.
|
||||
int start = std::max(1, max_threads / 2);
|
||||
for (int t = start; t <= max_threads; t++)
|
||||
candidates.push_back(t);
|
||||
}
|
||||
|
||||
/// Average warmup duration based on tests completed so far
|
||||
float avgWarmupSecs() const {
|
||||
if (current_index > 0)
|
||||
return total_warmup_secs / (float)current_index;
|
||||
return (MIN_WARMUP_SECS + MAX_WARMUP_SECS) * 0.5f; // initial estimate
|
||||
}
|
||||
|
||||
/// Estimated seconds per test (uses observed warmup average)
|
||||
float perTestSecs() const {
|
||||
return avgWarmupSecs() + MEASURE_SECS;
|
||||
}
|
||||
|
||||
float totalEstimatedSecs() const {
|
||||
int n = (int)candidates.size();
|
||||
if (n <= 0) return 0.0f;
|
||||
// Completed tests use actual time; remaining use estimate
|
||||
float completed_time = total_warmup_secs
|
||||
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
int remaining = n - current_index;
|
||||
float remaining_time = (float)remaining * (avgWarmupSecs() + MEASURE_SECS)
|
||||
+ (float)std::max(0, remaining - 1) * COOLDOWN_SECS;
|
||||
return completed_time + remaining_time;
|
||||
}
|
||||
|
||||
float elapsedSecs() const {
|
||||
float completed = total_warmup_secs
|
||||
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
return completed + phase_timer;
|
||||
}
|
||||
|
||||
float progress() const {
|
||||
float total = totalEstimatedSecs();
|
||||
return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f;
|
||||
}
|
||||
|
||||
void resetStabilityTracking() {
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
}
|
||||
};
|
||||
static ThreadBenchmark s_benchmark;
|
||||
|
||||
bool IsMiningBenchmarkActive() {
|
||||
return s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||
return s_benchmark.active();
|
||||
}
|
||||
|
||||
// Pool mode state
|
||||
@@ -178,59 +61,6 @@ 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()
|
||||
{
|
||||
int hw_threads = std::thread::hardware_concurrency();
|
||||
return std::max(1, hw_threads);
|
||||
}
|
||||
|
||||
// Format hashrate with appropriate units
|
||||
static std::string FormatHashrate(double hashrate)
|
||||
{
|
||||
char buf[64];
|
||||
if (hashrate >= 1e12) {
|
||||
snprintf(buf, sizeof(buf), "%.2f TH/s", hashrate / 1e12);
|
||||
} else if (hashrate >= 1e9) {
|
||||
snprintf(buf, sizeof(buf), "%.2f GH/s", hashrate / 1e9);
|
||||
} else if (hashrate >= 1e6) {
|
||||
snprintf(buf, sizeof(buf), "%.2f MH/s", hashrate / 1e6);
|
||||
} else if (hashrate >= 1e3) {
|
||||
snprintf(buf, sizeof(buf), "%.2f KH/s", hashrate / 1e3);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%.2f H/s", hashrate);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Calculate estimated hours to find a block
|
||||
static double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty)
|
||||
{
|
||||
if (localHashrate <= 0 || networkHashrate <= 0) return 0;
|
||||
double blocksPerHour = 3600.0 / 75.0;
|
||||
double yourShare = localHashrate / networkHashrate;
|
||||
if (yourShare <= 0) return 0;
|
||||
return 1.0 / (blocksPerHour * yourShare);
|
||||
}
|
||||
|
||||
// Format estimated time
|
||||
static std::string FormatEstTime(double est_hours)
|
||||
{
|
||||
char buf[64];
|
||||
if (est_hours <= 0) {
|
||||
return "N/A";
|
||||
} else if (est_hours < 1.0) {
|
||||
snprintf(buf, sizeof(buf), "~%.0f min", est_hours * 60.0);
|
||||
} else if (est_hours < 24.0) {
|
||||
snprintf(buf, sizeof(buf), "~%.1f hrs", est_hours);
|
||||
} else if (est_hours < 168.0) {
|
||||
snprintf(buf, sizeof(buf), "~%.1f days", est_hours / 24.0);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "~%.1f weeks", est_hours / 168.0);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
static void RenderMiningTabContent(App* app);
|
||||
|
||||
void RenderMiningTab(App* app)
|
||||
@@ -279,9 +109,9 @@ static void RenderMiningTabContent(App* app)
|
||||
if (!s_threads_initialized) {
|
||||
int saved = app->settings()->getPoolThreads();
|
||||
if (mining.generate)
|
||||
s_selected_threads = std::max(1, mining.genproclimit);
|
||||
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
|
||||
else if (saved > 0)
|
||||
s_selected_threads = std::min(saved, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(saved, max_threads);
|
||||
else
|
||||
s_selected_threads = 1;
|
||||
s_threads_initialized = true;
|
||||
@@ -295,11 +125,11 @@ static void RenderMiningTabContent(App* app)
|
||||
// than threads_active from the xmrig API which lags during restarts.
|
||||
int reqThreads = app->getXmrigRequestedThreads();
|
||||
if (reqThreads > 0)
|
||||
s_selected_threads = std::min(reqThreads, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(reqThreads, max_threads);
|
||||
else if (state.pool_mining.threads_active > 0)
|
||||
s_selected_threads = std::min(state.pool_mining.threads_active, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(state.pool_mining.threads_active, max_threads);
|
||||
} else if (mining.generate && mining.genproclimit > 0) {
|
||||
s_selected_threads = std::min(mining.genproclimit, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,15 +158,8 @@ static void RenderMiningTabContent(App* app)
|
||||
{
|
||||
static bool s_pool_worker_defaulted = false;
|
||||
std::string workerStr(s_pool_worker);
|
||||
if (!s_pool_worker_defaulted && !state.addresses.empty() &&
|
||||
(workerStr.empty() || workerStr == "x")) {
|
||||
std::string defaultAddr;
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "shielded" && !addr.address.empty()) {
|
||||
defaultAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDefaultPoolWorker(workerStr, s_pool_worker_defaulted) && !state.addresses.empty()) {
|
||||
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
|
||||
if (!defaultAddr.empty()) {
|
||||
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
@@ -368,136 +191,27 @@ static void RenderMiningTabContent(App* app)
|
||||
// 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 || state.pool_mining.xmrig_running);
|
||||
bool isMiningActive = IsPoolMiningActive(s_pool_mode,
|
||||
state.pool_mining.xmrig_running,
|
||||
mining.generate);
|
||||
|
||||
// ================================================================
|
||||
// Thread Benchmark state machine — runs pool mining at each candidate
|
||||
// thread count to find the optimal setting for this CPU.
|
||||
// ================================================================
|
||||
if (s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done) {
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
s_benchmark.phase_timer += dt;
|
||||
|
||||
switch (s_benchmark.phase) {
|
||||
case ThreadBenchmark::Phase::Starting:
|
||||
// Start pool mining at current candidate
|
||||
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
|
||||
int t = s_benchmark.candidates[s_benchmark.current_index];
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(t);
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::WarmingUp;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
s_benchmark.resetStabilityTracking();
|
||||
s_benchmark.measure_sum = 0.0;
|
||||
s_benchmark.measure_samples = 0;
|
||||
} else {
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::WarmingUp: {
|
||||
// Adaptive warmup: wait for hashrate to stabilize (thermal steady state).
|
||||
// After MIN_WARMUP (90s), compare rolling 10s hashrate windows.
|
||||
// Require 3 consecutive windows within 5% to confirm equilibrium.
|
||||
// Laptops can take 2-3+ minutes for thermal throttling to fully
|
||||
// manifest, so a single stable window isn't sufficient.
|
||||
bool past_min = s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS;
|
||||
bool past_max = s_benchmark.phase_timer >= ThreadBenchmark::MAX_WARMUP_SECS;
|
||||
|
||||
// Accumulate samples into current window
|
||||
if (state.pool_mining.hashrate_10s > 0.0) {
|
||||
s_benchmark.window_sum += state.pool_mining.hashrate_10s;
|
||||
s_benchmark.window_samples++;
|
||||
}
|
||||
s_benchmark.window_timer += dt;
|
||||
|
||||
bool stable = false;
|
||||
if (past_min && s_benchmark.window_timer >= ThreadBenchmark::STABILITY_WINDOW_SECS
|
||||
&& s_benchmark.window_samples > 0) {
|
||||
double current_avg = s_benchmark.window_sum / s_benchmark.window_samples;
|
||||
if (s_benchmark.prev_window_avg > 0.0) {
|
||||
double change = std::abs(current_avg - s_benchmark.prev_window_avg)
|
||||
/ s_benchmark.prev_window_avg;
|
||||
if (change < ThreadBenchmark::STABILITY_THRESHOLD)
|
||||
s_benchmark.consecutive_stable++;
|
||||
else
|
||||
s_benchmark.consecutive_stable = 0; // reset on instability
|
||||
if (s_benchmark.consecutive_stable >= ThreadBenchmark::STABLE_WINDOWS_NEEDED)
|
||||
stable = true;
|
||||
}
|
||||
// Shift window
|
||||
s_benchmark.prev_window_avg = current_avg;
|
||||
s_benchmark.window_sum = 0.0;
|
||||
s_benchmark.window_samples = 0;
|
||||
s_benchmark.window_timer = 0.0f;
|
||||
}
|
||||
|
||||
if (stable || past_max) {
|
||||
s_benchmark.total_warmup_secs += s_benchmark.phase_timer;
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Measuring;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
s_benchmark.measure_sum = 0.0;
|
||||
s_benchmark.measure_samples = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ThreadBenchmark::Phase::Measuring:
|
||||
// Sample average hashrate — reflects sustained (thermally throttled) performance
|
||||
if (state.pool_mining.hashrate_10s > 0.0) {
|
||||
s_benchmark.measure_sum += state.pool_mining.hashrate_10s;
|
||||
s_benchmark.measure_samples++;
|
||||
}
|
||||
if (s_benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) {
|
||||
int t = s_benchmark.candidates[s_benchmark.current_index];
|
||||
double avg = (s_benchmark.measure_samples > 0)
|
||||
? s_benchmark.measure_sum / s_benchmark.measure_samples
|
||||
: 0.0;
|
||||
s_benchmark.results.push_back({t, avg});
|
||||
if (avg > s_benchmark.optimal_hashrate) {
|
||||
s_benchmark.optimal_hashrate = avg;
|
||||
s_benchmark.optimal_threads = t;
|
||||
}
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Advancing;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::Advancing:
|
||||
if (s_benchmark.active()) {
|
||||
auto benchmarkUpdate = AdvanceThreadBenchmark(
|
||||
s_benchmark, ImGui::GetIO().DeltaTime, state.pool_mining.hashrate_10s);
|
||||
if (benchmarkUpdate.stopPoolMining) {
|
||||
app->stopPoolMining();
|
||||
s_benchmark.current_index++;
|
||||
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
|
||||
// Cool down before next test to reduce thermal throttling bias
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::CoolingDown;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
} else {
|
||||
// Done — apply optimal thread count
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
if (s_benchmark.optimal_threads > 0) {
|
||||
s_selected_threads = s_benchmark.optimal_threads;
|
||||
app->settings()->setPoolThreads(s_selected_threads);
|
||||
app->settings()->save();
|
||||
}
|
||||
// Restart mining if it was running before, using optimal count
|
||||
if (s_benchmark.was_pool_running && s_benchmark.optimal_threads > 0) {
|
||||
app->startPoolMining(s_benchmark.optimal_threads);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::CoolingDown:
|
||||
// Idle pause — let CPU temps drop before starting next test
|
||||
if (s_benchmark.phase_timer >= ThreadBenchmark::COOLDOWN_SECS) {
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (benchmarkUpdate.saveOptimalThreads) {
|
||||
s_selected_threads = benchmarkUpdate.optimalThreads;
|
||||
app->settings()->setPoolThreads(s_selected_threads);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (benchmarkUpdate.startPoolMining) {
|
||||
app->startPoolMining(benchmarkUpdate.startThreads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,10 +404,7 @@ static void RenderMiningTabContent(App* app)
|
||||
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; }
|
||||
}
|
||||
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
@@ -875,10 +586,7 @@ static void RenderMiningTabContent(App* app)
|
||||
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; }
|
||||
}
|
||||
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
@@ -1036,16 +744,10 @@ static void RenderMiningTabContent(App* app)
|
||||
OnSurfaceMedium(), resetIcon);
|
||||
|
||||
if (btnClk) {
|
||||
strncpy(s_pool_url, "pool.dragonx.is:3433", sizeof(s_pool_url) - 1);
|
||||
strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1);
|
||||
// Default to user's first shielded (z) address for pool payouts.
|
||||
// Leave blank if no z-address exists yet.
|
||||
std::string defaultAddr;
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "shielded" && !addr.address.empty()) {
|
||||
defaultAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
|
||||
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
|
||||
72
src/ui/windows/mining_tab_helpers.cpp
Normal file
72
src/ui/windows/mining_tab_helpers.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "mining_tab_helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
int GetMaxMiningThreads()
|
||||
{
|
||||
int hardwareThreads = static_cast<int>(std::thread::hardware_concurrency());
|
||||
return std::max(1, hardwareThreads);
|
||||
}
|
||||
|
||||
int ClampMiningThreads(int requestedThreads, int maxThreads)
|
||||
{
|
||||
int boundedMax = std::max(1, maxThreads);
|
||||
return std::clamp(requestedThreads, 1, boundedMax);
|
||||
}
|
||||
|
||||
bool IsPoolMiningActive(bool poolMode, bool xmrigRunning, bool soloMiningRunning)
|
||||
{
|
||||
return poolMode ? xmrigRunning : (soloMiningRunning || xmrigRunning);
|
||||
}
|
||||
|
||||
std::string FormatHashrate(double hashrate)
|
||||
{
|
||||
char buffer[64];
|
||||
if (hashrate >= 1e12) {
|
||||
snprintf(buffer, sizeof(buffer), "%.2f TH/s", hashrate / 1e12);
|
||||
} else if (hashrate >= 1e9) {
|
||||
snprintf(buffer, sizeof(buffer), "%.2f GH/s", hashrate / 1e9);
|
||||
} else if (hashrate >= 1e6) {
|
||||
snprintf(buffer, sizeof(buffer), "%.2f MH/s", hashrate / 1e6);
|
||||
} else if (hashrate >= 1e3) {
|
||||
snprintf(buffer, sizeof(buffer), "%.2f KH/s", hashrate / 1e3);
|
||||
} else {
|
||||
snprintf(buffer, sizeof(buffer), "%.2f H/s", hashrate);
|
||||
}
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty)
|
||||
{
|
||||
(void)difficulty;
|
||||
if (localHashrate <= 0.0 || networkHashrate <= 0.0) return 0.0;
|
||||
double blocksPerHour = 3600.0 / 75.0;
|
||||
double share = localHashrate / networkHashrate;
|
||||
if (share <= 0.0) return 0.0;
|
||||
return 1.0 / (blocksPerHour * share);
|
||||
}
|
||||
|
||||
std::string FormatEstTime(double estimatedHours)
|
||||
{
|
||||
char buffer[64];
|
||||
if (estimatedHours <= 0.0) {
|
||||
return "N/A";
|
||||
} else if (estimatedHours < 1.0) {
|
||||
snprintf(buffer, sizeof(buffer), "~%.0f min", estimatedHours * 60.0);
|
||||
} else if (estimatedHours < 24.0) {
|
||||
snprintf(buffer, sizeof(buffer), "~%.1f hrs", estimatedHours);
|
||||
} else if (estimatedHours < 168.0) {
|
||||
snprintf(buffer, sizeof(buffer), "~%.1f days", estimatedHours / 24.0);
|
||||
} else {
|
||||
snprintf(buffer, sizeof(buffer), "~%.1f weeks", estimatedHours / 168.0);
|
||||
}
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
16
src/ui/windows/mining_tab_helpers.h
Normal file
16
src/ui/windows/mining_tab_helpers.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
int GetMaxMiningThreads();
|
||||
int ClampMiningThreads(int requestedThreads, int maxThreads);
|
||||
bool IsPoolMiningActive(bool poolMode, bool xmrigRunning, bool soloMiningRunning);
|
||||
std::string FormatHashrate(double hashrate);
|
||||
double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty);
|
||||
std::string FormatEstTime(double estimatedHours);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -564,7 +564,6 @@ void RenderPeersTab(App* app)
|
||||
ImVec2 bMax(btnX + btnW, btnY + btnH);
|
||||
|
||||
bool btnHovered = material::IsRectHovered(bMin, bMax);
|
||||
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
|
||||
|
||||
// Glass panel background
|
||||
GlassPanelSpec btnGlass;
|
||||
@@ -635,7 +634,8 @@ void RenderPeersTab(App* app)
|
||||
} else {
|
||||
lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160);
|
||||
}
|
||||
float lblX = cx + iconSz * 0.5f + Layout::spacingXs();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -219,14 +219,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
|
||||
// Auto-select the address with the largest balance on first load
|
||||
if (!s_auto_selected && app->isConnected() && !state.addresses.empty()) {
|
||||
int bestIdx = -1;
|
||||
double bestBal = 0.0;
|
||||
for (size_t i = 0; i < state.addresses.size(); i++) {
|
||||
if (state.addresses[i].balance > bestBal && state.addresses[i].isSpendable()) {
|
||||
bestBal = state.addresses[i].balance;
|
||||
bestIdx = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
int bestIdx = bestSpendableAddressIndex(state.addresses);
|
||||
if (bestIdx >= 0) {
|
||||
s_selected_from_idx = bestIdx;
|
||||
snprintf(s_from_address, sizeof(s_from_address), "%s",
|
||||
@@ -260,16 +253,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
||||
} else {
|
||||
// Sort by balance descending, only show spendable addresses with balance
|
||||
std::vector<size_t> sortedIdx;
|
||||
sortedIdx.reserve(state.addresses.size());
|
||||
for (size_t i = 0; i < state.addresses.size(); i++) {
|
||||
if (state.addresses[i].balance > 0 && state.addresses[i].isSpendable())
|
||||
sortedIdx.push_back(i);
|
||||
}
|
||||
std::sort(sortedIdx.begin(), sortedIdx.end(),
|
||||
[&](size_t a, size_t b) {
|
||||
return state.addresses[a].balance > state.addresses[b].balance;
|
||||
});
|
||||
std::vector<size_t> sortedIdx = sortedSpendableAddressIndices(state.addresses);
|
||||
|
||||
if (sortedIdx.empty()) {
|
||||
ImGui::TextDisabled("%s", TR("send_no_balance"));
|
||||
@@ -731,14 +715,16 @@ void RenderSendConfirmPopup(App* app) {
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
|
||||
ImVec2 cMin = ImGui::GetCursorScreenPos();
|
||||
float cH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
|
||||
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
|
||||
float configuredH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
|
||||
float contentH = Layout::spacingMd() * 2.0f + capFont->LegacySize * 2.0f + sub1->LegacySize + rowStep * 2.0f;
|
||||
float cH = std::max(configuredH, contentH);
|
||||
ImVec2 cMax(cMin.x + popW, cMin.y + cH);
|
||||
GlassPanelSpec gs; gs.rounding = popGlassRound;
|
||||
DrawGlassPanel(popDl, cMin, cMax, gs);
|
||||
|
||||
float cx = cMin.x + Layout::spacingMd() + Layout::spacingXs();
|
||||
float cy = cMin.y + Layout::spacingSm() + Layout::spacingXs();
|
||||
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
|
||||
float cx = cMin.x + Layout::spacingLg();
|
||||
float cy = cMin.y + Layout::spacingMd();
|
||||
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_amount"));
|
||||
snprintf(buf, sizeof(buf), "%.8f %s", s_amount, DRAGONX_TICKER);
|
||||
@@ -756,11 +742,11 @@ void RenderSendConfirmPopup(App* app) {
|
||||
snprintf(buf, sizeof(buf), "$%.6f", s_fee * market.price_usd);
|
||||
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + usdX, cy), OnSurfaceDisabled(), buf);
|
||||
}
|
||||
cy += Layout::spacingSm();
|
||||
popDl->AddLine(ImVec2(cx, cy + Layout::spacingMd()),
|
||||
ImVec2(cx + popW - Layout::spacingXl(), cy + Layout::spacingMd()),
|
||||
cy += rowStep * 0.5f;
|
||||
popDl->AddLine(ImVec2(cx, cy),
|
||||
ImVec2(cMax.x - Layout::spacingLg(), cy),
|
||||
ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "confirm-divider-thickness").size);
|
||||
cy += rowStep;
|
||||
cy += rowStep * 0.5f;
|
||||
|
||||
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_total"));
|
||||
snprintf(buf, sizeof(buf), "%.8f %s", total, DRAGONX_TICKER);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../theme.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -175,16 +176,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
|
||||
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
||||
// Platform-specific URL opening
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
#elif __APPLE__
|
||||
std::string cmd = "open \"" + url + "\"";
|
||||
system(cmd.c_str());
|
||||
#else
|
||||
std::string cmd = "xdg-open \"" + url + "\" &";
|
||||
system(cmd.c_str());
|
||||
#endif
|
||||
util::Platform::openUrl(url);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "export_transactions_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../theme.h"
|
||||
@@ -722,15 +723,7 @@ void RenderTransactionsTab(App* app)
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TR("view_on_explorer"))) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
#elif __APPLE__
|
||||
std::string cmd = "open \"" + url + "\"";
|
||||
system(cmd.c_str());
|
||||
#else
|
||||
std::string cmd = "xdg-open \"" + url + "\" &";
|
||||
system(cmd.c_str());
|
||||
#endif
|
||||
util::Platform::openUrl(url);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("view_details"))) {
|
||||
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
|
||||
@@ -798,15 +791,7 @@ void RenderTransactionsTab(App* app)
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
|
||||
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
#elif __APPLE__
|
||||
std::string cmd2 = "open \"" + url + "\"";
|
||||
system(cmd2.c_str());
|
||||
#else
|
||||
std::string cmd2 = "xdg-open \"" + url + "\" &";
|
||||
system(cmd2.c_str());
|
||||
#endif
|
||||
util::Platform::openUrl(url);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton(TrId("full_details", "detail").c_str(), S.resolveFont("button"))) {
|
||||
|
||||
Reference in New Issue
Block a user