v1.1.0: explorer tab, bootstrap fixes, full theme overlay merge

Explorer tab:
- New block explorer tab with search, chain stats, mempool info,
  recent blocks table, block detail modal with tx expansion
- Sidebar nav entry, i18n strings, ui.toml layout values

Bootstrap fixes:
- Move wizard Done handler into render() — was dead code, preventing
  startEmbeddedDaemon() and tryConnect() from firing post-wizard
- Stop deleting BDB database/ dir during cleanup — caused LSN mismatch
  that salvaged wallet.dat into wallet.{timestamp}.bak
- Add banlist.dat, db.log, .lock to cleanup file list
- Fatal extraction failure for blocks/ and chainstate/ files
- Verification progress: split SHA-256 (0-50%) and MD5 (50-100%)

Theme system:
- Expand overlay merge to apply ALL sections (tabs, dialogs, components,
  screens, flat sections), not just theme+backdrop+effects
- Add screens and security section parsing to UISchema
- Build-time theme expansion via expand_themes.py (CMake + build.sh)

Other:
- Version bump to 1.1.0
- WalletState::clear() resets all fields (sync, daemon info, etc.)
- Sidebar item-height 42 → 36
This commit is contained in:
dan_s
2026-03-17 18:49:46 -05:00
parent 4a841fd032
commit 9e94952e0a
16 changed files with 1388 additions and 60 deletions

View File

@@ -17,6 +17,7 @@
#include "ui/windows/transactions_tab.h"
#include "ui/windows/mining_tab.h"
#include "ui/windows/peers_tab.h"
#include "ui/windows/explorer_tab.h"
#include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h"
@@ -705,6 +706,19 @@ void App::render()
return;
}
// Handle wizard completion — start daemon and connect
if (wizard_phase_ == WizardPhase::Done) {
wizard_phase_ = WizardPhase::None;
if (!state_.connected) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
startEmbeddedDaemon();
}
tryConnect();
}
settings_->setWizardCompleted(true);
settings_->save();
}
// Process deferred encryption from wizard (runs in background)
processDeferredEncryption();
@@ -1069,6 +1083,9 @@ void App::render()
case ui::NavPage::Peers:
ui::RenderPeersTab(this);
break;
case ui::NavPage::Explorer:
ui::RenderExplorerTab(this);
break;
case ui::NavPage::Market:
ui::RenderMarketTab(this);
break;
@@ -1892,10 +1909,11 @@ void App::setCurrentTab(int tab) {
ui::NavPage::Receive, // 2 = Receive
ui::NavPage::History, // 3 = Transactions
ui::NavPage::Mining, // 4 = Mining
ui::NavPage::Peers, // 5 = Peers
ui::NavPage::Market, // 6 = Market
ui::NavPage::Console, // 7 = Console
ui::NavPage::Settings, // 8 = Settings
ui::NavPage::Peers, // 5 = Peers
ui::NavPage::Market, // 6 = Market
ui::NavPage::Console, // 7 = Console
ui::NavPage::Explorer, // 8 = Explorer
ui::NavPage::Settings, // 9 = Settings
};
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
current_page_ = kTabMap[tab];

View File

@@ -94,21 +94,6 @@ void App::renderFirstRunWizard() {
ImU32 bgCol = ui::material::Surface();
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
// Handle Done/None — wizard complete
if (wizard_phase_ == WizardPhase::Done || wizard_phase_ == WizardPhase::None) {
wizard_phase_ = WizardPhase::None;
if (!state_.connected) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
startEmbeddedDaemon();
}
tryConnect();
}
settings_->setWizardCompleted(true);
settings_->save();
ImGui::End();
return;
}
// --- Determine which of the 3 masonry sections is focused ---
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
int focusIdx = 0;

View File

@@ -7,10 +7,10 @@
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "1.0.2"
#define DRAGONX_VERSION "1.1.0"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 0
#define DRAGONX_VERSION_PATCH 2
#define DRAGONX_VERSION_MINOR 1
#define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"

View File

@@ -249,7 +249,15 @@ struct WalletState {
void clear() {
connected = false;
daemon_version = 0;
daemon_subversion.clear();
protocol_version = 0;
p2p_port = 0;
longestchain = 0;
notarized = 0;
sync = SyncInfo{};
privateBalance = transparentBalance = totalBalance = 0.0;
unconfirmedBalance = 0.0;
encrypted = false;
locked = false;
unlocked_until = 0;

View File

@@ -75,12 +75,18 @@ bool UISchema::loadFromFile(const std::string& path) {
parseSections(static_cast<const void*>(components), "components");
}
// Parse screens as a 3-level section (screens.loading, screens.first-run, etc.)
if (auto* screens = root["screens"].as_table()) {
parseSections(static_cast<const void*>(screens), "screens");
}
// Parse flat sections (2-level: sectionName.elementName → {style object})
for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts",
"inline-dialogs", "sidebar", "panels", "typography", "effects"}) {
"inline-dialogs", "sidebar", "panels", "typography",
"effects", "security"}) {
if (auto* sec = root[flatSection].as_table()) {
parseFlatSection(static_cast<const void*>(sec), flatSection);
}
@@ -157,12 +163,18 @@ bool UISchema::loadFromString(const std::string& tomlStr, const std::string& lab
parseSections(static_cast<const void*>(components), "components");
}
// Parse screens as a 3-level section (screens.loading, screens.first-run, etc.)
if (auto* screens = root["screens"].as_table()) {
parseSections(static_cast<const void*>(screens), "screens");
}
// Parse flat sections (2-level: sectionName.elementName → {style object})
for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts",
"inline-dialogs", "sidebar", "panels", "typography", "effects"}) {
"inline-dialogs", "sidebar", "panels", "typography",
"effects", "security"}) {
if (auto* sec = root[flatSection].as_table()) {
parseFlatSection(static_cast<const void*>(sec), flatSection);
}
@@ -183,7 +195,7 @@ bool UISchema::loadFromString(const std::string& tomlStr, const std::string& lab
}
// ============================================================================
// Overlay merge (visual-only: theme + backdrop)
// Overlay merge — all sections (theme + layout + effects)
// ============================================================================
bool UISchema::mergeOverlayFromFile(const std::string& path) {
@@ -211,14 +223,40 @@ bool UISchema::mergeOverlayFromFile(const std::string& path) {
parseTheme(static_cast<const void*>(theme));
}
// Merge backdrop section (gradient colors, alpha/transparency values)
if (auto* sec = root["backdrop"].as_table()) {
parseFlatSection(static_cast<const void*>(sec), "backdrop");
// Merge breakpoints + globals
if (auto* bp = root["breakpoints"].as_table()) {
parseBreakpoints(static_cast<const void*>(bp));
}
if (auto* globals = root["globals"].as_table()) {
parseGlobals(static_cast<const void*>(globals));
}
// Merge effects section (theme visual effects configuration)
if (auto* sec = root["effects"].as_table()) {
parseFlatSection(static_cast<const void*>(sec), "effects");
// Merge tabs, dialogs, components (3-level sections)
if (auto* tabs = root["tabs"].as_table()) {
parseSections(static_cast<const void*>(tabs), "tabs");
}
if (auto* dialogs = root["dialogs"].as_table()) {
parseSections(static_cast<const void*>(dialogs), "dialogs");
}
if (auto* components = root["components"].as_table()) {
parseSections(static_cast<const void*>(components), "components");
}
// Merge screens (3-level section)
if (auto* screens = root["screens"].as_table()) {
parseSections(static_cast<const void*>(screens), "screens");
}
// Merge all flat sections (2-level)
for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts",
"inline-dialogs", "sidebar", "panels", "typography",
"effects", "security"}) {
if (auto* sec = root[flatSection].as_table()) {
parseFlatSection(static_cast<const void*>(sec), flatSection);
}
}
overlayPath_ = path;

View File

@@ -30,6 +30,7 @@ enum class NavPage {
// --- separator ---
Console,
Peers,
Explorer,
Settings,
Count_
};
@@ -51,6 +52,7 @@ inline const NavItem kNavItems[] = {
{ "Market", NavPage::Market, nullptr, "market", nullptr },
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
};
static_assert(sizeof(kNavItems) / sizeof(kNavItems[0]) == (int)NavPage::Count_,
@@ -76,6 +78,7 @@ inline const char* GetNavIconMD(NavPage page)
case NavPage::Market: return ICON_MD_TRENDING_UP;
case NavPage::Console: return ICON_MD_TERMINAL;
case NavPage::Peers: return ICON_MD_HUB;
case NavPage::Explorer: return ICON_MD_EXPLORE;
case NavPage::Settings: return ICON_MD_SETTINGS;
default: return ICON_MD_HOME;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
void RenderExplorerTab(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -405,7 +405,13 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir)
if (!mz_zip_reader_extract_to_file(&zip, i, destPath.c_str(), 0)) {
DEBUG_LOGF("[Bootstrap] Failed to extract: %s\n", filename.c_str());
// Non-fatal: continue with remaining files
// Critical chain data must extract successfully
if (filename.rfind("blocks/", 0) == 0 || filename.rfind("chainstate/", 0) == 0) {
setProgress(State::Failed, "Failed to extract critical file: " + filename);
mz_zip_reader_end(&zip);
return false;
}
// Non-fatal for other files: continue with remaining
}
}
@@ -431,7 +437,13 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir)
// ---------------------------------------------------------------------------
void Bootstrap::cleanChainData(const std::string& dataDir) {
// Directories to remove completely
// Directories to remove completely.
// NOTE: "database" is intentionally NOT removed here. It contains BDB
// environment/log files for wallet.dat. Deleting it while keeping wallet.dat
// creates a BDB LSN mismatch that causes the daemon to "salvage" the wallet
// (renaming it to wallet.{timestamp}.bak and creating an empty replacement).
// The daemon's DB_RECOVER flag in CDBEnv::Open() handles stale environment
// files gracefully when the environment dir still exists.
for (const char* subdir : {"blocks", "chainstate", "notarizations"}) {
fs::path p = fs::path(dataDir) / subdir;
std::error_code ec;
@@ -441,14 +453,14 @@ void Bootstrap::cleanChainData(const std::string& dataDir) {
}
}
// Individual files to remove (will be replaced by bootstrap)
for (const char* file : {"fee_estimates.dat", "peers.dat"}) {
for (const char* file : {"fee_estimates.dat", "peers.dat", "banlist.dat", "db.log", ".lock"}) {
fs::path p = fs::path(dataDir) / file;
std::error_code ec;
if (fs::exists(p, ec)) {
fs::remove(p, ec);
}
}
// NEVER remove: wallet.dat, debug.log, .lock, *.conf
// NEVER remove: wallet.dat, debug.log, *.conf
}
// ---------------------------------------------------------------------------
@@ -505,7 +517,7 @@ std::string Bootstrap::parseChecksumFile(const std::string& content) {
return {};
}
std::string Bootstrap::computeSHA256(const std::string& filePath) {
std::string Bootstrap::computeSHA256(const std::string& filePath, float pctBase, float pctRange) {
FILE* fp = fopen(filePath.c_str(), "rb");
if (!fp) return {};
@@ -530,12 +542,18 @@ std::string Bootstrap::computeSHA256(const std::string& filePath) {
// Update progress every ~4MB
if (fileSize > 0 && (processed % (4 * 1024 * 1024)) < (long long)sizeof(buf)) {
float pct = (float)(100.0 * processed / fileSize);
float filePct = (float)(100.0 * processed / fileSize);
float overallPct = pctBase + pctRange * (float)processed / (float)fileSize;
char msg[128];
snprintf(msg, sizeof(msg), "Verifying SHA-256... %.0f%% (%s / %s)",
pct, formatSize((double)processed).c_str(),
filePct, formatSize((double)processed).c_str(),
formatSize((double)fileSize).c_str());
setProgress(State::Verifying, msg, (double)processed, (double)fileSize);
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = State::Verifying;
progress_.status_text = msg;
progress_.percent = overallPct;
}
}
}
fclose(fp);
@@ -636,7 +654,7 @@ static void md5_final(MD5Context* ctx, uint8_t digest[16]) {
} // anon namespace
std::string Bootstrap::computeMD5(const std::string& filePath) {
std::string Bootstrap::computeMD5(const std::string& filePath, float pctBase, float pctRange) {
FILE* fp = fopen(filePath.c_str(), "rb");
if (!fp) return {};
@@ -661,12 +679,18 @@ std::string Bootstrap::computeMD5(const std::string& filePath) {
// Update progress every ~4MB
if (fileSize > 0 && (processed % (4 * 1024 * 1024)) < (long long)sizeof(buf)) {
float pct = (float)(100.0 * processed / fileSize);
float filePct = (float)(100.0 * processed / fileSize);
float overallPct = pctBase + pctRange * (float)processed / (float)fileSize;
char msg[128];
snprintf(msg, sizeof(msg), "Verifying MD5... %.0f%% (%s / %s)",
pct, formatSize((double)processed).c_str(),
filePct, formatSize((double)processed).c_str(),
formatSize((double)fileSize).c_str());
setProgress(State::Verifying, msg, (double)processed, (double)fileSize);
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = State::Verifying;
progress_.status_text = msg;
progress_.percent = overallPct;
}
}
}
fclose(fp);
@@ -705,11 +729,23 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
return true;
}
// Determine progress ranges: if both checksums exist, split 0-50% / 50-100%
float sha256Base = 0.0f, sha256Range = 0.0f;
float md5Base = 0.0f, md5Range = 0.0f;
if (haveSHA256 && haveMD5) {
sha256Base = 0.0f; sha256Range = 50.0f;
md5Base = 50.0f; md5Range = 50.0f;
} else if (haveSHA256) {
sha256Base = 0.0f; sha256Range = 100.0f;
} else {
md5Base = 0.0f; md5Range = 100.0f;
}
// --- SHA-256 ---
if (haveSHA256) {
setProgress(State::Verifying, "Verifying SHA-256...");
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1); // percent = 0
std::string expected = parseChecksumFile(sha256Content);
std::string actual = computeSHA256(zipPath);
std::string actual = computeSHA256(zipPath, sha256Base, sha256Range);
if (cancel_requested_) return false;
@@ -735,9 +771,9 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
// --- MD5 ---
if (haveMD5) {
setProgress(State::Verifying, "Verifying MD5...");
setProgress(State::Verifying, "Verifying MD5...", (double)md5Base, 100.0);
std::string expected = parseChecksumFile(md5Content);
std::string actual = computeMD5(zipPath);
std::string actual = computeMD5(zipPath, md5Base, md5Range);
if (cancel_requested_) return false;
@@ -761,7 +797,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str());
}
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93");
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93", 100.0, 100.0);
return true;
}

View File

@@ -88,12 +88,12 @@ private:
std::string downloadSmallFile(const std::string& url);
/// Compute SHA-256 of a file, return lowercase hex digest.
/// Updates progress with "Verifying SHA-256..." status during computation.
std::string computeSHA256(const std::string& filePath);
/// pctBase/pctRange map file progress onto a portion of the overall percent.
std::string computeSHA256(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f);
/// Compute MD5 of a file, return lowercase hex digest.
/// Updates progress with "Verifying MD5..." status during computation.
std::string computeMD5(const std::string& filePath);
/// pctBase/pctRange map file progress onto a portion of the overall percent.
std::string computeMD5(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f);
/// Parse the first hex token from a checksum file (handles "<hash> <filename>" format).
static std::string parseChecksumFile(const std::string& content);

View File

@@ -1068,6 +1068,25 @@ void I18n::loadBuiltinEnglish()
strings_["import_key_progress"] = "Importing %d/%d...";
strings_["click_to_copy"] = "Click to copy";
strings_["block_hash_copied"] = "Block hash copied";
// Explorer tab
strings_["explorer"] = "Explorer";
strings_["explorer_search"] = "Search";
strings_["explorer_chain_stats"] = "Chain";
strings_["explorer_mempool"] = "Mempool";
strings_["explorer_mempool_txs"] = "Transactions";
strings_["explorer_mempool_size"] = "Size";
strings_["explorer_recent_blocks"] = "Recent Blocks";
strings_["explorer_block_detail"] = "Block";
strings_["explorer_block_height"] = "Height";
strings_["explorer_block_hash"] = "Hash";
strings_["explorer_block_txs"] = "Transactions";
strings_["explorer_block_size"] = "Size";
strings_["explorer_block_time"] = "Time";
strings_["explorer_block_merkle"] = "Merkle Root";
strings_["explorer_tx_outputs"] = "Outputs";
strings_["explorer_tx_size"] = "Size";
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
}
const char* I18n::translate(const char* key) const