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:
665
src/app.cpp
665
src/app.cpp
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user