5 Commits

Author SHA1 Message Date
dan_s
8645a82e4f feat: sync thread grid during idle scaling, skip lock screen while pool mining, add paste preview to import key dialog
- Mining tab: sync s_selected_threads with actual thread count when idle
  thread scaling adjusts threads (solo via genproclimit, pool via
  threads_active), skipping sync during user drag
- Auto-lock: bypass lock screen overlay when xmrig pool mining is active
  so the mining UI remains accessible
- Import key dialog: add clipboard hover preview with transparent overlay
  on the input field, inline key type validation next to title (matching
  send tab paste button pattern), configurable via ui.toml
2026-03-19 06:10:46 -05:00
dan_s
9e94952e0a 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
2026-03-17 18:49:46 -05:00
dan_s
4a841fd032 daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup
- Add startup binary version checking for dragonxd/xmrig
- Display daemon version in UI
- Add idle mining thread count adjustment
- Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard
- Add paste button to import private key dialog with address validation
- Add z-address generation UI feedback (loading indicator)
- Add option to delete blockchain data while preserving wallet.dat
- Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus)
- Fix Windows RPC auth: trim \r from config values, add .cookie fallback
- Fix connection status message during block index loading
- Improve application shutdown to prevent lingering background process
2026-03-17 14:57:12 -05:00
dan_s
f0c87e4092 update version to v1.0.2 2026-03-12 02:29:08 -05:00
dan_s
c5ef4899bb fix: remove D3D11 debug layer flag that prevented startup on user machines
DRAGONX_DEBUG was defined unconditionally, causing D3D11CreateDevice() to
request the debug layer via D3D11_CREATE_DEVICE_DEBUG. This layer is only
available on machines with the Windows SDK or Graphics Tools installed,
so the call fails with DXGI_ERROR_SDK_COMPONENT_MISSING on regular user
machines — causing the app to silently exit.
2026-03-12 00:13:27 -05:00
42 changed files with 2488 additions and 2128 deletions

View File

@@ -1,173 +0,0 @@
# Copilot Instructions — DragonX ImGui Wallet
## UI Layout: All values in `ui.toml`
**Every UI layout constant must be defined in `res/themes/ui.toml` and read at runtime via the schema API.** Never hardcode pixel sizes, ratios, rounding values, thicknesses, or spacing constants directly in C++ source files. This is critical for maintainability, theming support, and hot-reload.
### Schema API reference
The singleton is accessed via `schema::UI()` (header: `#include "../schema/ui_schema.h"`).
| Method | Returns | Use for |
|---|---|---|
| `drawElement(section, name)` | `DrawElementStyle` | Custom DrawList layout values (`.size`, `.height`, `.thickness`, `.radius`, `.opacity`) |
| `button(section, name)` | `ButtonStyle` | Button width/height/font |
| `input(section, name)` | `InputStyle` | Input field dimensions |
| `label(section, name)` | `LabelStyle` | Label styling |
| `table(section, name)` | `TableStyle` | Table layout |
| `window(section, name)` | `WindowStyle` | Window/dialog dimensions |
| `combo(section, name)` | `ComboStyle` | Combo box styling |
| `slider(section, name)` | `SliderStyle` | Slider styling |
| `checkbox(section, name)` | `CheckboxStyle` | Checkbox styling |
| `separator(section, name)` | `SeparatorStyle` | Separator/divider styling |
### Section naming convention
Sections use dot-separated paths matching the file/feature:
- `tabs.send`, `tabs.receive`, `tabs.transactions`, `tabs.mining`, `tabs.peers`, `tabs.market` — tab-specific values
- `tabs.balance` — balance/home tab
- `components.main-layout`, `components.settings-page` — shared components
- `dialogs.about`, `dialogs.backup`, etc. — dialog-specific values
- `sidebar` — navigation sidebar
### How to add a new layout value
1. **Add the entry to `res/themes/ui.toml`** under the appropriate section:
```toml
[tabs.example]
my-element = {size = 42.0}
```
2. **Read it in C++** instead of using a literal:
```cpp
// WRONG — hardcoded
float myValue = 42.0f;
// CORRECT — schema-driven
float myValue = schema::UI().drawElement("tabs.example", "my-element").size;
```
3. For values used as min/max pairs with scaling:
```cpp
// WRONG
float h = std::max(18.0f, 24.0f * vScale);
// CORRECT
float h = std::max(
schema::UI().drawElement("tabs.example", "row-min-height").size,
schema::UI().drawElement("tabs.example", "row-height").size * vScale
);
```
### What belongs in `ui.toml`
- Pixel sizes (card heights, icon sizes, bar widths/heights)
- Ratios (column width ratios, max-width ratios)
- Rounding values (`FrameRounding`, corner radius)
- Thickness values (accent bars, chart lines, borders)
- Dot/circle radii
- Fade zones, padding constants
- Min/max dimension bounds
- Font selection (via schema font name strings, resolved with `S.resolveFont()`)
- Colors (via `schema::UI().resolveColor()` or color variable references like `"var(--primary)"`)
- Animation durations (transition times, fade durations, pulse speeds)
- Business logic values (fee amounts, ticker strings, buffer sizes, reward amounts)
### What does NOT belong in `ui.toml`
- Spacing that already goes through `Layout::spacing*()` or `spacing::dp()`
### Legacy system: `UILayout`
`UILayout::instance()` is the older layout system still used for fonts, typography, panels, and global spacing. New layout values should use `schema::UI().drawElement()` instead. Do not add new keys to `UILayout`.
### Validation
After editing `ui.toml`, always validate:
```bash
python3 -c "import toml; toml.load('res/themes/ui.toml'); print('Valid TOML')"
```
Or with the C++ toml++ parser (which is what the app uses):
```bash
cd build && make -j$(nproc)
```
### Build
```bash
# Linux
cd build && make -j$(nproc)
# Windows cross-compile
./build.sh --win-release
```
## Plans
When asked to "create a plan", always create a new markdown document in the `docs/` directory with the plan contents.
## Icons: Use Material Design icon font, never Unicode symbols
**Never use raw Unicode symbols or emoji characters** (e.g. ``, ``, ``, `🔍`, `📬`, `⚠️`, ``) for icons in C++ code. Always use the **Material Design Icons font** via the `ICON_MD_*` defines from `#include "../../embedded/IconsMaterialDesign.h"`.
### Icon font API
| Method | Size | Fallback |
|---|---|---|
| `Type().iconSmall()` | 14px | Body2 |
| `Type().iconMed()` | 18px | Body1 |
| `Type().iconLarge()` | 24px | H5 |
| `Type().iconXL()` | 40px | H3 |
### Correct usage
```cpp
#include "../../embedded/IconsMaterialDesign.h"
// WRONG — raw Unicode symbol
itemSpec.leadingIcon = "↙";
// CORRECT — Material Design icon codepoint
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
// WRONG — emoji for search
searchSpec.leadingIcon = "🔍";
// CORRECT — Material Design icon
searchSpec.leadingIcon = ICON_MD_SEARCH;
// For rendering with icon font directly:
ImGui::PushFont(Type().iconSmall());
ImGui::TextUnformatted(ICON_MD_ARROW_DOWNWARD);
ImGui::PopFont();
```
### Why
Raw Unicode symbols and emoji render inconsistently across platforms and may not be present in the loaded text fonts. The Material Design icon font is always loaded and provides consistent, scalable glyphs on both Linux and Windows.
### Audit for Unicode symbols
Before completing any task that touches UI code, search for and replace any raw Unicode symbols that may have been introduced. Common symbols to look for:
| Unicode | Replacement |
|---------|-------------|
| `` `` | `ICON_MD_PLAY_ARROW` |
| `` `` `` `` | `ICON_MD_STOP` or `ICON_MD_SQUARE` |
| `` `` `` `` | `ICON_MD_FIBER_MANUAL_RECORD` or `ICON_MD_CIRCLE` |
| `` `` `` `` | `ICON_MD_ARROW_UPWARD`, `_DOWNWARD`, `_BACK`, `_FORWARD` |
| `` `` `` `` | `ICON_MD_CALL_RECEIVED`, `_MADE`, etc. |
| `` `` | `ICON_MD_CHECK` |
| `` `` `` | `ICON_MD_CLOSE` |
| `` `⚠️` | `ICON_MD_WARNING` |
| `` `` | `ICON_MD_INFO` |
| `🔍` | `ICON_MD_SEARCH` |
| `📋` | `ICON_MD_CONTENT_COPY` or `ICON_MD_DESCRIPTION` |
| `🛡` `🛡️` | `ICON_MD_SHIELD` |
| `` | `ICON_MD_HOURGLASS_EMPTY` |
| `🔄` `` `` | `ICON_MD_SYNC` or `ICON_MD_REFRESH` |
| `` `⚙️` | `ICON_MD_SETTINGS` |
| `🔒` | `ICON_MD_LOCK` |
| `` `` | `ICON_MD_STAR` or `ICON_MD_STAR_BORDER` |

5
.gitignore vendored
View File

@@ -33,4 +33,7 @@ imgui.ini
*.bak* *.bak*
*.params *.params
asmap.dat asmap.dat
/external/xmrig-hac /external/xmrig-hac
/memory
/todo.md
/.github/

View File

@@ -4,7 +4,7 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(ObsidianDragon project(ObsidianDragon
VERSION 1.0.0 VERSION 1.1.0
LANGUAGES C CXX LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet" DESCRIPTION "DragonX Cryptocurrency Wallet"
) )
@@ -243,6 +243,7 @@ set(APP_SOURCES
src/ui/windows/transactions_tab.cpp src/ui/windows/transactions_tab.cpp
src/ui/windows/mining_tab.cpp src/ui/windows/mining_tab.cpp
src/ui/windows/peers_tab.cpp src/ui/windows/peers_tab.cpp
src/ui/windows/explorer_tab.cpp
src/ui/windows/market_tab.cpp src/ui/windows/market_tab.cpp
src/ui/windows/console_tab.cpp src/ui/windows/console_tab.cpp
src/ui/windows/settings_window.cpp src/ui/windows/settings_window.cpp
@@ -320,6 +321,7 @@ set(APP_HEADERS
src/ui/windows/transactions_tab.h src/ui/windows/transactions_tab.h
src/ui/windows/mining_tab.h src/ui/windows/mining_tab.h
src/ui/windows/peers_tab.h src/ui/windows/peers_tab.h
src/ui/windows/explorer_tab.h
src/ui/windows/market_tab.h src/ui/windows/market_tab.h
src/ui/windows/settings_window.h src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h src/ui/windows/about_dialog.h
@@ -373,12 +375,14 @@ endif()
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe) # Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
if(WIN32) if(WIN32)
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico") set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
# Generate manifest with version from project()
configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest.in
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest
@ONLY
)
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest") set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
# Version numbers for the VERSIONINFO resource block # Generate .rc with version from project()
set(DRAGONX_VER_MAJOR 1)
set(DRAGONX_VER_MINOR 0)
set(DRAGONX_VER_PATCH 0)
set(DRAGONX_VERSION "1.0.0")
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc ${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc
${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc
@@ -387,6 +391,13 @@ if(WIN32)
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc) set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
endif() endif()
# Generate version.h from the single project(VERSION ...) declaration
configure_file(
${CMAKE_SOURCE_DIR}/src/config/version.h.in
${CMAKE_SOURCE_DIR}/src/config/version.h
@ONLY
)
# Generate INCBIN font embedding source with absolute paths to .ttf files # Generate INCBIN font embedding source with absolute paths to .ttf files
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in ${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in
@@ -531,10 +542,29 @@ embed_resource(
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds, # Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
# following the same pattern as daemon embedding. # following the same pattern as daemon embedding.
# Copy theme files at BUILD time (not just cmake configure time) # Expand and copy theme files at BUILD time — skin files get layout sections
# so edits to res/themes/*.toml are picked up by 'make' without re-running cmake. # 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) 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}) foreach(THEME_FILE ${THEME_FILES})
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME) get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
add_custom_command( add_custom_command(
@@ -549,7 +579,7 @@ if(THEME_FILES)
endforeach() endforeach()
add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS}) add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS})
add_dependencies(ObsidianDragon copy_themes) add_dependencies(ObsidianDragon copy_themes)
message(STATUS " Theme files: ${THEME_FILES}") message(STATUS " Theme files: ${THEME_FILES} (plain copy, Python not found)")
endif() endif()
# Copy image files (including backgrounds/ subdirectories and logos/) # Copy image files (including backgrounds/ subdirectories and logos/)

View File

@@ -553,9 +553,13 @@ HDR
echo "};" >> "$GEN/embedded_data.h" echo "};" >> "$GEN/embedded_data.h"
# ── Overlay themes ─────────────────────────────────────────────── # ── Overlay themes ───────────────────────────────────────────────
# Expand skin files with layout sections from ui.toml before embedding
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h" 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 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") local tbn=$(basename "$tf")
[[ "$tbn" == "ui.toml" ]] && continue [[ "$tbn" == "ui.toml" ]] && continue
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g') local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
@@ -617,6 +621,10 @@ HDR
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/" [[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done done
# Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ──────────────────────────── # ── Single-file exe (all resources embedded) ────────────────────────────

View File

@@ -5,7 +5,7 @@
<assemblyIdentity <assemblyIdentity
type="win32" type="win32"
name="DragonX.ObsidianDragon.Wallet" name="DragonX.ObsidianDragon.Wallet"
version="1.0.0.0" version="1.1.0.0"
processorArchitecture="amd64" processorArchitecture="amd64"
/> />

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- Application identity —————————————————————————————— -->
<assemblyIdentity
type="win32"
name="DragonX.ObsidianDragon.Wallet"
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
processorArchitecture="amd64"
/>
<description>ObsidianDragon Wallet</description>
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 / 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -19,8 +19,8 @@
#include <winver.h> #include <winver.h>
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0 FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
PRODUCTVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0 PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32 FILEOS VOS_NT_WINDOWS32
@@ -33,12 +33,12 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "DragonX Developers\0" VALUE "CompanyName", "DragonX Developers\0"
VALUE "FileDescription", "ObsidianDragon Wallet\0" VALUE "FileDescription", "ObsidianDragon Wallet\0"
VALUE "FileVersion", "@DRAGONX_VERSION@\0" VALUE "FileVersion", "@PROJECT_VERSION@\0"
VALUE "InternalName", "ObsidianDragon\0" VALUE "InternalName", "ObsidianDragon\0"
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0" VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
VALUE "OriginalFilename", "ObsidianDragon.exe\0" VALUE "OriginalFilename", "ObsidianDragon.exe\0"
VALUE "ProductName", "ObsidianDragon\0" VALUE "ProductName", "ObsidianDragon\0"
VALUE "ProductVersion", "@DRAGONX_VERSION@\0" VALUE "ProductVersion", "@PROJECT_VERSION@\0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@@ -894,6 +894,18 @@ pair-bar-arrow-size = { size = 28.0 }
exchange-combo-width = { size = 180.0 } exchange-combo-width = { size = 180.0 }
exchange-top-gap = { size = 0.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] [tabs.console]
input-area-padding = 8.0 input-area-padding = 8.0
output-line-spacing = 2.0 output-line-spacing = 2.0
@@ -1156,6 +1168,8 @@ key-input = { height = 150 }
rescan-height-input = { width = 100 } rescan-height-input = { width = 100 }
import-button = { width = 120, font = "button" } import-button = { width = 120, font = "button" }
close-button = { width = 100, font = "button" } close-button = { width = 100, font = "button" }
paste-preview-alpha = { size = 0.3 }
paste-preview-max-chars = { size = 200 }
[components] [components]
@@ -1218,7 +1232,7 @@ collapse-anim-speed = { size = 10.0 }
auto-collapse-threshold = { size = 800.0 } auto-collapse-threshold = { size = 800.0 }
section-gap = { size = 4.0 } section-gap = { size = 4.0 }
section-label-pad-left = { size = 16.0 } section-label-pad-left = { size = 16.0 }
item-height = { size = 42.0 } item-height = { size = 36.0 }
item-pad-x = { size = 8.0 } item-pad-x = { size = 8.0 }
min-height = { size = 360.0 } min-height = { size = 360.0 }
margin-top = { size = -12 } margin-top = { size = -12 }

122
scripts/expand_themes.py Normal file
View 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()

View File

@@ -17,6 +17,7 @@
#include "ui/windows/transactions_tab.h" #include "ui/windows/transactions_tab.h"
#include "ui/windows/mining_tab.h" #include "ui/windows/mining_tab.h"
#include "ui/windows/peers_tab.h" #include "ui/windows/peers_tab.h"
#include "ui/windows/explorer_tab.h"
#include "ui/windows/market_tab.h" #include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h" #include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h" #include "ui/windows/about_dialog.h"
@@ -104,6 +105,12 @@ bool App::init()
if (!settings_->load()) { if (!settings_->load()) {
DEBUG_LOGF("Warning: Could not load settings, using defaults\n"); DEBUG_LOGF("Warning: Could not load settings, using defaults\n");
} }
// On upgrade (version mismatch), re-save to persist new defaults + current version
if (settings_->needsUpgradeSave()) {
DEBUG_LOGF("[INFO] Wallet upgraded — re-saving settings with new defaults\n");
settings_->save();
settings_->clearUpgradeSave();
}
// Apply verbose logging preference from saved settings // Apply verbose logging preference from saved settings
util::Logger::instance().setVerbose(settings_->getVerboseLogging()); util::Logger::instance().setVerbose(settings_->getVerboseLogging());
@@ -137,6 +144,27 @@ bool App::init()
{ {
std::string schemaPath = util::Platform::getExecutableDirectory() + "/res/themes/ui.toml"; std::string schemaPath = util::Platform::getExecutableDirectory() + "/res/themes/ui.toml";
bool loaded = false; bool loaded = false;
#if HAS_EMBEDDED_UI_TOML
// If on-disk ui.toml exists but differs in size from the embedded
// version, a newer wallet binary is running against stale theme
// files. Overwrite the on-disk copy so layout matches the binary.
if (std::filesystem::exists(schemaPath)) {
std::error_code ec;
auto diskSize = std::filesystem::file_size(schemaPath, ec);
if (!ec && diskSize != static_cast<std::uintmax_t>(embedded::ui_toml_size)) {
DEBUG_LOGF("[INFO] ui.toml on disk (%ju bytes) differs from embedded (%zu bytes) — updating\n",
(uintmax_t)diskSize, embedded::ui_toml_size);
std::ofstream ofs(schemaPath, std::ios::binary | std::ios::trunc);
if (ofs.is_open()) {
ofs.write(reinterpret_cast<const char*>(embedded::ui_toml_data),
embedded::ui_toml_size);
ofs.close();
}
}
}
#endif
if (std::filesystem::exists(schemaPath)) { if (std::filesystem::exists(schemaPath)) {
loaded = ui::schema::UISchema::instance().loadFromFile(schemaPath); loaded = ui::schema::UISchema::instance().loadFromFile(schemaPath);
} }
@@ -678,6 +706,19 @@ void App::render()
return; 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) // Process deferred encryption from wizard (runs in background)
processDeferredEncryption(); processDeferredEncryption();
@@ -1010,7 +1051,10 @@ void App::render()
current_page_ != ui::NavPage::Settings); current_page_ != ui::NavPage::Settings);
bool daemonReady = state_.connected; // don't gate on sync state bool daemonReady = state_.connected; // don't gate on sync state
if (state_.isLocked()) { // Don't show lock screen while pool mining — xmrig runs independently
// of the wallet and locking would block the mining UI needlessly.
bool poolMiningActive = xmrig_manager_ && xmrig_manager_->isRunning();
if (state_.isLocked() && !poolMiningActive) {
// Lock screen — covers tab content just like the loading overlay // Lock screen — covers tab content just like the loading overlay
renderLockScreen(); renderLockScreen();
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) { } else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
@@ -1042,6 +1086,9 @@ void App::render()
case ui::NavPage::Peers: case ui::NavPage::Peers:
ui::RenderPeersTab(this); ui::RenderPeersTab(this);
break; break;
case ui::NavPage::Explorer:
ui::RenderExplorerTab(this);
break;
case ui::NavPage::Market: case ui::NavPage::Market:
ui::RenderMarketTab(this); ui::RenderMarketTab(this);
break; break;
@@ -1548,6 +1595,9 @@ void App::renderImportKeyDialog()
return it != dlg.extraFloats.end() ? it->second : fb; return it != dlg.extraFloats.end() ? it->second : fb;
}; };
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) { if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
return; return;
} }
@@ -1561,6 +1611,50 @@ void App::renderImportKeyDialog()
ImGui::SetNextItemWidth(-1); ImGui::SetNextItemWidth(-1);
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_)); ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
// Paste & Clear buttons
if (ui::material::StyledButton(TR("paste"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
snprintf(import_key_input_, sizeof(import_key_input_), "%s", clipboard);
// Trim whitespace
std::string trimmed(import_key_input_);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
}
}
ImGui::SameLine();
if (ui::material::StyledButton(TR("clear"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
memset(import_key_input_, 0, sizeof(import_key_input_));
}
// Key validation indicator
if (import_key_input_[0] != '\0') {
std::string k(import_key_input_);
bool isZKey = (k.substr(0, 20) == "secret-extended-key-") ||
(k.length() >= 2 && k[0] == 'S' && k[1] == 'K');
bool isTKey = (k.length() >= 51 && k.length() <= 52 &&
(k[0] == '5' || k[0] == 'K' || k[0] == 'L' || k[0] == 'U'));
if (isZKey || isTKey) {
ImGui::PushFont(ui::material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), ICON_MD_CHECK_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s",
isZKey ? "Shielded spending key" : "Transparent private key");
} else {
ImGui::PushFont(ui::material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Unrecognized key format");
}
}
ImGui::Spacing(); ImGui::Spacing();
if (!import_status_.empty()) { if (!import_status_.empty()) {
@@ -1574,8 +1668,6 @@ void App::renderImportKeyDialog()
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); 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))) { if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string key(import_key_input_); std::string key(import_key_input_);
if (!key.empty()) { if (!key.empty()) {
@@ -1820,10 +1912,11 @@ void App::setCurrentTab(int tab) {
ui::NavPage::Receive, // 2 = Receive ui::NavPage::Receive, // 2 = Receive
ui::NavPage::History, // 3 = Transactions ui::NavPage::History, // 3 = Transactions
ui::NavPage::Mining, // 4 = Mining ui::NavPage::Mining, // 4 = Mining
ui::NavPage::Peers, // 5 = Peers ui::NavPage::Peers, // 5 = Peers
ui::NavPage::Market, // 6 = Market ui::NavPage::Market, // 6 = Market
ui::NavPage::Console, // 7 = Console ui::NavPage::Console, // 7 = Console
ui::NavPage::Settings, // 8 = Settings ui::NavPage::Explorer, // 8 = Explorer
ui::NavPage::Settings, // 9 = Settings
}; };
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0]))) if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
current_page_ = kTabMap[tab]; current_page_ = kTabMap[tab];
@@ -2037,6 +2130,57 @@ void App::rescanBlockchain()
}).detach(); }).detach();
} }
void App::deleteBlockchainData()
{
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
ui::Notifications::instance().warning(
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory.");
return;
}
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
std::thread([this]() {
DEBUG_LOGF("[App] Stopping daemon for blockchain deletion...\n");
stopEmbeddedDaemon();
if (shutting_down_) return;
for (int i = 0; i < 30 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
namespace fs = std::filesystem;
std::string dataDir = util::Platform::getDragonXDataDir();
// Directories to remove
const char* dirs[] = { "blocks", "chainstate", "database", "notarizations" };
// Files to remove
const char* files[] = { "peers.dat", "fee_estimates.dat", "banlist.dat",
"db.log", ".lock" };
int removed = 0;
std::error_code ec;
for (auto d : dirs) {
fs::path p = fs::path(dataDir) / d;
if (fs::exists(p, ec)) {
auto n = fs::remove_all(p, ec);
if (!ec) { removed += (int)n; DEBUG_LOGF("[App] Removed %s (%d entries)\n", d, (int)n); }
else { DEBUG_LOGF("[App] Failed to remove %s: %s\n", d, ec.message().c_str()); }
}
}
for (auto f : files) {
fs::path p = fs::path(dataDir) / f;
if (fs::remove(p, ec)) { removed++; DEBUG_LOGF("[App] Removed %s\n", f); }
}
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", removed);
daemon_output_offset_ = 0;
startEmbeddedDaemon();
}).detach();
}
double App::getDaemonMemoryUsageMB() const double App::getDaemonMemoryUsageMB() const
{ {
// If we have an embedded daemon with a tracked process handle, use it // If we have an embedded daemon with a tracked process handle, use it

View File

@@ -253,6 +253,7 @@ public:
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; } bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; } void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
// Get daemon memory usage in MB (uses embedded daemon handle if available, // Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons) // falls back to platform-level process scan for external daemons)
@@ -571,6 +572,7 @@ private:
// Mine-when-idle: auto-start/stop mining based on system idle state // Mine-when-idle: auto-start/stop mining based on system idle state
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
// Private methods - rendering // Private methods - rendering
void renderStatusBar(); void renderStatusBar();

View File

@@ -138,6 +138,37 @@ void App::tryConnect()
// endlessly — tell the user what's wrong. // endlessly — tell the user what's wrong.
bool authFailure = (connectErr.find("401") != std::string::npos); bool authFailure = (connectErr.find("401") != std::string::npos);
if (authFailure) { if (authFailure) {
// Try .cookie auth as fallback — the daemon may have
// generated a .cookie file instead of using DRAGONX.conf credentials
std::string dataDir = rpc::Connection::getDefaultDataDir();
std::string cookieUser, cookiePass;
if (rpc::Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) {
VERBOSE_LOGF("[connect #%d] HTTP 401 — retrying with .cookie auth from %s\n",
attempt, dataDir.c_str());
worker_->post([this, config, cookieUser, cookiePass, attempt]() -> rpc::RPCWorker::MainCb {
auto cookieConfig = config;
cookieConfig.rpcuser = cookieUser;
cookieConfig.rpcpassword = cookiePass;
bool ok = rpc_->connect(cookieConfig.host, cookieConfig.port, cookieConfig.rpcuser, cookieConfig.rpcpassword);
return [this, cookieConfig, ok, attempt]() {
connection_in_progress_ = false;
if (ok) {
VERBOSE_LOGF("[connect #%d] Connected via .cookie auth\n", attempt);
saved_config_ = cookieConfig;
onConnected();
} else {
state_.connected = false;
connection_status_ = "Auth failed — check rpcuser/rpcpassword";
VERBOSE_LOGF("[connect #%d] .cookie auth also failed\n", attempt);
ui::Notifications::instance().error(
"RPC authentication failed (HTTP 401). "
"The rpcuser/rpcpassword in DRAGONX.conf don't match the running daemon. "
"Restart the daemon or correct the credentials.");
}
};
});
return; // async retry in progress
}
state_.connected = false; state_.connected = false;
std::string confPath = rpc::Connection::getDefaultConfPath(); std::string confPath = rpc::Connection::getDefaultConfPath();
connection_status_ = "Auth failed — check rpcuser/rpcpassword"; connection_status_ = "Auth failed — check rpcuser/rpcpassword";
@@ -160,6 +191,17 @@ void App::tryConnect()
connection_status_ = "Connecting to daemon..."; connection_status_ = "Connecting to daemon...";
VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt); VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt);
refresh_timer_ = REFRESH_INTERVAL - 1.0f; refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else if (connectErr.find("Loading") != std::string::npos ||
connectErr.find("Verifying") != std::string::npos ||
connectErr.find("Activating") != std::string::npos ||
connectErr.find("Rewinding") != std::string::npos ||
connectErr.find("Rescanning") != std::string::npos ||
connectErr.find("Pruning") != std::string::npos) {
// Daemon is reachable but still in warmup (Loading block index, etc.)
state_.connected = false;
connection_status_ = connectErr;
VERBOSE_LOGF("[connect #%d] Daemon warmup: %s\n", attempt, connectErr.c_str());
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
} else { } else {
onDisconnected("Connection failed"); onDisconnected("Connection failed");
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt); VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
@@ -230,12 +272,18 @@ void App::onConnected()
state_.protocol_version = info["protocolversion"].get<int>(); state_.protocol_version = info["protocolversion"].get<int>();
if (info.contains("p2pport")) if (info.contains("p2pport"))
state_.p2p_port = info["p2pport"].get<int>(); state_.p2p_port = info["p2pport"].get<int>();
if (info.contains("longestchain")) if (info.contains("longestchain")) {
state_.longestchain = info["longestchain"].get<int>(); int lc = info["longestchain"].get<int>();
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
if (lc > 0) state_.longestchain = lc;
}
if (info.contains("notarized")) if (info.contains("notarized"))
state_.notarized = info["notarized"].get<int>(); state_.notarized = info["notarized"].get<int>();
if (info.contains("blocks")) if (info.contains("blocks"))
state_.sync.blocks = info["blocks"].get<int>(); state_.sync.blocks = info["blocks"].get<int>();
// longestchain can lag behind blocks when peer data is stale
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
state_.longestchain = state_.sync.blocks;
} catch (const std::exception& e) { } catch (const std::exception& e) {
DEBUG_LOGF("[onConnected] getinfo callback error: %s\n", e.what()); DEBUG_LOGF("[onConnected] getinfo callback error: %s\n", e.what());
} }
@@ -742,8 +790,14 @@ void App::refreshData()
state_.sync.headers = blockInfo["headers"].get<int>(); state_.sync.headers = blockInfo["headers"].get<int>();
if (blockInfo.contains("verificationprogress")) if (blockInfo.contains("verificationprogress"))
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>(); state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
if (blockInfo.contains("longestchain")) if (blockInfo.contains("longestchain")) {
state_.longestchain = blockInfo["longestchain"].get<int>(); int lc = blockInfo["longestchain"].get<int>();
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
if (lc > 0) state_.longestchain = lc;
}
// longestchain can lag behind blocks when peer data is stale
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
state_.longestchain = state_.sync.blocks;
// Use longestchain (actual network tip) for sync check when available, // Use longestchain (actual network tip) for sync check when available,
// since headers can be inflated by misbehaving peers. // since headers can be inflated by misbehaving peers.
if (state_.longestchain > 0) if (state_.longestchain > 0)
@@ -891,8 +945,14 @@ void App::refreshBalance()
state_.sync.headers = blockInfo["headers"].get<int>(); state_.sync.headers = blockInfo["headers"].get<int>();
if (blockInfo.contains("verificationprogress")) if (blockInfo.contains("verificationprogress"))
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>(); state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
if (blockInfo.contains("longestchain")) if (blockInfo.contains("longestchain")) {
state_.longestchain = blockInfo["longestchain"].get<int>(); int lc = blockInfo["longestchain"].get<int>();
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
if (lc > 0) state_.longestchain = lc;
}
// longestchain can lag behind blocks when peer data is stale
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
state_.longestchain = state_.sync.blocks;
if (state_.longestchain > 0) if (state_.longestchain > 0)
state_.sync.syncing = (state_.sync.blocks < state_.longestchain - 2); state_.sync.syncing = (state_.sync.blocks < state_.longestchain - 2);
else else
@@ -1358,7 +1418,7 @@ void App::startPoolMining(int threads)
if (cfg.wallet_address.empty()) { if (cfg.wallet_address.empty()) {
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n"); DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
ui::Notifications::instance().error("No wallet address available for pool mining"); ui::Notifications::instance().error("No wallet address available — generate a Z address in the Receive tab");
return; return;
} }

View File

@@ -415,6 +415,9 @@ void App::checkAutoLock() {
// =========================================================================== // ===========================================================================
// Mine when idle — auto-start/stop mining based on system idle state // Mine when idle — auto-start/stop mining based on system idle state
// Supports two modes:
// 1. Start/Stop mode (default): start mining when idle, stop when active
// 2. Thread scaling mode: mining stays running, thread count changes
// =========================================================================== // ===========================================================================
void App::checkIdleMining() { void App::checkIdleMining() {
@@ -422,6 +425,7 @@ void App::checkIdleMining() {
// Feature disabled — if we previously auto-started, stop now // Feature disabled — if we previously auto-started, stop now
if (idle_mining_active_) { if (idle_mining_active_) {
idle_mining_active_ = false; idle_mining_active_ = false;
idle_scaled_to_idle_ = false;
if (settings_ && settings_->getPoolMode()) { if (settings_ && settings_->getPoolMode()) {
if (xmrig_manager_ && xmrig_manager_->isRunning()) if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining(); stopPoolMining();
@@ -430,46 +434,89 @@ void App::checkIdleMining() {
stopMining(); stopMining();
} }
} }
// Reset scaling state when feature is off
if (idle_scaled_to_idle_) idle_scaled_to_idle_ = false;
return; return;
} }
int idleSec = util::Platform::getSystemIdleSeconds(); int idleSec = util::Platform::getSystemIdleSeconds();
int delay = settings_->getMineIdleDelay(); int delay = settings_->getMineIdleDelay();
bool isPool = settings_->getPoolMode(); bool isPool = settings_->getPoolMode();
bool threadScaling = settings_->getIdleThreadScaling();
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
// Check if mining is already running (manually started by user) // Check if mining is already running (manually started by user)
bool miningActive = isPool bool miningActive = isPool
? (xmrig_manager_ && xmrig_manager_->isRunning()) ? (xmrig_manager_ && xmrig_manager_->isRunning())
: state_.mining.generate; : state_.mining.generate;
if (idleSec >= delay) { if (threadScaling) {
// System is idle — start mining if not already running // --- Thread scaling mode ---
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) { // Mining must already be running (started by user). We just adjust threads.
// For solo mining, need daemon connected and synced if (!miningActive || mining_toggle_in_progress_.load()) return;
if (!isPool && (!state_.connected || state_.sync.syncing)) return;
int threads = settings_->getPoolThreads(); int activeThreads = settings_->getIdleThreadsActive();
if (threads <= 0) threads = std::max(1, (int)std::thread::hardware_concurrency() / 2); int idleThreads = settings_->getIdleThreadsIdle();
// Resolve auto values: active defaults to half, idle defaults to all
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
if (idleThreads <= 0) idleThreads = maxThreads;
idle_mining_active_ = true; if (idleSec >= delay) {
if (isPool) // System is idle — scale up to idle thread count
startPoolMining(threads); if (!idle_scaled_to_idle_) {
else idle_scaled_to_idle_ = true;
startMining(threads); if (isPool) {
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec); stopPoolMining();
startPoolMining(idleThreads);
} else {
startMining(idleThreads);
}
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
}
} else {
// User is active — scale down to active thread count
if (idle_scaled_to_idle_) {
idle_scaled_to_idle_ = false;
if (isPool) {
stopPoolMining();
startPoolMining(activeThreads);
} else {
startMining(activeThreads);
}
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads);
}
} }
} else { } else {
// User is active — stop mining if we auto-started it // --- Start/Stop mode (original behavior) ---
if (idle_mining_active_) { if (idleSec >= delay) {
idle_mining_active_ = false; // System is idle — start mining if not already running
if (isPool) { if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
if (xmrig_manager_ && xmrig_manager_->isRunning()) // For solo mining, need daemon connected and synced
stopPoolMining(); if (!isPool && (!state_.connected || state_.sync.syncing)) return;
} else {
if (state_.mining.generate) int threads = settings_->getPoolThreads();
stopMining(); if (threads <= 0) threads = std::max(1, maxThreads / 2);
idle_mining_active_ = true;
if (isPool)
startPoolMining(threads);
else
startMining(threads);
DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
}
} else {
// User is active — stop mining if we auto-started it
if (idle_mining_active_) {
idle_mining_active_ = false;
if (isPool) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
} else {
if (state_.mining.generate)
stopMining();
}
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
} }
DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
} }
} }
} }

View File

@@ -94,21 +94,6 @@ void App::renderFirstRunWizard() {
ImU32 bgCol = ui::material::Surface(); ImU32 bgCol = ui::material::Surface();
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol); 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 --- // --- Determine which of the 3 masonry sections is focused ---
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN // 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
int focusIdx = 0; int focusIdx = 0;
@@ -766,6 +751,15 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) { if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap retry");
}
bootstrap_ = std::make_unique<util::Bootstrap>(); bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir(); std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir); bootstrap_->start(dataDir);
@@ -894,7 +888,7 @@ void App::renderFirstRunWizard() {
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24); ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x; float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
const char* twText = "Only use bootstrap.dragonx.is. Using files from untrusted sources could compromise your node."; const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
float twWrap = contentW - iw - 4.0f * dp; float twWrap = contentW - iw - 4.0f * dp;
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText); ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
@@ -903,18 +897,29 @@ void App::renderFirstRunWizard() {
// Buttons (only when focused) // Buttons (only when focused)
if (isFocused) { if (isFocused) {
float dlBtnW = 180.0f * dp; float dlBtnW = 150.0f * dp;
float mirrorW = 150.0f * dp;
float skipW2 = 80.0f * dp; float skipW2 = 80.0f * dp;
float btnH2 = 40.0f * dp; float btnH2 = 40.0f * dp;
float totalBW = dlBtnW + 12.0f * dp + skipW2; float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f; float bx = rightX + (colW - totalBW) * 0.5f;
// --- Download button (main / Cloudflare) ---
ImGui::SetCursorScreenPos(ImVec2(bx, cy)); ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) { if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>(); bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir(); std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir); bootstrap_->start(dataDir);
@@ -923,7 +928,35 @@ void App::renderFirstRunWizard() {
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 12.0f * dp, cy)); // --- Mirror Download button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
if (isEmbeddedDaemonRunning()) {
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) { if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer; wizard_phase_ = WizardPhase::EncryptOffer;

View File

@@ -150,6 +150,9 @@ bool Settings::load(const std::string& path)
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>(); if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>(); if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>()); if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) { if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear(); saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"]) for (const auto& u : j["saved_pool_urls"])
@@ -166,7 +169,16 @@ bool Settings::load(const std::string& path)
window_width_ = j["window_width"].get<int>(); window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer()) if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>(); window_height_ = j["window_height"].get<int>();
// Version tracking — detect upgrades so we can re-save with new defaults
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
DRAGONX_VERSION);
needs_upgrade_save_ = true;
}
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
DEBUG_LOGF("Failed to parse settings: %s\n", e.what()); DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
@@ -235,6 +247,9 @@ bool Settings::save(const std::string& path)
j["pool_mode"] = pool_mode_; j["pool_mode"] = pool_mode_;
j["mine_when_idle"] = mine_when_idle_; j["mine_when_idle"] = mine_when_idle_;
j["mine_idle_delay"]= mine_idle_delay_; j["mine_idle_delay"]= mine_idle_delay_;
j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_;
j["idle_threads_idle"] = idle_threads_idle_;
j["saved_pool_urls"] = json::array(); j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_) for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u); j["saved_pool_urls"].push_back(u);
@@ -242,6 +257,7 @@ bool Settings::save(const std::string& path)
for (const auto& w : saved_pool_workers_) for (const auto& w : saved_pool_workers_)
j["saved_pool_workers"].push_back(w); j["saved_pool_workers"].push_back(w);
j["font_scale"] = font_scale_; j["font_scale"] = font_scale_;
j["settings_version"] = std::string(DRAGONX_VERSION);
if (window_width_ > 0 && window_height_ > 0) { if (window_width_ > 0 && window_height_ > 0) {
j["window_width"] = window_width_; j["window_width"] = window_width_;
j["window_height"] = window_height_; j["window_height"] = window_height_;

View File

@@ -214,6 +214,14 @@ public:
int getMineIdleDelay() const { return mine_idle_delay_; } int getMineIdleDelay() const { return mine_idle_delay_; }
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); } void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
// Idle thread scaling — scale thread count instead of start/stop
bool getIdleThreadScaling() const { return idle_thread_scaling_; }
void setIdleThreadScaling(bool v) { idle_thread_scaling_ = v; }
int getIdleThreadsActive() const { return idle_threads_active_; }
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
int getIdleThreadsIdle() const { return idle_threads_idle_; }
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
// Saved pool URLs (user-managed favorites dropdown) // Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; } const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
void addSavedPoolUrl(const std::string& url) { void addSavedPoolUrl(const std::string& url) {
@@ -248,6 +256,10 @@ public:
int getWindowHeight() const { return window_height_; } int getWindowHeight() const { return window_height_; }
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; } void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
// Returns true once after an upgrade (version mismatch detected on load)
bool needsUpgradeSave() const { return needs_upgrade_save_; }
void clearUpgradeSave() { needs_upgrade_save_ = false; }
private: private:
std::string settings_path_; std::string settings_path_;
@@ -290,13 +302,16 @@ private:
// Pool mining // Pool mining
std::string pool_url_ = "pool.dragonx.is:3433"; std::string pool_url_ = "pool.dragonx.is:3433";
std::string pool_algo_ = "rx/hush"; std::string pool_algo_ = "rx/hush";
std::string pool_worker_ = "x"; std::string pool_worker_ = "";
int pool_threads_ = 0; int pool_threads_ = 0;
bool pool_tls_ = false; bool pool_tls_ = false;
bool pool_hugepages_ = true; bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool bool pool_mode_ = false; // false=solo, true=pool
bool mine_when_idle_ = false; // auto-start mining when system idle bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts int mine_idle_delay_= 120; // seconds of idle before mining starts
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
int idle_threads_active_ = 0; // threads when user active (0 = auto)
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
@@ -306,6 +321,10 @@ private:
// Window size (logical pixels at 1x scale; 0 = use default 1200×775) // Window size (logical pixels at 1x scale; 0 = use default 1200×775)
int window_width_ = 0; int window_width_ = 0;
int window_height_ = 0; int window_height_ = 0;
// Wallet version that last wrote this settings file (for upgrade detection)
std::string settings_version_;
bool needs_upgrade_save_ = false; // true when version changed
}; };
} // namespace config } // namespace config

View File

@@ -4,9 +4,12 @@
#pragma once #pragma once
#define DRAGONX_VERSION "1.0.0" // !! 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.1.0"
#define DRAGONX_VERSION_MAJOR 1 #define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 0 #define DRAGONX_VERSION_MINOR 1
#define DRAGONX_VERSION_PATCH 0 #define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon" #define DRAGONX_APP_NAME "ObsidianDragon"

31
src/config/version.h.in Normal file
View File

@@ -0,0 +1,31 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// !! 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 "@PROJECT_VERSION@"
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"

View File

@@ -249,7 +249,15 @@ struct WalletState {
void clear() { void clear() {
connected = false; 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; privateBalance = transparentBalance = totalBalance = 0.0;
unconfirmedBalance = 0.0;
encrypted = false; encrypted = false;
locked = false; locked = false;
unlocked_until = 0; unlocked_until = 0;

View File

@@ -419,12 +419,20 @@ int main(int argc, char* argv[])
if (!g_single_instance.tryLock()) { if (!g_single_instance.tryLock()) {
fprintf(stderr, "Another instance of ObsidianDragon is already running.\n"); fprintf(stderr, "Another instance of ObsidianDragon is already running.\n");
DEBUG_LOGF("Please close the existing instance first.\n"); DEBUG_LOGF("Please close the existing instance first.\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.",
L"ObsidianDragon", MB_OK | MB_ICONINFORMATION);
#endif
return 1; return 1;
} }
// Initialize SDL // Initialize SDL
if (!InitSDL()) { if (!InitSDL()) {
fprintf(stderr, "Failed to initialize SDL!\n"); fprintf(stderr, "Failed to initialize SDL!\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Failed to initialize SDL. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
return 1; return 1;
} }
@@ -495,6 +503,8 @@ int main(int argc, char* argv[])
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
if (!nativeHwnd) { if (!nativeHwnd) {
fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError()); fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError());
MessageBoxW(nullptr, L"Failed to create window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
SDL_Quit(); SDL_Quit();
return 1; return 1;
} }
@@ -534,6 +544,8 @@ int main(int argc, char* argv[])
SDL_DestroyProperties(createProps); SDL_DestroyProperties(createProps);
if (window == nullptr) { if (window == nullptr) {
fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError()); fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError());
MessageBoxW(nullptr, L"Failed to create SDL window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
DestroyWindow(nativeHwnd); DestroyWindow(nativeHwnd);
SDL_Quit(); SDL_Quit();
return 1; return 1;
@@ -560,6 +572,8 @@ int main(int argc, char* argv[])
dragonx::platform::DX11Context dx; dragonx::platform::DX11Context dx;
if (!dx.init(window)) { if (!dx.init(window)) {
fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n"); fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n");
MessageBoxW(nullptr, L"Failed to initialize DirectX 11.\nPlease ensure your graphics drivers are up to date.\n\nCheck the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Graphics Error", MB_OK | MB_ICONERROR);
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
SDL_Quit(); SDL_Quit();
return 1; return 1;
@@ -635,6 +649,8 @@ int main(int argc, char* argv[])
// Initialize ImGui with DX11 backend // Initialize ImGui with DX11 backend
if (!InitImGui(window, dx)) { if (!InitImGui(window, dx)) {
fprintf(stderr, "Failed to initialize ImGui!\n"); fprintf(stderr, "Failed to initialize ImGui!\n");
MessageBoxW(nullptr, L"Failed to initialize ImGui. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
Shutdown(window, dx); Shutdown(window, dx);
return 1; return 1;
} }
@@ -808,6 +824,10 @@ int main(int argc, char* argv[])
dragonx::App app; dragonx::App app;
if (!app.init()) { if (!app.init()) {
fprintf(stderr, "Failed to initialize application!\n"); fprintf(stderr, "Failed to initialize application!\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Failed to initialize application. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
#ifdef DRAGONX_USE_DX11 #ifdef DRAGONX_USE_DX11
Shutdown(window, dx); Shutdown(window, dx);
#else #else
@@ -1780,6 +1800,17 @@ int main(int argc, char* argv[])
// while background cleanup (thread joins, RPC disconnect) continues. // while background cleanup (thread joins, RPC disconnect) continues.
SDL_HideWindow(window); SDL_HideWindow(window);
// Watchdog: if cleanup takes too long the process lingers without a
// window, showing up as a "Background Service" in Task Manager.
// Force-exit after 3 seconds — all critical state (settings, daemon
// stop) was handled in beginShutdown().
std::thread([]() {
std::this_thread::sleep_for(std::chrono::seconds(3));
fflush(stdout);
fflush(stderr);
_Exit(0);
}).detach();
// Final cleanup (daemon already stopped by beginShutdown) // Final cleanup (daemon already stopped by beginShutdown)
app.shutdown(); app.shutdown();
#ifdef DRAGONX_USE_DX11 #ifdef DRAGONX_USE_DX11

View File

@@ -61,13 +61,8 @@ bool DX11Context::init(SDL_Window* window)
D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_10_0,
}; };
UINT createDeviceFlags = 0;
#ifdef DRAGONX_DEBUG
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// Need BGRA support for DirectComposition // Need BGRA support for DirectComposition
createDeviceFlags |= D3D11_CREATE_DEVICE_BGRA_SUPPORT; UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
HRESULT hr = D3D11CreateDevice( HRESULT hr = D3D11CreateDevice(
nullptr, // Default adapter nullptr, // Default adapter

View File

@@ -144,6 +144,37 @@ int extractBundledThemes(const std::string& destDir)
return count; return count;
} }
int updateBundledThemes(const std::string& dir)
{
namespace fs = std::filesystem;
int count = 0;
const auto* themes = getEmbeddedThemes();
if (!themes || !themes->data) return 0;
if (!fs::exists(dir)) return 0;
for (const auto* t = themes; t->data != nullptr; ++t) {
fs::path dest = fs::path(dir) / t->filename;
if (!fs::exists(dest)) {
// New theme not yet on disk — extract it
} else {
std::error_code ec;
auto diskSize = fs::file_size(dest, ec);
if (!ec && diskSize == static_cast<std::uintmax_t>(t->size))
continue; // up to date
}
std::ofstream f(dest, std::ios::binary);
if (f.is_open()) {
f.write(reinterpret_cast<const char*>(t->data), t->size);
f.close();
DEBUG_LOGF("[INFO] EmbeddedResources: Updated stale theme: %s (%zu bytes)\n",
t->filename, t->size);
count++;
}
}
return count;
}
std::string getParamsDirectory() std::string getParamsDirectory()
{ {
#ifdef _WIN32 #ifdef _WIN32
@@ -169,6 +200,9 @@ std::string getParamsDirectory()
#endif #endif
} }
// Forward declaration — defined below extractResource()
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath);
bool needsParamsExtraction() bool needsParamsExtraction()
{ {
if (!hasEmbeddedResources()) { if (!hasEmbeddedResources()) {
@@ -177,21 +211,46 @@ bool needsParamsExtraction()
// Check daemon directory (dragonx/) — the only extraction target // Check daemon directory (dragonx/) — the only extraction target
std::string daemonDir = getDaemonDirectory(); std::string daemonDir = getDaemonDirectory();
std::string spendPath = daemonDir +
#ifdef _WIN32 #ifdef _WIN32
"\\sapling-spend.params"; const char pathSep = '\\';
#else #else
"/sapling-spend.params"; const char pathSep = '/';
#endif
std::string outputPath = daemonDir +
#ifdef _WIN32
"\\sapling-output.params";
#else
"/sapling-output.params";
#endif #endif
std::string spendPath = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
std::string outputPath = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
// Check if both params exist in daemon directory // Check if params are missing or stale (size mismatch → updated in newer build)
return !std::filesystem::exists(spendPath) || !std::filesystem::exists(outputPath); const auto* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
const auto* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
// Also check if daemon binaries need updating
#ifdef HAS_EMBEDDED_DAEMON
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
if (daemonRes && resourceNeedsUpdate(daemonRes, daemonPath)) return true;
#endif
#ifdef HAS_EMBEDDED_XMRIG
const auto* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
std::string xmrigPath = daemonDir + pathSep + RESOURCE_XMRIG;
if (xmrigRes && resourceNeedsUpdate(xmrigRes, xmrigPath)) return true;
#endif
return false;
}
// Check if an on-disk file is missing or differs in size from the embedded resource.
// A size mismatch means the binary was updated in a newer wallet build.
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath)
{
if (!res || !res->data || res->size == 0) return false;
if (!std::filesystem::exists(destPath)) return true;
std::error_code ec;
auto diskSize = std::filesystem::file_size(destPath, ec);
if (ec) return true; // can't stat → re-extract
return diskSize != static_cast<std::uintmax_t>(res->size);
} }
static bool extractResource(const EmbeddedResource* res, const std::string& destPath) static bool extractResource(const EmbeddedResource* res, const std::string& destPath)
@@ -251,7 +310,7 @@ bool extractEmbeddedResources()
const EmbeddedResource* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND); const EmbeddedResource* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
if (spendRes) { if (spendRes) {
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_SPEND; std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(spendRes, dest)) {
DEBUG_LOGF("[INFO] Extracting sapling-spend.params (%zu MB)...\n", spendRes->size / (1024*1024)); DEBUG_LOGF("[INFO] Extracting sapling-spend.params (%zu MB)...\n", spendRes->size / (1024*1024));
if (!extractResource(spendRes, dest)) { if (!extractResource(spendRes, dest)) {
success = false; success = false;
@@ -262,7 +321,7 @@ bool extractEmbeddedResources()
const EmbeddedResource* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT); const EmbeddedResource* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
if (outputRes) { if (outputRes) {
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT; std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(outputRes, dest)) {
DEBUG_LOGF("[INFO] Extracting sapling-output.params (%zu MB)...\n", outputRes->size / (1024*1024)); DEBUG_LOGF("[INFO] Extracting sapling-output.params (%zu MB)...\n", outputRes->size / (1024*1024));
if (!extractResource(outputRes, dest)) { if (!extractResource(outputRes, dest)) {
success = false; success = false;
@@ -274,7 +333,7 @@ bool extractEmbeddedResources()
const EmbeddedResource* asmapRes = getEmbeddedResource(RESOURCE_ASMAP); const EmbeddedResource* asmapRes = getEmbeddedResource(RESOURCE_ASMAP);
if (asmapRes) { if (asmapRes) {
std::string dest = daemonDir + pathSep + RESOURCE_ASMAP; std::string dest = daemonDir + pathSep + RESOURCE_ASMAP;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(asmapRes, dest)) {
DEBUG_LOGF("[INFO] Extracting asmap.dat...\n"); DEBUG_LOGF("[INFO] Extracting asmap.dat...\n");
if (!extractResource(asmapRes, dest)) { if (!extractResource(asmapRes, dest)) {
success = false; success = false;
@@ -291,33 +350,48 @@ bool extractEmbeddedResources()
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD); const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
if (daemonRes) { if (daemonRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD; std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(daemonRes, dest)) {
DEBUG_LOGF("[INFO] Extracting dragonxd.exe (%zu MB)...\n", daemonRes->size / (1024*1024)); if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
if (!extractResource(daemonRes, dest)) { if (!extractResource(daemonRes, dest)) {
success = false; success = false;
} }
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
} }
} }
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI); const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
if (cliRes) { if (cliRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI; std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(cliRes, dest)) {
DEBUG_LOGF("[INFO] Extracting dragonx-cli.exe (%zu MB)...\n", cliRes->size / (1024*1024)); if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
if (!extractResource(cliRes, dest)) { if (!extractResource(cliRes, dest)) {
success = false; success = false;
} }
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
} }
} }
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX); const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
if (txRes) { if (txRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX; std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(txRes, dest)) {
DEBUG_LOGF("[INFO] Extracting dragonx-tx.exe (%zu MB)...\n", txRes->size / (1024*1024)); if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
if (!extractResource(txRes, dest)) { if (!extractResource(txRes, dest)) {
success = false; success = false;
} }
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
} }
} }
#endif #endif
@@ -326,11 +400,16 @@ bool extractEmbeddedResources()
const EmbeddedResource* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG); const EmbeddedResource* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
if (xmrigRes) { if (xmrigRes) {
std::string dest = daemonDir + pathSep + RESOURCE_XMRIG; std::string dest = daemonDir + pathSep + RESOURCE_XMRIG;
if (!std::filesystem::exists(dest)) { if (resourceNeedsUpdate(xmrigRes, dest)) {
DEBUG_LOGF("[INFO] Extracting xmrig.exe (%zu MB)...\n", xmrigRes->size / (1024*1024)); if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale xmrig (size mismatch)...\n");
DEBUG_LOGF("[INFO] Extracting xmrig (%zu MB)...\n", xmrigRes->size / (1024*1024));
if (!extractResource(xmrigRes, dest)) { if (!extractResource(xmrigRes, dest)) {
success = false; success = false;
} }
#ifndef _WIN32
else { chmod(dest.c_str(), 0755); }
#endif
} }
} }
#endif #endif
@@ -360,7 +439,8 @@ bool needsDaemonExtraction()
#else #else
std::string daemonPath = daemonDir + "/dragonxd"; std::string daemonPath = daemonDir + "/dragonxd";
#endif #endif
return !std::filesystem::exists(daemonPath); const auto* res = getEmbeddedResource(RESOURCE_DRAGONXD);
return resourceNeedsUpdate(res, daemonPath);
#else #else
return false; return false;
#endif #endif
@@ -426,7 +506,8 @@ bool needsXmrigExtraction()
#else #else
std::string xmrigPath = daemonDir + "/xmrig"; std::string xmrigPath = daemonDir + "/xmrig";
#endif #endif
return !std::filesystem::exists(xmrigPath); const auto* res = getEmbeddedResource(RESOURCE_XMRIG);
return resourceNeedsUpdate(res, xmrigPath);
#else #else
return false; return false;
#endif #endif

View File

@@ -55,6 +55,11 @@ const EmbeddedTheme* getEmbeddedThemes();
// Returns number of themes extracted // Returns number of themes extracted
int extractBundledThemes(const std::string& destDir); int extractBundledThemes(const std::string& destDir);
// Update stale bundled theme files in the given directory.
// Compares on-disk file size to embedded size; overwrites on mismatch.
// Returns number of themes updated.
int updateBundledThemes(const std::string& dir);
// Check if daemon needs to be extracted // Check if daemon needs to be extracted
bool needsDaemonExtraction(); bool needsDaemonExtraction();

View File

@@ -118,13 +118,16 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
std::string key = line.substr(0, eq_pos); std::string key = line.substr(0, eq_pos);
std::string value = line.substr(eq_pos + 1); std::string value = line.substr(eq_pos + 1);
// Trim whitespace // Trim whitespace (including \r from Windows line endings)
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) { while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r')) {
key.pop_back(); key.pop_back();
} }
while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) { while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) {
value.erase(0, 1); value.erase(0, 1);
} }
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || value.back() == '\r')) {
value.pop_back();
}
// Map to config // Map to config
if (key == "rpcuser") { if (key == "rpcuser") {
@@ -172,6 +175,16 @@ ConnectionConfig Connection::autoDetectConfig()
} }
} }
// If rpcpassword is empty, the daemon may be using .cookie auth
if (config.rpcpassword.empty()) {
std::string cookieUser, cookiePass;
if (readAuthCookie(data_dir, cookieUser, cookiePass)) {
config.rpcuser = cookieUser;
config.rpcpassword = cookiePass;
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
}
}
// Set defaults for missing values // Set defaults for missing values
if (config.host.empty()) { if (config.host.empty()) {
config.host = DRAGONX_DEFAULT_RPC_HOST; config.host = DRAGONX_DEFAULT_RPC_HOST;
@@ -319,5 +332,40 @@ bool Connection::ensureEncryptionEnabled(const std::string& confPath)
return true; return true;
} }
bool Connection::readAuthCookie(const std::string& dataDir, std::string& user, std::string& password)
{
if (dataDir.empty()) return false;
#ifdef _WIN32
std::string cookiePath = dataDir + "\\.cookie";
#else
std::string cookiePath = dataDir + "/.cookie";
#endif
std::ifstream file(cookiePath);
if (!file.is_open()) return false;
std::string cookie;
std::getline(file, cookie);
file.close();
// Cookie format: __cookie__:base64encodedpassword
size_t colonPos = cookie.find(':');
if (colonPos == std::string::npos || colonPos == 0) return false;
user = cookie.substr(0, colonPos);
password = cookie.substr(colonPos + 1);
// Trim \r if present (Windows line endings)
while (!password.empty() && (password.back() == '\r' || password.back() == '\n')) {
password.pop_back();
}
if (user.empty() || password.empty()) return false;
DEBUG_LOGF("Read auth cookie from: %s (user=%s)\n", cookiePath.c_str(), user.c_str());
return true;
}
} // namespace rpc } // namespace rpc
} // namespace dragonx } // namespace dragonx

View File

@@ -87,6 +87,15 @@ public:
*/ */
static bool ensureEncryptionEnabled(const std::string& confPath); static bool ensureEncryptionEnabled(const std::string& confPath);
/**
* @brief Try to read .cookie auth file from the data directory
* @param dataDir Path to the daemon data directory
* @param user Output: cookie username (__cookie__)
* @param password Output: cookie password
* @return true if cookie file was read successfully
*/
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
private: private:
}; };

View File

@@ -100,7 +100,20 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
last_connect_error_ = e.what(); last_connect_error_ = e.what();
DEBUG_LOGF("Connection failed: %s\n", e.what()); // Daemon warmup messages (Loading block index, Verifying blocks, etc.)
// are normal startup progress — don't label them "Connection failed".
std::string msg = e.what();
bool isWarmup = (msg.find("Loading") != std::string::npos ||
msg.find("Verifying") != std::string::npos ||
msg.find("Activating") != std::string::npos ||
msg.find("Rewinding") != std::string::npos ||
msg.find("Rescanning") != std::string::npos ||
msg.find("Pruning") != std::string::npos);
if (isWarmup) {
DEBUG_LOGF("Daemon starting: %s\n", msg.c_str());
} else {
DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
}
} }
connected_ = false; connected_ = false;

View File

@@ -116,6 +116,9 @@ static bool sp_stop_external_daemon = false;
// Mining — mine when idle // Mining — mine when idle
static bool sp_mine_when_idle = false; static bool sp_mine_when_idle = false;
static int sp_mine_idle_delay = 120; static int sp_mine_idle_delay = 120;
static bool sp_idle_thread_scaling = false;
static int sp_idle_threads_active = 0;
static int sp_idle_threads_idle = 0;
static bool sp_verbose_logging = false; static bool sp_verbose_logging = false;
// Debug logging categories // Debug logging categories
@@ -125,6 +128,7 @@ static bool sp_debug_expanded = false; // collapsible card state
static bool sp_effects_expanded = false; // "Advanced Effects..." toggle static bool sp_effects_expanded = false; // "Advanced Effects..." toggle
static bool sp_tools_expanded = false; // "Tools & Actions..." toggle static bool sp_tools_expanded = false; // "Tools & Actions..." toggle
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
static bool sp_confirm_delete_blockchain = false; // confirmation dialog for deleting blockchain data
// (APPEARANCE card now uses ChannelsSplit like all other cards) // (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -181,6 +185,9 @@ static void loadSettingsPageState(config::Settings* settings) {
sp_stop_external_daemon = settings->getStopExternalDaemon(); sp_stop_external_daemon = settings->getStopExternalDaemon();
sp_mine_when_idle = settings->getMineWhenIdle(); sp_mine_when_idle = settings->getMineWhenIdle();
sp_mine_idle_delay = settings->getMineIdleDelay(); sp_mine_idle_delay = settings->getMineIdleDelay();
sp_idle_thread_scaling = settings->getIdleThreadScaling();
sp_idle_threads_active = settings->getIdleThreadsActive();
sp_idle_threads_idle = settings->getIdleThreadsIdle();
sp_verbose_logging = settings->getVerboseLogging(); sp_verbose_logging = settings->getVerboseLogging();
sp_debug_categories = settings->getDebugCategories(); sp_debug_categories = settings->getDebugCategories();
sp_debug_cats_dirty = false; sp_debug_cats_dirty = false;
@@ -230,6 +237,9 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->setStopExternalDaemon(sp_stop_external_daemon); settings->setStopExternalDaemon(sp_stop_external_daemon);
settings->setMineWhenIdle(sp_mine_when_idle); settings->setMineWhenIdle(sp_mine_when_idle);
settings->setMineIdleDelay(sp_mine_idle_delay); settings->setMineIdleDelay(sp_mine_idle_delay);
settings->setIdleThreadScaling(sp_idle_thread_scaling);
settings->setIdleThreadsActive(sp_idle_threads_active);
settings->setIdleThreadsIdle(sp_idle_threads_idle);
settings->setVerboseLogging(sp_verbose_logging); settings->setVerboseLogging(sp_verbose_logging);
settings->setDebugCategories(sp_debug_categories); settings->setDebugCategories(sp_debug_categories);
@@ -1485,6 +1495,15 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan")); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled(); ImGui::EndDisabled();
// Delete blockchain button (always available when using embedded daemon)
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
sp_confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
ImGui::EndDisabled();
} }
ImGui::PopFont(); ImGui::PopFont();
@@ -1737,6 +1756,20 @@ void RenderSettingsPage(App* app) {
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf); ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
ImGui::PopFont(); ImGui::PopFont();
// Daemon version
{
const auto& st = app->state();
if (st.daemon_version > 0) {
int dmaj = st.daemon_version / 1000000;
int dmin = (st.daemon_version / 10000) % 100;
int dpat = (st.daemon_version / 100) % 100;
ImGui::PushFont(body2);
snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf);
ImGui::PopFont();
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(body2); ImGui::PushFont(body2);
@@ -2009,6 +2042,39 @@ void RenderSettingsPage(App* app) {
} }
} }
// Confirmation dialog for deleting blockchain data
if (sp_confirm_delete_blockchain) {
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &sp_confirm_delete_blockchain, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
sp_confirm_delete_blockchain = 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(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
app->deleteBlockchainData();
sp_confirm_delete_blockchain = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
} }
} // namespace ui } // namespace ui

View File

@@ -1,848 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "settings_page.h"
#include "../../app.h"
#include "../../version.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../rpc/rpc_client.h"
#include "../theme.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../schema/skin_manager.h"
#include "../notifications.h"
#include "../effects/imgui_acrylic.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../windows/validate_address_dialog.h"
#include "../windows/address_book_dialog.h"
#include "../windows/shield_dialog.h"
#include "../windows/request_payment_dialog.h"
#include "../windows/block_info_dialog.h"
#include "../windows/export_all_keys_dialog.h"
#include "../windows/export_transactions_dialog.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <vector>
#include <filesystem>
#include <algorithm>
#include <cmath>
namespace dragonx {
namespace ui {
using namespace material;
// ============================================================================
// Settings state — loaded from config::Settings on first render
// ============================================================================
static bool sp_initialized = false;
static int sp_language_index = 0;
static bool sp_save_ztxs = true;
static bool sp_allow_custom_fees = false;
static bool sp_auto_shield = false;
static bool sp_fetch_prices = true;
static bool sp_use_tor = false;
static char sp_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
static char sp_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
static char sp_rpc_user[64] = "";
static char sp_rpc_password[64] = "";
static char sp_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
static char sp_addr_explorer[256] = "https://explorer.dragonx.is/address/";
// Acrylic settings
static bool sp_acrylic_enabled = true;
static int sp_acrylic_quality = 2;
static float sp_blur_multiplier = 1.0f;
static bool sp_reduced_transparency = false;
static void loadSettingsPageState(config::Settings* settings) {
if (!settings) return;
sp_save_ztxs = settings->getSaveZtxs();
sp_allow_custom_fees = settings->getAllowCustomFees();
sp_auto_shield = settings->getAutoShield();
sp_fetch_prices = settings->getFetchPrices();
sp_use_tor = settings->getUseTor();
strncpy(sp_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(sp_tx_explorer) - 1);
strncpy(sp_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(sp_addr_explorer) - 1);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::string current_lang = settings->getLanguage();
if (current_lang.empty()) current_lang = "en";
sp_language_index = 0;
int idx = 0;
for (const auto& lang : languages) {
if (lang.first == current_lang) {
sp_language_index = idx;
break;
}
idx++;
}
sp_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
sp_acrylic_quality = static_cast<int>(effects::ImGuiAcrylic::GetQuality());
sp_blur_multiplier = effects::ImGuiAcrylic::GetBlurMultiplier();
sp_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
sp_initialized = true;
}
static void saveSettingsPageState(config::Settings* settings) {
if (!settings) return;
settings->setTheme(settings->getSkinId());
settings->setSaveZtxs(sp_save_ztxs);
settings->setAllowCustomFees(sp_allow_custom_fees);
settings->setAutoShield(sp_auto_shield);
settings->setFetchPrices(sp_fetch_prices);
settings->setUseTor(sp_use_tor);
settings->setTxExplorerUrl(sp_tx_explorer);
settings->setAddressExplorerUrl(sp_addr_explorer);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
auto it = languages.begin();
std::advance(it, sp_language_index);
if (it != languages.end()) {
settings->setLanguage(it->first);
}
settings->save();
}
// ============================================================================
// Settings Page Renderer
// ============================================================================
void RenderSettingsPage(App* app) {
// Load settings state on first render
if (!sp_initialized && app->settings()) {
loadSettingsPageState(app->settings());
}
auto& S = schema::UI();
// Responsive layout — matches other tabs
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
float availWidth = contentAvail.x;
float hs = Layout::hScale(availWidth);
float vs = Layout::vScale(contentAvail.y);
float pad = Layout::cardInnerPadding();
float gap = Layout::cardGap();
float glassRound = Layout::glassRounding();
(void)vs;
char buf[256];
// Label column position — adaptive to width
float labelW = std::max(100.0f, 120.0f * hs);
// Input field width — fill remaining space in card
float inputW = std::max(180.0f, availWidth - labelW - pad * 3);
// Scrollable content area — NoBackground matches other tabs
ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false,
ImGuiWindowFlags_NoBackground);
// Get draw list AFTER BeginChild so we draw on the child window's list
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
ImFont* sub1 = Type().subtitle1();
// ====================================================================
// GENERAL — Appearance & Preferences card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "APPEARANCE");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Measure content height for card
// We'll use ImGui cursor-based layout inside the card
ImVec2 cardMin = ImGui::GetCursorScreenPos();
// Use a child window inside the glass panel for layout
// First draw the glass panel, then place content
// We need to estimate height — use a generous estimate and clip
float rowH = body2->LegacySize + Layout::spacingSm();
float sectionGap = Layout::spacingMd();
float cardH = pad // top pad
+ rowH // Theme
+ rowH // Language
+ sectionGap
+ rowH * 5 // Visual effects (acrylic + quality + blur + reduce + gap)
+ pad; // bottom pad
if (!sp_acrylic_enabled) cardH -= rowH * 2;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// --- Theme row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Theme");
ImGui::SameLine(labelW);
auto& skinMgr = schema::SkinManager::instance();
const auto& skins = skinMgr.available();
std::string active_preview = "DragonX";
bool active_is_custom = false;
for (const auto& skin : skins) {
if (skin.id == skinMgr.activeSkinId()) {
active_preview = skin.name;
active_is_custom = !skin.bundled;
break;
}
}
float refreshBtnW = 80.0f;
ImGui::SetNextItemWidth(inputW - refreshBtnW - Layout::spacingSm());
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
ImGui::TextDisabled("Built-in");
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (!skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) ImGui::SetItemDefaultFocus();
}
bool has_custom = false;
for (const auto& skin : skins) {
if (!skin.bundled) { has_custom = true; break; }
}
if (has_custom) {
ImGui::Spacing();
ImGui::TextDisabled("Custom");
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string lbl = skin.name + " (invalid)";
ImGui::Selectable(lbl.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", skin.validationError.c_str());
} else {
std::string lbl = skin.name;
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
if (ImGui::Selectable(lbl.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) ImGui::SetItemDefaultFocus();
}
}
}
ImGui::EndCombo();
}
if (active_is_custom) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Custom theme active");
}
ImGui::SameLine();
if (TactileButton("Refresh", ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
schema::SkinManager::instance().refresh();
Notifications::instance().info("Theme list refreshed");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// --- Language row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Language");
ImGui::SameLine(labelW);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::vector<const char*> lang_names;
lang_names.reserve(languages.size());
for (const auto& lang : languages) {
lang_names.push_back(lang.second.c_str());
}
ImGui::SetNextItemWidth(inputW);
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
static_cast<int>(lang_names.size()))) {
auto it = languages.begin();
std::advance(it, sp_language_index);
i18n.loadLanguage(it->first);
}
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Visual Effects subsection ---
dl->AddText(ovFont, ovFont->LegacySize, ImGui::GetCursorScreenPos(), OnSurfaceMedium(), "VISUAL EFFECTS");
ImGui::Dummy(ImVec2(0, ovFont->LegacySize + Layout::spacingXs()));
{
// Two-column: left = acrylic toggle + reduce toggle, right = quality + blur
float colW = (availWidth - pad * 2 - Layout::spacingLg()) * 0.5f;
if (ImGui::Checkbox("Acrylic effects", &sp_acrylic_enabled)) {
effects::ImGuiAcrylic::SetEnabled(sp_acrylic_enabled);
}
if (sp_acrylic_enabled) {
ImGui::SameLine(labelW + colW + Layout::spacingLg());
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Quality");
ImGui::PopFont();
ImGui::SameLine();
const char* quality_levels[] = { "Off", "Low", "Medium", "High" };
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
if (ImGui::Combo("##AcrylicQuality", &sp_acrylic_quality, quality_levels,
IM_ARRAYSIZE(quality_levels))) {
effects::ImGuiAcrylic::SetQuality(
static_cast<effects::AcrylicQuality>(sp_acrylic_quality));
}
}
if (ImGui::Checkbox("Reduce transparency", &sp_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(sp_reduced_transparency);
}
if (sp_acrylic_enabled) {
ImGui::SameLine(labelW + colW + Layout::spacingLg());
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Blur");
ImGui::PopFont();
ImGui::SameLine();
ImGui::SetNextItemWidth(std::max(100.0f, colW - 80.0f));
if (ImGui::SliderFloat("##BlurAmount", &sp_blur_multiplier, 0.5f, 2.0f, "%.1fx")) {
effects::ImGuiAcrylic::SetBlurMultiplier(sp_blur_multiplier);
}
}
}
// Recalculate actual card bottom from cursor
ImVec2 cardEnd = ImGui::GetCursorScreenPos();
float actualH = (cardEnd.y - cardMin.y) + pad;
if (actualH != cardH) {
// Redraw glass panel with correct height
cardMax.y = cardMin.y + actualH;
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// PRIVACY & OPTIONS — Two cards side by side
// ====================================================================
{
float colW = (availWidth - gap) * 0.5f;
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
// --- Privacy card (left) ---
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PRIVACY");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
ImGui::Checkbox("Save shielded tx history", &sp_save_ztxs);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Checkbox("Auto-shield transparent funds", &sp_auto_shield);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Checkbox("Use Tor for connections", &sp_use_tor);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// --- Options card (right) ---
{
float rightX = rowOrigin.x + colW + gap;
// Position cursor at the same Y as privacy label
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "OPTIONS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (body2->LegacySize + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
ImGui::Checkbox("Allow custom transaction fees", &sp_allow_custom_fees);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Checkbox("Fetch price data from CoinGecko", &sp_fetch_prices);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// Advance past the side-by-side row
// Find the maximum bottom
float rowBottom = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// EXPLORER URLS + SAVE — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK EXPLORER & SETTINGS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingSm();
float cardH = pad + rowH * 2 + Layout::spacingSm()
+ body2->LegacySize + Layout::spacingMd() // save/reset row
+ pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// Transaction URL
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Transaction URL");
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(inputW);
ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer));
ImGui::PopFont();
}
// Address URL
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Address URL");
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(inputW);
ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Save / Reset — right-aligned
{
float saveBtnW = 120.0f;
float resetBtnW = 140.0f;
float btnGap = Layout::spacingSm();
if (TactileButton("Save Settings", ImVec2(saveBtnW, 0), S.resolveFont("button"))) {
saveSettingsPageState(app->settings());
Notifications::instance().success("Settings saved");
}
ImGui::SameLine(0, btnGap);
if (TactileButton("Reset to Defaults", ImVec2(resetBtnW, 0), S.resolveFont("button"))) {
if (app->settings()) {
loadSettingsPageState(app->settings());
Notifications::instance().info("Settings reloaded from disk");
}
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// KEYS & BACKUP — card with two rows
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "KEYS & BACKUP");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + btnRowH * 2 + Layout::spacingSm() + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// Keys row — spread buttons across width
{
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
if (TactileButton("Import Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
app->showImportKeyDialog();
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Export Private Key...", ImVec2(btnW, 0), S.resolveFont("button"))) {
app->showExportKeyDialog();
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Export All Keys...", ImVec2(btnW, 0), S.resolveFont("button"))) {
ExportAllKeysDialog::show();
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Backup row
{
float btnW = (availWidth - pad * 2 - Layout::spacingSm()) / 2.0f;
if (TactileButton("Backup wallet.dat...", ImVec2(btnW, 0), S.resolveFont("button"))) {
app->showBackupDialog();
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Export Transactions CSV...", ImVec2(btnW, 0), S.resolveFont("button"))) {
ExportTransactionsDialog::show();
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// WALLET — Two cards side by side: Tools | Maintenance
// ====================================================================
{
float colW = (availWidth - gap) * 0.5f;
ImVec2 rowOrigin = ImGui::GetCursorScreenPos();
float btnH = std::max(28.0f, 34.0f * vs);
// --- Wallet Tools card (left) ---
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET TOOLS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
float innerBtnW = colW - pad * 2;
if (TactileButton("Address Book...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
AddressBookDialog::show();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Validate Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
ValidateAddressDialog::show();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Request Payment...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
RequestPaymentDialog::show();
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// --- Shielding & Maintenance card (right) ---
{
float rightX = rowOrigin.x + colW + gap;
ImGui::SetCursorScreenPos(ImVec2(rightX, rowOrigin.y));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SHIELDING & MAINTENANCE");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = pad + (btnH + Layout::spacingSm()) * 3 + pad;
ImVec2 cardMax(cardMin.x + colW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
float innerBtnW = colW - pad * 2;
if (TactileButton("Shield Mining Rewards...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Merge to Address...", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("Rescan Blockchain", ImVec2(innerBtnW, btnH), S.resolveFont("button"))) {
if (app->rpc() && app->rpc()->isConnected()) {
app->rpc()->rescanBlockchain(0, [](bool success, const nlohmann::json&) {
if (success)
Notifications::instance().success("Blockchain rescan started");
else
Notifications::instance().error("Failed to start rescan");
});
} else {
Notifications::instance().warning("Not connected to daemon");
}
}
ImGui::EndDisabled();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(colW, 0));
}
// Advance past sidebar row
float rowBottom = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rowOrigin.x, rowBottom));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// WALLET INFO — Small card with file path + clear history
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET INFO");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingSm();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + rowH * 2 + btnRowH + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Location");
ImGui::SameLine(labelW);
ImGui::TextUnformatted(wallet_path.c_str());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("File size");
ImGui::SameLine(labelW);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::TextUnformatted(size_str.c_str());
} else {
ImGui::TextDisabled("Not found");
}
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton("Clear Z-Transaction History", ImVec2(0, 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");
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// NODE / RPC — card with two-column inputs
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NODE / RPC CONNECTION");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingSm();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + rowH * 2 + Layout::spacingSm() + rowH * 2 + Layout::spacingSm()
+ capFont->LegacySize + Layout::spacingSm()
+ btnRowH + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// Two-column: Host+Port on one line, User+Pass on next
float halfInput = (availWidth - pad * 2 - labelW * 2 - Layout::spacingLg()) * 0.5f;
float rpcLabelW = std::max(70.0f, 85.0f * hs);
ImGui::PushFont(body2);
// Row 1: Host + Port
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Host");
ImGui::SameLine(rpcLabelW);
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Port");
ImGui::SameLine();
ImGui::SetNextItemWidth(std::max(60.0f, halfInput * 0.4f));
ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Row 2: Username + Password
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Username");
ImGui::SameLine(rpcLabelW);
ImGui::SetNextItemWidth(halfInput + labelW - rpcLabelW);
ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Password");
ImGui::SameLine();
ImGui::SetNextItemWidth(halfInput);
ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password),
ImGuiInputTextFlags_Password);
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
"Connection settings are usually auto-detected from DRAGONX.conf");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (TactileButton("Test Connection", ImVec2(0, 0), S.resolveFont("button"))) {
if (app->rpc() && app->rpc()->isConnected()) {
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
(void)result;
if (error.empty())
Notifications::instance().success("RPC connection OK");
else
Notifications::instance().error("RPC error: " + error);
});
} else {
Notifications::instance().warning("Not connected to daemon");
}
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Block Info...", ImVec2(0, 0), S.resolveFont("button"))) {
BlockInfoDialog::show(app->getBlockHeight());
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// ABOUT — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ABOUT");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float rowH = body2->LegacySize + Layout::spacingXs();
float btnRowH = std::max(28.0f, 34.0f * vs) + Layout::spacingSm();
float cardH = pad + sub1->LegacySize + rowH * 2 + Layout::spacingSm()
+ body2->LegacySize * 2 + Layout::spacingSm()
+ capFont->LegacySize * 2 + Layout::spacingMd()
+ btnRowH + pad;
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + pad, cardMin.y + pad));
// App name + version on same line
ImGui::PushFont(sub1);
ImGui::TextUnformatted(DRAGONX_APP_NAME);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingLg());
ImGui::PushFont(body2);
snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION);
ImGui::TextUnformatted(buf);
ImGui::SameLine(0, Layout::spacingLg());
snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION);
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(body2);
ImGui::PushTextWrapPos(cardMax.x - pad);
ImGui::TextUnformatted(
"A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::PopTextWrapPos();
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(capFont);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License");
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Buttons — spread across width
{
float btnW = (availWidth - pad * 2 - Layout::spacingSm() * 2) / 3.0f;
if (TactileButton("Website", ImVec2(btnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://dragonx.is");
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Report Bug", ImVec2(btnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://git.hush.is/hush/SilentDragonX/issues");
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Block Explorer", ImVec2(btnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://explorer.dragonx.is");
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
ImGui::EndChild(); // ##SettingsPageScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -43,6 +43,11 @@ std::string SkinManager::getBundledSkinsDirectory() {
fs::path themes_dir = exe_dir / "res" / "themes"; fs::path themes_dir = exe_dir / "res" / "themes";
if (fs::exists(themes_dir)) { if (fs::exists(themes_dir)) {
// Update any stale overlay themes from embedded versions
int updated = resources::updateBundledThemes(themes_dir.string());
if (updated > 0)
DEBUG_LOGF("[SkinManager] Updated %d stale theme(s) in %s\n",
updated, themes_dir.string().c_str());
return themes_dir.string(); return themes_dir.string();
} }

View File

@@ -75,12 +75,18 @@ bool UISchema::loadFromFile(const std::string& path) {
parseSections(static_cast<const void*>(components), "components"); 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}) // Parse flat sections (2-level: sectionName.elementName → {style object})
for (const auto& flatSection : {"business", "animations", "console", for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar", "backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive", "qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts", "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()) { if (auto* sec = root[flatSection].as_table()) {
parseFlatSection(static_cast<const void*>(sec), flatSection); 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"); 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}) // Parse flat sections (2-level: sectionName.elementName → {style object})
for (const auto& flatSection : {"business", "animations", "console", for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar", "backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive", "qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts", "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()) { if (auto* sec = root[flatSection].as_table()) {
parseFlatSection(static_cast<const void*>(sec), flatSection); 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) { bool UISchema::mergeOverlayFromFile(const std::string& path) {
@@ -211,14 +223,40 @@ bool UISchema::mergeOverlayFromFile(const std::string& path) {
parseTheme(static_cast<const void*>(theme)); parseTheme(static_cast<const void*>(theme));
} }
// Merge backdrop section (gradient colors, alpha/transparency values) // Merge breakpoints + globals
if (auto* sec = root["backdrop"].as_table()) { if (auto* bp = root["breakpoints"].as_table()) {
parseFlatSection(static_cast<const void*>(sec), "backdrop"); 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) // Merge tabs, dialogs, components (3-level sections)
if (auto* sec = root["effects"].as_table()) { if (auto* tabs = root["tabs"].as_table()) {
parseFlatSection(static_cast<const void*>(sec), "effects"); 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; overlayPath_ = path;

View File

@@ -528,7 +528,8 @@ inline void SettingsScreen::renderAboutSection() {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - nameSize.x) * 0.5f); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - nameSize.x) * 0.5f);
Typography::instance().text(TypeStyle::H6, appName); Typography::instance().text(TypeStyle::H6, appName);
const char* version = "Version 1.0.0-imgui"; char version[64];
snprintf(version, sizeof(version), "Version %s-imgui", DRAGONX_VERSION);
ImVec2 versionSize = ImGui::CalcTextSize(version); ImVec2 versionSize = ImGui::CalcTextSize(version);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - versionSize.x) * 0.5f); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - versionSize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), version); Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), version);

View File

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

View File

@@ -142,6 +142,7 @@ static constexpr int s_legacyLayoutCount = 10;
static std::vector<BalanceLayoutEntry> s_balanceLayouts; static std::vector<BalanceLayoutEntry> s_balanceLayouts;
static std::string s_defaultLayoutId = "classic"; static std::string s_defaultLayoutId = "classic";
static bool s_layoutConfigLoaded = false; static bool s_layoutConfigLoaded = false;
static bool s_generating_z_address = false;
static void LoadBalanceLayoutConfig() static void LoadBalanceLayoutConfig()
{ {
@@ -803,8 +804,16 @@ static void RenderBalanceClassic(App* app)
bool addrSyncing = state.sync.syncing && !state.sync.isSynced(); bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(addrSyncing); ImGui::BeginDisabled(addrSyncing);
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) { app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str()); DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
}); });
} }
@@ -1420,9 +1429,18 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced(); bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(sharedAddrSyncing); ImGui::BeginDisabled(sharedAddrSyncing);
if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) { app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str()); DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
}); });
} }

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

@@ -33,6 +33,8 @@ static std::string s_status;
static int s_total_keys = 0; static int s_total_keys = 0;
static int s_imported_keys = 0; static int s_imported_keys = 0;
static int s_failed_keys = 0; static int s_failed_keys = 0;
static bool s_paste_previewing = false;
static std::string s_preview_text;
// Helper to detect key type // Helper to detect key type
static std::string detectKeyType(const std::string& key) static std::string detectKeyType(const std::string& key)
@@ -95,6 +97,8 @@ void ImportKeyDialog::show()
s_total_keys = 0; s_total_keys = 0;
s_imported_keys = 0; s_imported_keys = 0;
s_failed_keys = 0; s_failed_keys = 0;
s_paste_previewing = false;
s_preview_text.clear();
} }
bool ImportKeyDialog::isOpen() bool ImportKeyDialog::isOpen()
@@ -134,6 +138,43 @@ void ImportKeyDialog::render(App* app)
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("import_key_tooltip")); ImGui::SetTooltip("%s", TR("import_key_tooltip"));
} }
// Validation indicator inline with title — check preview text during hover,
// otherwise check the actual input
{
const char* checkSrc = s_key_input;
if (s_paste_previewing && !s_preview_text.empty())
checkSrc = s_preview_text.c_str();
if (checkSrc[0] != '\0') {
auto previewKeys = splitKeys(checkSrc);
int pz = 0, pt = 0, pu = 0;
for (const auto& k : previewKeys) {
std::string kt = detectKeyType(k);
if (kt == "z-spending") pz++;
else if (kt == "t-privkey") pt++;
else pu++;
}
if (pz > 0 || pt > 0) {
ImGui::SameLine();
char vbuf[128];
if (pz > 0 && pt > 0)
snprintf(vbuf, sizeof(vbuf), "%d shielded, %d transparent", pz, pt);
else if (pz > 0)
snprintf(vbuf, sizeof(vbuf), "%d shielded key(s)", pz);
else
snprintf(vbuf, sizeof(vbuf), "%d transparent key(s)", pt);
material::Type().textColored(material::TypeStyle::Caption, material::Success(), vbuf);
if (pu > 0) {
ImGui::SameLine();
snprintf(vbuf, sizeof(vbuf), "(%d unrecognized)", pu);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), vbuf);
}
} else if (pu > 0 && !s_paste_previewing) {
ImGui::SameLine();
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
}
}
}
if (s_importing) { if (s_importing) {
ImGui::BeginDisabled(); ImGui::BeginDisabled();
@@ -142,18 +183,82 @@ void ImportKeyDialog::render(App* app)
ImGui::SetNextItemWidth(-1); ImGui::SetNextItemWidth(-1);
ImGui::InputTextMultiline("##KeyInput", s_key_input, sizeof(s_key_input), ImGui::InputTextMultiline("##KeyInput", s_key_input, sizeof(s_key_input),
ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput); ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput);
ImVec2 inputMin = ImGui::GetItemRectMin();
ImVec2 inputMax = ImGui::GetItemRectMax();
// Detect paste button hover before drawing it
ImVec2 pasteBtnPos = ImGui::GetCursorScreenPos();
// Estimate button size for hover detection
ImFont* btnFont = S.resolveFont(importBtn.font);
ImVec2 pasteBtnSize = btnFont
? ImVec2(btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("paste_from_clipboard")).x
+ ImGui::GetStyle().FramePadding.x * 2,
ImGui::GetFrameHeight())
: ImVec2(150, ImGui::GetFrameHeight());
bool paste_hovered = material::IsRectHovered(pasteBtnPos,
ImVec2(pasteBtnPos.x + pasteBtnSize.x, pasteBtnPos.y + pasteBtnSize.y));
// Handle preview state
if (paste_hovered && !s_paste_previewing) {
const char* clip = ImGui::GetClipboardText();
if (clip && clip[0] != '\0') {
std::string trimmed(clip);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
if (!trimmed.empty() && s_key_input[0] == '\0') {
s_preview_text = trimmed;
s_paste_previewing = true;
}
}
} else if (!paste_hovered && s_paste_previewing) {
s_paste_previewing = false;
s_preview_text.clear();
}
// Draw transparent preview text overlay on the input field
if (s_paste_previewing && !s_preview_text.empty()) {
ImVec2 textPos(inputMin.x + ImGui::GetStyle().FramePadding.x,
inputMin.y + ImGui::GetStyle().FramePadding.y);
ImVec4 previewCol = ImGui::GetStyleColorVec4(ImGuiCol_Text);
previewCol.w = S.drawElement("dialogs.import-key", "paste-preview-alpha").sizeOr(0.3f);
size_t maxChars = (size_t)S.drawElement("dialogs.import-key", "paste-preview-max-chars").sizeOr(200.0f);
// Clip to input rect
ImGui::GetWindowDrawList()->PushClipRect(inputMin, inputMax, true);
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::ColorConvertFloat4ToU32(previewCol),
s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), maxChars));
ImGui::GetWindowDrawList()->PopClipRect();
}
// Paste button // Paste button
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) { if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
const char* clipboard = ImGui::GetClipboardText(); if (s_paste_previewing) {
if (clipboard) { snprintf(s_key_input, sizeof(s_key_input), "%s", s_preview_text.c_str());
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1); s_paste_previewing = false;
s_preview_text.clear();
} else {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
std::string trimmed(clipboard);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
}
} }
} }
ImGui::SameLine(); ImGui::SameLine();
if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) { if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
s_key_input[0] = '\0'; s_key_input[0] = '\0';
s_paste_previewing = false;
s_preview_text.clear();
} }
ImGui::Spacing(); ImGui::Spacing();

View File

@@ -140,6 +140,15 @@ void RenderMiningTab(App* app)
s_threads_initialized = true; s_threads_initialized = true;
} }
// Sync thread grid with actual count when idle thread scaling adjusts threads
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active) {
if (s_pool_mode && state.pool_mining.xmrig_running && state.pool_mining.threads_active > 0) {
s_selected_threads = std::min(state.pool_mining.threads_active, max_threads);
} else if (mining.generate && mining.genproclimit > 0) {
s_selected_threads = std::min(mining.genproclimit, max_threads);
}
}
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec; GlassPanelSpec glassSpec;
glassSpec.rounding = Layout::glassRounding(); glassSpec.rounding = Layout::glassRounding();
@@ -156,7 +165,9 @@ void RenderMiningTab(App* app)
s_pool_state_loaded = true; s_pool_state_loaded = true;
} }
// Default pool worker to user's first shielded address once addresses are available // Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
// is prompted to generate one before mining.
{ {
static bool s_pool_worker_defaulted = false; static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker); std::string workerStr(s_pool_worker);
@@ -169,18 +180,14 @@ void RenderMiningTab(App* app)
break; break;
} }
} }
if (defaultAddr.empty()) {
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
if (!defaultAddr.empty()) { if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true; s_pool_settings_dirty = true;
} else {
// No z-address yet — clear the placeholder "x" so field shows empty
s_pool_worker[0] = '\0';
s_pool_settings_dirty = true;
} }
s_pool_worker_defaulted = true; s_pool_worker_defaulted = true;
} }
@@ -536,7 +543,12 @@ void RenderMiningTab(App* app)
s_pool_settings_dirty = true; s_pool_settings_dirty = true;
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip")); std::string currentWorkerStr(s_pool_worker);
if (currentWorkerStr.empty()) {
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
} else {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
}
} }
// --- Worker: Dropdown arrow button --- // --- Worker: Dropdown arrow button ---
@@ -739,7 +751,8 @@ void RenderMiningTab(App* app)
if (btnClk) { if (btnClk) {
strncpy(s_pool_url, "pool.dragonx.is", sizeof(s_pool_url) - 1); strncpy(s_pool_url, "pool.dragonx.is", sizeof(s_pool_url) - 1);
// Default to user's first shielded address for pool payouts // Default to user's first shielded (z) address for pool payouts.
// Leave blank if no z-address exists yet.
std::string defaultAddr; std::string defaultAddr;
for (const auto& addr : state.addresses) { for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) { if (addr.type == "shielded" && !addr.address.empty()) {
@@ -747,15 +760,6 @@ void RenderMiningTab(App* app)
break; break;
} }
} }
if (defaultAddr.empty()) {
// Fallback to transparent if no shielded available
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true; s_pool_settings_dirty = true;
@@ -840,6 +844,7 @@ void RenderMiningTab(App* app)
float idleRightEdge = cardMax.x - pad; float idleRightEdge = cardMax.x - pad;
{ {
bool idleOn = app->settings()->getMineWhenIdle(); bool idleOn = app->settings()->getMineWhenIdle();
bool threadScaling = app->settings()->getIdleThreadScaling();
ImFont* icoFont = Type().iconSmall(); ImFont* icoFont = Type().iconSmall();
const char* idleIcon = ICON_MD_SCHEDULE; const char* idleIcon = ICON_MD_SCHEDULE;
float icoH = icoFont->LegacySize; float icoH = icoFont->LegacySize;
@@ -875,8 +880,40 @@ void RenderMiningTab(App* app)
idleRightEdge = btnX - 4.0f * dp; idleRightEdge = btnX - 4.0f * dp;
// Idle delay combo (to the left of the icon when enabled) // Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
if (idleOn) { if (idleOn) {
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
float sBtnX = idleRightEdge - btnSz;
float sBtnY = btnY;
if (threadScaling) {
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
sIcoCol, scaleIcon);
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setIdleThreadScaling(!threadScaling);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", threadScaling
? TR("mining_idle_scale_on_tooltip")
: TR("mining_idle_scale_off_tooltip"));
}
idleRightEdge = sBtnX - 4.0f * dp;
}
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
if (idleOn && !threadScaling) {
struct DelayOption { int seconds; const char* label; }; struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = { static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"} {30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
@@ -907,6 +944,111 @@ void RenderMiningTab(App* app)
idleRightEdge = comboX - 4.0f * dp; idleRightEdge = comboX - 4.0f * dp;
} }
// Thread scaling controls: idle delay + active threads / idle threads combos
if (idleOn && threadScaling) {
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
// Idle delay combo
{
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
// Idle threads combo (threads when system is idle)
{
int curVal = app->settings()->getIdleThreadsIdle();
if (curVal <= 0) curVal = hwThreads;
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsIdle(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
// Separator arrow icon
{
const char* arrowIcon = ICON_MD_ARROW_BACK;
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
float arrX = idleRightEdge - arrSz.x;
float arrY = curY + (headerH - arrSz.y) * 0.5f;
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
idleRightEdge = arrX - 4.0f * dp;
}
// Active threads combo (threads when user is active)
{
int curVal = app->settings()->getIdleThreadsActive();
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsActive(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
}
ImGui::SetCursorScreenPos(savedCur); ImGui::SetCursorScreenPos(savedCur);
} }

View File

@@ -65,6 +65,7 @@ static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config) // Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels; static std::map<std::string, std::string> s_address_labels;
static std::string s_pending_select_address; static std::string s_pending_select_address;
static bool s_generating_address = false;
// ============================================================================ // ============================================================================
// Helpers // Helpers
@@ -302,10 +303,18 @@ static void RenderAddressDropdown(App* app, float width) {
// New address button on same line // New address button on same line
ImGui::SameLine(0, Layout::spacingSm()); ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected()); ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) { if (s_generating_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
s_generating_address = true;
if (s_addr_type_filter != 2) { if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) { app->createNewZAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty()) if (addr.empty())
Notifications::instance().error(TR("failed_create_shielded")); Notifications::instance().error(TR("failed_create_shielded"));
else { else {
@@ -315,6 +324,7 @@ static void RenderAddressDropdown(App* app, float width) {
}); });
} else { } else {
app->createNewTAddress([](const std::string& addr) { app->createNewTAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty()) if (addr.empty())
Notifications::instance().error(TR("failed_create_transparent")); Notifications::instance().error(TR("failed_create_transparent"));
else { else {

View File

@@ -1,934 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Layout G: QR-Centered Hero
// - QR code dominates center as hero element
// - Address info wraps around the QR
// - Payment request section below QR
// - Horizontal address strip at bottom for fast switching
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../version.h"
#include "../../wallet_state.h"
#include "../../ui/widgets/qr_code.h"
#include "../sidebar.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../notifications.h"
#include "imgui.h"
#include <string>
#include <algorithm>
#include <cmath>
#include <map>
namespace dragonx {
namespace ui {
using namespace material;
// ============================================================================
// State
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[256] = "";
static std::string s_cached_qr_data;
static uintptr_t s_qr_texture = 0;
static bool s_payment_request_open = false;
// Track newly created addresses for NEW badge
static std::map<std::string, double> s_new_address_timestamps;
static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static char s_label_edit_buf[64] = "";
// Address type filter
static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T
// ============================================================================
// Helpers
// ============================================================================
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 35) {
if (addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
static void OpenExplorerURL(const std::string& address) {
std::string url = "https://explorer.dragonx.com/address/" + address;
#ifdef _WIN32
std::string cmd = "start \"\" \"" + url + "\"";
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
#else
std::string cmd = "xdg-open \"" + url + "\"";
#endif
system(cmd.c_str());
}
// ============================================================================
// Sync banner
// ============================================================================
static void RenderSyncBanner(const WalletState& state) {
if (!state.sync.syncing || state.sync.isSynced()) return;
float syncPct = (state.sync.headers > 0)
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf),
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.4f, 0.0f, 0.15f));
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, 28),
false, ImGuiWindowFlags_NoScrollbar);
ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), 6));
Type().textColored(TypeStyle::Caption, Warning(), syncBuf);
ImGui::EndChild();
ImGui::PopStyleColor();
}
// ============================================================================
// Track new addresses (detect creations)
// ============================================================================
static void TrackNewAddresses(const WalletState& state) {
if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) {
for (const auto& a : state.addresses) {
if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) {
s_new_address_timestamps[a.address] = ImGui::GetTime();
}
}
} else if (s_prev_address_count == 0) {
for (const auto& a : state.addresses) {
s_new_address_timestamps[a.address] = 0.0;
}
}
s_prev_address_count = state.addresses.size();
}
// ============================================================================
// Build sorted address groups
// ============================================================================
struct AddressGroups {
std::vector<int> shielded;
std::vector<int> transparent;
};
static AddressGroups BuildSortedAddressGroups(const WalletState& state) {
AddressGroups groups;
for (int i = 0; i < (int)state.addresses.size(); i++) {
if (state.addresses[i].type == "shielded")
groups.shielded.push_back(i);
else
groups.transparent.push_back(i);
}
std::sort(groups.shielded.begin(), groups.shielded.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
std::sort(groups.transparent.begin(), groups.transparent.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
return groups;
}
// ============================================================================
// QR Hero — the centerpiece of Layout G
// ============================================================================
static void RenderQRHero(App* app, ImDrawList* dl, const AddressInfo& addr,
float width, float qrSize,
const std::string& qr_data,
const GlassPanelSpec& glassSpec,
const WalletState& state,
ImFont* sub1, ImFont* /*body2*/, ImFont* capFont) {
char buf[128];
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
const char* typeBadge = isZ ? "Shielded" : "Transparent";
float qrPadding = Layout::spacingLg();
float totalQrSize = qrSize + qrPadding * 2;
float heroH = totalQrSize + 80.0f; // QR + info below
ImVec2 heroMin = ImGui::GetCursorScreenPos();
ImVec2 heroMax(heroMin.x + width, heroMin.y + heroH);
GlassPanelSpec heroGlass = glassSpec;
heroGlass.fillAlpha = 16;
heroGlass.borderAlpha = 35;
DrawGlassPanel(dl, heroMin, heroMax, heroGlass);
// --- Address info bar above QR ---
float infoBarH = 32.0f;
float cx = heroMin.x + Layout::spacingLg();
float cy = heroMin.y + Layout::spacingSm();
// Type badge circle + label
dl->AddCircleFilled(ImVec2(cx + 8, cy + 10), 8.0f, IM_COL32(255, 255, 255, 20));
const char* typeChar = isZ ? "Z" : "T";
ImVec2 tcSz = sub1->CalcTextSizeA(sub1->LegacySize, 100, 0, typeChar);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(cx + 8 - tcSz.x * 0.5f, cy + 10 - tcSz.y * 0.5f),
typeCol, typeChar);
// Education tooltip on badge
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::InvisibleButton("##TypeBadgeHero", ImVec2(22, 22));
if (ImGui::IsItemHovered()) {
if (isZ) {
ImGui::SetTooltip(
"Shielded Address (Z)\n"
"- Full transaction privacy\n"
"- Encrypted sender, receiver, amount\n"
"- Supports encrypted memos\n"
"- Recommended for privacy");
} else {
ImGui::SetTooltip(
"Transparent Address (T)\n"
"- Publicly visible on blockchain\n"
"- Similar to Bitcoin addresses\n"
"- No memo support\n"
"- Use Z addresses for privacy");
}
}
// Type label text
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 24, cy + 4), typeCol, typeBadge);
// Balance right-aligned
snprintf(buf, sizeof(buf), "%.8f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
float balX = heroMax.x - balSz.x - Layout::spacingLg();
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(balX, cy + 2), typeCol, buf);
// USD value
if (state.market.price_usd > 0 && addr.balance > 0) {
double usd = addr.balance * state.market.price_usd;
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(heroMax.x - usdSz.x - Layout::spacingLg(), cy + sub1->LegacySize + 4),
OnSurfaceDisabled(), buf);
}
// --- QR Code centered ---
float qrOffset = (width - totalQrSize) * 0.5f;
ImVec2 qrPanelMin(heroMin.x + qrOffset, heroMin.y + infoBarH + Layout::spacingSm());
ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize);
// Subtle inner panel for QR
GlassPanelSpec qrGlass;
qrGlass.rounding = glassSpec.rounding * 0.75f;
qrGlass.fillAlpha = 12;
qrGlass.borderAlpha = 25;
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
if (s_qr_texture) {
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - 50,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), "QR unavailable");
}
// Click QR to copy
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy %s",
s_request_amount > 0 ? "payment URI" : "address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? "Payment URI copied to clipboard"
: "Address copied to clipboard");
}
// --- Address strip below QR ---
float addrStripY = qrPanelMax.y + Layout::spacingMd();
float addrStripX = heroMin.x + Layout::spacingLg();
float addrStripW = width - Layout::spacingXxl();
// Full address (word-wrapped)
ImVec2 fullAddrPos(addrStripX, addrStripY);
float wrapWidth = addrStripW;
ImVec2 addrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
wrapWidth, addr.address.c_str());
dl->AddText(capFont, capFont->LegacySize, fullAddrPos,
OnSurface(), addr.address.c_str(), nullptr, wrapWidth);
// Address click-to-copy overlay
ImGui::SetCursorScreenPos(fullAddrPos);
ImGui::InvisibleButton("##addrCopyHero", ImVec2(wrapWidth, addrSz.y));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Action buttons row
float btnRowY = addrStripY + addrSz.y + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(addrStripX, btnRowY));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
{
// Copy — primary (uses global glass style)
if (TactileSmallButton("Copy Address##hero", schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
ImGui::SameLine();
// Explorer
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileSmallButton("Explorer##hero", schema::UI().resolveFont("button"))) {
OpenExplorerURL(addr.address);
}
ImGui::PopStyleColor(3);
// Send From
if (addr.balance > 0) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
if (TactileSmallButton("Send \xe2\x86\x97##hero", schema::UI().resolveFont("button"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::PopStyleColor(3);
}
// Label editor (inline)
ImGui::SameLine(0, Layout::spacingXl());
auto lblIt = s_address_labels.find(addr.address);
std::string currentLabel = (lblIt != s_address_labels.end()) ? lblIt->second : "";
snprintf(s_label_edit_buf, sizeof(s_label_edit_buf), "%s", currentLabel.c_str());
ImGui::SetNextItemWidth(std::min(200.0f, addrStripW * 0.3f));
if (ImGui::InputTextWithHint("##LabelHero", "Add label...",
s_label_edit_buf, sizeof(s_label_edit_buf))) {
s_address_labels[addr.address] = std::string(s_label_edit_buf);
}
}
ImGui::PopStyleVar();
// Update hero height based on actual content
float actualBottom = btnRowY + 24;
heroH = actualBottom - heroMin.y + Layout::spacingMd();
heroMax.y = heroMin.y + heroH;
ImGui::SetCursorScreenPos(ImVec2(heroMin.x, heroMax.y));
ImGui::Dummy(ImVec2(width, 0));
}
// ============================================================================
// Payment request section (below QR hero)
// ============================================================================
static void RenderPaymentRequest(ImDrawList* dl, const AddressInfo& addr,
float innerW, const GlassPanelSpec& glassSpec,
const char* suffix) {
auto& S = schema::UI();
const float kLabelPos = S.label("tabs.receive", "label-column").position;
bool hasMemo = (addr.type == "shielded");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Compute card height
float prCardH = 16.0f + 24.0f + 8.0f + 12.0f;
if (hasMemo) prCardH += 24.0f;
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImFont* capF = Type().caption();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
innerW - 24, s_cached_qr_data.c_str());
prCardH += uriSz.y + 8.0f;
}
if (s_request_amount > 0) prCardH += 32.0f;
if (s_request_amount > 0 || s_request_memo[0]) prCardH += 4.0f;
ImVec2 prMin = ImGui::GetCursorScreenPos();
ImVec2 prMax(prMin.x + innerW, prMin.y + prCardH);
DrawGlassPanel(dl, prMin, prMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(prMin.x + Layout::spacingLg(), prMin.y + Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, 0));
ImGui::Text("Amount:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(std::max(S.input("tabs.receive", "amount-input").width, innerW * 0.4f));
char amtId[32];
snprintf(amtId, sizeof(amtId), "##RequestAmount%s", suffix);
ImGui::InputDouble(amtId, &s_request_amount, 0.01, 1.0, "%.8f");
ImGui::SameLine();
ImGui::Text("%s", DRAGONX_TICKER);
if (hasMemo) {
ImGui::Text("Memo:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(innerW - kLabelPos - Layout::spacingXxl());
char memoId[32];
snprintf(memoId, sizeof(memoId), "##RequestMemo%s", suffix);
ImGui::InputText(memoId, s_request_memo, sizeof(s_request_memo));
}
// Live URI preview
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImGui::Spacing();
ImFont* capF = Type().caption();
ImVec2 uriPos = ImGui::GetCursorScreenPos();
float uriWrapW = innerW - Layout::spacingXxl();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
uriWrapW, s_cached_qr_data.c_str());
dl->AddText(capF, capF->LegacySize, uriPos,
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
ImGui::Dummy(ImVec2(uriWrapW, uriSz.y + Layout::spacingSm()));
}
ImGui::Spacing();
if (s_request_amount > 0) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
char copyUriId[64];
snprintf(copyUriId, sizeof(copyUriId), "Copy Payment URI%s", suffix);
if (TactileButton(copyUriId, ImVec2(innerW - Layout::spacingXxl(), 0), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_cached_qr_data.c_str());
Notifications::instance().info("Payment URI copied to clipboard");
}
ImGui::PopStyleVar();
// Share as text
char shareId[32];
snprintf(shareId, sizeof(shareId), "Share as Text%s", suffix);
if (TactileSmallButton(shareId, S.resolveFont("button"))) {
char shareBuf[1024];
snprintf(shareBuf, sizeof(shareBuf),
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
s_request_amount, DRAGONX_TICKER,
addr.address.c_str(), s_cached_qr_data.c_str());
ImGui::SetClipboardText(shareBuf);
Notifications::instance().info("Payment request copied to clipboard");
}
}
if (s_request_amount > 0 || s_request_memo[0]) {
ImGui::SameLine();
char clearId[32];
snprintf(clearId, sizeof(clearId), "Clear%s", suffix);
if (TactileSmallButton(clearId, S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_memo[0] = '\0';
}
}
ImGui::SetCursorScreenPos(ImVec2(prMin.x, prMax.y));
ImGui::Dummy(ImVec2(innerW, 0));
}
// ============================================================================
// Recent received transactions for selected address
// ============================================================================
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& addr,
const WalletState& state, float width,
ImFont* capFont) {
char buf[128];
int recvCount = 0;
for (const auto& tx : state.transactions) {
if (tx.address == addr.address && tx.type == "receive") recvCount++;
}
if (recvCount == 0) return;
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
snprintf(buf, sizeof(buf), "RECENT RECEIVED (%d)", std::min(recvCount, 3));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
int shown = 0;
for (const auto& tx : state.transactions) {
if (tx.address != addr.address || tx.type != "receive") continue;
if (shown >= 3) break;
ImVec2 rMin = ImGui::GetCursorScreenPos();
float rH = 22.0f;
ImVec2 rMax(rMin.x + width, rMin.y + rH);
GlassPanelSpec rsGlass;
rsGlass.rounding = Layout::glassRounding() * 0.5f;
rsGlass.fillAlpha = 8;
DrawGlassPanel(dl, rMin, rMax, rsGlass);
float rx = rMin.x + Layout::spacingMd();
float ry = rMin.y + (rH - capFont->LegacySize) * 0.5f;
// Arrow indicator
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx, ry),
Success(), "\xe2\x86\x90");
snprintf(buf, sizeof(buf), "+%.8f %s %s %s",
tx.amount, DRAGONX_TICKER,
tx.getTimeString().c_str(),
tx.confirmations < 1 ? "(unconfirmed)" : "");
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + 16, ry),
tx.confirmations >= 1 ? Success() : Warning(), buf);
ImGui::Dummy(ImVec2(width, rH));
ImGui::Dummy(ImVec2(0, 2));
shown++;
}
}
// ============================================================================
// Horizontal Address Strip — bottom switching bar (Layout G signature)
// ============================================================================
static void RenderAddressStrip(App* app, ImDrawList* dl, const WalletState& state,
float width, float hs,
ImFont* /*sub1*/, ImFont* capFont) {
char buf[128];
// Header row with filter and + New button
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR ADDRESSES");
float btnW = std::max(70.0f, 85.0f * hs);
float comboW = std::max(48.0f, 58.0f * hs);
ImGui::SameLine(width - btnW - comboW - Layout::spacingMd());
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
const char* types[] = { "All", "Z", "T" };
ImGui::SetNextItemWidth(comboW);
ImGui::Combo("##AddrTypeStrip", &s_addr_type_filter, types, 3);
ImGui::SameLine();
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("+ New##strip", ImVec2(btnW, 0), schema::UI().resolveFont("button"))) {
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new shielded address");
else
Notifications::instance().success("New shielded address created");
});
} else {
app->createNewTAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new transparent address");
else
Notifications::instance().success("New transparent address created");
});
}
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (!app->isConnected()) {
Type().textColored(TypeStyle::Caption, Warning(), "Waiting for connection...");
return;
}
if (state.addresses.empty()) {
// Loading skeleton
ImVec2 skelPos = ImGui::GetCursorScreenPos();
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
for (int sk = 0; sk < 3; sk++) {
dl->AddRectFilled(
ImVec2(skelPos.x + sk * (130 + 8), skelPos.y),
ImVec2(skelPos.x + sk * (130 + 8) + 120, skelPos.y + 56),
skelCol, 6.0f);
}
ImGui::Dummy(ImVec2(width, 60));
return;
}
TrackNewAddresses(state);
AddressGroups groups = BuildSortedAddressGroups(state);
// Build filtered list
std::vector<int> filteredIdxs;
if (s_addr_type_filter != 2)
for (int idx : groups.shielded) filteredIdxs.push_back(idx);
if (s_addr_type_filter != 1)
for (int idx : groups.transparent) filteredIdxs.push_back(idx);
// Horizontal scrolling strip
float cardW = std::max(140.0f, std::min(200.0f, width * 0.22f));
float cardH = std::max(52.0f, 64.0f * hs);
float stripH = cardH + 8;
ImGui::BeginChild("##AddrStrip", ImVec2(width, stripH), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoBackground);
ImDrawList* sdl = ImGui::GetWindowDrawList();
for (size_t fi = 0; fi < filteredIdxs.size(); fi++) {
int i = filteredIdxs[fi];
const auto& addr = state.addresses[i];
bool isCurrent = (i == s_selected_address_idx);
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
bool hasBalance = addr.balance > 0;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
// Card background
GlassPanelSpec cardGlass;
cardGlass.rounding = Layout::glassRounding() * 0.75f;
cardGlass.fillAlpha = isCurrent ? 28 : 14;
cardGlass.borderAlpha = isCurrent ? 50 : 25;
DrawGlassPanel(sdl, cardMin, cardMax, cardGlass);
// Selected indicator — top accent bar
if (isCurrent) {
sdl->AddRectFilled(cardMin, ImVec2(cardMax.x, cardMin.y + 3), Primary(),
cardGlass.rounding);
}
float ix = cardMin.x + Layout::spacingMd();
float iy = cardMin.y + Layout::spacingSm() + (isCurrent ? 4 : 0);
// Type dot
sdl->AddCircleFilled(ImVec2(ix + 4, iy + 6), 3.5f, typeCol);
// Address label or truncated address
auto lblIt = s_address_labels.find(addr.address);
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
size_t addrTruncLen = static_cast<size_t>(std::max(8.0f, (cardW - 30) / 9.0f));
if (hasLabel) {
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? PrimaryLight() : OnSurfaceMedium(),
lblIt->second.c_str());
std::string shortAddr = TruncateAddress(addr.address, std::max((size_t)6, addrTruncLen / 2));
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy + capFont->LegacySize + 2),
OnSurfaceDisabled(), shortAddr.c_str());
} else {
std::string dispAddr = TruncateAddress(addr.address, addrTruncLen);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? OnSurface() : OnSurfaceDisabled(),
dispAddr.c_str());
}
// Balance
snprintf(buf, sizeof(buf), "%.4f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float balY = cardMax.y - balSz.y - Layout::spacingSm();
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, balY),
hasBalance ? typeCol : OnSurfaceDisabled(), buf);
// NEW badge
double now = ImGui::GetTime();
auto newIt = s_new_address_timestamps.find(addr.address);
if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) {
double age = now - newIt->second;
if (age < 10.0) {
float alpha = (float)std::max(0.0, 1.0 - age / 10.0);
int a = (int)(alpha * 220);
ImVec2 badgePos(cardMax.x - 32, cardMin.y + 4);
sdl->AddRectFilled(badgePos, ImVec2(badgePos.x + 28, badgePos.y + 14),
IM_COL32(77, 204, 255, a / 4), 3.0f);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(badgePos.x + 4, badgePos.y + 1),
IM_COL32(77, 204, 255, a), "NEW");
}
}
// Click interaction
ImGui::SetCursorScreenPos(cardMin);
ImGui::PushID(i);
ImGui::InvisibleButton("##addrCard", ImVec2(cardW, cardH));
if (ImGui::IsItemHovered()) {
if (!isCurrent)
sdl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 10),
cardGlass.rounding);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s\nBalance: %.8f %s%s\nDouble-click to copy | Right-click for options",
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? " (selected)" : "");
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
s_selected_address_idx = i;
s_cached_qr_data.clear();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Context menu
if (ImGui::BeginPopupContextItem("##addrStripCtx")) {
if (ImGui::MenuItem("Copy Address")) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
if (ImGui::MenuItem("View on Explorer")) {
OpenExplorerURL(addr.address);
}
if (addr.balance > 0) {
if (ImGui::MenuItem("Send From This Address")) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
}
ImGui::EndPopup();
}
ImGui::PopID();
ImGui::SameLine(0, Layout::spacingSm());
}
// Total balance at end of strip
{
double totalBal = 0;
for (const auto& a : state.addresses) totalBal += a.balance;
ImVec2 totPos = ImGui::GetCursorScreenPos();
float totCardW = std::max(100.0f, cardW * 0.6f);
ImVec2 totMax(totPos.x + totCardW, totPos.y + cardH);
GlassPanelSpec totGlass;
totGlass.rounding = Layout::glassRounding() * 0.75f;
totGlass.fillAlpha = 8;
totGlass.borderAlpha = 15;
DrawGlassPanel(sdl, totPos, totMax, totGlass);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(), totPos.y + Layout::spacingSm()),
OnSurfaceMedium(), "TOTAL");
snprintf(buf, sizeof(buf), "%.8f", totalBal);
ImVec2 totSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm()),
OnSurface(), buf);
snprintf(buf, sizeof(buf), "%s", DRAGONX_TICKER);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm() - capFont->LegacySize - 2),
OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(totCardW, cardH));
}
// Keyboard navigation
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
int next = s_selected_address_idx + 1;
if (next < (int)state.addresses.size()) {
s_selected_address_idx = next;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
int prev = s_selected_address_idx - 1;
if (prev >= 0) {
s_selected_address_idx = prev;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
}
}
ImGui::EndChild(); // ##AddrStrip
}
// ============================================================================
// MAIN: RenderReceiveTab — Layout G: QR-Centered Hero
// ============================================================================
void RenderReceiveTab(App* app)
{
const auto& state = app->getWalletState();
RenderSyncBanner(state);
ImVec2 recvAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##ReceiveScroll", recvAvail, false, ImGuiWindowFlags_NoBackground);
float hs = Layout::hScale(recvAvail.x);
float vScale = Layout::vScale(recvAvail.y);
float glassRound = Layout::glassRounding();
float availWidth = ImGui::GetContentRegionAvail().x;
float contentWidth = std::min(availWidth * 0.92f, 1200.0f * hs);
float offsetX = (availWidth - contentWidth) * 0.5f;
if (offsetX > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
float sectionGap = Layout::spacingXl() * vScale;
ImGui::BeginGroup();
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2();
// Auto-select first address
if (!state.addresses.empty() &&
(s_selected_address_idx < 0 ||
s_selected_address_idx >= (int)state.addresses.size())) {
s_selected_address_idx = 0;
}
const AddressInfo* selected = nullptr;
if (s_selected_address_idx >= 0 &&
s_selected_address_idx < (int)state.addresses.size()) {
selected = &state.addresses[s_selected_address_idx];
}
// Generate QR data
std::string qr_data;
if (selected) {
qr_data = selected->address;
if (s_request_amount > 0) {
qr_data = std::string("dragonx:") + selected->address +
"?amount=" + std::to_string(s_request_amount);
if (s_request_memo[0] && selected->type == "shielded") {
qr_data += "&memo=" + std::string(s_request_memo);
}
}
if (qr_data != s_cached_qr_data) {
if (s_qr_texture) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
int w, h;
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
s_cached_qr_data = qr_data;
}
}
// ================================================================
// Not connected / empty state
// ================================================================
if (!app->isConnected()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 120.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
OnSurfaceDisabled(), "Waiting for daemon connection...");
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + 8),
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
if (state.addresses.empty()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 100.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
ImVec2(emptyMin.x + contentWidth * 0.6f, emptyMin.y + Layout::spacingLg() + 16),
skelCol, 4.0f);
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + 24),
ImVec2(emptyMin.x + contentWidth * 0.4f, emptyMin.y + Layout::spacingLg() + 36),
skelCol, 4.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - 24),
OnSurfaceDisabled(), "Loading addresses...");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
// ================================================================
// QR HERO — dominates center (Layout G signature)
// ================================================================
if (selected) {
// Calculate QR size based on available space
float maxQrForWidth = std::min(contentWidth * 0.6f, 400.0f);
float maxQrForHeight = std::min(recvAvail.y * 0.45f, 400.0f);
float qrSize = std::max(140.0f, std::min(maxQrForWidth, maxQrForHeight));
// Center the hero horizontally
float heroW = std::min(contentWidth, 700.0f * hs);
float heroOffsetX = (contentWidth - heroW) * 0.5f;
if (heroOffsetX > 4) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + heroOffsetX);
}
RenderQRHero(app, dl, *selected, heroW, qrSize, qr_data,
glassSpec, state, sub1, body2, capFont);
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- PAYMENT REQUEST (collapsible on narrow) ----
constexpr float kTwoColumnThreshold = 800.0f;
bool isNarrow = contentWidth < kTwoColumnThreshold;
if (isNarrow) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1, 1, 1, 0.05f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1, 1, 1, 0.08f));
ImGui::PushFont(Type().overline());
s_payment_request_open = ImGui::CollapsingHeader(
"PAYMENT REQUEST (OPTIONAL)",
s_payment_request_open ? ImGuiTreeNodeFlags_DefaultOpen : 0);
ImGui::PopFont();
ImGui::PopStyleColor(3);
if (s_payment_request_open) {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
} else {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- RECENT RECEIVED ----
{
float rcvW = std::min(contentWidth, 600.0f * hs);
float rcvOffX = (contentWidth - rcvW) * 0.5f;
if (rcvOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rcvOffX);
RenderRecentReceived(dl, *selected, state, rcvW, capFont);
}
ImGui::Dummy(ImVec2(0, sectionGap));
}
// ================================================================
// ADDRESS STRIP — horizontal switching bar at bottom
// ================================================================
RenderAddressStrip(app, dl, state, contentWidth, hs, sub1, capFont);
ImGui::EndGroup();
ImGui::EndChild(); // ##ReceiveScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -3,6 +3,7 @@
// Released under the GPLv3 // Released under the GPLv3
#include "bootstrap.h" #include "bootstrap.h"
#include "../daemon/embedded_daemon.h"
#include <curl/curl.h> #include <curl/curl.h>
#include <miniz.h> #include <miniz.h>
@@ -135,11 +136,33 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
worker_running_ = false; worker_running_ = false;
return; return;
} }
// Step 3: Clean old chain data // Step 3: Ensure daemon is fully stopped before touching chain data
{
setProgress(State::Extracting, "Waiting for daemon to stop...");
int waited = 0;
while (daemon::EmbeddedDaemon::isRpcPortInUse() && waited < 60 && !cancel_requested_) {
std::this_thread::sleep_for(std::chrono::seconds(1));
waited++;
if (waited % 5 == 0)
DEBUG_LOGF("[Bootstrap] Still waiting for daemon to stop... (%ds)\n", waited);
}
if (cancel_requested_) {
setProgress(State::Failed, "Cancelled while waiting for daemon");
worker_running_ = false;
return;
}
if (daemon::EmbeddedDaemon::isRpcPortInUse()) {
setProgress(State::Failed, "Daemon is still running — stop it before using bootstrap");
worker_running_ = false;
return;
}
}
// Step 4: Clean old chain data
setProgress(State::Extracting, "Removing old chain data..."); setProgress(State::Extracting, "Removing old chain data...");
cleanChainData(dataDir); cleanChainData(dataDir);
// Step 4: Extract (skipping wallet.dat) // Step 5: Extract (skipping wallet.dat)
if (!extract(zipPath, dataDir)) { if (!extract(zipPath, dataDir)) {
if (cancel_requested_) if (cancel_requested_)
setProgress(State::Failed, "Extraction cancelled"); setProgress(State::Failed, "Extraction cancelled");
@@ -382,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)) { if (!mz_zip_reader_extract_to_file(&zip, i, destPath.c_str(), 0)) {
DEBUG_LOGF("[Bootstrap] Failed to extract: %s\n", filename.c_str()); 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
} }
} }
@@ -408,7 +437,13 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void Bootstrap::cleanChainData(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"}) { for (const char* subdir : {"blocks", "chainstate", "notarizations"}) {
fs::path p = fs::path(dataDir) / subdir; fs::path p = fs::path(dataDir) / subdir;
std::error_code ec; std::error_code ec;
@@ -418,14 +453,14 @@ void Bootstrap::cleanChainData(const std::string& dataDir) {
} }
} }
// Individual files to remove (will be replaced by bootstrap) // 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; fs::path p = fs::path(dataDir) / file;
std::error_code ec; std::error_code ec;
if (fs::exists(p, ec)) { if (fs::exists(p, ec)) {
fs::remove(p, ec); fs::remove(p, ec);
} }
} }
// NEVER remove: wallet.dat, debug.log, .lock, *.conf // NEVER remove: wallet.dat, debug.log, *.conf
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -482,7 +517,7 @@ std::string Bootstrap::parseChecksumFile(const std::string& content) {
return {}; 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"); FILE* fp = fopen(filePath.c_str(), "rb");
if (!fp) return {}; if (!fp) return {};
@@ -507,12 +542,18 @@ std::string Bootstrap::computeSHA256(const std::string& filePath) {
// Update progress every ~4MB // Update progress every ~4MB
if (fileSize > 0 && (processed % (4 * 1024 * 1024)) < (long long)sizeof(buf)) { 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]; char msg[128];
snprintf(msg, sizeof(msg), "Verifying SHA-256... %.0f%% (%s / %s)", 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()); 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); fclose(fp);
@@ -613,7 +654,7 @@ static void md5_final(MD5Context* ctx, uint8_t digest[16]) {
} // anon namespace } // 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"); FILE* fp = fopen(filePath.c_str(), "rb");
if (!fp) return {}; if (!fp) return {};
@@ -638,12 +679,18 @@ std::string Bootstrap::computeMD5(const std::string& filePath) {
// Update progress every ~4MB // Update progress every ~4MB
if (fileSize > 0 && (processed % (4 * 1024 * 1024)) < (long long)sizeof(buf)) { 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]; char msg[128];
snprintf(msg, sizeof(msg), "Verifying MD5... %.0f%% (%s / %s)", 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()); 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); fclose(fp);
@@ -682,11 +729,23 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
return true; 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 --- // --- SHA-256 ---
if (haveSHA256) { 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 expected = parseChecksumFile(sha256Content);
std::string actual = computeSHA256(zipPath); std::string actual = computeSHA256(zipPath, sha256Base, sha256Range);
if (cancel_requested_) return false; if (cancel_requested_) return false;
@@ -712,9 +771,9 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
// --- MD5 --- // --- MD5 ---
if (haveMD5) { if (haveMD5) {
setProgress(State::Verifying, "Verifying MD5..."); setProgress(State::Verifying, "Verifying MD5...", (double)md5Base, 100.0);
std::string expected = parseChecksumFile(md5Content); std::string expected = parseChecksumFile(md5Content);
std::string actual = computeMD5(zipPath); std::string actual = computeMD5(zipPath, md5Base, md5Range);
if (cancel_requested_) return false; if (cancel_requested_) return false;
@@ -738,7 +797,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str()); 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; return true;
} }

View File

@@ -51,6 +51,7 @@ public:
/// Base URL for bootstrap downloads (zip + checksum files). /// Base URL for bootstrap downloads (zip + checksum files).
static constexpr const char* kBaseUrl = "https://bootstrap.dragonx.is"; static constexpr const char* kBaseUrl = "https://bootstrap.dragonx.is";
static constexpr const char* kMirrorUrl = "https://bootstrap2.dragonx.is";
static constexpr const char* kZipName = "DRAGONX.zip"; static constexpr const char* kZipName = "DRAGONX.zip";
/// Start the bootstrap process on a background thread. /// Start the bootstrap process on a background thread.
@@ -87,12 +88,12 @@ private:
std::string downloadSmallFile(const std::string& url); std::string downloadSmallFile(const std::string& url);
/// Compute SHA-256 of a file, return lowercase hex digest. /// Compute SHA-256 of a file, return lowercase hex digest.
/// Updates progress with "Verifying SHA-256..." status during computation. /// pctBase/pctRange map file progress onto a portion of the overall percent.
std::string computeSHA256(const std::string& filePath); std::string computeSHA256(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f);
/// Compute MD5 of a file, return lowercase hex digest. /// Compute MD5 of a file, return lowercase hex digest.
/// Updates progress with "Verifying MD5..." status during computation. /// pctBase/pctRange map file progress onto a portion of the overall percent.
std::string computeMD5(const std::string& filePath); 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). /// Parse the first hex token from a checksum file (handles "<hash> <filename>" format).
static std::string parseChecksumFile(const std::string& content); static std::string parseChecksumFile(const std::string& content);

View File

@@ -297,7 +297,7 @@ void I18n::loadBuiltinEnglish()
strings_["tt_noise"] = "Grain texture intensity (0%% = off, 100%% = maximum)"; strings_["tt_noise"] = "Grain texture intensity (0%% = off, 100%% = maximum)";
strings_["tt_ui_opacity"] = "Card and sidebar opacity (100%% = fully opaque, lower = more see-through)"; strings_["tt_ui_opacity"] = "Card and sidebar opacity (100%% = fully opaque, lower = more see-through)";
strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)"; strings_["tt_window_opacity"] = "Background opacity (lower = desktop visible through window)";
strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x)."; strings_["tt_font_scale"] = "Scale all text and UI (1.0x = default, up to 1.5x). Hotkey: Alt + Scroll Wheel";
strings_["tt_custom_theme"] = "Custom theme active"; strings_["tt_custom_theme"] = "Custom theme active";
strings_["tt_address_book"] = "Manage saved addresses for quick sending"; strings_["tt_address_book"] = "Manage saved addresses for quick sending";
strings_["tt_validate"] = "Check if a DragonX address is valid"; strings_["tt_validate"] = "Check if a DragonX address is valid";
@@ -321,6 +321,12 @@ void I18n::loadBuiltinEnglish()
strings_["tt_rpc_pass"] = "RPC authentication password"; strings_["tt_rpc_pass"] = "RPC authentication password";
strings_["tt_test_conn"] = "Verify the RPC connection to the daemon"; strings_["tt_test_conn"] = "Verify the RPC connection to the daemon";
strings_["tt_rescan"] = "Rescan the blockchain for missing transactions"; strings_["tt_rescan"] = "Rescan the blockchain for missing transactions";
strings_["tt_delete_blockchain"] = "Delete all blockchain data and start a fresh sync. Your wallet.dat and config are preserved.";
strings_["delete_blockchain"] = "Delete Blockchain";
strings_["delete_blockchain_confirm"] = "Delete & Resync";
strings_["confirm_delete_blockchain_title"] = "Delete Blockchain Data";
strings_["confirm_delete_blockchain_msg"] = "This will stop the daemon, delete all blockchain data (blocks, chainstate, peers), and start a fresh sync from scratch. This can take several hours to complete.";
strings_["confirm_delete_blockchain_safe"] = "Your wallet.dat, config, and transaction history are safe and will not be deleted.";
strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase"; strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase";
strings_["tt_change_pass"] = "Change the wallet encryption passphrase"; strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
strings_["tt_lock"] = "Lock the wallet immediately"; strings_["tt_lock"] = "Lock the wallet immediately";
@@ -718,6 +724,7 @@ void I18n::loadBuiltinEnglish()
strings_["console_daemon_error"] = "Daemon error!"; strings_["console_daemon_error"] = "Daemon error!";
strings_["console_daemon_started"] = "Daemon started"; strings_["console_daemon_started"] = "Daemon started";
strings_["console_daemon_stopped"] = "Daemon stopped"; strings_["console_daemon_stopped"] = "Daemon stopped";
strings_["daemon_version"] = "Daemon";
strings_["console_disconnected"] = "Disconnected from daemon"; strings_["console_disconnected"] = "Disconnected from daemon";
strings_["console_errors"] = "Errors"; strings_["console_errors"] = "Errors";
strings_["console_filter_hint"] = "Filter output..."; strings_["console_filter_hint"] = "Filter output...";
@@ -836,6 +843,10 @@ void I18n::loadBuiltinEnglish()
strings_["mining_filter_tip_solo"] = "Show solo earnings only"; strings_["mining_filter_tip_solo"] = "Show solo earnings only";
strings_["mining_idle_off_tooltip"] = "Enable idle mining"; strings_["mining_idle_off_tooltip"] = "Enable idle mining";
strings_["mining_idle_on_tooltip"] = "Disable idle mining"; strings_["mining_idle_on_tooltip"] = "Disable idle mining";
strings_["mining_idle_scale_on_tooltip"] = "Thread scaling: ON\nClick to switch to start/stop mode";
strings_["mining_idle_scale_off_tooltip"] = "Start/stop mode: ON\nClick to switch to thread scaling mode";
strings_["mining_idle_threads_active_tooltip"] = "Threads when user is active";
strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle";
strings_["mining_local_hashrate"] = "Local Hashrate"; strings_["mining_local_hashrate"] = "Local Hashrate";
strings_["mining_mine"] = "Mine"; strings_["mining_mine"] = "Mine";
strings_["mining_mining_addr"] = "Mining Addr"; strings_["mining_mining_addr"] = "Mining Addr";
@@ -847,6 +858,7 @@ void I18n::loadBuiltinEnglish()
strings_["mining_open_in_explorer"] = "Open in explorer"; strings_["mining_open_in_explorer"] = "Open in explorer";
strings_["mining_payout_address"] = "Payout Address"; strings_["mining_payout_address"] = "Payout Address";
strings_["mining_payout_tooltip"] = "Address to receive mining rewards"; strings_["mining_payout_tooltip"] = "Address to receive mining rewards";
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
strings_["mining_pool"] = "Pool"; strings_["mining_pool"] = "Pool";
strings_["mining_pool_hashrate"] = "Pool Hashrate"; strings_["mining_pool_hashrate"] = "Pool Hashrate";
strings_["mining_pool_url"] = "Pool URL"; strings_["mining_pool_url"] = "Pool URL";
@@ -927,6 +939,7 @@ void I18n::loadBuiltinEnglish()
// --- Receive Tab --- // --- Receive Tab ---
strings_["click_copy_address"] = "Click to copy address"; strings_["click_copy_address"] = "Click to copy address";
strings_["click_copy_uri"] = "Click to copy URI"; strings_["click_copy_uri"] = "Click to copy URI";
strings_["generating"] = "Generating";
strings_["failed_create_shielded"] = "Failed to create shielded address"; strings_["failed_create_shielded"] = "Failed to create shielded address";
strings_["failed_create_transparent"] = "Failed to create transparent address"; strings_["failed_create_transparent"] = "Failed to create transparent address";
strings_["new_shielded_created"] = "New shielded address created"; strings_["new_shielded_created"] = "New shielded address created";
@@ -1055,6 +1068,25 @@ void I18n::loadBuiltinEnglish()
strings_["import_key_progress"] = "Importing %d/%d..."; strings_["import_key_progress"] = "Importing %d/%d...";
strings_["click_to_copy"] = "Click to copy"; strings_["click_to_copy"] = "Click to copy";
strings_["block_hash_copied"] = "Block hash copied"; 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 const char* I18n::translate(const char* key) const