3 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 a43ac0156f feat(chat): make the key-verify lock clickable with a fingerprint dialog
The header key-verify lock was an unexplained hover-only icon. It's now
clickable, opening a dialog that shows the contact's identity-key fingerprint
and explains comparing it out-of-band, with a Copy-key button and a clearer
tooltip. The crypto/verification logic is unchanged (the wording is careful that
the lock means "encrypted to this key", since there is no trusted/verified flag).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:35:32 -05:00
DanSandClaude Opus 4.8 534120d3de feat(peers): success toasts for ban/unban/clear + confirm the bulk Clear-All
Peer ban actions only toasted on failure; success was silent. Added success
toasts for ban/unban/clear-all, and the bulk "Clear All Bans" now takes a
confirming second click (per-row unban stays one-click). The setban/clearbanned
RPC calls are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:35:32 -05:00
DanSandClaude Opus 4.8 a90aa29478 feat(ux): wizard handoff note, daemon restart reminder, settings polish; drop legacy settings_window
- First-run handoff: one info toast ("node starting, will keep syncing in the
  background") at the completion point, instead of dropping the user onto a busy
  blank window with no explanation.
- Daemon updater: a newer installed binary now sets a persistent "restart
  pending" reminder (surfaced in Settings → Daemon Binary with a Restart button)
  and the updater's Done screen leads with a warning-level "restart required"
  headline — easy to miss before.
- Settings: the debug-categories "restart required" note is raised from ~35%
  alpha to a readable color with the restart prompt moved directly beneath it;
  the DEBUG OPTIONS header shows a lock + explanatory caption so it's clearly a
  gated diagnostics section (the auth gate itself is unchanged); a couple of
  hardcoded color/size literals now use material tokens / dpiScale.
- Removed the legacy duplicate settings_window.cpp/.h (only reachable via a dev
  flag, never in production) so edits can't silently miss one of two settings UIs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:35:32 -05:00
5 changed files with 83 additions and 635 deletions
+3 -2
View File
@@ -3772,7 +3772,7 @@ void App::banPeer(const std::string& ip, int duration_seconds)
}
return [this, err]() {
if (!err.empty()) ui::Notifications::instance().error(std::string(TR("toast_ban_failed")) + util::friendlyRpcError(err));
else refreshPeerInfo();
else { ui::Notifications::instance().success(TR("toast_peer_banned")); refreshPeerInfo(); }
};
});
}
@@ -3790,7 +3790,7 @@ void App::unbanPeer(const std::string& ip)
}
return [this, err]() {
if (!err.empty()) ui::Notifications::instance().error(std::string(TR("toast_unban_failed")) + util::friendlyRpcError(err));
else refreshPeerInfo();
else { ui::Notifications::instance().success(TR("toast_peer_unbanned")); refreshPeerInfo(); }
};
});
}
@@ -3809,6 +3809,7 @@ void App::clearBans()
return [this, err]() {
if (!err.empty()) { ui::Notifications::instance().error(std::string(TR("toast_clear_bans_failed")) + util::friendlyRpcError(err)); return; }
state_.banned_peers.clear();
ui::Notifications::instance().success(TR("toast_bans_cleared"));
refreshPeerInfo();
};
});
+62 -4
View File
@@ -106,6 +106,8 @@ std::string s_delete_name; // its peer name (for the confirm copy / bl
bool s_show_blocked = false; // blocked-conversations manager overlay (unblock)
bool s_show_export_confirm = false; // "export as plaintext" confirm overlay
std::string s_export_cid; // conversation targeted by the export confirm
bool s_show_fingerprint = false; // key-fingerprint verification dialog
std::string s_fingerprint_cid; // conversation targeted by the fingerprint dialog
// Export file path for a conversation (the same scheme the inline export used).
static std::string chatExportPathFor(const std::string& peerName) {
@@ -1324,13 +1326,22 @@ void RenderChatTab(App* app)
// same row. Hidden while renaming (the field takes the whole span).
if (hasAddr && !renaming) {
float ax = textX + std::min(nameW, nameAvail) + nameSpacing;
// Key-verify lock — we hold the peer's identity key; hover shows a comparable fingerprint.
// Key-verify lock — we hold the peer's identity key; now clickable to open a dialog with the
// fingerprint for out-of-band comparison (the old hover-only lock was unexplained).
if (showLock) {
const float ly = hp.y + (rowH - icoSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(ax, ly));
const bool lockClicked = ImGui::InvisibleButton("##hdr_keylock",
ImVec2(std::max(1.0f, lockW - 5.0f * hdpi), icoSz));
const bool lockHov = ImGui::IsItemHovered();
hdl->AddText(icoFont, icoSz, ImVec2(ax, ly),
material::WithAlpha(material::Primary(), 220), ICON_MD_LOCK);
if (ImGui::IsMouseHoveringRect(ImVec2(ax, ly), ImVec2(ax + lockW, ly + icoSz)))
material::Tooltip("%s\n%s", TR("chat_verify_key"), keyFingerprint(sel->peerPubKey).c_str());
lockHov ? material::Primary() : material::WithAlpha(material::Primary(), 220),
ICON_MD_LOCK);
if (lockHov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("chat_verify_tip"));
}
if (lockClicked) { s_fingerprint_cid = sel->cid; s_show_fingerprint = true; }
ax += lockW;
}
// Click-to-copy compact address.
@@ -2207,6 +2218,53 @@ void RenderChatTab(App* app)
}
}
// ---- Key-fingerprint verification dialog ----
if (s_show_fingerprint) {
const ConvSummary* fp = nullptr;
for (const auto& c : convs) if (c.cid == s_fingerprint_cid) { fp = &c; break; }
if (!fp || fp->peerPubKey.empty()) {
s_show_fingerprint = false;
s_fingerprint_cid.clear();
} else {
material::OverlayDialogSpec ov;
ov.title = TR("chat_verify_title");
ov.p_open = &s_show_fingerprint;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = 500.0f;
ov.idSuffix = "chatfp";
if (material::BeginOverlayDialog(ov)) {
const float fdp = Layout::dpiScale();
const std::string fpStr = keyFingerprint(fp->peerPubKey);
ImGui::PushTextWrapPos(0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("chat_verify_body"));
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::TextWrapped("%s", fp->peerName.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::TextWrapped("%s", fpStr.c_str());
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
const float fgap = ImGui::GetStyle().ItemSpacing.x;
float wCopy = std::max(110.0f * fdp, ImGui::CalcTextSize(TR("chat_verify_copy")).x + 24.0f * fdp);
float wClose = std::max(90.0f * fdp, ImGui::CalcTextSize(TR("chat_cancel")).x + 24.0f * fdp);
material::BeginOverlayDialogFooter(wCopy + wClose + fgap, false);
if (material::TactileButton(TR("chat_verify_copy"), ImVec2(wCopy, 0))) {
ImGui::SetClipboardText(fp->peerPubKey.c_str());
Notifications::instance().success(TR("copied"));
}
ImGui::SameLine(0, fgap);
if (material::TactileButton(TR("chat_cancel"), ImVec2(wClose, 0))) {
s_show_fingerprint = false;
s_fingerprint_cid.clear();
}
material::EndOverlayDialog();
}
}
}
// ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ----
if (s_show_delete_confirm) {
const float dp = Layout::dpiScale();
+18 -2
View File
@@ -33,6 +33,7 @@ using namespace material;
// Track selected peer for ban action
static int s_selected_peer_idx = -1;
static int s_selected_banned_idx = -1;
static bool s_confirm_clear_bans = false; // inline confirm for the bulk Clear-All-Bans action
// Helper: Extract IP without port
static std::string ExtractIP(const std::string& addr)
@@ -931,10 +932,25 @@ void RenderPeersTab(App* app)
// ================================================================
if (s_show_banned && !state.bannedPeers.empty()) {
ImGui::BeginDisabled(!app->isConnected());
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button"))) {
app->clearBans();
if (!s_confirm_clear_bans) {
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button")))
s_confirm_clear_bans = true; // bulk action takes a confirming second click
} else {
char cb[96];
snprintf(cb, sizeof(cb), TR("peers_clear_bans_confirm"), (int)state.bannedPeers.size());
ImGui::TextColored(material::WarningVec4(), "%s", cb);
ImGui::SameLine();
if (TactileSmallButton(TR("peers_clear_bans_confirm_yes"), S.resolveFont("button"))) {
app->clearBans();
s_confirm_clear_bans = false;
}
ImGui::SameLine();
if (TactileSmallButton(TR("cancel"), S.resolveFont("button")))
s_confirm_clear_bans = false;
}
ImGui::EndDisabled();
} else {
s_confirm_clear_bans = false; // no stale confirm across tab toggles / after bans clear
}
ImGui::EndChild(); // ##PeersScroll
-608
View File
@@ -1,608 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "settings_window.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../rpc/rpc_client.h"
#include "../theme.h"
#include "../schema/ui_schema.h"
#include "../schema/skin_manager.h"
#include "../notifications.h"
#include "../effects/imgui_acrylic.h"
#include "../material/draw_helpers.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <vector>
#include <filesystem>
// Icon text for settings UI
#define ICON_CUSTOM_THEME ICON_MD_TUNE
#define ICON_REFRESH_THEMES ICON_MD_REFRESH
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;
}
// Settings state - these get loaded from Settings on window open
static bool s_initialized = false;
static int s_language_index = 0;
static bool s_save_ztxs = true;
static bool s_allow_custom_fees = false;
static bool s_auto_shield = false;
static bool s_fetch_prices = true;
static bool s_use_tor = false;
static char s_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
static char s_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
static char s_rpc_user[64] = "";
static char s_rpc_password[64] = "";
static char s_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
static char s_addr_explorer[256] = "https://explorer.dragonx.is/address/";
// Acrylic settings
static bool s_acrylic_enabled = true;
static float s_blur_amount = 1.5f; // 0.0=Off, continuous blur multiplier
static float s_noise_opacity = 0.5f;
static bool s_reduced_transparency = false; // Accessibility option
static bool s_gradient_background = false; // Gradient background mode
// Saved skin ID for cancel/revert
static std::string s_saved_skin_id;
// Load current settings into UI state
static void loadSettingsToUI(config::Settings* settings) {
if (!settings) return;
s_saved_skin_id = settings->getSkinId();
s_save_ztxs = settings->getSaveZtxs();
s_allow_custom_fees = settings->getAllowCustomFees();
s_auto_shield = settings->getAutoShield();
s_fetch_prices = settings->getFetchPrices();
s_use_tor = settings->getUseTor();
strncpy(s_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_tx_explorer) - 1);
strncpy(s_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_addr_explorer) - 1);
// Set language index
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::string current_lang = settings->getLanguage();
if (current_lang.empty()) current_lang = "en";
s_language_index = 0;
int idx = 0;
for (const auto& lang : languages) {
if (lang.first == current_lang) {
s_language_index = idx;
break;
}
idx++;
}
s_initialized = true;
}
// Save UI state to settings
static void saveSettingsFromUI(config::Settings* settings) {
if (!settings) return;
settings->setTheme(settings->getSkinId()); // Theme now synced with skin
settings->setSaveZtxs(s_save_ztxs);
settings->setAllowCustomFees(s_allow_custom_fees);
settings->setAutoShield(s_auto_shield);
settings->setFetchPrices(s_fetch_prices);
settings->setUseTor(s_use_tor);
settings->setTxExplorerUrl(s_tx_explorer);
settings->setAddressExplorerUrl(s_addr_explorer);
// Save language
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
auto it = languages.begin();
std::advance(it, s_language_index);
if (it != languages.end()) {
settings->setLanguage(it->first);
}
// Save acrylic / visual effects settings
settings->setAcrylicEnabled(s_acrylic_enabled);
settings->setAcrylicQuality(s_blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
settings->setBlurMultiplier(s_blur_amount);
settings->setReducedTransparency(s_reduced_transparency);
settings->setNoiseOpacity(s_noise_opacity);
settings->setGradientBackground(s_gradient_background);
settings->save();
}
void RenderSettingsWindow(App* app, bool* p_open)
{
// Load settings on first open
if (!s_initialized && app->settings()) {
loadSettingsToUI(app->settings());
// Initialize acrylic settings from current state
s_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
s_blur_amount = effects::ImGuiAcrylic::GetBlurMultiplier();
s_noise_opacity = effects::ImGuiAcrylic::GetNoiseOpacity();
s_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
s_gradient_background = schema::SkinManager::instance().isGradientMode();
}
auto& S = schema::UI();
auto win = S.window("dialogs.settings");
auto lbl = S.label("dialogs.settings", "label");
auto cmb = S.combo("dialogs.settings", "combo");
auto connLbl = S.label("dialogs.settings", "connection-label");
auto portInput = S.input("dialogs.settings", "port-input");
auto walletBtn = S.button("dialogs.settings", "wallet-button");
auto saveBtn = S.button("dialogs.settings", "save-button");
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
if (!material::BeginOverlayDialog(TR("settings"), p_open, win.width, 0.94f)) {
return;
}
if (ImGui::BeginTabBar("SettingsTabs")) {
// General settings tab
if (ImGui::BeginTabItem(TR("general"))) {
ImGui::Spacing();
// Skin/theme selection
ImGui::Text("%s", TR("theme"));
ImGui::SameLine(lbl.position);
// Active skin combo (populated from SkinManager)
auto& skinMgr = schema::SkinManager::instance();
const auto& skins = skinMgr.available();
// Find active skin for preview text
std::string active_preview = "DragonX";
bool active_is_custom = false;
for (const auto& skin : skins) {
if (skin.id == skinMgr.activeSkinId()) {
active_preview = skin.name;
active_is_custom = !skin.bundled;
break;
}
}
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
// Bundled themes header
ImGui::TextDisabled("%s", TR("settings_builtin"));
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (!skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
// Custom themes (if any)
bool has_custom = false;
for (const auto& skin : skins) {
if (!skin.bundled) { has_custom = true; break; }
}
if (has_custom) {
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("settings_custom"));
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string label = skin.name + TR("swin_invalid_suffix");
ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
material::Tooltip("%s", skin.validationError.c_str());
}
} else {
std::string label = skin.name;
if (!skin.author.empty()) {
label += " (" + skin.author + ")";
}
if (ImGui::Selectable(label.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
}
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
material::Tooltip("%s", TR("tt_theme_hotkey"));
// Show indicator if custom theme is active
if (active_is_custom) {
ImGui::SameLine();
ImGui::PushFont(material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("tt_custom_theme"));
}
}
ImGui::SameLine();
ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh();
Notifications::instance().info(TR("swin_theme_list_refreshed"));
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::Spacing();
// Language selection
ImGui::Text("%s", TR("language"));
ImGui::SameLine(lbl.position);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
// Build language display names array
std::vector<const char*> lang_names;
lang_names.reserve(languages.size());
for (const auto& lang : languages) {
lang_names.push_back(lang.second.c_str()); // Display name
}
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::Combo("##Language", &s_language_index, lang_names.data(), static_cast<int>(lang_names.size()))) {
// Get locale code from index
auto it = languages.begin();
std::advance(it, s_language_index);
i18n.loadLanguage(it->first);
}
ImGui::TextDisabled(" %s", TR("settings_language_note"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Acrylic Effects settings
ImGui::Text("%s", TR("settings_visual_effects"));
ImGui::Spacing();
ImGui::Text("%s", TR("settings_acrylic_level"));
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
char blur_fmt[16];
if (s_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &s_blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
if (s_blur_amount > 0.0f && s_blur_amount < 0.15f) s_blur_amount = 0.0f;
s_acrylic_enabled = (s_blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
}
}
ImGui::TextDisabled(" %s", TR("tt_blur"));
ImGui::Spacing();
ImGui::Text("%s", TR("settings_noise_opacity"));
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
char noise_fmt[16];
if (s_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &s_noise_opacity, 0.0f, 1.0f, noise_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
}
}
ImGui::TextDisabled(" %s", TR("tt_noise"));
ImGui::Spacing();
// Accessibility: Reduced transparency
if (ImGui::Checkbox(TrId("settings_reduce_transparency", "reduce_trans").c_str(), &s_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
}
ImGui::TextDisabled(" %s", TR("settings_solid_colors_desc"));
ImGui::Spacing();
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_gradient_background);
}
ImGui::TextDisabled(" %s", TR("settings_gradient_desc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Privacy settings
ImGui::Text("%s", TR("settings_privacy"));
ImGui::Spacing();
ImGui::Checkbox(TrId("settings_save_shielded_local", "save_ztx_w").c_str(), &s_save_ztxs);
ImGui::TextDisabled(" %s", TR("settings_save_shielded_desc"));
ImGui::Spacing();
ImGui::Checkbox(TrId("settings_auto_shield_funds", "auto_shld_w").c_str(), &s_auto_shield);
ImGui::TextDisabled(" %s", TR("settings_auto_shield_desc"));
ImGui::Spacing();
{
// Not implemented: disabled and never shown as "on", even for a stored use_tor=true.
bool torOff = false;
ImGui::BeginDisabled();
ImGui::Checkbox(TrId("settings_use_tor_network", "tor_w").c_str(), &torOff);
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
const float wrapW = S.drawElement("components.settings-page", "tor-tooltip-wrap-width")
.sizeOr(420.0f) * Layout::dpiScale();
material::BeginTooltip();
ImGui::PushTextWrapPos(wrapW);
ImGui::TextUnformatted(TR(app && app->isLiteBuild() ? "tt_tor_unavailable_lite"
: "tt_tor_unavailable"));
ImGui::PopTextWrapPos();
material::EndTooltip();
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Other settings
ImGui::Text("%s", TR("settings_other"));
ImGui::Spacing();
ImGui::Checkbox(TrId("custom_fees", "fees_w").c_str(), &s_allow_custom_fees);
ImGui::Checkbox(TrId("fetch_prices", "prices_w").c_str(), &s_fetch_prices);
ImGui::EndTabItem();
}
// Connection settings tab
if (ImGui::BeginTabItem(TR("settings_connection"))) {
ImGui::Spacing();
ImGui::Text("%s", TR("settings_rpc_connection"));
ImGui::TextDisabled("%s", TR("settings_configure_rpc"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
material::LabeledInputRow(TR("rpc_host"), connLbl.position, "##RPCHost",
s_rpc_host, sizeof(s_rpc_host), cmb.width);
material::LabeledInputRow(TR("rpc_port"), connLbl.position, "##RPCPort",
s_rpc_port, sizeof(s_rpc_port), portInput.width);
ImGui::Spacing();
material::LabeledInputRow(TR("rpc_user"), connLbl.position, "##RPCUser",
s_rpc_user, sizeof(s_rpc_user), cmb.width);
material::LabeledInputRow(TR("rpc_pass"), connLbl.position, "##RPCPassword",
s_rpc_password, sizeof(s_rpc_password), cmb.width, nullptr,
ImGuiInputTextFlags_Password);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("settings_rpc_note"));
ImGui::Spacing();
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
if (app->rpc()) {
rpc::RPCClient::TraceScope trace("Settings / Test connection");
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");
std::string msg = std::string(TR("swin_connection_successful")) + version;
Notifications::instance().success(msg);
} else {
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
}
});
} else {
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
ImGui::EndTabItem();
}
// Wallet tab
if (ImGui::BeginTabItem(TR("wallet"))) {
ImGui::Spacing();
ImGui::Text("%s", TR("settings_wallet_maintenance"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (material::StyledButton(TR("rescan"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (app->rpc()) {
// Start rescan from block 0
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
int start = result.value("start_height", 0);
int end = result.value("stop_height", 0);
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
TR("swin_rescan_to") + std::to_string(end);
Notifications::instance().success(msg);
} else {
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
}
});
} else {
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
ImGui::Spacing();
static bool s_confirm_clear_ztx = false;
if (material::StyledButton(TR("settings_clear_ztx_long"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
s_confirm_clear_ztx = true;
}
ImGui::TextDisabled(" %s", TR("settings_clear_ztx_desc"));
// Confirmation dialog
if (s_confirm_clear_ztx) {
// Distinct idSuffix: this confirm renders nested inside (and the same frame as) the parent
// settings dialog, so it must not share the default ##OverlayDialogContent key — otherwise it
// inherits the parent's OverlayCardState (incl. the sticky overflow flag) and collides on the
// child window id.
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f,
0.94f, 0.85f, "settings_clearztx")) {
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_w").c_str(),
true, doCancel, doConfirm);
if (doCancel) {
s_confirm_clear_ztx = false;
}
if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success(TR("swin_ztx_history_cleared"));
} else {
Notifications::instance().info(TR("swin_no_history_file_found"));
}
s_confirm_clear_ztx = false;
}
material::EndOverlayDialog();
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("%s", TR("settings_wallet_info"));
ImGui::Spacing();
// Get actual wallet size
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::Text(TR("settings_wallet_file_size"), size_str.c_str());
} else {
ImGui::TextDisabled("%s", TR("settings_wallet_not_found"));
}
ImGui::Text(TR("settings_wallet_location"), wallet_path.c_str());
ImGui::EndTabItem();
}
// Explorer tab
if (ImGui::BeginTabItem(TR("explorer"))) {
ImGui::Spacing();
ImGui::Text("%s", TR("settings_block_explorer_urls"));
ImGui::TextDisabled("%s", TR("settings_configure_explorer"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("%s", TR("transaction_url"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
ImGui::Text("%s", TR("address_url"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("settings_explorer_hint"));
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Save/Cancel buttons
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success(TR("swin_settings_saved"));
*p_open = false;
}
ImGui::SameLine();
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
// Reload settings to revert changes
loadSettingsToUI(app->settings());
// Revert skin to what was active when settings opened
if (!s_saved_skin_id.empty()) {
schema::SkinManager::instance().setActiveSkin(s_saved_skin_id);
if (app->settings()) {
app->settings()->setSkinId(s_saved_skin_id);
app->settings()->save();
}
}
*p_open = false;
}
material::EndOverlayDialog();
}
} // namespace ui
} // namespace dragonx
-19
View File
@@ -1,19 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Settings window
* Modal dialog for application settings
*/
void RenderSettingsWindow(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx