feat: blockchain rescan via daemon restart + status bar progress

- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
This commit is contained in:
dan_s
2026-02-28 15:06:35 -06:00
parent f5378a55ed
commit 4b815fc9d1
42 changed files with 1113 additions and 687 deletions

View File

@@ -350,18 +350,31 @@ APPRUN
local ARCH
ARCH=$(uname -m)
local IMG_NAME="DragonX_Wallet-${VERSION}-${ARCH}.AppImage"
local IMG_NAME="ObsidianDragon-${ARCH}.AppImage"
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "$IMG_NAME" 2>/dev/null && {
cp "$IMG_NAME" "$out/"
info "AppImage: $out/$IMG_NAME ($(du -h "$IMG_NAME" | cut -f1))"
# Rename to match Windows convention: ObsidianDragon.AppImage
mv "$out/$IMG_NAME" "$out/ObsidianDragon.AppImage"
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
} || warn "AppImage creation failed (appimagetool issue) — raw binary still in release/linux/"
# Clean up: keep only AppImage + raw binary in release/linux/
if ls "$out"/*.AppImage 1>/dev/null 2>&1; then
# AppImage succeeded — remove everything except AppImage and the binary
find "$out" -maxdepth 1 -type f ! -name '*.AppImage' ! -name 'ObsidianDragon' -delete
# Clean up: keep only AppImage + zip in release/linux/
if [[ -f "$out/ObsidianDragon.AppImage" ]]; then
# AppImage succeeded — remove everything except AppImage
find "$out" -maxdepth 1 -type f ! -name 'ObsidianDragon.AppImage' -delete
rm -rf "$out/res" 2>/dev/null
# Create zip matching Windows naming convention
local DIST="DragonX-Wallet-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "$out/ObsidianDragon.AppImage" "$dist_dir/"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
fi
info "Linux release artifacts: $out/"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 370 KiB

View File

@@ -2,7 +2,7 @@
name = "Color Pop Dark"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#26262E", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#282836", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
@@ -14,7 +14,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--secondary-light = "#FF9CDC"
--background = "#0E0E14"
--surface = "#161620"
--surface-variant = "#20202C"
--surface-variant = "#282836"
--on-primary = "#FFFFFF"
--on-secondary = "#FFFFFF"
--on-background = "#E8E6F0"
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--surface-active = "rgba(200,190,240,0.10)"
--glass-button = "rgba(124,108,255,0.08)"
--glass-button-hover = "rgba(124,108,255,0.16)"
--card-border = "rgba(200,190,240,0.10)"
--card-border = "rgba(200,190,240,0.24)"
--text-shadow = "rgba(0,0,0,0.45)"
--input-overlay-text = "rgba(232,230,240,0.25)"
--slider-text = "rgba(232,230,240,0.82)"
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--tooltip-bg = "rgba(14,14,22,0.94)"
--tooltip-border = "rgba(124,108,255,0.18)"
--glass-fill = "rgba(200,190,240,0.06)"
--glass-border = "rgba(200,190,240,0.10)"
--glass-border = "rgba(124,108,255,0.28)"
--glass-noise-tint = "rgba(124,108,255,0.03)"
--tactile-top = "rgba(200,190,240,0.07)"
--tactile-bottom = "rgba(200,190,240,0.0)"
--hover-overlay = "rgba(124,108,255,0.05)"
--active-overlay = "rgba(124,108,255,0.10)"
--rim-light = "rgba(124,108,255,0.10)"
--rim-light = "rgba(124,108,255,0.16)"
--status-divider = "rgba(200,190,240,0.06)"
--sidebar-hover = "rgba(124,108,255,0.10)"
--sidebar-icon = "rgba(232,230,240,0.45)"

View File

@@ -2,7 +2,7 @@
name = "Dark"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2C2C2E", --elevation-3 = "#363638", --elevation-4 = "#404044" }
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2E2E32", --elevation-3 = "#363638", --elevation-4 = "#404044" }
images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
@@ -14,7 +14,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--secondary-light = "#9DC5BE"
--background = "#141416"
--surface = "#1A1A1C"
--surface-variant = "#262628"
--surface-variant = "#2E2E32"
--on-primary = "#000000"
--on-secondary = "#000000"
--on-background = "#D0D0D4"
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--surface-active = "rgba(220,220,225,0.08)"
--glass-button = "rgba(220,220,225,0.05)"
--glass-button-hover = "rgba(220,220,225,0.10)"
--card-border = "rgba(220,220,225,0.12)"
--card-border = "rgba(220,220,225,0.24)"
--text-shadow = "rgba(0,0,0,0.40)"
--input-overlay-text = "rgba(208,208,212,0.28)"
--slider-text = "rgba(208,208,212,0.85)"
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--tooltip-bg = "rgba(18,18,22,0.92)"
--tooltip-border = "rgba(220,220,225,0.10)"
--glass-fill = "rgba(220,220,225,0.07)"
--glass-border = "rgba(220,220,225,0.12)"
--glass-border = "rgba(154,175,200,0.28)"
--glass-noise-tint = "rgba(220,220,225,0.03)"
--tactile-top = "rgba(220,220,225,0.06)"
--tactile-bottom = "rgba(220,220,225,0.0)"
--hover-overlay = "rgba(220,220,225,0.04)"
--active-overlay = "rgba(220,220,225,0.08)"
--rim-light = "rgba(220,220,225,0.08)"
--rim-light = "rgba(220,220,225,0.14)"
--status-divider = "rgba(220,220,225,0.06)"
--sidebar-hover = "rgba(220,220,225,0.08)"
--sidebar-icon = "rgba(220,220,225,0.40)"

View File

@@ -2,19 +2,19 @@
name = "Obsidian"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#1C1625", --elevation-3 = "#211A2C", --elevation-4 = "#261E33" }
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#252030", --elevation-3 = "#2D2838", --elevation-4 = "#353040" }
images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
--primary = "#AB47BC"
--primary-variant = "#8E24AA"
--primary-light = "#CE93D8"
--secondary = "#B388FF"
--secondary-variant = "#7C4DFF"
--secondary-light = "#D1C4E9"
--primary = "#9A6BA8"
--primary-variant = "#7A4888"
--primary-light = "#C5A8CC"
--secondary = "#A898D8"
--secondary-variant = "#8074C8"
--secondary-light = "#CCC4D9"
--background = "#0A0810"
--surface = "#110E18"
--surface-variant = "#1C1625"
--surface-variant = "#252030"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#E8E0F0"
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--surface-active = "rgba(200,180,255,0.10)"
--glass-button = "rgba(200,180,255,0.06)"
--glass-button-hover = "rgba(200,180,255,0.12)"
--card-border = "rgba(200,180,255,0.14)"
--card-border = "rgba(200,180,255,0.26)"
--text-shadow = "rgba(0,0,0,0.50)"
--input-overlay-text = "rgba(232,224,240,0.30)"
--slider-text = "rgba(232,224,240,0.85)"
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--tooltip-bg = "rgba(14,11,20,0.92)"
--tooltip-border = "rgba(200,180,255,0.12)"
--glass-fill = "rgba(200,180,255,0.08)"
--glass-border = "rgba(200,180,255,0.14)"
--glass-border = "rgba(154,107,168,0.30)"
--glass-noise-tint = "rgba(200,180,255,0.03)"
--tactile-top = "rgba(200,180,255,0.06)"
--tactile-bottom = "rgba(200,180,255,0.0)"
--hover-overlay = "rgba(200,180,255,0.05)"
--active-overlay = "rgba(200,180,255,0.10)"
--rim-light = "rgba(200,180,255,0.08)"
--rim-light = "rgba(200,180,255,0.14)"
--status-divider = "rgba(200,180,255,0.08)"
--sidebar-hover = "rgba(200,180,255,0.10)"
--sidebar-icon = "rgba(200,180,255,0.42)"
@@ -65,19 +65,19 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--window-control-hover = "rgba(200,180,255,0.12)"
--window-close-hover = "rgba(232,17,35,0.78)"
--spinner-track = "rgba(200,180,255,0.10)"
--spinner-active = "rgba(179,136,255,0.85)"
--spinner-active = "rgba(168,152,216,0.85)"
--shutdown-panel-bg = "rgba(10,8,16,0.90)"
--shutdown-panel-border = "rgba(200,180,255,0.07)"
--ram-bar-app = "#AB47BC"
--ram-bar-app = "#9A6BA8"
--ram-bar-system = "rgba(255,255,255,0.18)"
--accent-total = "#CE93D8"
--accent-shielded = "#80CBC4"
--accent-transparent = "#FFAB91"
--accent-action = "#AB47BC"
--accent-market = "#80CBC4"
--accent-portfolio = "#B388FF"
--toast-info-accent = "#AB47BC"
--toast-info-text = "#CE93D8"
--accent-total = "#C5A8CC"
--accent-shielded = "#8AB8B4"
--accent-transparent = "#D8B0A0"
--accent-action = "#9A6BA8"
--accent-market = "#8AB8B4"
--accent-portfolio = "#A898D8"
--toast-info-accent = "#9A6BA8"
--toast-info-text = "#C5A8CC"
--toast-success-accent = "rgba(50,180,80,1.0)"
--toast-success-text = "rgba(180,255,180,1.0)"
--toast-warning-accent = "rgba(204,166,50,1.0)"
@@ -86,10 +86,10 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--toast-error-text = "rgba(255,153,153,1.0)"
--snackbar-bg = "rgba(40,35,55,0.95)"
--snackbar-text = "rgba(232,224,240,0.87)"
--snackbar-action = "rgba(179,136,255,1.0)"
--snackbar-action-hover = "rgba(206,147,216,1.0)"
--snackbar-action = "rgba(168,152,216,1.0)"
--snackbar-action-hover = "rgba(197,168,204,1.0)"
--switch-track-off = "rgba(200,180,255,0.12)"
--switch-track-on = "rgba(171,71,188,0.50)"
--switch-track-on = "rgba(154,107,168,0.50)"
--switch-thumb-off = "#B0A0C0"
--switch-thumb-on = "#E8E0F0"
--control-shadow = "rgba(0,0,0,0.24)"
@@ -144,18 +144,18 @@ gradient-border-enabled = { size = 1.0 }
gradient-border-speed = { size = 0.12 }
gradient-border-thickness = { size = 1.5 }
gradient-border-alpha = { size = 0.55 }
gradient-border-color-a = { color = "#CE93D8" }
gradient-border-color-b = { color = "#3F51B5" }
gradient-border-color-a = { color = "#C5A8CC" }
gradient-border-color-b = { color = "#6878A8" }
ember-rise-enabled = { size = 0.0 }
# Shader-like viewport overlay — deep indigo crystal atmosphere
viewport-wash-enabled = { size = 1.0 }
viewport-wash-alpha = { size = 0.05 }
viewport-wash-tl = { color = "#4A148C" }
viewport-wash-tr = { color = "#1A237E" }
viewport-wash-bl = { color = "#311B92" }
viewport-wash-br = { color = "#6A1B9A" }
viewport-wash-tl = { color = "#3A2860" }
viewport-wash-tr = { color = "#2A3058" }
viewport-wash-bl = { color = "#302860" }
viewport-wash-br = { color = "#4A3068" }
viewport-wash-rotate = { size = 0.015 }
viewport-wash-pulse = { size = 0.0 }
viewport-wash-pulse-depth = { size = 0.0 }

View File

@@ -20,7 +20,7 @@ spacing-tokens = { xs = 2.0, sm = 4.0, md = 8.0, lg = 12.0, xl = 16.0, xxl = 24.
name = "DragonX"
author = "DanS"
dark = true
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#201410", --elevation-3 = "#261914", --elevation-4 = "#2C1E18" }
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#281C16", --elevation-3 = "#30241C", --elevation-4 = "#382C22" }
images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
@@ -32,7 +32,7 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--secondary-light = "#FF9E40"
--background = "#0C0606"
--surface = "#120A08"
--surface-variant = "#201410"
--surface-variant = "#281C16"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#F0E0D8"
@@ -66,13 +66,13 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--tooltip-bg = "rgba(12,8,6,0.92)"
--tooltip-border = "rgba(255,180,140,0.12)"
--glass-fill = "rgba(255,180,140,0.08)"
--glass-border = "rgba(255,180,140,0.13)"
--glass-border = "rgba(211,47,47,0.26)"
--glass-noise-tint = "rgba(255,180,140,0.03)"
--tactile-top = "rgba(255,180,140,0.06)"
--tactile-bottom = "rgba(255,180,140,0.0)"
--hover-overlay = "rgba(255,180,140,0.05)"
--active-overlay = "rgba(255,180,140,0.10)"
--rim-light = "rgba(255,180,140,0.08)"
--rim-light = "rgba(255,180,140,0.14)"
--status-divider = "rgba(255,180,140,0.08)"
--sidebar-hover = "rgba(255,180,140,0.10)"
--sidebar-icon = "rgba(255,180,140,0.36)"

View File

@@ -281,6 +281,38 @@ void App::update()
fast_refresh_timer_ = 0.0f;
if (state_.connected && !state_.isLocked()) {
refreshMiningInfo();
// Poll getrescaninfo for rescan progress (if rescan flag is set)
if (state_.sync.rescanning && fast_worker_) {
fast_worker_->post([this]() -> rpc::RPCWorker::MainCb {
try {
auto info = rpc_->call("getrescaninfo");
bool rescanning = info.value("rescanning", false);
float progress = 0.0f;
if (info.contains("rescan_progress")) {
std::string progStr = info["rescan_progress"].get<std::string>();
try { progress = std::stof(progStr) * 100.0f; } catch (...) {}
}
return [this, rescanning, progress]() {
if (rescanning) {
state_.sync.rescanning = true;
if (progress > 0.0f) {
state_.sync.rescan_progress = progress / 100.0f;
}
} else if (state_.sync.rescanning) {
// Rescan just finished
ui::Notifications::instance().success("Blockchain rescan complete");
state_.sync.rescanning = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
}
};
} catch (...) {
// RPC not available yet or failed
return [](){};
}
});
}
}
// Poll xmrig stats every ~2 seconds (use a simple toggle)
@@ -315,6 +347,100 @@ void App::update()
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
state_.pool_mining.xmrig_running = false;
}
// Check daemon output for rescan progress
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
if (!newOutput.empty()) {
// Look for rescan progress patterns in new output
// Hush patterns: "Still rescanning. At block X. Progress=Y" or "Rescanning..." with percentage
bool foundRescan = false;
float rescanPct = 0.0f;
// Search line by line for rescan info
size_t pos = 0;
while (pos < newOutput.size()) {
size_t eol = newOutput.find('\n', pos);
if (eol == std::string::npos) eol = newOutput.size();
std::string line = newOutput.substr(pos, eol - pos);
pos = eol + 1;
// Check for "Rescanning from height" (rescan starting)
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
state_.sync.rescan_status = line;
}
// Check for "Still rescanning" with progress
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
// Try to extract progress (Progress=0.XXXX)
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9; // strlen("Progress=")
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
}
if (numEnd > numStart) {
try {
rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f;
} catch (...) {}
}
}
state_.sync.rescan_status = line;
}
// Check for "Rescanning..." with percentage (ShowProgress output)
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
// Try to extract percentage
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
// Walk backwards to find the number
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try {
rescanPct = std::stof(line.substr(numStart, numEnd - numStart));
} catch (...) {}
}
}
state_.sync.rescan_status = line;
}
// Check for "Done rescanning" (rescan complete)
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
if (state_.sync.rescanning) {
ui::Notifications::instance().success("Blockchain rescan complete");
}
state_.sync.rescanning = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
}
}
if (foundRescan) {
state_.sync.rescanning = true;
if (rescanPct > 0.0f) {
state_.sync.rescan_progress = rescanPct / 100.0f;
}
}
}
} else if (!embedded_daemon_ || !embedded_daemon_->isRunning()) {
// Clear rescan state if daemon is not running (but preserve during restart)
if (state_.sync.rescanning && state_.sync.rescan_progress >= 0.99f) {
state_.sync.rescanning = false;
state_.sync.rescan_status.clear();
}
}
}
// Regular refresh every 5 seconds
@@ -965,12 +1091,8 @@ void App::render()
renderBackupDialog();
}
// Encrypt wallet / change passphrase dialogs (from Settings)
renderEncryptWalletDialog();
renderDecryptWalletDialog();
// PIN setup / change / remove dialogs (from Settings)
renderPinDialogs();
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
// to ensure they appear on top of all other content
// Send confirm popup
ui::RenderSendConfirmPopup(this);
@@ -1020,6 +1142,11 @@ void App::render()
ImGui::ShowDemoWindow(&show_demo_window_);
}
// Security overlay dialogs (must render LAST to be on top of everything)
renderEncryptWalletDialog();
renderDecryptWalletDialog();
renderPinDialogs();
// Render notifications (toast messages)
ui::Notifications::instance().render();
}
@@ -1111,7 +1238,19 @@ void App::renderStatusBar()
ImGui::SameLine(0, sbSectionGap);
ImGui::TextDisabled("|");
ImGui::SameLine(0, sbSeparatorGap);
if (state_.sync.syncing) {
if (state_.sync.rescanning) {
// Show rescan progress (takes priority over sync)
// Use animated dots if progress is unknown (0%)
if (state_.sync.rescan_progress > 0.01f) {
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning %.0f%%",
state_.sync.rescan_progress * 100.0f);
} else {
// Animated "Rescanning..." with pulsing dots
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning%s", dotStr);
}
} else if (state_.sync.syncing) {
int blocksLeft = state_.sync.headers - state_.sync.blocks;
if (blocksLeft < 0) blocksLeft = 0;
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
@@ -1226,40 +1365,33 @@ void App::renderAboutDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("About ObsidianDragon");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 400.0f), dlg.height > 0 ? dlg.height : 250.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("About ObsidianDragon", &show_about_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, DRAGONX_APP_NAME);
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::Text("Version: %s", DRAGONX_VERSION);
ImGui::Text("ImGui: %s", IMGUI_VERSION);
ImGui::Spacing();
ImGui::TextWrapped("A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::Spacing();
ImGui::Text("Copyright 2024-2026 The Hush Developers");
ImGui::Text("Released under the GPLv3 License");
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont((int)dlgF("button-font", 1)))) {
show_about_ = false;
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("About ObsidianDragon", &show_about_, dlgF("width", 400.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, DRAGONX_APP_NAME);
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::Text("Version: %s", DRAGONX_VERSION);
ImGui::Text("ImGui: %s", IMGUI_VERSION);
ImGui::Spacing();
ImGui::TextWrapped("A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::Spacing();
ImGui::Text("Copyright 2024-2026 The Hush Developers");
ImGui::Text("Released under the GPLv3 License");
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont((int)dlgF("button-font", 1)))) {
show_about_ = false;
}
ui::material::EndOverlayDialog();
}
void App::renderImportKeyDialog()
@@ -1269,62 +1401,55 @@ void App::renderImportKeyDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("Import Private Key");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 500.0f), dlg.height > 0 ? dlg.height : 200.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Import Private Key", &show_import_key_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Import Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
ImGui::Spacing();
ImGui::Text("Private Key:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
ImGui::Spacing();
if (!import_status_.empty()) {
if (import_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", import_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string key(import_key_input_);
if (!key.empty()) {
importPrivateKey(key, [this](bool success, const std::string& msg) {
import_success_ = success;
import_status_ = msg;
if (success) {
memset(import_key_input_, 0, sizeof(import_key_input_));
}
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_import_key_ = false;
import_status_.clear();
memset(import_key_input_, 0, sizeof(import_key_input_));
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Import Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
ImGui::Spacing();
ImGui::Text("Private Key:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
ImGui::Spacing();
if (!import_status_.empty()) {
if (import_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", import_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string key(import_key_input_);
if (!key.empty()) {
importPrivateKey(key, [this](bool success, const std::string& msg) {
import_success_ = success;
import_status_ = msg;
if (success) {
memset(import_key_input_, 0, sizeof(import_key_input_));
}
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_import_key_ = false;
import_status_.clear();
memset(import_key_input_, 0, sizeof(import_key_input_));
}
ui::material::EndOverlayDialog();
}
void App::renderExportKeyDialog()
@@ -1334,78 +1459,71 @@ void App::renderExportKeyDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("Export Private Key");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 600.0f), dlg.height > 0 ? dlg.height : 300.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
int btnFont = (int)dlgF("button-font", 1);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Export Private Key", &show_export_key_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Export Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
"WARNING: Anyone with this key can spend your coins!");
ImGui::Spacing();
// Address selector
ImGui::Text("Select Address:");
std::vector<std::string> all_addrs;
for (const auto& a : state_.t_addresses) all_addrs.push_back(a.address);
for (const auto& a : state_.z_addresses) all_addrs.push_back(a.address);
int addrFrontLen = (int)dlgF("addr-front-len", 20);
int addrBackLen = (int)dlgF("addr-back-len", 8);
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
for (const auto& addr : all_addrs) {
bool selected = (export_address_ == addr);
std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen);
if (ImGui::Selectable(display.c_str(), selected)) {
export_address_ = addr;
export_result_.clear();
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
if (!export_address_.empty()) {
exportPrivateKey(export_address_, [this](const std::string& key) {
export_result_ = key;
});
}
}
ImGui::Spacing();
if (!export_result_.empty()) {
ImGui::Text("Private Key:");
ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(),
export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly);
if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
ImGui::SetClipboardText(export_result_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int closeBtnFont = (int)dlgF("close-button-font", -1);
if (closeBtnFont < 0) closeBtnFont = btnFont;
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) {
show_export_key_ = false;
export_result_.clear();
export_address_.clear();
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Export Private Key", &show_export_key_, dlgF("width", 600.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Export Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
"WARNING: Anyone with this key can spend your coins!");
ImGui::Spacing();
// Address selector
ImGui::Text("Select Address:");
std::vector<std::string> all_addrs;
for (const auto& a : state_.t_addresses) all_addrs.push_back(a.address);
for (const auto& a : state_.z_addresses) all_addrs.push_back(a.address);
int addrFrontLen = (int)dlgF("addr-front-len", 20);
int addrBackLen = (int)dlgF("addr-back-len", 8);
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
for (const auto& addr : all_addrs) {
bool selected = (export_address_ == addr);
std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen);
if (ImGui::Selectable(display.c_str(), selected)) {
export_address_ = addr;
export_result_.clear();
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
if (!export_address_.empty()) {
exportPrivateKey(export_address_, [this](const std::string& key) {
export_result_ = key;
});
}
}
ImGui::Spacing();
if (!export_result_.empty()) {
ImGui::Text("Private Key:");
ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(),
export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly);
if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
ImGui::SetClipboardText(export_result_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int closeBtnFont = (int)dlgF("close-button-font", -1);
if (closeBtnFont < 0) closeBtnFont = btnFont;
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) {
show_export_key_ = false;
export_result_.clear();
export_address_.clear();
}
ui::material::EndOverlayDialog();
}
void App::renderBackupDialog()
@@ -1415,59 +1533,52 @@ void App::renderBackupDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("Backup Wallet");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 500.0f), dlg.height > 0 ? dlg.height : 200.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Backup Wallet", &show_backup_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Backup Wallet");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Export all private keys to a file. Keep this file secure!");
ImGui::Spacing();
ImGui::Text("Backup File Path:");
static char backup_path[512] = "dragonx-backup.txt";
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##backuppath", backup_path, sizeof(backup_path));
ImGui::Spacing();
if (!backup_status_.empty()) {
if (backup_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", backup_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", backup_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Save Backup", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string path(backup_path);
if (!path.empty()) {
backupWallet(path, [this](bool success, const std::string& msg) {
backup_success_ = success;
backup_status_ = msg;
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_backup_ = false;
backup_status_.clear();
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Backup Wallet", &show_backup_, dlgF("width", 500.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Backup Wallet");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Export all private keys to a file. Keep this file secure!");
ImGui::Spacing();
ImGui::Text("Backup File Path:");
static char backup_path[512] = "dragonx-backup.txt";
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##backuppath", backup_path, sizeof(backup_path));
ImGui::Spacing();
if (!backup_status_.empty()) {
if (backup_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", backup_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", backup_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Save Backup", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string path(backup_path);
if (!path.empty()) {
backupWallet(path, [this](bool success, const std::string& msg) {
backup_success_ = success;
backup_status_ = msg;
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_backup_ = false;
backup_status_.clear();
}
ui::material::EndOverlayDialog();
}
void App::renderAntivirusHelpDialog()
@@ -1475,57 +1586,46 @@ void App::renderAntivirusHelpDialog()
#ifdef _WIN32
if (!pending_antivirus_dialog_) return;
ImGui::OpenPopup("Windows Defender Blocked Miner");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(560.0f, 360.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Windows Defender Blocked Miner", &pending_antivirus_dialog_,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Windows Defender Blocked xmrig");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped(
"Mining software is often flagged as potentially unwanted. "
"Follow these steps to enable pool mining:");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 1: Add Exclusion");
ImGui::BulletText("Open Windows Security > Virus & threat protection");
ImGui::BulletText("Click Manage settings > Exclusions > Add or remove");
ImGui::BulletText("Add folder: %%APPDATA%%\\ObsidianDragon\\");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 2: Restore from Quarantine (if needed)");
ImGui::BulletText("Windows Security > Protection history");
ImGui::BulletText("Find xmrig.exe and click Restore");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 3: Restart wallet and try again");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
ImGui::Separator();
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
float btnW = 160.0f;
if (ui::material::StyledButton("Open Windows Security", ImVec2(btnW, 0))) {
// Open Windows Security app to the exclusions page
ShellExecuteA(NULL, "open", "windowsdefender://threat", NULL, NULL, SW_SHOWNORMAL);
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(100.0f, 0))) {
pending_antivirus_dialog_ = false;
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Windows Defender Blocked Miner", &pending_antivirus_dialog_, 560.0f, 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Windows Defender Blocked xmrig");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped(
"Mining software is often flagged as potentially unwanted. "
"Follow these steps to enable pool mining:");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 1: Add Exclusion");
ImGui::BulletText("Open Windows Security > Virus & threat protection");
ImGui::BulletText("Click Manage settings > Exclusions > Add or remove");
ImGui::BulletText("Add folder: %%APPDATA%%\\ObsidianDragon\\");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 2: Restore from Quarantine (if needed)");
ImGui::BulletText("Windows Security > Protection history");
ImGui::BulletText("Find xmrig.exe and click Restore");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 3: Restart wallet and try again");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
ImGui::Separator();
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
float btnW = 160.0f;
if (ui::material::StyledButton("Open Windows Security", ImVec2(btnW, 0))) {
// Open Windows Security app to the exclusions page
ShellExecuteA(NULL, "open", "windowsdefender://threat", NULL, NULL, SW_SHOWNORMAL);
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(100.0f, 0))) {
pending_antivirus_dialog_ = false;
}
ui::material::EndOverlayDialog();
#endif
}
@@ -1723,6 +1823,43 @@ bool App::isEmbeddedDaemonRunning() const
return embedded_daemon_ && embedded_daemon_->isRunning();
}
void App::rescanBlockchain()
{
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
ui::Notifications::instance().warning(
"Rescan requires embedded daemon. Restart your daemon with -rescan manually.");
return;
}
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
// Initialize rescan state for status bar display
state_.sync.rescanning = true;
state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = "Starting rescan...";
// Set rescan flag BEFORE stopping so it's ready when we restart
embedded_daemon_->setRescanOnNextStart(true);
DEBUG_LOGF("[App] Rescan flag set, rescanOnNextStart=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
// Stop daemon, then restart
std::thread([this]() {
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
stopEmbeddedDaemon();
// Wait for daemon to fully stop
DEBUG_LOGF("[App] Waiting for daemon to fully stop...\n");
std::this_thread::sleep_for(std::chrono::seconds(3));
// Reset output offset so we parse fresh output for rescan progress
daemon_output_offset_ = 0;
DEBUG_LOGF("[App] Starting daemon with rescan flag=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
startEmbeddedDaemon();
}).detach();
}
double App::getDaemonMemoryUsageMB() const
{
// If we have an embedded daemon with a tracked process handle, use it

View File

@@ -241,6 +241,7 @@ public:
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
@@ -304,6 +305,7 @@ public:
void showDecryptDialog() {
show_decrypt_dialog_ = true;
decrypt_phase_ = 0; // passphrase entry
decrypt_step_ = 0;
decrypt_status_.clear();
decrypt_in_progress_ = false;
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
@@ -368,6 +370,7 @@ private:
bool use_embedded_daemon_ = true;
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// Export/Import state
std::string export_result_;
@@ -505,6 +508,7 @@ private:
// Decrypt wallet dialog state
bool show_decrypt_dialog_ = false;
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
int decrypt_step_ = 0; // 0=unlock, 1=export, 2=stop, 3=rename, 4=restart, 5=import
char decrypt_pass_buf_[256] = {};
std::string decrypt_status_;
bool decrypt_in_progress_ = false;

View File

@@ -16,6 +16,8 @@
#include "ui/material/typography.h"
#include "ui/material/draw_helpers.h"
#include "ui/schema/ui_schema.h"
#include "ui/theme.h"
#include "ui/effects/imgui_acrylic.h"
#include "util/platform.h"
#include "util/secure_vault.h"
#include "util/perf_log.h"
@@ -694,21 +696,18 @@ void App::renderLockScreen() {
void App::renderEncryptWalletDialog() {
if (!show_encrypt_dialog_ && !show_change_passphrase_) return;
using namespace ui::material;
// Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
if (show_encrypt_dialog_) {
const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
? "Quick-Unlock PIN##EncDlg" : "Encrypt Wallet##EncDlg";
? "Quick-Unlock PIN" : "Encrypt Wallet";
// Prevent closing via X button while encrypting
bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;
ImGui::SetNextWindowSize(ImVec2(460, 0), ImGuiCond_FirstUseEver);
ImGuiWindowFlags dlgFlags = ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_AlwaysAutoResize;
if (ImGui::Begin(dlgTitle, pOpen, dlgFlags)) {
if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) {
// ---- Phase 1: Passphrase entry ----
if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
@@ -911,8 +910,8 @@ void App::renderEncryptWalletDialog() {
show_encrypt_dialog_ = false;
}
}
EndOverlayDialog();
}
ImGui::End();
// Clean up saved passphrase if dialog was closed via X button
if (!show_encrypt_dialog_ && !enc_dlg_saved_passphrase_.empty()) {
@@ -924,9 +923,8 @@ void App::renderEncryptWalletDialog() {
// Change passphrase dialog
if (show_change_passphrase_) {
ImGui::SetNextWindowSize(ImVec2(440, 320), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Change Passphrase##ChgDlg", &show_change_passphrase_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) {
ImGui::Text("Current Passphrase:");
ImGui::PushItemWidth(-1);
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
@@ -959,8 +957,8 @@ void App::renderEncryptWalletDialog() {
std::string(change_new_pass_buf_));
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
}
@@ -978,16 +976,10 @@ void App::renderDecryptWalletDialog() {
if (!show_decrypt_dialog_) return;
using namespace ui::material;
const char* title = "Remove Wallet Encryption##DecryptDlg";
bool canClose = (decrypt_phase_ != 1); // don't close while working
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
ImGui::SetNextWindowSize(ImVec2(480, 0), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_AlwaysAutoResize;
if (ImGui::Begin(title, pOpen, flags)) {
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
// ---- Phase 0: Passphrase entry ----
if (decrypt_phase_ == 0) {
@@ -1026,6 +1018,7 @@ void App::renderDecryptWalletDialog() {
std::string passphrase(decrypt_pass_buf_);
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
decrypt_phase_ = 1;
decrypt_step_ = 0;
decrypt_in_progress_ = true;
decrypt_status_ = "Unlocking wallet...";
@@ -1044,129 +1037,163 @@ void App::renderDecryptWalletDialog() {
};
}
// Step 2: Export wallet to temp file
std::string exportFile = "obsidian_decrypt_export_" +
std::to_string(std::time(nullptr));
// Update step on main thread
return [this]() {
decrypt_step_ = 1;
decrypt_status_ = "Exporting wallet keys...";
// Continue with step 2
worker_->post([this]() -> rpc::RPCWorker::MainCb {
std::string dataDir = util::Platform::getDragonXDataDir();
std::string exportFile = "obsidiandecryptexport" +
std::to_string(std::time(nullptr));
std::string exportPath = dataDir + exportFile;
// Update status on main thread
// (we can't easily do mid-flow updates from worker,
// so we just proceed — the UI shows "working")
try {
rpc_->call("z_exportwallet", {exportFile});
} catch (const std::exception& e) {
std::string err = e.what();
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Export failed: " + err;
decrypt_phase_ = 3;
};
}
// Step 3: Stop daemon
try {
rpc_->call("stop");
} catch (...) {
// stop often throws because connection drops
}
// Wait for daemon to fully stop
std::this_thread::sleep_for(std::chrono::seconds(3));
// Step 4: Rename encrypted wallet.dat → wallet.dat.encrypted.bak
std::string dataDir = util::Platform::getDragonXDataDir();
std::string walletPath = dataDir + "wallet.dat";
std::string backupPath = dataDir + "wallet.dat.encrypted.bak";
std::error_code ec;
if (std::filesystem::exists(walletPath, ec)) {
// Remove old backup if exists
std::filesystem::remove(backupPath, ec);
std::filesystem::rename(walletPath, backupPath, ec);
if (ec) {
std::string err = ec.message();
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Failed to rename wallet.dat: " + err;
decrypt_phase_ = 3;
};
}
}
// Step 5: Restart daemon (creates fresh unencrypted wallet)
return [this, exportFile]() {
decrypt_status_ = "Restarting daemon...";
auto restartAndImport = [this, exportFile]() {
// Give daemon time to stop fully
std::this_thread::sleep_for(std::chrono::seconds(2));
if (isUsingEmbeddedDaemon()) {
stopEmbeddedDaemon();
std::this_thread::sleep_for(std::chrono::seconds(1));
startEmbeddedDaemon();
}
// Wait for daemon to become available
int maxWait = 60; // seconds
bool daemonUp = false;
for (int i = 0; i < maxWait; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
try {
rpc_->call("getinfo");
daemonUp = true;
break;
} catch (...) {}
}
if (!daemonUp) {
// Schedule error on main thread — can't directly update
// but we'll let the import attempt fail
}
// Step 6: Import wallet (includes rescan)
try {
rpc_->call("z_importwallet", {exportFile});
rpc_->call("z_exportwallet", {exportFile});
} catch (const std::exception& e) {
std::string err = e.what();
// Post result back to main thread via worker
if (worker_) {
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Import failed: " + err +
"\nYour encrypted wallet backup is at wallet.dat.encrypted.bak";
decrypt_phase_ = 3;
};
});
}
return;
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Export failed: " + err;
decrypt_phase_ = 3;
};
}
// Success — post to main thread
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Wallet decrypted successfully!";
decrypt_phase_ = 2;
return [this, exportPath]() {
decrypt_step_ = 2;
decrypt_status_ = "Stopping daemon...";
// Continue with step 3
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
try {
rpc_->call("stop");
} catch (...) {
// stop often throws because connection drops
}
// Clean up PIN vault since encryption is gone
if (vault_ && vault_->hasVault()) {
vault_->removeVault();
}
if (settings_ && settings_->getPinEnabled()) {
settings_->setPinEnabled(false);
settings_->save();
}
// Wait for daemon to fully stop
std::this_thread::sleep_for(std::chrono::seconds(3));
refreshWalletEncryptionState();
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
return [this, exportPath]() {
decrypt_step_ = 3;
decrypt_status_ = "Backing up encrypted wallet...";
// Continue with step 4 (rename)
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
std::string dataDir = util::Platform::getDragonXDataDir();
std::string walletPath = dataDir + "wallet.dat";
std::string backupPath = dataDir + "wallet.dat.encrypted.bak";
std::error_code ec;
if (std::filesystem::exists(walletPath, ec)) {
std::filesystem::remove(backupPath, ec);
std::filesystem::rename(walletPath, backupPath, ec);
if (ec) {
std::string err = ec.message();
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Failed to rename wallet.dat: " + err;
decrypt_phase_ = 3;
};
}
}
return [this, exportPath]() {
decrypt_step_ = 4;
decrypt_status_ = "Restarting daemon...";
auto restartAndImport = [this, exportPath]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
if (isUsingEmbeddedDaemon()) {
stopEmbeddedDaemon();
std::this_thread::sleep_for(std::chrono::seconds(1));
startEmbeddedDaemon();
}
// Wait for daemon to become available
int maxWait = 60;
bool daemonUp = false;
for (int i = 0; i < maxWait; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
try {
rpc_->call("getinfo");
daemonUp = true;
break;
} catch (...) {}
}
if (!daemonUp) {
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Daemon failed to restart";
decrypt_phase_ = 3;
};
});
}
return;
}
// Update step on main thread
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_step_ = 5;
decrypt_status_ = "Importing keys (this may take a while)...";
};
});
}
// Step 6: Import wallet (use full path)
try {
rpc_->call("z_importwallet", {exportPath});
} catch (const std::exception& e) {
std::string err = e.what();
if (worker_) {
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Import failed: " + err +
"\nYour encrypted wallet backup is at wallet.dat.encrypted.bak";
decrypt_phase_ = 3;
};
});
}
return;
}
// Success
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Wallet decrypted successfully!";
decrypt_phase_ = 2;
if (vault_ && vault_->hasVault()) {
vault_->removeVault();
}
if (settings_ && settings_->getPinEnabled()) {
settings_->setPinEnabled(false);
settings_->save();
}
refreshWalletEncryptionState();
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
};
});
}
};
std::thread(restartAndImport).detach();
};
});
};
});
}
};
std::thread(restartAndImport).detach();
};
});
};
});
}
@@ -1181,7 +1208,45 @@ void App::renderDecryptWalletDialog() {
// ---- Phase 1: Working ----
} else if (decrypt_phase_ == 1) {
ImGui::Text("%s", decrypt_status_.empty() ? "Working..." : decrypt_status_.c_str());
// Step checklist
const char* stepLabels[] = {
"Unlocking wallet",
"Exporting wallet keys",
"Stopping daemon",
"Backing up encrypted wallet",
"Restarting daemon",
"Importing keys (rescan)"
};
const int numSteps = 6;
ImGui::Spacing();
for (int i = 0; i < numSteps; i++) {
ImGui::PushFont(Type().iconMed());
if (i < decrypt_step_) {
// Completed
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE);
} else if (i == decrypt_step_) {
// In progress - animate
float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f);
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, alpha), ICON_MD_PENDING);
} else {
// Not started
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.5f), ICON_MD_RADIO_BUTTON_UNCHECKED);
}
ImGui::PopFont();
ImGui::SameLine();
if (i == decrypt_step_) {
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f), "%s...", stepLabels[i]);
} else if (i < decrypt_step_) {
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
} else {
ImGui::TextDisabled("%s", stepLabels[i]);
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Indeterminate progress bar
@@ -1242,6 +1307,7 @@ void App::renderDecryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Try Again", ImVec2(btnW, 40))) {
decrypt_phase_ = 0;
decrypt_step_ = 0;
decrypt_status_.clear();
}
ImGui::SameLine();
@@ -1249,8 +1315,8 @@ void App::renderDecryptWalletDialog() {
show_decrypt_dialog_ = false;
}
}
EndOverlayDialog();
}
ImGui::End();
}
// ===========================================================================
@@ -1258,11 +1324,12 @@ void App::renderDecryptWalletDialog() {
// ===========================================================================
void App::renderPinDialogs() {
using namespace ui::material;
// ---- Set PIN dialog ----
if (show_pin_setup_) {
ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Set PIN##PinSetupDlg", &show_pin_setup_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) {
ImGui::TextWrapped(
"Set a 4-8 digit PIN for quick wallet unlock. "
"Your wallet passphrase will be encrypted with this PIN "
@@ -1352,15 +1419,14 @@ void App::renderPinDialogs() {
}
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
// ---- Change PIN dialog ----
if (show_pin_change_) {
ImGui::SetNextWindowSize(ImVec2(420, 300), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Change PIN##PinChangeDlg", &show_pin_change_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) {
ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN.");
ImGui::Spacing();
ImGui::Separator();
@@ -1426,15 +1492,14 @@ void App::renderPinDialogs() {
}
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
// ---- Remove PIN dialog ----
if (show_pin_remove_) {
ImGui::SetNextWindowSize(ImVec2(400, 220), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Remove PIN##PinRemoveDlg", &show_pin_remove_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) {
ImGui::TextWrapped(
"Enter your current PIN to confirm removal. "
"You will need to use your full passphrase to unlock.");
@@ -1490,8 +1555,8 @@ void App::renderPinDialogs() {
}
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
}

View File

@@ -318,6 +318,12 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
for (const auto& cat : debug_categories_) {
args.push_back("-debug=" + cat);
}
// Add -rescan flag if requested (one-shot)
if (rescan_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
args.push_back("-rescan");
}
if (!startProcess(daemon_path, args)) {
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());

View File

@@ -152,6 +152,12 @@ public:
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
/**
* @brief Request a blockchain rescan on the next daemon start
*/
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
int getCrashCount() const { return crash_count_.load(); }
/** Reset crash counter (call on successful connection or manual restart) */
@@ -189,6 +195,7 @@ private:
std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_;
std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
};
} // namespace daemon

View File

@@ -113,6 +113,11 @@ struct SyncInfo {
bool syncing = false;
std::string best_blockhash;
// Rescan state (detected from daemon output)
bool rescanning = false;
float rescan_progress = 0.0f; // 0.0 - 1.0
std::string rescan_status; // e.g. "Rescanning... 25%"
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
};

View File

@@ -158,6 +158,10 @@ ConnectionConfig Connection::autoDetectConfig()
std::string conf_path = getDefaultConfPath();
if (fs::exists(conf_path)) {
// Ensure exportdir is present in existing config
ensureExportDir(conf_path);
// Ensure encryption flags are present
ensureEncryptionEnabled(conf_path);
config = parseConfFile(conf_path);
config.hush_dir = data_dir;
} else {
@@ -203,6 +207,9 @@ bool Connection::createDefaultConfig(const std::string& path)
return false;
}
// Get the data directory for exportdir
std::string dataDir = getDefaultDataDir();
file << "# DragonX configuration file\n";
file << "# Auto-generated by DragonX Wallet\n";
file << "\n";
@@ -211,6 +218,9 @@ bool Connection::createDefaultConfig(const std::string& path)
file << "rpcport=" << DRAGONX_DEFAULT_RPC_PORT << "\n";
file << "server=1\n";
file << "txindex=1\n";
file << "exportdir=" << dataDir << "\n";
file << "experimentalfeatures=1\n";
file << "developerencryptwallet=1\n";
file << "addnode=195.201.20.230\n";
file << "addnode=195.201.137.219\n";
@@ -220,5 +230,94 @@ bool Connection::createDefaultConfig(const std::string& path)
return true;
}
bool Connection::ensureExportDir(const std::string& confPath)
{
// Read existing config and check if exportdir is present
std::ifstream inFile(confPath);
if (!inFile.is_open()) {
return false;
}
std::string contents;
std::string line;
bool hasExportDir = false;
while (std::getline(inFile, line)) {
contents += line + "\n";
// Check for exportdir (case insensitive check for key)
if (line.find("exportdir=") == 0 || line.find("exportdir =") == 0) {
hasExportDir = true;
}
}
inFile.close();
if (hasExportDir) {
return true; // Already has exportdir
}
// Append exportdir to config
std::ofstream outFile(confPath, std::ios::app);
if (!outFile.is_open()) {
DEBUG_LOGF("Failed to open config for appending exportdir: %s\n", confPath.c_str());
return false;
}
std::string dataDir = getDefaultDataDir();
outFile << "\n# Export directory for wallet backups (required for z_exportwallet)\n";
outFile << "exportdir=" << dataDir << "\n";
outFile.close();
DEBUG_LOGF("Added exportdir to config: %s\n", confPath.c_str());
return true;
}
bool Connection::ensureEncryptionEnabled(const std::string& confPath)
{
// Read existing config and check if encryption flags are present
std::ifstream inFile(confPath);
if (!inFile.is_open()) {
return false;
}
std::string contents;
std::string line;
bool hasExperimental = false;
bool hasEncrypt = false;
while (std::getline(inFile, line)) {
contents += line + "\n";
if (line.find("experimentalfeatures=") == 0 || line.find("experimentalfeatures =") == 0) {
hasExperimental = true;
}
if (line.find("developerencryptwallet=") == 0 || line.find("developerencryptwallet =") == 0) {
hasEncrypt = true;
}
}
inFile.close();
if (hasExperimental && hasEncrypt) {
return true; // Already has both flags
}
// Append missing flags to config
std::ofstream outFile(confPath, std::ios::app);
if (!outFile.is_open()) {
DEBUG_LOGF("Failed to open config for appending encryption flags: %s\n", confPath.c_str());
return false;
}
outFile << "\n# Enable wallet encryption support\n";
if (!hasExperimental) {
outFile << "experimentalfeatures=1\n";
}
if (!hasEncrypt) {
outFile << "developerencryptwallet=1\n";
}
outFile.close();
DEBUG_LOGF("Added encryption flags to config: %s\n", confPath.c_str());
return true;
}
} // namespace rpc
} // namespace dragonx

View File

@@ -73,6 +73,20 @@ public:
*/
static bool createDefaultConfig(const std::string& path);
/**
* @brief Ensure exportdir is set in DRAGONX.conf
* @param confPath Path to the conf file
* @return true if exportdir exists or was added
*/
static bool ensureExportDir(const std::string& confPath);
/**
* @brief Ensure wallet encryption flags are set in DRAGONX.conf
* @param confPath Path to the conf file
* @return true if flags exist or were added
*/
static bool ensureEncryptionEnabled(const std::string& confPath);
private:
};

View File

@@ -15,6 +15,7 @@
#include "../effects/imgui_acrylic.h"
#include "../theme.h"
#include "../../util/noise_texture.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include "imgui_internal.h"
#include <algorithm>
@@ -774,6 +775,171 @@ inline void ApplySmoothScroll(float speed = 12.0f)
ImGui::SetScrollY(s.current);
}
// ============================================================================
// Dialog Title Bar with Close Button
// ============================================================================
// Draws a custom title bar for modal popups with a title and close button.
// Returns true if the close button was clicked.
inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col = 0)
{
bool closeClicked = false;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
float winWidth = ImGui::GetWindowWidth();
float barHeight = 36.0f;
// Get accent color from theme if not provided
if (!accent_col) {
accent_col = schema::UI().resolveColor("var(--primary)", IM_COL32(76, 175, 80, 255));
}
// Draw title bar background with subtle gradient
ImVec2 barMin = winPos;
ImVec2 barMax(winPos.x + winWidth, winPos.y + barHeight);
// Slightly darker top edge
ImU32 barTop = IM_COL32(0, 0, 0, 40);
ImU32 barBot = IM_COL32(0, 0, 0, 20);
dl->AddRectFilledMultiColor(barMin, barMax, barTop, barTop, barBot, barBot);
// Accent line at bottom of title bar
dl->AddLine(ImVec2(barMin.x, barMax.y), ImVec2(barMax.x, barMax.y),
ScaleAlpha(accent_col, 0.6f), 1.0f);
// Title text
ImFont* titleFont = Type().subtitle1();
ImGui::PushFont(titleFont);
ImVec2 titleSize = ImGui::CalcTextSize(title);
float titleX = barMin.x + 16.0f;
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
ImGui::PopFont();
// Close button (X) on right side
if (p_open) {
float btnSize = 24.0f;
float btnX = barMax.x - btnSize - 12.0f;
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
bool hovered = ImGui::IsMouseHoveringRect(btnMin, btnMax);
bool held = false;
if (hovered && ImGui::IsMouseClicked(0)) {
held = true;
}
// Button background on hover
if (hovered) {
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
}
// Draw X icon
ImGui::PushFont(Type().iconSmall());
ImVec2 iconSize = ImGui::CalcTextSize(ICON_MD_CLOSE);
float iconX = btnX + (btnSize - iconSize.x) * 0.5f;
float iconY = btnY + (btnSize - iconSize.y) * 0.5f;
dl->AddText(ImVec2(iconX, iconY),
hovered ? IM_COL32(255, 255, 255, 255) : OnSurfaceMedium(),
ICON_MD_CLOSE);
ImGui::PopFont();
// Handle click
if (hovered && ImGui::IsMouseReleased(0)) {
*p_open = false;
closeClicked = true;
}
}
// Reserve space for title bar so content starts below it
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
return closeClicked;
}
// ============================================================================
// Fullscreen Overlay Dialog
// ============================================================================
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
// Similar to the shutdown screen pattern but for interactive dialogs.
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f)
{
ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 vp_pos = vp->Pos;
ImVec2 vp_size = vp->Size;
// Fullscreen scrim overlay
ImGui::SetNextWindowPos(vp_pos);
ImGui::SetNextWindowSize(vp_size);
ImGui::SetNextWindowFocus();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.04f, 0.04f, 0.06f, scrimOpacity));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
bool opened = ImGui::Begin("##OverlayScrim", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoSavedSettings);
if (!opened) {
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
return false;
}
ImDrawList* dl = ImGui::GetWindowDrawList();
// Calculate card position (centered)
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
float cardY = vp_pos.y + vp_size.y * 0.15f;
// Draw glass card background
ImVec2 cardMin(cardX, cardY);
ImVec2 cardMax(cardX + cardWidth, vp_pos.y + vp_size.y * 0.85f);
// Card background with glass effect
GlassPanelSpec cardGlass;
cardGlass.rounding = 16.0f;
cardGlass.fillAlpha = 35;
cardGlass.borderAlpha = 50;
cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
// Set up child region for card content
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28, 24));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // Transparent - glass already drawn
bool childVisible = ImGui::BeginChild("##OverlayDialogContent",
ImVec2(cardWidth, 0), // 0 height = auto-size
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
ImGuiWindowFlags_NoScrollbar);
// Draw title bar with close button
if (childVisible && title) {
DrawDialogTitleBar(title, p_open);
}
return childVisible;
}
inline void EndOverlayDialog()
{
ImGui::EndChild();
ImGui::PopStyleColor(); // ChildBg
ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child)
ImGui::End();
ImGui::PopStyleVar(3); // WindowRounding, WindowBorderSize, WindowPadding (for scrim)
ImGui::PopStyleColor(); // WindowBg scrim
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -108,6 +108,7 @@ static bool sp_stop_external_daemon = false;
static std::set<std::string> sp_debug_categories;
static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted
static bool sp_debug_expanded = false; // collapsible card state
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
// (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -969,46 +970,41 @@ void RenderSettingsPage(App* app) {
ImGui::Indent(pad);
float contentW = availWidth - pad * 2;
bool wideBtns = availWidth >= S.drawElement("components.settings-page", "compact-breakpoint").size;
// Content-aware button sizing: uniform per-row width based on widest label
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
float btnSpacing = Layout::spacingMd();
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
auto rowBtnW = [&](std::initializer_list<const char*> labels) -> float {
float maxTextW = 0;
for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x);
return std::max(minBtnW, maxTextW + btnPad * 2);
};
// Calculate button width that fits available space
// 6 buttons total: on wide screens 3+3, on narrow screens 2+2+2
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
// Clamp to reasonable size
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
// Row 1 — Tools & Actions
{
float bw = rowBtnW({"Address Book...", "Validate Address...", "Request Payment...", "Shield Mining...", "Merge to Address...", "Clear Z-Tx History"});
if (TactileButton("Address Book...", ImVec2(bw, 0), S.resolveFont("button")))
AddressBookDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Manage saved addresses for quick sending");
ImGui::SameLine(0, Layout::spacingMd());
ImGui::SameLine(0, btnSpacing);
if (TactileButton("Validate Address...", ImVec2(bw, 0), S.resolveFont("button")))
ValidateAddressDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Check if a DragonX address is valid");
ImGui::SameLine(0, Layout::spacingMd());
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton("Request Payment...", ImVec2(bw, 0), S.resolveFont("button")))
RequestPaymentDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Generate a payment request with QR code");
if (wideBtns) ImGui::SameLine(0, Layout::spacingMd()); else ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
if (TactileButton("Shield Mining...", ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move transparent mining rewards to a shielded address");
ImGui::SameLine(0, Layout::spacingMd());
ImGui::SameLine(0, btnSpacing);
if (TactileButton("Merge to Address...", ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Consolidate multiple UTXOs into one address");
ImGui::SameLine(0, Layout::spacingMd());
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton("Clear Z-Tx History", ImVec2(bw, 0), S.resolveFont("button"))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file))
Notifications::instance().success("Z-transaction history cleared");
else
Notifications::instance().info("No history file found");
sp_confirm_clear_ztx = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Delete locally cached z-transaction history");
}
@@ -1348,24 +1344,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Verify the RPC connection to the daemon");
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton("Rescan Blockchain", ImVec2(nodeBtnW, 0), btnFont)) {
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
Notifications::instance().info("Starting blockchain rescan...");
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
try {
rpc->call("rescanblockchain", {0});
return []() {
Notifications::instance().success("Blockchain rescan started");
};
} catch (const std::exception& e) {
std::string err = e.what();
return [err]() {
Notifications::instance().error("Failed to start rescan: " + err);
};
}
});
} else {
Notifications::instance().warning("Not connected to daemon");
}
app->rescanBlockchain();
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Rescan the blockchain for missing transactions");
ImGui::EndDisabled();
@@ -1839,6 +1818,48 @@ void RenderSettingsPage(App* app) {
ImGui::EndChild(); // ##SettingsPageScroll
// Confirmation dialog for clearing z-tx history
if (sp_confirm_clear_ztx) {
if (BeginOverlayDialog("Confirm Clear Z-Tx History", &sp_confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Warning");
ImGui::Spacing();
ImGui::TextWrapped(
"Clearing z-transaction history may cause your shielded balance to show as 0 "
"until a wallet rescan is performed.");
ImGui::Spacing();
ImGui::TextWrapped(
"If this happens, you will need to re-import your z-address private keys with "
"rescan enabled to recover your balance.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
sp_confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button("Clear Anyway", ImVec2(btnW, 40))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
} else {
Notifications::instance().info("No history file found");
}
sp_confirm_clear_ztx = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
}
} // namespace ui

View File

@@ -5,8 +5,6 @@
#include "about_dialog.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
@@ -25,16 +23,7 @@ void RenderAboutDialog(App* app, bool* p_open)
auto versionLbl = S.label("dialogs.about", "version-label");
auto editionLbl = S.label("dialogs.about", "edition-label");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
// Use acrylic modal popup from current theme
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("About ObsidianDragon");
if (!effects::ImGuiAcrylic::BeginAcrylicPopupModal("About ObsidianDragon", p_open,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
effects::ImGuiAcrylic::EndAcrylicPopup();
if (!material::BeginOverlayDialog("About ObsidianDragon", p_open, win.width, 0.94f)) {
return;
}
@@ -173,7 +162,7 @@ void RenderAboutDialog(App* app, bool* p_open)
}
ImGui::PopFont(); // Body2
effects::ImGuiAcrylic::EndAcrylicPopup();
material::EndOverlayDialog();
}
} // namespace ui

View File

@@ -6,8 +6,6 @@
#include "../../app.h"
#include "../../data/address_book.h"
#include "../notifications.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
@@ -67,14 +65,7 @@ void AddressBookDialog::render(App* app)
auto notesInput = S.input("dialogs.address-book", "notes-input");
auto actionBtn = S.button("dialogs.address-book", "action-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Address Book");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Address Book", &s_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Address Book", &s_open, win.width, 0.94f)) {
auto& book = getAddressBook();
// Toolbar
@@ -192,15 +183,16 @@ void AddressBookDialog::render(App* app)
// Status line
ImGui::TextDisabled("%zu addresses saved", book.size());
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
// Add dialog
if (s_show_add_dialog) {
ImGui::OpenPopup("Add Address");
}
// Re-use center from above (already defined at start of render)
// 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)) {

View File

@@ -12,7 +12,6 @@
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <string>
@@ -62,14 +61,7 @@ void BackupWalletDialog::render(App* app)
auto backupBtn = S.button("dialogs.backup-wallet", "backup-button");
auto closeBtn = S.button("dialogs.backup-wallet", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Backup Wallet");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Backup Wallet", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Backup Wallet", &s_open, win.width, 0.94f)) {
ImGui::TextWrapped(
"Create a backup of your wallet.dat file. This file contains all your "
"private keys and transaction history. Store the backup in a secure location."
@@ -191,8 +183,8 @@ void BackupWalletDialog::render(App* app)
ImGui::BulletText("Store backups on external drives or cloud storage");
ImGui::BulletText("Create multiple backups in different locations");
ImGui::BulletText("Test restoring from backup periodically");
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -9,8 +9,6 @@
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
@@ -101,14 +99,7 @@ void BlockInfoDialog::render(App* app)
auto hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
auto closeBtn = S.button("dialogs.block-info", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Block Information");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Block Information", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Block Information", &s_open, win.width, 0.94f)) {
auto* rpc = app->rpc();
const auto& state = app->getWalletState();
@@ -307,8 +298,8 @@ void BlockInfoDialog::render(App* app)
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -347,12 +347,8 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
void ConsoleTab::renderCommandsPopupModal()
{
if (!show_commands_popup_) {
renderCommandsPopup();
return;
}
// Called at top-level window scope so the modal blocks all input.
ImGui::OpenPopup("RPC Command Reference");
show_commands_popup_ = false;
renderCommandsPopup();
}
@@ -1287,18 +1283,8 @@ void ConsoleTab::renderCommandsPopup()
{
using namespace material;
// Center the modal
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
float popH = std::min(schema::UI().drawElement("tabs.console", "popup-max-height").size, ImGui::GetMainViewport()->Size.y * schema::UI().drawElement("tabs.console", "popup-height-ratio").size);
ImGui::SetNextWindowSize(ImVec2(popW, popH), ImGuiCond_Appearing);
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingXl(), Layout::spacingLg()));
if (!effects::ImGuiAcrylic::BeginAcrylicPopupModal("RPC Command Reference", nullptr,
ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ImGui::PopStyleVar();
if (!material::BeginOverlayDialog("RPC Command Reference", &show_commands_popup_, popW, 0.94f)) {
return;
}
@@ -1591,11 +1577,10 @@ void ConsoleTab::renderCommandsPopup()
// Close button
if (ImGui::Button("Close", ImVec2(-1, 0))) {
cmdFilter[0] = '\0';
ImGui::CloseCurrentPopup();
show_commands_popup_ = false;
}
effects::ImGuiAcrylic::EndAcrylicPopup();
ImGui::PopStyleVar();
material::EndOverlayDialog();
}
void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc::RPCWorker* worker)

View File

@@ -13,7 +13,6 @@
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
@@ -70,14 +69,7 @@ void ExportAllKeysDialog::render(App* app)
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Export All Private Keys");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Export All Private Keys", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Export All Private Keys", &s_open, win.width, 0.94f)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
@@ -250,8 +242,8 @@ void ExportAllKeysDialog::render(App* app)
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -10,7 +10,6 @@
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <string>
@@ -73,14 +72,7 @@ void ExportTransactionsDialog::render(App* app)
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Export Transactions to CSV");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Export Transactions to CSV", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Export Transactions to CSV", &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
ImGui::Text("Export %zu transactions to CSV file.", state.transactions.size());
@@ -165,8 +157,8 @@ void ExportTransactionsDialog::render(App* app)
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -11,8 +11,6 @@
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
@@ -115,14 +113,7 @@ void ImportKeyDialog::render(App* app)
auto importBtn = S.button("dialogs.import-key", "import-button");
auto closeBtn = S.button("dialogs.import-key", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Import Private Key");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Import Private Key", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Import Private Key", &s_open, win.width, 0.94f)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
@@ -283,8 +274,8 @@ void ImportKeyDialog::render(App* app)
ImGui::TextDisabled("Supported key formats:");
ImGui::BulletText("Z-address spending keys (secret-extended-key-...)");
ImGui::BulletText("T-address WIF private keys");
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -7,8 +7,6 @@
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
@@ -58,15 +56,7 @@ void KeyExportDialog::render(App* app)
auto copyBtn = S.button("dialogs.key-export", "copy-button");
auto closeBtn = S.button("dialogs.key-export", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImGui::OpenPopup(title);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal(title, &s_open,
ImGuiWindowFlags_NoResize, acrylicTheme.popup)) {
if (material::BeginOverlayDialog(title, &s_open, win.width, 0.94f)) {
ImGui::Spacing();
// Warning section with colored background
@@ -239,7 +229,7 @@ void KeyExportDialog::render(App* app)
s_show_key = false;
}
effects::ImGuiAcrylic::EndAcrylicPopup();
material::EndOverlayDialog();
}
}

View File

@@ -8,7 +8,6 @@
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
namespace dragonx {
@@ -64,14 +63,7 @@ void QRPopupDialog::render(App* app)
auto addrInput = S.input("dialogs.qr-popup", "address-input");
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("QR Code");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("QR Code", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("QR Code", &s_open, win.width, 0.94f)) {
// Label if present
if (!s_label.empty()) {
@@ -137,8 +129,8 @@ void QRPopupDialog::render(App* app)
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
close();
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
// Handle window close button
if (!s_open && s_qr_texture != 0) {

View File

@@ -9,8 +9,6 @@
#include "../schema/ui_schema.h"
#include "../widgets/qr_code.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <string>
@@ -130,14 +128,7 @@ void RequestPaymentDialog::render(App* app)
auto qr = S.drawElement("dialogs.request-payment", "qr-code");
auto actionBtn = S.button("dialogs.request-payment", "action-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Request Payment");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Request Payment", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Request Payment", &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
ImGui::TextWrapped(
@@ -284,8 +275,8 @@ void RequestPaymentDialog::render(App* app)
if (material::StyledButton("Close", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
// Cleanup on close
if (!s_open && s_qr_texture != 0) {

View File

@@ -668,20 +668,13 @@ void RenderSendConfirmPopup(App* app) {
double total = s_amount + s_fee;
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
float popupAvailW = ImGui::GetMainViewport()->Size.x * S.drawElement("tabs.send", "confirm-popup-width-ratio").size;
float popupW = std::min(schema::UI().drawElement("tabs.send", "confirm-popup-max-width").size, popupAvailW);
float popVs = Layout::vScale();
ImGui::SetNextWindowSize(ImVec2(popupW, 0), ImGuiCond_Always);
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingXl(), Layout::spacingLg()));
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal(TR("confirm_send"), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize, acrylicTheme.popup)) {
if (material::BeginOverlayDialog(TR("confirm_send"), nullptr, popupW, 0.94f)) {
if (ImGui::IsKeyPressed(ImGuiKey_Escape) && !s_sending) {
s_show_confirm = false;
ImGui::CloseCurrentPopup();
}
float popW = ImGui::GetContentRegionAvail().x;
@@ -825,21 +818,15 @@ void RenderSendConfirmPopup(App* app) {
}
);
s_show_confirm = false;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton("Cancel", ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
s_show_confirm = false;
ImGui::CloseCurrentPopup();
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
} else {
// BeginPopupModal returned false — popup was closed externally
s_show_confirm = false;
material::EndOverlayDialog();
}
ImGui::PopStyleVar();
}
// ============================================================================

View File

@@ -140,15 +140,8 @@ void RenderSettingsWindow(App* app, bool* p_open)
auto walletBtn = S.button("dialogs.settings", "wallet-button");
auto saveBtn = S.button("dialogs.settings", "save-button");
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
// Use acrylic modal popup
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Settings");
if (!effects::ImGuiAcrylic::BeginAcrylicPopupModal("Settings", p_open, ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
effects::ImGuiAcrylic::EndAcrylicPopup();
if (!material::BeginOverlayDialog("Settings", p_open, win.width, 0.94f)) {
return;
}
@@ -476,16 +469,53 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Spacing();
static bool s_confirm_clear_ztx = false;
if (material::StyledButton("Clear Saved Z-Transaction History", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
// Clear z-transaction history file
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
} else {
Notifications::instance().info("No history file found");
}
s_confirm_clear_ztx = true;
}
ImGui::TextDisabled(" Delete locally stored shielded transaction data");
// Confirmation dialog
if (s_confirm_clear_ztx) {
if (material::BeginOverlayDialog("Confirm Clear Z-Tx History", &s_confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(material::Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Warning");
ImGui::Spacing();
ImGui::TextWrapped(
"Clearing z-transaction history may cause your shielded balance to show as 0 "
"until a wallet rescan is performed.");
ImGui::Spacing();
ImGui::TextWrapped(
"If this happens, you will need to re-import your z-address private keys with "
"rescan enabled to recover your balance.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
s_confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button("Clear Anyway", ImVec2(btnW, 40))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
} else {
Notifications::instance().info("No history file found");
}
s_confirm_clear_ztx = false;
}
ImGui::PopStyleColor(2);
material::EndOverlayDialog();
}
}
ImGui::Spacing();
ImGui::Separator();
@@ -560,7 +590,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
*p_open = false;
}
effects::ImGuiAcrylic::EndAcrylicPopup();
material::EndOverlayDialog();
}
} // namespace ui

View File

@@ -11,8 +11,6 @@
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <vector>
@@ -81,14 +79,7 @@ void ShieldDialog::render(App* app)
? "Shield Coinbase Rewards"
: "Merge to Address";
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup(title);
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal(title, &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog(title, &s_open, win.width, 0.94f)) {
const auto& state = app->getWalletState();
// Description
@@ -304,8 +295,8 @@ void ShieldDialog::render(App* app)
}
}
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -7,7 +7,6 @@
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
@@ -45,14 +44,7 @@ void TransactionDetailsDialog::render(App* app)
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
auto bottomBtn = S.button("dialogs.transaction-details", "bottom-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Transaction Details");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Transaction Details", &s_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Transaction Details", &s_open, win.width, 0.94f)) {
const auto& tx = s_transaction;
// Type indicator with color
@@ -200,8 +192,8 @@ void TransactionDetailsDialog::render(App* app)
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui

View File

@@ -9,7 +9,6 @@
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
namespace dragonx {
@@ -53,14 +52,7 @@ void ValidateAddressDialog::render(App* app)
auto lbl = S.label("dialogs.validate-address", "label");
auto closeBtn = S.button("dialogs.validate-address", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Validate Address");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Validate Address", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
if (material::BeginOverlayDialog("Validate Address", &s_open, win.width, 0.94f)) {
ImGui::TextWrapped("Enter a DragonX address to check if it's valid and whether it belongs to this wallet.");
ImGui::Spacing();
@@ -216,8 +208,8 @@ void ValidateAddressDialog::render(App* app)
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
material::EndOverlayDialog();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui