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:
@@ -4,7 +4,7 @@
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ObsidianDragon
|
||||
VERSION 1.0.2
|
||||
VERSION 1.1.0
|
||||
LANGUAGES C CXX
|
||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||
)
|
||||
@@ -243,6 +243,7 @@ set(APP_SOURCES
|
||||
src/ui/windows/transactions_tab.cpp
|
||||
src/ui/windows/mining_tab.cpp
|
||||
src/ui/windows/peers_tab.cpp
|
||||
src/ui/windows/explorer_tab.cpp
|
||||
src/ui/windows/market_tab.cpp
|
||||
src/ui/windows/console_tab.cpp
|
||||
src/ui/windows/settings_window.cpp
|
||||
@@ -320,6 +321,7 @@ set(APP_HEADERS
|
||||
src/ui/windows/transactions_tab.h
|
||||
src/ui/windows/mining_tab.h
|
||||
src/ui/windows/peers_tab.h
|
||||
src/ui/windows/explorer_tab.h
|
||||
src/ui/windows/market_tab.h
|
||||
src/ui/windows/settings_window.h
|
||||
src/ui/windows/about_dialog.h
|
||||
@@ -540,10 +542,29 @@ embed_resource(
|
||||
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
|
||||
# following the same pattern as daemon embedding.
|
||||
|
||||
# Copy theme files at BUILD time (not just cmake configure time)
|
||||
# so edits to res/themes/*.toml are picked up by 'make' without re-running cmake.
|
||||
# Expand and copy theme files at BUILD time — skin files get layout sections
|
||||
# from ui.toml appended automatically so users can see/edit all properties.
|
||||
# Source skin files stay minimal; the merged output goes to build/bin/res/themes/.
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(NOT Python3_FOUND)
|
||||
find_program(Python3_EXECUTABLE NAMES python3 python)
|
||||
endif()
|
||||
file(GLOB THEME_FILES ${CMAKE_SOURCE_DIR}/res/themes/*.toml)
|
||||
if(THEME_FILES)
|
||||
if(THEME_FILES AND Python3_EXECUTABLE)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
|
||||
${CMAKE_SOURCE_DIR}/res/themes
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
|
||||
DEPENDS ${THEME_FILES} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
|
||||
COMMENT "Expanding theme files (merging layout from ui.toml)"
|
||||
)
|
||||
add_custom_target(copy_themes ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded)
|
||||
add_dependencies(ObsidianDragon copy_themes)
|
||||
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
|
||||
elseif(THEME_FILES)
|
||||
# Fallback: plain copy if Python is not available
|
||||
foreach(THEME_FILE ${THEME_FILES})
|
||||
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
|
||||
add_custom_command(
|
||||
@@ -558,7 +579,7 @@ if(THEME_FILES)
|
||||
endforeach()
|
||||
add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS})
|
||||
add_dependencies(ObsidianDragon copy_themes)
|
||||
message(STATUS " Theme files: ${THEME_FILES}")
|
||||
message(STATUS " Theme files: ${THEME_FILES} (plain copy, Python not found)")
|
||||
endif()
|
||||
|
||||
# Copy image files (including backgrounds/ subdirectories and logos/)
|
||||
|
||||
6
build.sh
6
build.sh
@@ -553,9 +553,13 @@ HDR
|
||||
echo "};" >> "$GEN/embedded_data.h"
|
||||
|
||||
# ── Overlay themes ───────────────────────────────────────────────
|
||||
# Expand skin files with layout sections from ui.toml before embedding
|
||||
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h"
|
||||
local THEME_STAGE_DIR="$bd/_expanded_themes"
|
||||
mkdir -p "$THEME_STAGE_DIR"
|
||||
python3 "$SCRIPT_DIR/scripts/expand_themes.py" "$SCRIPT_DIR/res/themes" "$THEME_STAGE_DIR"
|
||||
local THEME_TABLE="" THEME_COUNT=0
|
||||
for tf in "$SCRIPT_DIR/res/themes"/*.toml; do
|
||||
for tf in "$THEME_STAGE_DIR"/*.toml; do
|
||||
local tbn=$(basename "$tf")
|
||||
[[ "$tbn" == "ui.toml" ]] && continue
|
||||
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="DragonX.ObsidianDragon.Wallet"
|
||||
version="1.0.2.0"
|
||||
version="1.1.0.0"
|
||||
processorArchitecture="amd64"
|
||||
/>
|
||||
|
||||
|
||||
@@ -894,6 +894,18 @@ pair-bar-arrow-size = { size = 28.0 }
|
||||
exchange-combo-width = { size = 180.0 }
|
||||
exchange-top-gap = { size = 0.0 }
|
||||
|
||||
[tabs.explorer]
|
||||
search-input-width = { size = 400.0 }
|
||||
search-button-width = { size = 100.0 }
|
||||
search-bar-height = { size = 32.0 }
|
||||
row-height = { size = 32.0 }
|
||||
row-rounding = { size = 4.0 }
|
||||
tx-row-height = { size = 28.0 }
|
||||
label-column = { size = 160.0 }
|
||||
detail-modal-width = { size = 700.0 }
|
||||
detail-max-height = { size = 600.0 }
|
||||
scroll-fade-zone = { size = 24.0 }
|
||||
|
||||
[tabs.console]
|
||||
input-area-padding = 8.0
|
||||
output-line-spacing = 2.0
|
||||
@@ -1218,7 +1230,7 @@ collapse-anim-speed = { size = 10.0 }
|
||||
auto-collapse-threshold = { size = 800.0 }
|
||||
section-gap = { size = 4.0 }
|
||||
section-label-pad-left = { size = 16.0 }
|
||||
item-height = { size = 42.0 }
|
||||
item-height = { size = 36.0 }
|
||||
item-pad-x = { size = 8.0 }
|
||||
min-height = { size = 360.0 }
|
||||
margin-top = { size = -12 }
|
||||
|
||||
122
scripts/expand_themes.py
Normal file
122
scripts/expand_themes.py
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build-time theme expander — merges layout sections from ui.toml into skin files.
|
||||
|
||||
Called by CMake during build. Reads source skin files + ui.toml, writes merged
|
||||
output files to the build directory. Source files are never modified.
|
||||
|
||||
Usage:
|
||||
python3 expand_themes.py <source_themes_dir> <output_themes_dir>
|
||||
|
||||
For each .toml file in source_themes_dir (except ui.toml), the script:
|
||||
1. Copies the skin file contents (theme/palette/backdrop/effects)
|
||||
2. Appends all layout sections from ui.toml (fonts, tabs, components, etc.)
|
||||
3. Writes the merged result to output_themes_dir/<filename>
|
||||
|
||||
ui.toml itself is copied unchanged.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Sections to SKIP when extracting from ui.toml (theme-specific, already in skins)
|
||||
SKIP_SECTIONS = {"theme", "theme.palette", "backdrop", "effects"}
|
||||
|
||||
|
||||
def extract_layout_sections(ui_toml_path):
|
||||
"""Extract non-theme sections from ui.toml as a string."""
|
||||
sections = []
|
||||
current_section = None
|
||||
current_lines = []
|
||||
section_re = re.compile(r'^\[{1,2}([^\]]+)\]{1,2}\s*$')
|
||||
|
||||
with open(ui_toml_path, 'r') as f:
|
||||
for line in f:
|
||||
m = section_re.match(line.strip())
|
||||
if m:
|
||||
if current_section is not None or current_lines:
|
||||
sections.append((current_section, current_lines))
|
||||
current_section = m.group(1).strip()
|
||||
current_lines = [line]
|
||||
else:
|
||||
current_lines.append(line)
|
||||
if current_section is not None or current_lines:
|
||||
sections.append((current_section, current_lines))
|
||||
|
||||
layout_parts = []
|
||||
for section_name, lines in sections:
|
||||
if section_name is None:
|
||||
# Preamble: only include top-level key=value lines
|
||||
kv_lines = [l for l in lines
|
||||
if l.strip() and not l.strip().startswith('#') and '=' in l]
|
||||
if kv_lines:
|
||||
layout_parts.append(''.join(kv_lines))
|
||||
continue
|
||||
if section_name in SKIP_SECTIONS:
|
||||
continue
|
||||
layout_parts.append(''.join(lines))
|
||||
|
||||
return '\n'.join(layout_parts)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <source_themes_dir> <output_themes_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
src_dir = sys.argv[1]
|
||||
out_dir = sys.argv[2]
|
||||
ui_toml = os.path.join(src_dir, "ui.toml")
|
||||
|
||||
if not os.path.exists(ui_toml):
|
||||
print(f"ERROR: ui.toml not found at {ui_toml}")
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
layout_content = extract_layout_sections(ui_toml)
|
||||
|
||||
separator = (
|
||||
"\n# ===========================================================================\n"
|
||||
"# Layout & Component Properties\n"
|
||||
"# All values below can be customized per-theme. Edit and save to see\n"
|
||||
"# changes reflected in the app in real time via hot-reload.\n"
|
||||
"# ===========================================================================\n\n"
|
||||
)
|
||||
|
||||
for fname in sorted(os.listdir(src_dir)):
|
||||
if not fname.endswith('.toml'):
|
||||
continue
|
||||
src_path = os.path.join(src_dir, fname)
|
||||
dst_path = os.path.join(out_dir, fname)
|
||||
|
||||
if fname == "ui.toml":
|
||||
# Copy ui.toml unchanged
|
||||
with open(src_path, 'r') as f:
|
||||
content = f.read()
|
||||
with open(dst_path, 'w') as f:
|
||||
f.write(content)
|
||||
print(f" COPY {fname}")
|
||||
continue
|
||||
|
||||
# Skin file — append layout sections
|
||||
with open(src_path, 'r') as f:
|
||||
skin = f.read()
|
||||
if not skin.endswith('\n'):
|
||||
skin += '\n'
|
||||
|
||||
merged = skin + separator + layout_content
|
||||
if not merged.endswith('\n'):
|
||||
merged += '\n'
|
||||
|
||||
with open(dst_path, 'w') as f:
|
||||
f.write(merged)
|
||||
lines = merged.count('\n')
|
||||
print(f" MERGE {fname} → {lines} lines")
|
||||
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
src/app.cpp
26
src/app.cpp
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1047
src/ui/windows/explorer_tab.cpp
Normal file
1047
src/ui/windows/explorer_tab.cpp
Normal file
File diff suppressed because it is too large
Load Diff
15
src/ui/windows/explorer_tab.h
Normal file
15
src/ui/windows/explorer_tab.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user