Compare commits
4 Commits
v1.0.2
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8645a82e4f | ||
|
|
9e94952e0a | ||
|
|
4a841fd032 | ||
|
|
f0c87e4092 |
173
.github/copilot-instructions.md
vendored
173
.github/copilot-instructions.md
vendored
@@ -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
5
.gitignore
vendored
@@ -33,4 +33,7 @@ imgui.ini
|
|||||||
*.bak*
|
*.bak*
|
||||||
*.params
|
*.params
|
||||||
asmap.dat
|
asmap.dat
|
||||||
/external/xmrig-hac
|
/external/xmrig-hac
|
||||||
|
/memory
|
||||||
|
/todo.md
|
||||||
|
/.github/
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.0.1
|
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
|
||||||
@@ -540,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(
|
||||||
@@ -558,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/)
|
||||||
|
|||||||
10
build.sh
10
build.sh
@@ -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) ────────────────────────────
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<assemblyIdentity
|
<assemblyIdentity
|
||||||
type="win32"
|
type="win32"
|
||||||
name="DragonX.ObsidianDragon.Wallet"
|
name="DragonX.ObsidianDragon.Wallet"
|
||||||
version="1.0.1.0"
|
version="1.1.0.0"
|
||||||
processorArchitecture="amd64"
|
processorArchitecture="amd64"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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
122
scripts/expand_themes.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build-time theme expander — merges layout sections from ui.toml into skin files.
|
||||||
|
|
||||||
|
Called by CMake during build. Reads source skin files + ui.toml, writes merged
|
||||||
|
output files to the build directory. Source files are never modified.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 expand_themes.py <source_themes_dir> <output_themes_dir>
|
||||||
|
|
||||||
|
For each .toml file in source_themes_dir (except ui.toml), the script:
|
||||||
|
1. Copies the skin file contents (theme/palette/backdrop/effects)
|
||||||
|
2. Appends all layout sections from ui.toml (fonts, tabs, components, etc.)
|
||||||
|
3. Writes the merged result to output_themes_dir/<filename>
|
||||||
|
|
||||||
|
ui.toml itself is copied unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Sections to SKIP when extracting from ui.toml (theme-specific, already in skins)
|
||||||
|
SKIP_SECTIONS = {"theme", "theme.palette", "backdrop", "effects"}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_layout_sections(ui_toml_path):
|
||||||
|
"""Extract non-theme sections from ui.toml as a string."""
|
||||||
|
sections = []
|
||||||
|
current_section = None
|
||||||
|
current_lines = []
|
||||||
|
section_re = re.compile(r'^\[{1,2}([^\]]+)\]{1,2}\s*$')
|
||||||
|
|
||||||
|
with open(ui_toml_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
m = section_re.match(line.strip())
|
||||||
|
if m:
|
||||||
|
if current_section is not None or current_lines:
|
||||||
|
sections.append((current_section, current_lines))
|
||||||
|
current_section = m.group(1).strip()
|
||||||
|
current_lines = [line]
|
||||||
|
else:
|
||||||
|
current_lines.append(line)
|
||||||
|
if current_section is not None or current_lines:
|
||||||
|
sections.append((current_section, current_lines))
|
||||||
|
|
||||||
|
layout_parts = []
|
||||||
|
for section_name, lines in sections:
|
||||||
|
if section_name is None:
|
||||||
|
# Preamble: only include top-level key=value lines
|
||||||
|
kv_lines = [l for l in lines
|
||||||
|
if l.strip() and not l.strip().startswith('#') and '=' in l]
|
||||||
|
if kv_lines:
|
||||||
|
layout_parts.append(''.join(kv_lines))
|
||||||
|
continue
|
||||||
|
if section_name in SKIP_SECTIONS:
|
||||||
|
continue
|
||||||
|
layout_parts.append(''.join(lines))
|
||||||
|
|
||||||
|
return '\n'.join(layout_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(f"Usage: {sys.argv[0]} <source_themes_dir> <output_themes_dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
src_dir = sys.argv[1]
|
||||||
|
out_dir = sys.argv[2]
|
||||||
|
ui_toml = os.path.join(src_dir, "ui.toml")
|
||||||
|
|
||||||
|
if not os.path.exists(ui_toml):
|
||||||
|
print(f"ERROR: ui.toml not found at {ui_toml}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
layout_content = extract_layout_sections(ui_toml)
|
||||||
|
|
||||||
|
separator = (
|
||||||
|
"\n# ===========================================================================\n"
|
||||||
|
"# Layout & Component Properties\n"
|
||||||
|
"# All values below can be customized per-theme. Edit and save to see\n"
|
||||||
|
"# changes reflected in the app in real time via hot-reload.\n"
|
||||||
|
"# ===========================================================================\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
for fname in sorted(os.listdir(src_dir)):
|
||||||
|
if not fname.endswith('.toml'):
|
||||||
|
continue
|
||||||
|
src_path = os.path.join(src_dir, fname)
|
||||||
|
dst_path = os.path.join(out_dir, fname)
|
||||||
|
|
||||||
|
if fname == "ui.toml":
|
||||||
|
# Copy ui.toml unchanged
|
||||||
|
with open(src_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
with open(dst_path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" COPY {fname}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skin file — append layout sections
|
||||||
|
with open(src_path, 'r') as f:
|
||||||
|
skin = f.read()
|
||||||
|
if not skin.endswith('\n'):
|
||||||
|
skin += '\n'
|
||||||
|
|
||||||
|
merged = skin + separator + layout_content
|
||||||
|
if not merged.endswith('\n'):
|
||||||
|
merged += '\n'
|
||||||
|
|
||||||
|
with open(dst_path, 'w') as f:
|
||||||
|
f.write(merged)
|
||||||
|
lines = merged.count('\n')
|
||||||
|
print(f" MERGE {fname} → {lines} lines")
|
||||||
|
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
158
src/app.cpp
158
src/app.cpp
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
// !! 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 ...)
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||||
|
|
||||||
#define DRAGONX_VERSION "1.0.1"
|
#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 1
|
#define DRAGONX_VERSION_PATCH 0
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
11
src/main.cpp
11
src/main.cpp
@@ -1800,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
1047
src/ui/windows/explorer_tab.cpp
Normal file
1047
src/ui/windows/explorer_tab.cpp
Normal file
File diff suppressed because it is too large
Load Diff
15
src/ui/windows/explorer_tab.h
Normal file
15
src/ui/windows/explorer_tab.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
void RenderExplorerTab(App* app);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user