feat(lite): ObsidianDragonLite Network tab — server browser
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant) to manage lightwalletd servers, replacing the basic selector that was in Settings. - Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an Official/Custom pill. Official DragonX servers get a glowing outline. - Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode); selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the controller against the new server and resyncs — its dtor detaches the uninterruptible sync thread, so this doesn't block). - Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle). - Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button. Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added to lite_connection_service. The Settings page lite-server selector + its plumbing are removed (single source of truth = the tab). Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested (liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is: online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI smoke-launched without crash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -105,12 +105,6 @@ struct SettingsPageState {
|
||||
LowSpecSnapshot low_spec_snapshot;
|
||||
bool keep_daemon_running = false;
|
||||
bool stop_external_daemon = false;
|
||||
int lite_server_mode = 0;
|
||||
char lite_server_url[256] = "https://lite.dragonx.is";
|
||||
int lite_random_seed = 0;
|
||||
bool lite_persist_selected_server = true;
|
||||
std::vector<wallet::LiteServerEndpoint> lite_servers;
|
||||
std::string lite_server_status;
|
||||
bool lite_lifecycle_expanded = false;
|
||||
int lite_lifecycle_operation = 0;
|
||||
char lite_wallet_path[256] = "";
|
||||
@@ -151,28 +145,6 @@ struct SettingsPageState {
|
||||
|
||||
static SettingsPageState s_settingsState;
|
||||
|
||||
static void copyToSettingsBuffer(char* dest, std::size_t destSize, const std::string& value) {
|
||||
if (!dest || destSize == 0) return;
|
||||
std::strncpy(dest, value.c_str(), destSize - 1);
|
||||
dest[destSize - 1] = '\0';
|
||||
}
|
||||
|
||||
static wallet::LiteConnectionSettings liteConnectionSettingsFromPageState(config::Settings* settings) {
|
||||
wallet::LiteConnectionSettings connectionSettings = settings
|
||||
? wallet::liteConnectionSettingsFromAppSettings(*settings)
|
||||
: wallet::defaultLiteConnectionSettings();
|
||||
if (!s_settingsState.lite_servers.empty()) {
|
||||
connectionSettings.servers = s_settingsState.lite_servers;
|
||||
}
|
||||
connectionSettings.selectionMode = s_settingsState.lite_server_mode == 1
|
||||
? wallet::LiteServerSelectionMode::Random
|
||||
: wallet::LiteServerSelectionMode::Sticky;
|
||||
connectionSettings.stickyServerUrl = s_settingsState.lite_server_url;
|
||||
connectionSettings.chainName = wallet::kDragonXLiteChainName;
|
||||
connectionSettings.randomSelectionSeed = static_cast<std::size_t>(std::max(0, s_settingsState.lite_random_seed));
|
||||
return connectionSettings;
|
||||
}
|
||||
|
||||
static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
|
||||
switch (s_settingsState.lite_lifecycle_operation) {
|
||||
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
|
||||
@@ -181,42 +153,6 @@ static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState(
|
||||
}
|
||||
}
|
||||
|
||||
static void saveLiteServerSelectionFromPageState(App* app) {
|
||||
if (!app || !app->settings()) return;
|
||||
|
||||
const auto connectionSettings = liteConnectionSettingsFromPageState(app->settings());
|
||||
wallet::LiteWalletServerSelectionUiExecutionInput input;
|
||||
input.capabilities = app->walletCapabilities();
|
||||
input.intent.selectedServerIntentProvided = true;
|
||||
input.intent.selectionMode = connectionSettings.selectionMode;
|
||||
input.intent.selectedServerUrl = connectionSettings.stickyServerUrl;
|
||||
input.intent.randomSelectionSeed = connectionSettings.randomSelectionSeed;
|
||||
input.intent.chainName = connectionSettings.chainName;
|
||||
input.intent.replaceServers = true;
|
||||
input.intent.servers = connectionSettings.servers;
|
||||
input.persistence.settingsLoaded = true;
|
||||
input.persistence.havePersistedSelectionIntent = true;
|
||||
input.persistence.persistSelectedServer = s_settingsState.lite_persist_selected_server;
|
||||
input.persistence.persistenceOwnerReady = true;
|
||||
input.persistence.writeSettings = true;
|
||||
input.ui.selectedServerDisplayReady = true;
|
||||
input.ui.lifecycleUiOwnerReady = true;
|
||||
input.ui.operationConfirmed = true;
|
||||
input.ui.privateDataRedactionReady = true;
|
||||
input.ui.syncPlannerFeedReady = true;
|
||||
input.requireLifecycleReadiness = false;
|
||||
|
||||
const auto result = wallet::executeLiteWalletServerSelectionUi(*app->settings(), input);
|
||||
if (result.settingsWritten) {
|
||||
s_settingsState.lite_server_status = "Saved";
|
||||
// Rebuild the lite controller so the newly-saved server actually takes effect (it is
|
||||
// otherwise captured once at startup). No-op if a wallet is already open mid-session.
|
||||
app->rebuildLiteWallet();
|
||||
} else if (!result.error.empty()) {
|
||||
s_settingsState.lite_server_status = result.error;
|
||||
Notifications::instance().warning(result.error);
|
||||
}
|
||||
}
|
||||
|
||||
static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
||||
if (!app || !app->settings()) return;
|
||||
@@ -359,17 +295,7 @@ static void loadSettingsPageState(config::Settings* settings) {
|
||||
Layout::setUserFontScale(s_settingsState.font_scale); // sync with Layout on load
|
||||
s_settingsState.keep_daemon_running = settings->getKeepDaemonRunning();
|
||||
s_settingsState.stop_external_daemon = settings->getStopExternalDaemon();
|
||||
{
|
||||
const auto liteSettings = wallet::liteConnectionSettingsFromAppSettings(*settings);
|
||||
s_settingsState.lite_server_mode = liteSettings.selectionMode == wallet::LiteServerSelectionMode::Random ? 1 : 0;
|
||||
copyToSettingsBuffer(s_settingsState.lite_server_url,
|
||||
sizeof(s_settingsState.lite_server_url),
|
||||
liteSettings.stickyServerUrl);
|
||||
s_settingsState.lite_random_seed = static_cast<int>(liteSettings.randomSelectionSeed);
|
||||
s_settingsState.lite_persist_selected_server = settings->getLitePersistSelectedServer();
|
||||
s_settingsState.lite_servers = liteSettings.servers;
|
||||
s_settingsState.lite_server_status.clear();
|
||||
}
|
||||
// Lite-server selection is managed entirely by the Network tab (not the Settings page).
|
||||
s_settingsState.mine_when_idle = settings->getMineWhenIdle();
|
||||
s_settingsState.mine_idle_delay = settings->getMineIdleDelay();
|
||||
s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling();
|
||||
@@ -424,11 +350,7 @@ static void saveSettingsPageState(config::Settings* settings) {
|
||||
settings->setFontScale(s_settingsState.font_scale);
|
||||
settings->setKeepDaemonRunning(s_settingsState.keep_daemon_running);
|
||||
settings->setStopExternalDaemon(s_settingsState.stop_external_daemon);
|
||||
{
|
||||
auto liteSettings = liteConnectionSettingsFromPageState(settings);
|
||||
wallet::applyLiteConnectionSettingsToAppSettings(*settings, liteSettings);
|
||||
settings->setLitePersistSelectedServer(s_settingsState.lite_persist_selected_server);
|
||||
}
|
||||
// Lite-server selection is owned by the Network tab; the Settings page no longer writes it.
|
||||
settings->setMineWhenIdle(s_settingsState.mine_when_idle);
|
||||
settings->setMineIdleDelay(s_settingsState.mine_idle_delay);
|
||||
settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling);
|
||||
@@ -1558,90 +1480,10 @@ void RenderSettingsPage(App* app) {
|
||||
if (app->isLiteBuild()) {
|
||||
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
|
||||
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
|
||||
const char* modeLabels[] = {"Sticky", "Random"};
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Mode");
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(liteInputW);
|
||||
if (ImGui::BeginCombo("##LiteServerMode", modeLabels[s_settingsState.lite_server_mode == 1 ? 1 : 0])) {
|
||||
for (int modeIndex = 0; modeIndex < 2; ++modeIndex) {
|
||||
const bool selected = s_settingsState.lite_server_mode == modeIndex;
|
||||
if (ImGui::Selectable(modeLabels[modeIndex], selected)) {
|
||||
s_settingsState.lite_server_mode = modeIndex;
|
||||
saveLiteServerSelectionFromPageState(app);
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Preset");
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
std::string presetPreview = s_settingsState.lite_server_url[0] != '\0'
|
||||
? std::string(s_settingsState.lite_server_url)
|
||||
: std::string("Select");
|
||||
for (const auto& server : s_settingsState.lite_servers) {
|
||||
if (server.url == s_settingsState.lite_server_url && !server.label.empty()) {
|
||||
presetPreview = server.label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(liteInputW);
|
||||
if (ImGui::BeginCombo("##LiteServerPreset", presetPreview.c_str())) {
|
||||
for (const auto& server : s_settingsState.lite_servers) {
|
||||
if (!server.enabled) continue;
|
||||
const std::string label = server.label.empty() ? server.url : server.label;
|
||||
const bool selected = server.url == s_settingsState.lite_server_url;
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
copyToSettingsBuffer(s_settingsState.lite_server_url,
|
||||
sizeof(s_settingsState.lite_server_url),
|
||||
server.url);
|
||||
s_settingsState.lite_server_mode = 0;
|
||||
saveLiteServerSelectionFromPageState(app);
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Server");
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(liteInputW);
|
||||
ImGui::InputText("##LiteServerUrl", s_settingsState.lite_server_url,
|
||||
sizeof(s_settingsState.lite_server_url));
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
s_settingsState.lite_server_mode = 0;
|
||||
saveLiteServerSelectionFromPageState(app);
|
||||
}
|
||||
|
||||
if (s_settingsState.lite_server_mode == 1) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Seed");
|
||||
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||
if (ImGui::InputInt("##LiteRandomSeed", &s_settingsState.lite_random_seed)) {
|
||||
if (s_settingsState.lite_random_seed < 0) s_settingsState.lite_random_seed = 0;
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) saveLiteServerSelectionFromPageState(app);
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
if (ImGui::Checkbox("Persist selected server##LitePersistServer",
|
||||
&s_settingsState.lite_persist_selected_server)) {
|
||||
saveLiteServerSelectionFromPageState(app);
|
||||
}
|
||||
|
||||
if (!s_settingsState.lite_server_status.empty()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
s_settingsState.lite_server_status.c_str());
|
||||
}
|
||||
// Lite-server selection lives in the dedicated Network tab now.
|
||||
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
||||
"Lite servers are managed in the Network tab.");
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) {
|
||||
|
||||
@@ -31,6 +31,7 @@ enum class NavPage {
|
||||
// --- separator ---
|
||||
Console,
|
||||
Peers,
|
||||
LiteNetwork, // lite-only "Network" tab (full-node uses Peers); exactly one shows per variant
|
||||
Explorer,
|
||||
Settings,
|
||||
Count_
|
||||
@@ -52,7 +53,8 @@ inline const NavItem kNavItems[] = {
|
||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
||||
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
||||
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
||||
{ "Network", NavPage::LiteNetwork, nullptr, "network", nullptr },
|
||||
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
|
||||
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
||||
};
|
||||
@@ -78,6 +80,7 @@ inline wallet::WalletUiSurface NavPageSurface(NavPage page)
|
||||
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
||||
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
||||
case NavPage::Peers: return wallet::WalletUiSurface::Peers;
|
||||
case NavPage::LiteNetwork: return wallet::WalletUiSurface::LiteNetwork;
|
||||
case NavPage::Explorer: return wallet::WalletUiSurface::Explorer;
|
||||
case NavPage::Settings: return wallet::WalletUiSurface::Settings;
|
||||
default: return wallet::WalletUiSurface::Overview;
|
||||
@@ -101,6 +104,7 @@ inline const char* GetNavIconMD(NavPage page)
|
||||
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
||||
case NavPage::Console: return ICON_MD_TERMINAL;
|
||||
case NavPage::Peers: return ICON_MD_HUB;
|
||||
case NavPage::LiteNetwork: return ICON_MD_HUB;
|
||||
case NavPage::Explorer: return ICON_MD_EXPLORE;
|
||||
case NavPage::Settings: return ICON_MD_SETTINGS;
|
||||
default: return ICON_MD_HOME;
|
||||
|
||||
285
src/ui/windows/network_tab.cpp
Normal file
285
src/ui/windows/network_tab.cpp
Normal file
@@ -0,0 +1,285 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "network_tab.h"
|
||||
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/lite_server_probe.h"
|
||||
#include "../../wallet/lite_connection_service.h"
|
||||
#include "../theme.h"
|
||||
#include "../layout.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../effects/theme_effects.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
using namespace material;
|
||||
using LiteMode = config::Settings::LiteServerSelectionPreferenceMode;
|
||||
|
||||
namespace {
|
||||
|
||||
util::LiteServerProbe s_probe;
|
||||
bool s_probeStarted = false;
|
||||
bool s_showHidden = false;
|
||||
char s_addUrl[256] = "";
|
||||
char s_addLabel[128] = "";
|
||||
std::string s_addError;
|
||||
|
||||
// (Re)probe every server in the list (visible + hidden), so latency is ready when unhidden too.
|
||||
void startProbe(config::Settings* st)
|
||||
{
|
||||
std::vector<std::string> urls;
|
||||
for (const auto& s : st->getLiteServers()) urls.push_back(s.url);
|
||||
s_probe.start(std::move(urls));
|
||||
}
|
||||
|
||||
ImU32 latencyColor(int ms)
|
||||
{
|
||||
if (ms < 100) return Success();
|
||||
if (ms < 500) return Warning();
|
||||
return Error();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RenderLiteNetworkTab(App* app)
|
||||
{
|
||||
if (!app || !app->settings()) return;
|
||||
config::Settings* st = app->settings();
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
if (!s_probeStarted) { startProbe(st); s_probeStarted = true; }
|
||||
const auto probe = s_probe.results();
|
||||
|
||||
const bool randomMode = st->getLiteServerSelectionMode() == LiteMode::Random;
|
||||
const std::string sticky = st->getLiteStickyServerUrl();
|
||||
|
||||
auto applyAndRebuild = [&]() { st->save(); app->rebuildLiteWallet(/*force=*/true); };
|
||||
auto pinServer = [&](const std::string& url) {
|
||||
st->setLiteServerSelectionMode(LiteMode::Sticky);
|
||||
st->setLiteStickyServerUrl(url);
|
||||
applyAndRebuild();
|
||||
};
|
||||
|
||||
ImFont* sub2 = Type().subtitle2();
|
||||
ImFont* body2 = Type().body2();
|
||||
ImFont* cap = Type().caption();
|
||||
|
||||
// ── Header ───────────────────────────────────────────────────────────────
|
||||
Type().text(TypeStyle::H6, TR("lite_net_title"));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_net_intro"));
|
||||
ImGui::Spacing();
|
||||
|
||||
bool useRandom = randomMode;
|
||||
if (ImGui::Checkbox(TR("lite_net_use_random"), &useRandom)) {
|
||||
st->setLiteServerSelectionMode(useRandom ? LiteMode::Random : LiteMode::Sticky);
|
||||
applyAndRebuild();
|
||||
}
|
||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 110.0f * dp);
|
||||
if (TactileButton(TR("lite_net_refresh"), ImVec2(100.0f * dp, 0))) startProbe(st);
|
||||
if (randomMode)
|
||||
Type().textColored(TypeStyle::Caption, Primary(), TR("lite_net_random_active"));
|
||||
ImGui::Spacing();
|
||||
|
||||
// ── Add custom server ──────────────────────────────────────────────────────
|
||||
{
|
||||
const float availW = ImGui::GetContentRegionAvail().x;
|
||||
const float addBtnW = 80.0f * dp;
|
||||
const float urlW = (availW - addBtnW - 16.0f * dp) * 0.6f;
|
||||
const float lblW = (availW - addBtnW - 16.0f * dp) * 0.4f;
|
||||
ImGui::SetNextItemWidth(urlW);
|
||||
ImGui::InputTextWithHint("##LiteAddUrl", TR("lite_net_add_url_hint"), s_addUrl, sizeof(s_addUrl));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(lblW);
|
||||
ImGui::InputTextWithHint("##LiteAddLabel", TR("lite_net_add_label_hint"), s_addLabel, sizeof(s_addLabel));
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("lite_net_add"), ImVec2(addBtnW, 0))) {
|
||||
std::string url = s_addUrl;
|
||||
if (!wallet::isLiteServerUrlUsable(url)) {
|
||||
s_addError = TR("lite_net_invalid_url");
|
||||
} else {
|
||||
auto servers = st->getLiteServers();
|
||||
bool exists = false;
|
||||
for (const auto& s : servers) if (s.url == url) { exists = true; break; }
|
||||
if (!exists) {
|
||||
config::Settings::LiteServerPreference p;
|
||||
p.url = url;
|
||||
p.label = s_addLabel[0] ? std::string(s_addLabel) : url;
|
||||
p.enabled = true;
|
||||
servers.push_back(p);
|
||||
st->setLiteServers(servers);
|
||||
st->save();
|
||||
startProbe(st);
|
||||
}
|
||||
s_addUrl[0] = '\0'; s_addLabel[0] = '\0'; s_addError.clear();
|
||||
}
|
||||
}
|
||||
if (!s_addError.empty())
|
||||
Type().textColored(TypeStyle::Caption, Error(), s_addError.c_str());
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
// ── Card renderer ──────────────────────────────────────────────────────────
|
||||
const float cardH = 58.0f * dp;
|
||||
const float pad = 12.0f * dp;
|
||||
const float gap = 8.0f * dp;
|
||||
const float hideW = 44.0f * dp;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
auto drawCard = [&](const config::Settings::LiteServerPreference& sv, bool hiddenList) {
|
||||
const std::string host = util::liteServerHost(sv.url);
|
||||
const bool official = wallet::isOfficialLiteServer(sv.url);
|
||||
const bool selected = !randomMode && sv.url == sticky;
|
||||
|
||||
util::LiteServerProbeResult pr;
|
||||
auto it = probe.find(sv.url);
|
||||
if (it != probe.end()) pr = it->second;
|
||||
|
||||
const float cardW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||
const float rnd = 8.0f * dp;
|
||||
const float cardBtnW = cardW - hideW;
|
||||
|
||||
// Background + selection/official emphasis.
|
||||
GlassPanelSpec spec;
|
||||
spec.rounding = rnd;
|
||||
spec.fillAlpha = selected ? 34 : 18;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, spec);
|
||||
if (official) {
|
||||
auto& fx = effects::ThemeEffects::instance();
|
||||
if (fx.isEnabled()) {
|
||||
fx.drawGlowPulse(dl, cardMin, cardMax, rnd);
|
||||
}
|
||||
// Always-visible static outline so officials are distinguishable even without effects.
|
||||
float pulse = 0.75f + 0.25f * (float)std::sin(ImGui::GetTime() * 2.0);
|
||||
dl->AddRect(cardMin, cardMax, WithAlpha(Primary(), (int)(150 * pulse)), rnd, 0, 1.6f * dp);
|
||||
}
|
||||
if (selected)
|
||||
dl->AddRectFilled(cardMin, ImVec2(cardMin.x + 3.0f * dp, cardMax.y), Primary(), rnd);
|
||||
|
||||
// Main click area selects the server (left of the hide strip).
|
||||
ImGui::SetCursorScreenPos(cardMin);
|
||||
ImGui::InvisibleButton(("##litecard_" + sv.url).c_str(), ImVec2(cardBtnW, cardH));
|
||||
const bool cardHovered = ImGui::IsItemHovered();
|
||||
const bool cardClicked = ImGui::IsItemClicked();
|
||||
if (cardHovered && !hiddenList) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (!selected) dl->AddRectFilled(cardMin, cardMax, WithAlpha(OnSurface(), 8), rnd);
|
||||
}
|
||||
if (cardClicked && !hiddenList) pinServer(sv.url);
|
||||
|
||||
// Left: label + host + ip.
|
||||
float tx = cardMin.x + pad + 4.0f * dp;
|
||||
dl->AddText(sub2, sub2->LegacySize, ImVec2(tx, cardMin.y + 9.0f * dp),
|
||||
OnSurface(), sv.label.c_str());
|
||||
std::string sub = host;
|
||||
if (!pr.ip.empty() && pr.ip != host) sub += " · " + pr.ip;
|
||||
dl->AddText(cap, cap->LegacySize, ImVec2(tx, cardMin.y + 9.0f * dp + sub2->LegacySize + 3.0f * dp),
|
||||
OnSurfaceMedium(), sub.c_str());
|
||||
|
||||
// Right (within the card-button area): latency dot + text, official/custom pill.
|
||||
float rx = cardMin.x + cardBtnW - pad;
|
||||
// latency text
|
||||
char lat[24];
|
||||
ImU32 latCol;
|
||||
if (!pr.probed) { snprintf(lat, sizeof(lat), "%s", TR("lite_net_checking")); latCol = OnSurfaceDisabled(); }
|
||||
else if (!pr.online) { snprintf(lat, sizeof(lat), "%s", TR("lite_net_offline")); latCol = Error(); }
|
||||
else { snprintf(lat, sizeof(lat), "%dms", pr.latencyMs); latCol = latencyColor(pr.latencyMs); }
|
||||
ImVec2 latSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, lat);
|
||||
float latX = rx - latSz.x;
|
||||
dl->AddText(cap, cap->LegacySize, ImVec2(latX, cardMin.y + cardH * 0.5f + 2.0f * dp), latCol, lat);
|
||||
// status dot left of latency text
|
||||
if (pr.probed) {
|
||||
float dotR = 3.5f * dp;
|
||||
dl->AddCircleFilled(ImVec2(latX - dotR - 4.0f * dp, cardMin.y + cardH * 0.5f + 2.0f * dp + cap->LegacySize * 0.5f),
|
||||
dotR, pr.online ? latencyColor(pr.latencyMs) : Error());
|
||||
}
|
||||
// official/custom + in-use pills (top-right)
|
||||
const char* tag = official ? TR("lite_net_official") : TR("lite_net_custom");
|
||||
ImU32 tagBg = official ? WithAlpha(Primary(), 40) : WithAlpha(Secondary(), 30);
|
||||
ImU32 tagFg = official ? WithAlpha(Primary(), 230) : WithAlpha(Secondary(), 220);
|
||||
ImVec2 tagSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, tag);
|
||||
float tagX = rx - tagSz.x;
|
||||
float tagY = cardMin.y + 9.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(tagX - 6 * dp, tagY - 1 * dp), ImVec2(rx + 6 * dp, tagY + tagSz.y + 1 * dp),
|
||||
tagBg, 4.0f * dp);
|
||||
dl->AddText(cap, cap->LegacySize, ImVec2(tagX, tagY), tagFg, tag);
|
||||
if (selected) {
|
||||
const char* inUse = TR("lite_net_in_use");
|
||||
ImVec2 uSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse);
|
||||
dl->AddText(cap, cap->LegacySize, ImVec2(tagX - uSz.x - 10 * dp, tagY), Success(), inUse);
|
||||
}
|
||||
|
||||
// Hide / unhide button (far-right strip).
|
||||
ImVec2 hideMin(cardMin.x + cardBtnW, cardMin.y);
|
||||
ImGui::SetCursorScreenPos(hideMin);
|
||||
ImGui::InvisibleButton(("##litehide_" + sv.url).c_str(), ImVec2(hideW, cardH));
|
||||
bool hideHov = ImGui::IsItemHovered();
|
||||
if (hideHov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"));
|
||||
}
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (hiddenList) st->unhideLiteServer(sv.url);
|
||||
else st->hideLiteServer(sv.url);
|
||||
st->save();
|
||||
}
|
||||
ImFont* ico = Type().iconSmall();
|
||||
const char* eye = hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
||||
ImVec2 icoSz = ico->CalcTextSizeA(ico->LegacySize, FLT_MAX, 0, eye);
|
||||
dl->AddText(ico, ico->LegacySize,
|
||||
ImVec2(hideMin.x + (hideW - icoSz.x) * 0.5f, cardMin.y + (cardH - icoSz.y) * 0.5f),
|
||||
hideHov ? OnSurface() : OnSurfaceDisabled(), eye);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y + gap));
|
||||
};
|
||||
|
||||
// ── Visible servers ────────────────────────────────────────────────────────
|
||||
ImVec2 scrollAvail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##LiteServerScroll", scrollAvail, false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
for (const auto& sv : st->getLiteServers()) {
|
||||
if (st->isLiteServerHidden(sv.url)) continue;
|
||||
drawCard(sv, /*hiddenList=*/false);
|
||||
}
|
||||
|
||||
// ── Hidden servers (toggle) ─────────────────────────────────────────────────
|
||||
{
|
||||
bool anyHidden = false;
|
||||
for (const auto& sv : st->getLiteServers())
|
||||
if (st->isLiteServerHidden(sv.url)) { anyHidden = true; break; }
|
||||
if (anyHidden) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Checkbox(TR("lite_net_show_hidden"), &s_showHidden);
|
||||
if (s_showHidden) {
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_net_hidden_section"));
|
||||
ImGui::Spacing();
|
||||
for (const auto& sv : st->getLiteServers()) {
|
||||
if (!st->isLiteServerHidden(sv.url)) continue;
|
||||
drawCard(sv, /*hiddenList=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
(void)body2;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
18
src/ui/windows/network_tab.h
Normal file
18
src/ui/windows/network_tab.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Lite-wallet "Network" tab: a card list of lightwalletd servers with latency + host/IP, an
|
||||
// official-server glow, random-vs-pinned selection, add-custom-server, and hide/unhide. Lite
|
||||
// builds only (full-node uses the Peers tab). See src/ui/windows/network_tab.cpp.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
void RenderLiteNetworkTab(App* app);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user