Compare commits
42
Commits
v1.1.0
..
28b9e0dffb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b9e0dffb | ||
|
|
6be0a58c26 | ||
|
|
fbdba1a001 | ||
|
|
821c54ba2b | ||
|
|
3ff62ca248 | ||
|
|
bbf53a130c | ||
|
|
1f9e43d7b2 | ||
|
|
5ebccceffc | ||
|
|
9d2d581474 | ||
|
|
096f8ee90e | ||
|
|
ca199ef195 | ||
|
|
97bd2f8168 | ||
|
|
09f287fbc5 | ||
|
|
b3d43ba0ad | ||
|
|
430290f97a | ||
|
|
30fc5da520 | ||
|
|
f02c965929 | ||
|
|
f0b7b88ef2 | ||
|
|
53d08de639 | ||
|
|
8645a82e4f | ||
|
|
9e94952e0a | ||
|
|
4a841fd032 | ||
|
|
f0c87e4092 | ||
|
|
c5ef4899bb | ||
|
|
36b67e69d0 | ||
|
|
06c80ef51c | ||
|
|
6bd5341507 | ||
|
|
5284c0dbb6 | ||
|
|
cf520fdf40 | ||
|
|
96c27bb949 | ||
|
|
cc617dd5be | ||
|
|
653a90de62 | ||
|
|
4b16a2a2c4 | ||
|
|
c51d3dafff | ||
|
|
68c2a59d09 | ||
|
|
45a2ccd9f3 | ||
|
|
0ca1caf148 | ||
|
|
7fb1f1de9d | ||
|
|
386cc857b0 | ||
|
|
3e6136983a | ||
|
|
2c1862aed3 | ||
|
|
4b815fc9d1 |
@@ -37,6 +37,7 @@ asmap.dat
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
/ObsidianDragon-agent/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
+26
-1
@@ -3,8 +3,19 @@
|
||||
# Released under the GPLv3
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
# macOS: set deployment target and universal architectures BEFORE project()
|
||||
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
|
||||
if(APPLE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
|
||||
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
|
||||
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
project(ObsidianDragon
|
||||
VERSION 1.1.0
|
||||
VERSION 1.2.0
|
||||
LANGUAGES C CXX
|
||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||
)
|
||||
@@ -408,6 +419,20 @@ configure_file(
|
||||
@ONLY
|
||||
)
|
||||
|
||||
# INCBIN uses .incbin assembler directives that reference font files at
|
||||
# assembly time — CMake doesn't track these implicit dependencies.
|
||||
# Tell CMake that the generated source depends on the actual font binaries
|
||||
# so a font file change triggers recompilation.
|
||||
set_source_files_properties(
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
|
||||
PROPERTIES OBJECT_DEPENDS
|
||||
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
||||
)
|
||||
|
||||
add_executable(ObsidianDragon
|
||||
${APP_SOURCES}
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VERSION="1.0.0"
|
||||
VERSION="1.1.1"
|
||||
|
||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
@@ -197,7 +197,14 @@ bundle_linux_daemon() {
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
build_dev() {
|
||||
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
|
||||
local bd="$SCRIPT_DIR/build/linux"
|
||||
|
||||
# Use platform-appropriate build directory
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
local bd="$SCRIPT_DIR/build/mac"
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
else
|
||||
local bd="$SCRIPT_DIR/build/linux"
|
||||
fi
|
||||
|
||||
if $CLEAN; then
|
||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||
@@ -261,7 +268,7 @@ build_release_linux() {
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
|
||||
local DIST="ObsidianDragon-Linux-x64"
|
||||
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
|
||||
local dist_dir="$out/$DIST"
|
||||
mkdir -p "$dist_dir"
|
||||
|
||||
@@ -379,9 +386,9 @@ APPRUN
|
||||
local ARCH
|
||||
ARCH=$(uname -m)
|
||||
cd "$bd"
|
||||
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${ARCH}.AppImage" 2>/dev/null && {
|
||||
cp "ObsidianDragon-${ARCH}.AppImage" "$out/ObsidianDragon.AppImage"
|
||||
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
|
||||
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
||||
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
|
||||
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
|
||||
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
||||
|
||||
info "Linux release artifacts: $out/"
|
||||
@@ -604,7 +611,7 @@ HDR
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
|
||||
local DIST="ObsidianDragon-Windows-x64"
|
||||
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
|
||||
local dist_dir="$out/$DIST"
|
||||
mkdir -p "$dist_dir"
|
||||
cp bin/ObsidianDragon.exe "$dist_dir/"
|
||||
@@ -628,8 +635,8 @@ HDR
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||
cp bin/ObsidianDragon.exe "$out/"
|
||||
info "Single-file exe: $out/ObsidianDragon.exe ($(du -h "$out/ObsidianDragon.exe" | cut -f1))"
|
||||
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
|
||||
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
|
||||
|
||||
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||
if command -v zip &>/dev/null; then
|
||||
@@ -732,7 +739,9 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
MAC_ARCH=$(uname -m)
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
MAC_ARCH="universal"
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
||||
@@ -811,12 +820,31 @@ TOOLCHAIN
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
||||
else
|
||||
info "Configuring (native) ..."
|
||||
# Build libsodium as universal if needed
|
||||
local need_sodium=false
|
||||
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
|
||||
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
fi
|
||||
fi
|
||||
if $need_sodium; then
|
||||
info "Building libsodium (universal) ..."
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native universal arm64+x86_64) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
|
||||
fi
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
@@ -836,6 +864,11 @@ TOOLCHAIN
|
||||
else
|
||||
info "Stripping ..."
|
||||
strip bin/ObsidianDragon
|
||||
# Verify universal binary
|
||||
if command -v lipo &>/dev/null; then
|
||||
info "Architecture info:"
|
||||
lipo -info bin/ObsidianDragon
|
||||
fi
|
||||
fi
|
||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
||||
|
||||
@@ -1019,6 +1052,13 @@ PLIST
|
||||
|
||||
info ".app bundle created: $APP"
|
||||
|
||||
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
||||
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||
if command -v zip &>/dev/null; then
|
||||
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
|
||||
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="DragonX.ObsidianDragon.Wallet"
|
||||
version="1.1.0.0"
|
||||
version="1.2.0.0"
|
||||
processorArchitecture="amd64"
|
||||
/>
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "Anzeigen",
|
||||
"show_hidden": "Ausgeblendete anzeigen (%d)",
|
||||
"show_qr_code": "QR-Code anzeigen",
|
||||
"showing_transactions": "Zeige %dâ%d von %d Transaktionen (gesamt: %zu)",
|
||||
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)",
|
||||
"simple_background": "Einfacher Hintergrund",
|
||||
"start_mining": "Mining starten",
|
||||
"status": "Status",
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "Afficher",
|
||||
"show_hidden": "Afficher masqués (%d)",
|
||||
"show_qr_code": "Afficher le code QR",
|
||||
"showing_transactions": "Affichage %dâ%d sur %d transactions (total : %zu)",
|
||||
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)",
|
||||
"simple_background": "Arrière-plan simple",
|
||||
"start_mining": "Démarrer le minage",
|
||||
"status": "Statut",
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "表示",
|
||||
"show_hidden": "非表示を表示 (%d)",
|
||||
"show_qr_code": "QRコードを表示",
|
||||
"showing_transactions": "%dâ%d / %d 件の取引を表示中(合計:%zu)",
|
||||
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu)",
|
||||
"simple_background": "シンプル背景",
|
||||
"start_mining": "マイニング開始",
|
||||
"status": "ステータス",
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "표시",
|
||||
"show_hidden": "숨겨진 항목 표시 (%d)",
|
||||
"show_qr_code": "QR 코드 표시",
|
||||
"showing_transactions": "%dâ%d / %d건의 거래 표시 중 (총: %zu)",
|
||||
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
|
||||
"simple_background": "단순 배경",
|
||||
"start_mining": "채굴 시작",
|
||||
"status": "상태",
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "Mostrar",
|
||||
"show_hidden": "Mostrar ocultos (%d)",
|
||||
"show_qr_code": "Mostrar Código QR",
|
||||
"showing_transactions": "Mostrando %dâ%d de %d transações (total: %zu)",
|
||||
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)",
|
||||
"simple_background": "Fundo simples",
|
||||
"start_mining": "Iniciar Mineração",
|
||||
"status": "Status",
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "Показать",
|
||||
"show_hidden": "Показать скрытые (%d)",
|
||||
"show_qr_code": "Показать QR-код",
|
||||
"showing_transactions": "Показано %dâ%d из %d транзакций (всего: %zu)",
|
||||
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)",
|
||||
"simple_background": "Простой фон",
|
||||
"start_mining": "Начать майнинг",
|
||||
"status": "Статус",
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@
|
||||
"show": "显示",
|
||||
"show_hidden": "显示已隐藏 (%d)",
|
||||
"show_qr_code": "显示二维码",
|
||||
"showing_transactions": "显示第 %dâ%d 笔,共 %d 笔交易(总计:%zu)",
|
||||
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu)",
|
||||
"simple_background": "简单背景",
|
||||
"start_mining": "开始挖矿",
|
||||
"status": "状态",
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build a NotoSansCJK subset font containing all characters used by
|
||||
the zh, ja, and ko translation files, plus common CJK punctuation
|
||||
and symbols.
|
||||
|
||||
Usage:
|
||||
python3 scripts/build_cjk_subset.py
|
||||
|
||||
Requires: pip install fonttools brotli
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools import subset as ftsubset
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
|
||||
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
|
||||
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
|
||||
|
||||
# Collect all characters used in CJK translation files
|
||||
needed = set()
|
||||
for lang in ['zh', 'ja', 'ko']:
|
||||
path = os.path.join(LANG_DIR, f'{lang}.json')
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for v in data.values():
|
||||
if isinstance(v, str):
|
||||
for c in v:
|
||||
cp = ord(c)
|
||||
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
|
||||
needed.add(cp)
|
||||
|
||||
# Also add common CJK ranges that future translations might use:
|
||||
# - CJK punctuation and symbols (3000-303F)
|
||||
# - Hiragana (3040-309F)
|
||||
# - Katakana (30A0-30FF)
|
||||
# - Bopomofo (3100-312F)
|
||||
# - CJK quotation marks, brackets
|
||||
for cp in range(0x3000, 0x3100):
|
||||
needed.add(cp)
|
||||
for cp in range(0x3100, 0x3130):
|
||||
needed.add(cp)
|
||||
# Fullwidth ASCII variants (commonly mixed in CJK text)
|
||||
for cp in range(0xFF01, 0xFF5F):
|
||||
needed.add(cp)
|
||||
|
||||
print(f"Total non-ASCII characters to include: {len(needed)}")
|
||||
|
||||
# Check which of these the source font supports
|
||||
font = TTFont(SOURCE_FONT)
|
||||
cmap = font.getBestCmap()
|
||||
supportable = needed & set(cmap.keys())
|
||||
unsupported = needed - set(cmap.keys())
|
||||
|
||||
print(f"Supported by source font: {len(supportable)}")
|
||||
if unsupported:
|
||||
print(f"Not in source font (will use fallback): {len(unsupported)}")
|
||||
for cp in sorted(unsupported)[:10]:
|
||||
print(f" U+{cp:04X} {chr(cp)}")
|
||||
|
||||
# Build the subset using pyftsubset CLI-style API
|
||||
args = [
|
||||
SOURCE_FONT,
|
||||
f'--output-file={OUTPUT_FONT}',
|
||||
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
|
||||
'--no-hinting',
|
||||
'--desubroutinize',
|
||||
]
|
||||
|
||||
ftsubset.main(args)
|
||||
|
||||
# Convert CFF outlines to TrueType (glyf) outlines.
|
||||
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
|
||||
from fontTools.pens.cu2quPen import Cu2QuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
from fontTools.ttLib import newTable
|
||||
|
||||
tmp_otf = OUTPUT_FONT + '.tmp.otf'
|
||||
os.rename(OUTPUT_FONT, tmp_otf)
|
||||
|
||||
conv = TTFont(tmp_otf)
|
||||
if 'CFF ' in conv:
|
||||
print("Converting CFF -> TrueType outlines...")
|
||||
glyphOrder = conv.getGlyphOrder()
|
||||
glyphSet = conv.getGlyphSet()
|
||||
glyf_table = newTable("glyf")
|
||||
glyf_table.glyphs = {}
|
||||
glyf_table.glyphOrder = glyphOrder
|
||||
loca_table = newTable("loca")
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
|
||||
for gname in glyphOrder:
|
||||
try:
|
||||
ttPen = TTGlyphPen(glyphSet)
|
||||
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
|
||||
glyphSet[gname].draw(cu2quPen)
|
||||
glyf_table.glyphs[gname] = ttPen.glyph()
|
||||
except Exception:
|
||||
glyf_table.glyphs[gname] = TTGlyph()
|
||||
del conv['CFF ']
|
||||
if 'VORG' in conv:
|
||||
del conv['VORG']
|
||||
conv['glyf'] = glyf_table
|
||||
conv['loca'] = loca_table
|
||||
conv['head'].indexToLocFormat = 1
|
||||
if 'maxp' in conv:
|
||||
conv['maxp'].version = 0x00010000
|
||||
conv.sfntVersion = "\x00\x01\x00\x00"
|
||||
conv.save(OUTPUT_FONT)
|
||||
conv.close()
|
||||
os.remove(tmp_otf)
|
||||
|
||||
size = os.path.getsize(OUTPUT_FONT)
|
||||
print(f"\nOutput: {OUTPUT_FONT}")
|
||||
print(f"Size: {size / 1024:.0f} KB")
|
||||
|
||||
# Verify
|
||||
verify = TTFont(OUTPUT_FONT)
|
||||
verify_cmap = set(verify.getBestCmap().keys())
|
||||
still_missing = needed - verify_cmap
|
||||
print(f"Verified glyphs in subset: {len(verify_cmap)}")
|
||||
if still_missing:
|
||||
# These are chars not in the source font - expected for some Hangul/Hiragana
|
||||
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
|
||||
for cp in sorted(still_missing)[:10]:
|
||||
print(f" U+{cp:04X} {chr(cp)}")
|
||||
else:
|
||||
print("All needed characters are covered!")
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check which characters in translation files fall outside the font glyph ranges."""
|
||||
import json
|
||||
import unicodedata
|
||||
import glob
|
||||
import os
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
|
||||
|
||||
# Glyph ranges from typography.cpp (regular font + CJK merge)
|
||||
RANGES = [
|
||||
# Regular font ranges
|
||||
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
|
||||
(0x0100, 0x024F), # Latin Extended-A + B
|
||||
(0x0370, 0x03FF), # Greek and Coptic
|
||||
(0x0400, 0x04FF), # Cyrillic
|
||||
(0x0500, 0x052F), # Cyrillic Supplement
|
||||
(0x2000, 0x206F), # General Punctuation
|
||||
(0x2190, 0x21FF), # Arrows
|
||||
(0x2200, 0x22FF), # Mathematical Operators
|
||||
(0x2600, 0x26FF), # Miscellaneous Symbols
|
||||
# CJK ranges
|
||||
(0x2E80, 0x2FDF), # CJK Radicals
|
||||
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
|
||||
(0x3100, 0x312F), # Bopomofo
|
||||
(0x31F0, 0x31FF), # Katakana Extensions
|
||||
(0x3400, 0x4DBF), # CJK Extension A
|
||||
(0x4E00, 0x9FFF), # CJK Unified Ideographs
|
||||
(0xAC00, 0xD7AF), # Hangul Syllables
|
||||
(0xFF00, 0xFFEF), # Fullwidth Forms
|
||||
]
|
||||
|
||||
def in_ranges(cp):
|
||||
return any(lo <= cp <= hi for lo, hi in RANGES)
|
||||
|
||||
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
|
||||
lang = os.path.basename(path).replace('.json', '')
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
missing = {}
|
||||
for key, val in data.items():
|
||||
if not isinstance(val, str):
|
||||
continue
|
||||
for c in val:
|
||||
cp = ord(c)
|
||||
if cp > 0x7F and not in_ranges(cp):
|
||||
if c not in missing:
|
||||
missing[c] = []
|
||||
missing[c].append(key)
|
||||
|
||||
if missing:
|
||||
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
|
||||
for c in sorted(missing, key=lambda x: ord(x)):
|
||||
cp = ord(c)
|
||||
name = unicodedata.name(c, 'UNKNOWN')
|
||||
keys = missing[c][:3]
|
||||
key_str = ', '.join(keys)
|
||||
if len(missing[c]) > 3:
|
||||
key_str += f' (+{len(missing[c])-3} more)'
|
||||
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
|
||||
else:
|
||||
print(f"=== {lang}.json: OK (all characters covered) ===")
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check which characters needed by translations are missing from bundled fonts."""
|
||||
import json
|
||||
import os
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
|
||||
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
|
||||
|
||||
# Load font cmaps
|
||||
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
|
||||
cjk_cmap = set(cjk.getBestCmap().keys())
|
||||
|
||||
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
|
||||
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
|
||||
|
||||
combined = cjk_cmap | ubuntu_cmap
|
||||
|
||||
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
|
||||
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
|
||||
print(f"Combined: {len(combined)}")
|
||||
print()
|
||||
|
||||
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
|
||||
path = os.path.join(LANG_DIR, f'{lang}.json')
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
needed = set()
|
||||
for v in data.values():
|
||||
if isinstance(v, str):
|
||||
for c in v:
|
||||
needed.add(ord(c))
|
||||
|
||||
missing = sorted(needed - combined)
|
||||
if missing:
|
||||
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
|
||||
for cp in missing[:20]:
|
||||
c = chr(cp)
|
||||
print(f" U+{cp:04X} {c}")
|
||||
if len(missing) > 20:
|
||||
print(f" ... and {len(missing) - 20} more")
|
||||
else:
|
||||
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
|
||||
|
||||
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
|
||||
so we need glyf-based TrueType outlines instead.
|
||||
|
||||
Two approaches:
|
||||
1. Direct CFF->TTF conversion via cu2qu (fontTools)
|
||||
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
|
||||
|
||||
This script tries approach 1 first, falls back to approach 2.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
|
||||
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
|
||||
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
|
||||
|
||||
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
|
||||
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
|
||||
|
||||
|
||||
def get_needed_codepoints():
|
||||
"""Collect all unique codepoints from CJK translation files."""
|
||||
codepoints = set()
|
||||
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
|
||||
with open(lang_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for value in data.values():
|
||||
if isinstance(value, str):
|
||||
for ch in value:
|
||||
cp = ord(ch)
|
||||
# Include CJK + Hangul + fullwidth + CJK symbols/kana
|
||||
if cp >= 0x2E80:
|
||||
codepoints.add(cp)
|
||||
return codepoints
|
||||
|
||||
|
||||
def convert_cff_to_ttf():
|
||||
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.cu2quPen import Cu2QuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
|
||||
print(f"Loading {SRC_OTF}...")
|
||||
font = TTFont(SRC_OTF)
|
||||
|
||||
# Verify it's CFF
|
||||
if "CFF " not in font:
|
||||
print("Font is not CFF, skipping conversion")
|
||||
return False
|
||||
|
||||
cff = font["CFF "]
|
||||
top = cff.cff.topDictIndex[0]
|
||||
print(f"ROS: {getattr(top, 'ROS', None)}")
|
||||
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
|
||||
|
||||
glyphOrder = font.getGlyphOrder()
|
||||
print(f"Glyphs: {len(glyphOrder)}")
|
||||
|
||||
# Use fontTools' built-in otf2ttf if available
|
||||
try:
|
||||
from fontTools.otf2ttf import otf_to_ttf
|
||||
otf_to_ttf(font)
|
||||
font.save(DST_TTF)
|
||||
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||
font.close()
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Manual conversion using cu2qu
|
||||
print("Using manual CFF->TTF conversion with cu2qu...")
|
||||
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
from fontTools.pens.pointPen import SegmentToPointPen
|
||||
from fontTools import ttLib
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
|
||||
import struct
|
||||
|
||||
# Get glyph set
|
||||
glyphSet = font.getGlyphSet()
|
||||
|
||||
# Create new glyf table
|
||||
from fontTools.ttLib import newTable
|
||||
|
||||
glyf_table = newTable("glyf")
|
||||
glyf_table.glyphs = {}
|
||||
glyf_table.glyphOrder = glyphOrder
|
||||
|
||||
loca_table = newTable("loca")
|
||||
|
||||
max_error = 1.0 # em-units tolerance for cubic->quadratic
|
||||
|
||||
for gname in glyphOrder:
|
||||
try:
|
||||
ttPen = TTGlyphPen(glyphSet)
|
||||
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
|
||||
glyphSet[gname].draw(cu2quPen)
|
||||
glyf_table.glyphs[gname] = ttPen.glyph()
|
||||
except Exception as e:
|
||||
# Fallback: empty glyph
|
||||
glyf_table.glyphs[gname] = TTGlyph()
|
||||
|
||||
# Replace CFF with glyf
|
||||
del font["CFF "]
|
||||
if "VORG" in font:
|
||||
del font["VORG"]
|
||||
|
||||
font["glyf"] = glyf_table
|
||||
font["loca"] = loca_table
|
||||
|
||||
# Add required tables for TTF
|
||||
# head table needs indexToLocFormat
|
||||
font["head"].indexToLocFormat = 1 # long format
|
||||
|
||||
# Create maxp for TrueType
|
||||
if "maxp" in font:
|
||||
font["maxp"].version = 0x00010000
|
||||
|
||||
# Update sfntVersion
|
||||
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
|
||||
|
||||
font.save(DST_TTF)
|
||||
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||
font.close()
|
||||
return True
|
||||
|
||||
|
||||
def download_and_subset():
|
||||
"""Download NotoSansSC-Regular.ttf and subset it."""
|
||||
import urllib.request
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools import subset
|
||||
|
||||
# Google Fonts provides static TTF files
|
||||
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
|
||||
# Actually, we want TTF. Let's try the variable font approach.
|
||||
# Or better: use google-fonts API for static TTF
|
||||
|
||||
# NotoSansSC static TTF from Google Fonts CDN
|
||||
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
|
||||
|
||||
if not os.path.exists(tmp_font):
|
||||
print(f"Downloading NotoSansSC-Regular.ttf...")
|
||||
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
|
||||
# This is a TTC (font collection), too large.
|
||||
# Use the OTF we already have and convert it.
|
||||
return False
|
||||
|
||||
print(f"Using {tmp_font}")
|
||||
font = TTFont(tmp_font)
|
||||
cmap = font.getBestCmap()
|
||||
print(f"Source has {len(cmap)} cmap entries")
|
||||
|
||||
needed = get_needed_codepoints()
|
||||
print(f"Need {len(needed)} CJK codepoints")
|
||||
|
||||
# Subset
|
||||
subsetter = subset.Subsetter()
|
||||
subsetter.populate(unicodes=needed)
|
||||
subsetter.subset(font)
|
||||
|
||||
font.save(DST_TTF)
|
||||
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||
font.close()
|
||||
return True
|
||||
|
||||
|
||||
def verify_result():
|
||||
"""Verify the output TTF has glyf outlines and correct characters."""
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
font = TTFont(DST_TTF)
|
||||
cmap = font.getBestCmap()
|
||||
|
||||
print(f"\n--- Verification ---")
|
||||
print(f"Format: {font.sfntVersion!r}")
|
||||
print(f"Has glyf: {'glyf' in font}")
|
||||
print(f"Has CFF: {'CFF ' in font}")
|
||||
print(f"Cmap entries: {len(cmap)}")
|
||||
|
||||
# Check key characters
|
||||
test_chars = {
|
||||
"历": 0x5386, "史": 0x53F2, # Chinese: history
|
||||
"概": 0x6982, "述": 0x8FF0, # Chinese: overview
|
||||
"设": 0x8BBE, "置": 0x7F6E, # Chinese: settings
|
||||
}
|
||||
for name, cp in test_chars.items():
|
||||
status = "YES" if cp in cmap else "NO"
|
||||
print(f" {name} (U+{cp:04X}): {status}")
|
||||
|
||||
size = os.path.getsize(DST_TTF)
|
||||
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
|
||||
font.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== CJK Font CFF -> TTF Converter ===\n")
|
||||
|
||||
if convert_cff_to_ttf():
|
||||
verify_result()
|
||||
else:
|
||||
print("Direct conversion failed, trying download approach...")
|
||||
if download_and_subset():
|
||||
verify_result()
|
||||
else:
|
||||
print("ERROR: Could not convert font")
|
||||
sys.exit(1)
|
||||
@@ -8,7 +8,7 @@ set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||
APPDIR="${BUILD_DIR}/AppDir"
|
||||
VERSION="1.0.0"
|
||||
VERSION="1.1.1"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
|
||||
@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
|
||||
curl -fSL -o "$TARBALL" "$SODIUM_URL"
|
||||
fi
|
||||
|
||||
# Verify checksum
|
||||
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
}
|
||||
# Verify checksum (sha256sum on Linux, shasum on macOS)
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
}
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
|
||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# ── Extract ─────────────────────────────────────────────────────────────────
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
@@ -115,6 +123,69 @@ case "$TARGET" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
|
||||
IS_MACOS_NATIVE=false
|
||||
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
|
||||
IS_MACOS_NATIVE=true
|
||||
fi
|
||||
|
||||
if $IS_MACOS_NATIVE; then
|
||||
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
|
||||
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
|
||||
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
|
||||
|
||||
for ARCH in arm64 x86_64; do
|
||||
echo "[fetch-libsodium] Building for $ARCH..."
|
||||
cd "$SRC_DIR"
|
||||
make clean 2>/dev/null || true
|
||||
make distclean 2>/dev/null || true
|
||||
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
ARCH_INSTALL="$INSTALL_ARM64"
|
||||
HOST_TRIPLE="aarch64-apple-darwin"
|
||||
else
|
||||
ARCH_INSTALL="$INSTALL_X86_64"
|
||||
HOST_TRIPLE="x86_64-apple-darwin"
|
||||
fi
|
||||
|
||||
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
|
||||
|
||||
./configure \
|
||||
--prefix="$ARCH_INSTALL" \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
--with-pic \
|
||||
--host="$HOST_TRIPLE" \
|
||||
CFLAGS="$ARCH_CFLAGS" \
|
||||
LDFLAGS="-arch $ARCH" \
|
||||
> /dev/null
|
||||
|
||||
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
|
||||
make install > /dev/null
|
||||
done
|
||||
|
||||
# Merge with lipo
|
||||
echo "[fetch-libsodium] Creating universal binary with lipo..."
|
||||
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
|
||||
lipo -create \
|
||||
"$INSTALL_ARM64/lib/libsodium.a" \
|
||||
"$INSTALL_X86_64/lib/libsodium.a" \
|
||||
-output "$INSTALL_DIR/lib/libsodium.a"
|
||||
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
|
||||
|
||||
# Clean up per-arch builds
|
||||
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
|
||||
cd "$PROJECT_DIR"
|
||||
rm -rf "$SRC_DIR"
|
||||
rm -f "$TARBALL"
|
||||
|
||||
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
|
||||
lipo -info "$INSTALL_DIR/lib/libsodium.a"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
|
||||
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
|
||||
import os
|
||||
import glob
|
||||
|
||||
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
|
||||
|
||||
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
|
||||
MOJIBAKE_FIXES = {
|
||||
'\u00e2\u0080\u0093': '\u2013', # en dash
|
||||
'\u00e2\u0080\u0094': '\u2014', # em dash
|
||||
'\u00e2\u0080\u0099': '\u2019', # right single quote
|
||||
'\u00e2\u0080\u009c': '\u201c', # left double quote
|
||||
'\u00e2\u0080\u009d': '\u201d', # right double quote
|
||||
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
|
||||
}
|
||||
|
||||
total_fixed = 0
|
||||
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
raw = f.read()
|
||||
|
||||
original = raw
|
||||
for bad, good in MOJIBAKE_FIXES.items():
|
||||
if bad in raw:
|
||||
count = raw.count(bad)
|
||||
raw = raw.replace(bad, good)
|
||||
lang = os.path.basename(path)
|
||||
print(f" {lang}: fixed {count} x {repr(good)}")
|
||||
total_fixed += count
|
||||
|
||||
if raw != original:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(raw)
|
||||
|
||||
print(f"\nTotal fixes: {total_fixed}")
|
||||
@@ -7,7 +7,7 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||
VERSION="1.0.0"
|
||||
VERSION="1.1.1"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
|
||||
+177
-30
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// app.cpp — Main application: init, shutdown, ImGui render loop, NavPage
|
||||
// dispatch, dialog rendering, and frame-level state management.
|
||||
|
||||
#include "app.h"
|
||||
#include "config/version.h"
|
||||
@@ -313,7 +316,8 @@ void App::update()
|
||||
// Track user interaction for auto-lock
|
||||
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||
io.InputQueueCharacters.Size > 0) {
|
||||
io.InputQueueCharacters.Size > 0 ||
|
||||
ImGui::IsAnyItemActive()) {
|
||||
last_interaction_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
@@ -340,7 +344,10 @@ void App::update()
|
||||
}
|
||||
|
||||
// Update timers
|
||||
refresh_timer_ += io.DeltaTime;
|
||||
core_timer_ += io.DeltaTime;
|
||||
address_timer_ += io.DeltaTime;
|
||||
transaction_timer_ += io.DeltaTime;
|
||||
peer_timer_ += io.DeltaTime;
|
||||
price_timer_ += io.DeltaTime;
|
||||
fast_refresh_timer_ += io.DeltaTime;
|
||||
tx_age_timer_ += io.DeltaTime;
|
||||
@@ -564,31 +571,61 @@ void App::update()
|
||||
};
|
||||
}
|
||||
}
|
||||
return [this, done, anySuccess]() {
|
||||
// Extract txids from successful operations so shielded
|
||||
// sends are discoverable by z_viewtransaction.
|
||||
std::vector<std::string> successTxids;
|
||||
for (const auto& op : result) {
|
||||
if (op.value("status", "") == "success"
|
||||
&& op.contains("result") && op["result"].contains("txid")) {
|
||||
successTxids.push_back(op["result"]["txid"].get<std::string>());
|
||||
}
|
||||
}
|
||||
return [this, done, anySuccess,
|
||||
successTxids = std::move(successTxids)]() {
|
||||
for (const auto& id : done) {
|
||||
pending_opids_.erase(
|
||||
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
||||
pending_opids_.end());
|
||||
}
|
||||
if (anySuccess) {
|
||||
for (const auto& txid : successTxids) {
|
||||
send_txids_.insert(txid);
|
||||
}
|
||||
// Transaction confirmed by daemon — force immediate data refresh
|
||||
transactions_dirty_ = true;
|
||||
addresses_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
refresh_timer_ = REFRESH_INTERVAL;
|
||||
core_timer_ = active_core_interval_;
|
||||
transaction_timer_ = active_tx_interval_;
|
||||
address_timer_ = active_addr_interval_;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Regular refresh every 5 seconds
|
||||
// Per-category refresh with tab-aware intervals
|
||||
// Skip when wallet is locked — same reason as above.
|
||||
if (refresh_timer_ >= REFRESH_INTERVAL) {
|
||||
refresh_timer_ = 0.0f;
|
||||
if (state_.connected && !state_.isLocked()) {
|
||||
refreshData();
|
||||
} else if (!connection_in_progress_ &&
|
||||
wizard_phase_ == WizardPhase::None) {
|
||||
if (state_.connected && !state_.isLocked()) {
|
||||
if (core_timer_ >= active_core_interval_) {
|
||||
core_timer_ = 0.0f;
|
||||
refreshCoreData();
|
||||
}
|
||||
if (transaction_timer_ >= active_tx_interval_) {
|
||||
transaction_timer_ = 0.0f;
|
||||
refreshTransactionData();
|
||||
}
|
||||
if (address_timer_ >= active_addr_interval_) {
|
||||
address_timer_ = 0.0f;
|
||||
refreshAddressData();
|
||||
}
|
||||
if (peer_timer_ >= active_peer_interval_) {
|
||||
peer_timer_ = 0.0f;
|
||||
refreshPeerInfo();
|
||||
}
|
||||
} else if (core_timer_ >= active_core_interval_) {
|
||||
core_timer_ = 0.0f;
|
||||
if (!connection_in_progress_ &&
|
||||
wizard_phase_ == WizardPhase::None) {
|
||||
tryConnect();
|
||||
}
|
||||
}
|
||||
@@ -603,7 +640,7 @@ void App::update()
|
||||
|
||||
// Keyboard shortcut: Ctrl+, to open Settings page
|
||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
||||
current_page_ = ui::NavPage::Settings;
|
||||
setCurrentPage(ui::NavPage::Settings);
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Ctrl+Left/Right to cycle themes
|
||||
@@ -988,7 +1025,8 @@ void App::render()
|
||||
bool prevCollapsed = sidebar_collapsed_;
|
||||
{
|
||||
PERF_SCOPE("Render.Sidebar");
|
||||
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_);
|
||||
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_,
|
||||
state_.isLocked());
|
||||
}
|
||||
if (sbStatus.exitClicked) {
|
||||
requestQuit();
|
||||
@@ -1003,7 +1041,7 @@ void App::render()
|
||||
|
||||
// Page transition: detect change, ramp alpha
|
||||
if (current_page_ != prev_page_) {
|
||||
page_alpha_ = ui::effects::isLowSpecMode() ? 1.0f : 0.0f;
|
||||
page_alpha_ = (ui::effects::isLowSpecMode() || (settings_ && settings_->getReduceMotion())) ? 1.0f : 0.0f;
|
||||
prev_page_ = current_page_;
|
||||
}
|
||||
if (page_alpha_ < 1.0f) {
|
||||
@@ -1058,6 +1096,18 @@ void App::render()
|
||||
// Lock screen — covers tab content just like the loading overlay
|
||||
renderLockScreen();
|
||||
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
||||
// Track how long we've been waiting for encryption state
|
||||
if (state_.connected && !state_.encryption_state_known) {
|
||||
encryption_check_timer_ += ImGui::GetIO().DeltaTime;
|
||||
if (encryption_check_timer_ >= 15.0f) {
|
||||
DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n");
|
||||
state_.encryption_state_known = true;
|
||||
state_.encrypted = false;
|
||||
encryption_check_timer_ = 0.0f;
|
||||
}
|
||||
} else {
|
||||
encryption_check_timer_ = 0.0f;
|
||||
}
|
||||
// Reset lock screen focus flag so it auto-focuses next time
|
||||
lock_screen_was_visible_ = false;
|
||||
// Show loading overlay instead of tab content
|
||||
@@ -1095,9 +1145,10 @@ void App::render()
|
||||
case ui::NavPage::Console:
|
||||
// Use fast-lane worker for console commands to avoid head-of-line
|
||||
// blocking behind the consolidated refreshData() batch.
|
||||
// Fall back to main rpc/worker if fast-lane hasn't connected yet.
|
||||
console_tab_.render(embedded_daemon_.get(),
|
||||
fast_rpc_ ? fast_rpc_.get() : rpc_.get(),
|
||||
fast_worker_ ? fast_worker_.get() : worker_.get(),
|
||||
(fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(),
|
||||
(fast_rpc_ && fast_rpc_->isConnected() && fast_worker_) ? fast_worker_.get() : worker_.get(),
|
||||
xmrig_manager_.get());
|
||||
break;
|
||||
case ui::NavPage::Settings:
|
||||
@@ -1410,8 +1461,35 @@ void App::renderStatusBar()
|
||||
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
|
||||
int blocksLeft = chainTip - state_.sync.blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
|
||||
state_.sync.verification_progress * 100.0, blocksLeft);
|
||||
|
||||
// Calculate sync speed (blocks/sec) using a smoothed rolling average
|
||||
static int s_prev_blocks = 0;
|
||||
static double s_prev_time = 0.0;
|
||||
static double s_blocks_per_sec = 0.0;
|
||||
double now = ImGui::GetTime();
|
||||
if (s_prev_time > 0.0 && state_.sync.blocks > s_prev_blocks) {
|
||||
double dt = now - s_prev_time;
|
||||
if (dt > 0.5) {
|
||||
double raw = (state_.sync.blocks - s_prev_blocks) / dt;
|
||||
// Exponential smoothing (alpha ~0.3 per update)
|
||||
s_blocks_per_sec = s_blocks_per_sec > 0.0
|
||||
? s_blocks_per_sec * 0.7 + raw * 0.3
|
||||
: raw;
|
||||
s_prev_blocks = state_.sync.blocks;
|
||||
s_prev_time = now;
|
||||
}
|
||||
} else {
|
||||
s_prev_blocks = state_.sync.blocks;
|
||||
s_prev_time = now;
|
||||
}
|
||||
|
||||
if (s_blocks_per_sec > 0.1) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left, %.0f blk/s)",
|
||||
state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec);
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
|
||||
state_.sync.verification_progress * 100.0, blocksLeft);
|
||||
}
|
||||
} else if (state_.connected) {
|
||||
ImGui::Text("Peers: %zu", state_.peers.size());
|
||||
}
|
||||
@@ -1869,7 +1947,11 @@ void App::renderAntivirusHelpDialog()
|
||||
|
||||
void App::refreshNow()
|
||||
{
|
||||
refresh_timer_ = REFRESH_INTERVAL; // Trigger immediate refresh
|
||||
// Trigger immediate refresh on all categories
|
||||
core_timer_ = active_core_interval_;
|
||||
transaction_timer_ = active_tx_interval_;
|
||||
address_timer_ = active_addr_interval_;
|
||||
peer_timer_ = active_peer_interval_;
|
||||
transactions_dirty_ = true; // Force transaction list update
|
||||
addresses_dirty_ = true; // Force address/balance update
|
||||
last_tx_block_height_ = -1; // Reset tx cache
|
||||
@@ -1892,7 +1974,7 @@ void App::handlePaymentURI(const std::string& uri)
|
||||
pending_label_ = payment.label;
|
||||
|
||||
// Switch to Send page
|
||||
current_page_ = ui::NavPage::Send;
|
||||
setCurrentPage(ui::NavPage::Send);
|
||||
|
||||
// Notify user
|
||||
std::string msg = "Payment request loaded";
|
||||
@@ -1919,7 +2001,7 @@ void App::setCurrentTab(int tab) {
|
||||
ui::NavPage::Settings, // 9 = Settings
|
||||
};
|
||||
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
|
||||
current_page_ = kTabMap[tab];
|
||||
setCurrentPage(kTabMap[tab]);
|
||||
}
|
||||
|
||||
bool App::startEmbeddedDaemon()
|
||||
@@ -2058,6 +2140,7 @@ bool App::startEmbeddedDaemon()
|
||||
// Sync debug logging categories from user settings
|
||||
if (settings_) {
|
||||
embedded_daemon_->setDebugCategories(settings_->getDebugCategories());
|
||||
embedded_daemon_->setMaxConnections(settings_->getMaxConnections());
|
||||
}
|
||||
|
||||
return embedded_daemon_->start();
|
||||
@@ -2124,15 +2207,19 @@ void App::stopEmbeddedDaemon()
|
||||
}
|
||||
|
||||
if (stop_sent) {
|
||||
DEBUG_LOGF("Waiting for daemon to begin shutdown...\n");
|
||||
shutdown_status_ = "Waiting for daemon to begin shutdown...";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n");
|
||||
shutdown_status_ = "Waiting for daemon to flush block index...";
|
||||
// Give the daemon time to flush LevelDB to disk before we
|
||||
// escalate to SIGTERM. On macOS/APFS, LevelDB compaction +
|
||||
// fsync can take 15-20s on a large chain. The stop() method
|
||||
// will wait this long for a *natural* exit (via the RPC stop
|
||||
// we already sent) before falling back to SIGTERM.
|
||||
}
|
||||
|
||||
// Wait for process to exit; SIGTERM/TerminateProcess as last resort.
|
||||
// 10 seconds is generous — if the daemon hasn't exited by then it's stuck.
|
||||
shutdown_status_ = "Waiting for dragonxd process to exit...";
|
||||
embedded_daemon_->stop(10000);
|
||||
// 20s grace period for the RPC "stop" to complete (LevelDB flush).
|
||||
// Only after that does stop() escalate to SIGTERM, then SIGKILL.
|
||||
embedded_daemon_->stop(20000);
|
||||
}
|
||||
|
||||
bool App::isEmbeddedDaemonRunning() const
|
||||
@@ -2358,11 +2445,14 @@ void App::renderShutdownScreen()
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::Begin("##ShutdownOverlay", nullptr,
|
||||
ImGuiWindowFlags shutdownFlags =
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
// Allow input after 10s so Force Quit button is clickable
|
||||
if (shutdown_timer_ < 10.0f)
|
||||
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
// Convert local centre to screen coords for draw-list primitives
|
||||
@@ -2460,6 +2550,63 @@ void App::renderShutdownScreen()
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 4b. Force Quit button — appears after 10 seconds
|
||||
// -------------------------------------------------------------------
|
||||
if (shutdown_timer_ >= 10.0f) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
const char* forceLabel = TR("force_quit");
|
||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
||||
if (ImGui::Button(forceLabel, btnSize)) {
|
||||
force_quit_confirm_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", TR("force_quit_warning"));
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
// Force Quit confirmation popup
|
||||
if (force_quit_confirm_) {
|
||||
ImGui::OpenPopup("##ForceQuitConfirm");
|
||||
force_quit_confirm_ = false;
|
||||
}
|
||||
ImVec2 popupCenter(wp.x + vp_size.x * 0.5f, wp.y + vp_size.y * 0.5f);
|
||||
ImGui::SetNextWindowPos(popupCenter, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
if (ImGui::BeginPopupModal("##ForceQuitConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
||||
ImGui::PushFont(Type().subtitle1());
|
||||
ImGui::TextUnformatted(TR("force_quit_confirm_title"));
|
||||
ImGui::PopFont();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(TR("force_quit_confirm_msg"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
float btnW = 120.0f;
|
||||
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
|
||||
|
||||
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
||||
if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) {
|
||||
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
|
||||
shutdown_complete_ = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
|
||||
@@ -173,6 +173,9 @@ public:
|
||||
// Pool mining (xmrig)
|
||||
void startPoolMining(int threads);
|
||||
void stopPoolMining();
|
||||
int getXmrigRequestedThreads() const {
|
||||
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
||||
}
|
||||
|
||||
// Mine-when-idle state query
|
||||
bool isIdleMiningActive() const { return idle_mining_active_; }
|
||||
@@ -218,13 +221,19 @@ public:
|
||||
void refreshPeerInfo();
|
||||
void refreshMarketData();
|
||||
|
||||
/// @brief Per-category refresh intervals, adjusted by active tab
|
||||
struct RefreshIntervals {
|
||||
float core; // balance + sync status
|
||||
float transactions; // tx list + enrichment
|
||||
float addresses; // address lists + balances
|
||||
float peers; // peer info (0 = disabled)
|
||||
};
|
||||
|
||||
/// @brief Get recommended refresh intervals for a given page
|
||||
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
|
||||
|
||||
// UI navigation
|
||||
void setCurrentPage(ui::NavPage page) {
|
||||
if (page != current_page_) {
|
||||
current_page_ = page;
|
||||
if (page == ui::NavPage::Peers) refreshPeerInfo();
|
||||
}
|
||||
}
|
||||
void setCurrentPage(ui::NavPage page);
|
||||
ui::NavPage getCurrentPage() const { return current_page_; }
|
||||
|
||||
// Dialog triggers (used by settings page to open modal dialogs)
|
||||
@@ -359,17 +368,20 @@ private:
|
||||
// Shutdown state
|
||||
std::atomic<bool> shutting_down_{false};
|
||||
std::atomic<bool> shutdown_complete_{false};
|
||||
std::atomic<bool> refresh_in_progress_{false};
|
||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||
std::string shutdown_status_;
|
||||
std::thread shutdown_thread_;
|
||||
float shutdown_timer_ = 0.0f;
|
||||
bool force_quit_confirm_ = false;
|
||||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||||
|
||||
// Daemon restart (e.g. after changing debug log categories)
|
||||
std::atomic<bool> daemon_restarting_{false};
|
||||
std::thread daemon_restart_thread_;
|
||||
|
||||
// Encryption state check timeout
|
||||
float encryption_check_timer_ = 0.0f;
|
||||
|
||||
// UI State
|
||||
bool quit_requested_ = false;
|
||||
bool show_demo_window_ = false;
|
||||
@@ -435,16 +447,33 @@ private:
|
||||
std::string pending_memo_;
|
||||
std::string pending_label_;
|
||||
|
||||
// Timers (in seconds since last update)
|
||||
float refresh_timer_ = 0.0f;
|
||||
// Per-category timers (in seconds since last refresh)
|
||||
float core_timer_ = 0.0f; // balance + sync status
|
||||
float address_timer_ = 0.0f; // address lists
|
||||
float transaction_timer_ = 0.0f; // transaction list
|
||||
float peer_timer_ = 0.0f; // peer info
|
||||
float price_timer_ = 0.0f;
|
||||
float fast_refresh_timer_ = 0.0f; // For mining stats
|
||||
|
||||
// Refresh intervals (seconds)
|
||||
static constexpr float REFRESH_INTERVAL = 5.0f;
|
||||
// Default refresh intervals (seconds)
|
||||
static constexpr float CORE_INTERVAL_DEFAULT = 5.0f;
|
||||
static constexpr float ADDRESS_INTERVAL_DEFAULT = 15.0f;
|
||||
static constexpr float TX_INTERVAL_DEFAULT = 10.0f;
|
||||
static constexpr float PEER_INTERVAL_DEFAULT = 10.0f;
|
||||
static constexpr float PRICE_INTERVAL = 60.0f;
|
||||
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
|
||||
|
||||
// Active intervals — adjusted by tab priority via applyRefreshPolicy()
|
||||
float active_core_interval_ = CORE_INTERVAL_DEFAULT;
|
||||
float active_tx_interval_ = TX_INTERVAL_DEFAULT;
|
||||
float active_addr_interval_ = ADDRESS_INTERVAL_DEFAULT;
|
||||
float active_peer_interval_ = PEER_INTERVAL_DEFAULT;
|
||||
|
||||
// Per-category refresh guards (prevent worker queue pileup)
|
||||
std::atomic<bool> core_refresh_in_progress_{false};
|
||||
std::atomic<bool> address_refresh_in_progress_{false};
|
||||
std::atomic<bool> tx_refresh_in_progress_{false};
|
||||
|
||||
// Mining refresh guard (prevents worker queue pileup)
|
||||
std::atomic<bool> mining_refresh_in_progress_{false};
|
||||
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
||||
@@ -494,6 +523,12 @@ private:
|
||||
float opid_poll_timer_ = 0.0f;
|
||||
static constexpr float OPID_POLL_INTERVAL = 2.0f;
|
||||
|
||||
// Txids from completed z_sendmany operations.
|
||||
// Ensures shielded sends are discoverable by z_viewtransaction
|
||||
// even when they don't appear in listtransactions or
|
||||
// z_listreceivedbyaddress.
|
||||
std::unordered_set<std::string> send_txids_;
|
||||
|
||||
// First-run wizard state
|
||||
WizardPhase wizard_phase_ = WizardPhase::None;
|
||||
std::unique_ptr<util::Bootstrap> bootstrap_;
|
||||
@@ -595,11 +630,17 @@ private:
|
||||
void applyDefaultBanlist();
|
||||
|
||||
// Private methods - data refresh
|
||||
void refreshData();
|
||||
void refreshBalance();
|
||||
void refreshAddresses();
|
||||
void refreshData(); // Orchestrator: dispatches per-category refreshes
|
||||
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
||||
void refreshAddressData(); // Address lists + balances
|
||||
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
|
||||
void refreshEncryptionState(); // Wallet encryption/lock state
|
||||
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
||||
void refreshAddresses(); // Legacy: standalone address refresh
|
||||
void refreshPrice();
|
||||
void refreshWalletEncryptionState();
|
||||
void applyRefreshPolicy(ui::NavPage page);
|
||||
bool shouldRefreshTransactions() const;
|
||||
void checkAutoLock();
|
||||
void checkIdleMining();
|
||||
};
|
||||
|
||||
+463
-577
File diff suppressed because it is too large
Load Diff
+50
-11
@@ -19,6 +19,7 @@
|
||||
#include "ui/schema/ui_schema.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/effects/imgui_acrylic.h"
|
||||
#include "ui/windows/mining_tab.h"
|
||||
#include "util/platform.h"
|
||||
#include "util/secure_vault.h"
|
||||
#include "util/perf_log.h"
|
||||
@@ -68,10 +69,13 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
}
|
||||
|
||||
ui::Notifications::instance().info(
|
||||
"Wallet encrypted successfully");
|
||||
"Wallet encrypted successfully", 5.0f);
|
||||
|
||||
// The daemon shuts itself down after encryptwallet
|
||||
// The daemon shuts itself down after encryptwallet.
|
||||
// Update connection_status_ so the loading overlay
|
||||
// explains why the daemon is restarting.
|
||||
if (isUsingEmbeddedDaemon()) {
|
||||
connection_status_ = TR("restarting_after_encryption");
|
||||
// Give daemon a moment to shut down, then restart
|
||||
// (do this off the main thread to avoid stalling the UI)
|
||||
std::thread([this]() {
|
||||
@@ -163,13 +167,13 @@ void App::processDeferredEncryption() {
|
||||
if (pinStored) {
|
||||
settings_->setPinEnabled(true);
|
||||
settings_->save();
|
||||
ui::Notifications::instance().info("Wallet encrypted & PIN set");
|
||||
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
|
||||
} else {
|
||||
ui::Notifications::instance().warning(
|
||||
"Wallet encrypted but PIN vault failed");
|
||||
}
|
||||
} else {
|
||||
ui::Notifications::instance().info("Wallet encrypted successfully");
|
||||
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
|
||||
}
|
||||
|
||||
// Securely clear deferred state
|
||||
@@ -263,7 +267,7 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
|
||||
state_.unlocked_until = std::time(nullptr) + timeout;
|
||||
} else {
|
||||
lock_attempts_++;
|
||||
lock_error_msg_ = "Incorrect passphrase";
|
||||
lock_error_msg_ = TR("incorrect_passphrase");
|
||||
lock_error_timer_ = 3.0f;
|
||||
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
||||
|
||||
@@ -439,12 +443,25 @@ void App::checkIdleMining() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip idle mining adjustments while thread benchmark is running
|
||||
if (ui::IsMiningBenchmarkActive()) return;
|
||||
|
||||
int idleSec = util::Platform::getSystemIdleSeconds();
|
||||
int delay = settings_->getMineIdleDelay();
|
||||
bool isPool = settings_->getPoolMode();
|
||||
bool threadScaling = settings_->getIdleThreadScaling();
|
||||
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||
|
||||
// GPU-aware idle detection: if enabled, treat GPU utilization >= 10%
|
||||
// as "user active" (e.g. watching a video). Disabled = unrestricted
|
||||
// mode that only looks at keyboard/mouse input.
|
||||
bool gpuBusy = false;
|
||||
if (settings_->getIdleGpuAware()) {
|
||||
int gpuUtil = util::Platform::getGpuUtilization();
|
||||
gpuBusy = (gpuUtil >= 10);
|
||||
}
|
||||
bool systemIdle = (idleSec >= delay) && !gpuBusy;
|
||||
|
||||
// Check if mining is already running (manually started by user)
|
||||
bool miningActive = isPool
|
||||
? (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||
@@ -461,7 +478,7 @@ void App::checkIdleMining() {
|
||||
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
|
||||
if (idleThreads <= 0) idleThreads = maxThreads;
|
||||
|
||||
if (idleSec >= delay) {
|
||||
if (systemIdle) {
|
||||
// System is idle — scale up to idle thread count
|
||||
if (!idle_scaled_to_idle_) {
|
||||
idle_scaled_to_idle_ = true;
|
||||
@@ -474,7 +491,7 @@ void App::checkIdleMining() {
|
||||
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
|
||||
}
|
||||
} else {
|
||||
// User is active — scale down to active thread count
|
||||
// User is active (or GPU busy) — scale down to active thread count
|
||||
if (idle_scaled_to_idle_) {
|
||||
idle_scaled_to_idle_ = false;
|
||||
if (isPool) {
|
||||
@@ -484,11 +501,26 @@ void App::checkIdleMining() {
|
||||
startMining(activeThreads);
|
||||
}
|
||||
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads);
|
||||
} else {
|
||||
// Mining just started while user is active — ensure active
|
||||
// thread count is applied (grid selection may differ).
|
||||
int currentThreads = isPool
|
||||
? xmrig_manager_->getStats().threads_active
|
||||
: state_.mining.genproclimit;
|
||||
if (currentThreads > 0 && currentThreads != activeThreads) {
|
||||
if (isPool) {
|
||||
stopPoolMining();
|
||||
startPoolMining(activeThreads);
|
||||
} else {
|
||||
startMining(activeThreads);
|
||||
}
|
||||
DEBUG_LOGF("[App] Idle thread scaling: initial %d -> %d threads (active)\n", currentThreads, activeThreads);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// --- Start/Stop mode (original behavior) ---
|
||||
if (idleSec >= delay) {
|
||||
if (systemIdle) {
|
||||
// System is idle — start mining if not already running
|
||||
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
|
||||
// For solo mining, need daemon connected and synced
|
||||
@@ -759,10 +791,17 @@ void App::renderLockScreen() {
|
||||
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
|
||||
|
||||
if (!vaultOk) {
|
||||
return [this]() {
|
||||
bool noVault = !vault_ || !vault_->hasVault();
|
||||
return [this, noVault]() {
|
||||
lock_unlock_in_progress_ = false;
|
||||
lock_attempts_++;
|
||||
lock_error_msg_ = "Incorrect PIN";
|
||||
if (noVault) {
|
||||
// Vault file missing — switch to passphrase mode
|
||||
lock_error_msg_ = TR("pin_not_set");
|
||||
lock_use_pin_ = false;
|
||||
} else {
|
||||
lock_attempts_++;
|
||||
lock_error_msg_ = TR("incorrect_pin");
|
||||
}
|
||||
lock_error_timer_ = 3.0f;
|
||||
|
||||
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
||||
|
||||
+8
-2
@@ -1294,7 +1294,10 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
// Start daemon + finish wizard immediately
|
||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||
startEmbeddedDaemon();
|
||||
if (!startEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning(
|
||||
TR("wizard_daemon_start_failed"));
|
||||
}
|
||||
}
|
||||
tryConnect();
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
@@ -1313,7 +1316,10 @@ void App::renderFirstRunWizard() {
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||
startEmbeddedDaemon();
|
||||
if (!startEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning(
|
||||
TR("wizard_daemon_start_failed"));
|
||||
}
|
||||
}
|
||||
tryConnect();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// settings.cpp — JSON settings persistence. Loads/saves user preferences
|
||||
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
|
||||
|
||||
#include "settings.h"
|
||||
#include "version.h"
|
||||
@@ -129,6 +132,7 @@ bool Settings::load(const std::string& path)
|
||||
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
|
||||
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
|
||||
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
|
||||
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
|
||||
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
||||
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||
debug_categories_.clear();
|
||||
@@ -137,6 +141,7 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
||||
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
||||
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
|
||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
||||
@@ -153,6 +158,7 @@ bool Settings::load(const std::string& path)
|
||||
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("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
|
||||
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||
saved_pool_urls_.clear();
|
||||
for (const auto& u : j["saved_pool_urls"])
|
||||
@@ -230,12 +236,14 @@ bool Settings::save(const std::string& path)
|
||||
j["pin_enabled"] = pin_enabled_;
|
||||
j["keep_daemon_running"] = keep_daemon_running_;
|
||||
j["stop_external_daemon"] = stop_external_daemon_;
|
||||
j["max_connections"] = max_connections_;
|
||||
j["verbose_logging"] = verbose_logging_;
|
||||
j["debug_categories"] = json::array();
|
||||
for (const auto& cat : debug_categories_)
|
||||
j["debug_categories"].push_back(cat);
|
||||
j["theme_effects_enabled"] = theme_effects_enabled_;
|
||||
j["low_spec_mode"] = low_spec_mode_;
|
||||
j["reduce_motion"] = reduce_motion_;
|
||||
j["selected_exchange"] = selected_exchange_;
|
||||
j["selected_pair"] = selected_pair_;
|
||||
j["pool_url"] = pool_url_;
|
||||
@@ -250,6 +258,7 @@ bool Settings::save(const std::string& path)
|
||||
j["idle_thread_scaling"] = idle_thread_scaling_;
|
||||
j["idle_threads_active"] = idle_threads_active_;
|
||||
j["idle_threads_idle"] = idle_threads_idle_;
|
||||
j["idle_gpu_aware"] = idle_gpu_aware_;
|
||||
j["saved_pool_urls"] = json::array();
|
||||
for (const auto& u : saved_pool_urls_)
|
||||
j["saved_pool_urls"].push_back(u);
|
||||
|
||||
@@ -165,6 +165,10 @@ public:
|
||||
bool getStopExternalDaemon() const { return stop_external_daemon_; }
|
||||
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
|
||||
|
||||
// Daemon — maximum peer connections (0 = daemon default)
|
||||
int getMaxConnections() const { return max_connections_; }
|
||||
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
||||
|
||||
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
||||
bool getVerboseLogging() const { return verbose_logging_; }
|
||||
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
||||
@@ -186,6 +190,10 @@ public:
|
||||
bool getLowSpecMode() const { return low_spec_mode_; }
|
||||
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
|
||||
|
||||
// Reduce motion — disables animated transitions for accessibility
|
||||
bool getReduceMotion() const { return reduce_motion_; }
|
||||
void setReduceMotion(bool v) { reduce_motion_ = v; }
|
||||
|
||||
// Market — last selected exchange + pair
|
||||
std::string getSelectedExchange() const { return selected_exchange_; }
|
||||
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
||||
@@ -221,6 +229,8 @@ public:
|
||||
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); }
|
||||
bool getIdleGpuAware() const { return idle_gpu_aware_; }
|
||||
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
|
||||
|
||||
// Saved pool URLs (user-managed favorites dropdown)
|
||||
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
|
||||
@@ -297,10 +307,12 @@ private:
|
||||
bool pin_enabled_ = false;
|
||||
bool keep_daemon_running_ = false;
|
||||
bool stop_external_daemon_ = false;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
bool verbose_logging_ = false;
|
||||
std::set<std::string> debug_categories_;
|
||||
bool theme_effects_enabled_ = true;
|
||||
bool low_spec_mode_ = false;
|
||||
bool reduce_motion_ = false;
|
||||
std::string selected_exchange_ = "TradeOgre";
|
||||
std::string selected_pair_ = "DRGX/BTC";
|
||||
|
||||
@@ -317,6 +329,7 @@ private:
|
||||
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)
|
||||
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
|
||||
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
|
||||
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||
|
||||
#define DRAGONX_VERSION "1.1.0"
|
||||
#define DRAGONX_VERSION "1.2.0"
|
||||
#define DRAGONX_VERSION_MAJOR 1
|
||||
#define DRAGONX_VERSION_MINOR 1
|
||||
#define DRAGONX_VERSION_MINOR 2
|
||||
#define DRAGONX_VERSION_PATCH 0
|
||||
|
||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
|
||||
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
|
||||
|
||||
#include "embedded_daemon.h"
|
||||
#include "../config/version.h"
|
||||
@@ -34,6 +37,9 @@
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#ifdef __APPLE__
|
||||
#include <sys/sysctl.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
@@ -152,6 +158,41 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
// DragonX chain parameters.
|
||||
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
|
||||
// On Linux, -printtoconsole is used for pipe-based output capture.
|
||||
// Auto-detect a reasonable -dbcache based on available physical RAM.
|
||||
// Default LevelDB cache is small (~450MB); larger caches improve sync
|
||||
// performance and reduce disk I/O — especially on macOS with APFS.
|
||||
std::string dbcache_arg = "-dbcache=450";
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
// sysctl hw.memsize returns total physical RAM in bytes
|
||||
int64_t memsize = 0;
|
||||
size_t len = sizeof(memsize);
|
||||
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
|
||||
int totalMB = static_cast<int>(memsize / (1024 * 1024));
|
||||
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
|
||||
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
long pages = sysconf(_SC_PHYS_PAGES);
|
||||
long page_size = sysconf(_SC_PAGE_SIZE);
|
||||
if (pages > 0 && page_size > 0) {
|
||||
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
|
||||
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
MEMORYSTATUSEX memInfo;
|
||||
memInfo.dwLength = sizeof(memInfo);
|
||||
if (GlobalMemoryStatusEx(&memInfo)) {
|
||||
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
|
||||
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||
}
|
||||
#endif
|
||||
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
|
||||
}
|
||||
|
||||
return {
|
||||
"-tls=only",
|
||||
#ifndef _WIN32
|
||||
@@ -164,9 +205,14 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
"-ac_reward=300000000",
|
||||
"-ac_blocktime=36",
|
||||
"-ac_private=1",
|
||||
"-addnode=176.126.87.241",
|
||||
"-addnode=node.dragonx.is",
|
||||
"-addnode=node1.dragonx.is",
|
||||
"-addnode=node2.dragonx.is",
|
||||
"-addnode=node3.dragonx.is",
|
||||
"-addnode=node4.dragonx.is",
|
||||
"-experimentalfeatures",
|
||||
"-developerencryptwallet"
|
||||
"-developerencryptwallet",
|
||||
dbcache_arg
|
||||
};
|
||||
}
|
||||
|
||||
@@ -431,6 +477,11 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-debug=" + cat);
|
||||
}
|
||||
|
||||
// Append max connections if configured (0 = daemon default)
|
||||
if (max_connections_ > 0) {
|
||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||
}
|
||||
|
||||
// Add -rescan flag if requested (one-shot)
|
||||
if (rescan_on_next_start_.exchange(false)) {
|
||||
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
||||
@@ -1045,14 +1096,12 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
if (process_pid_ > 0) {
|
||||
setState(State::Stopping, "Stopping dragonxd...");
|
||||
|
||||
// Send SIGTERM to the entire process group (negative PID).
|
||||
// This ensures that if dragonxd is a shell script wrapper,
|
||||
// both bash AND the actual dragonxd child receive the signal.
|
||||
// Without this, only bash is killed and dragonxd is orphaned.
|
||||
DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_);
|
||||
kill(-process_pid_, SIGTERM);
|
||||
|
||||
// Wait for process to exit, draining stdout each iteration
|
||||
// Phase 1: Wait for the daemon to exit naturally.
|
||||
// The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
|
||||
// tells the daemon to flush LevelDB, close sockets, and exit cleanly.
|
||||
// On macOS/APFS the LevelDB flush can take several seconds — we must
|
||||
// NOT send SIGTERM until the daemon has had enough time to finish.
|
||||
DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (isRunning()) {
|
||||
drainOutput();
|
||||
@@ -1061,15 +1110,34 @@ void EmbeddedDaemon::stop(int wait_ms)
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
|
||||
if (elapsed >= wait_ms) {
|
||||
// Force kill the entire process group
|
||||
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
|
||||
kill(-process_pid_, SIGKILL);
|
||||
break;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
// Phase 2: If still running, send SIGTERM and wait a further 10s.
|
||||
if (isRunning()) {
|
||||
DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
|
||||
kill(-process_pid_, SIGTERM);
|
||||
|
||||
auto sigterm_start = std::chrono::steady_clock::now();
|
||||
while (isRunning()) {
|
||||
drainOutput();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - sigterm_start).count();
|
||||
if (elapsed >= 10000) {
|
||||
// Phase 3: Force kill
|
||||
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
|
||||
kill(-process_pid_, SIGKILL);
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
|
||||
}
|
||||
|
||||
drainOutput(); // read any remaining output
|
||||
|
||||
// Reap the child process
|
||||
|
||||
@@ -152,6 +152,11 @@ public:
|
||||
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
||||
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
|
||||
|
||||
/**
|
||||
* @brief Set maximum peer connections (0 = use daemon default)
|
||||
*/
|
||||
void setMaxConnections(int v) { max_connections_ = v; }
|
||||
|
||||
/**
|
||||
* @brief Request a blockchain rescan on the next daemon start
|
||||
*/
|
||||
@@ -194,6 +199,7 @@ private:
|
||||
std::thread monitor_thread_;
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::set<std::string> debug_categories_;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
|
||||
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
#include "../resources/embedded_resources.h"
|
||||
|
||||
@@ -88,6 +88,10 @@ public:
|
||||
const PoolStats& getStats() const { return stats_; }
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
|
||||
/// Thread count requested at start() — available immediately, unlike
|
||||
/// PoolStats::threads_active which requires an API response.
|
||||
int getRequestedThreads() const { return threads_; }
|
||||
|
||||
/**
|
||||
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
|
||||
*/
|
||||
|
||||
@@ -134,6 +134,7 @@ struct MarketInfo {
|
||||
double change_24h = 0.0;
|
||||
double market_cap = 0.0;
|
||||
std::string last_updated;
|
||||
std::chrono::steady_clock::time_point last_fetch_time{};
|
||||
|
||||
// Price history for chart
|
||||
std::vector<double> price_history;
|
||||
|
||||
@@ -12,4 +12,4 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
|
||||
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
|
||||
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
|
||||
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf");
|
||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
||||
|
||||
+668
-668
File diff suppressed because it is too large
Load Diff
+704
-704
File diff suppressed because it is too large
Load Diff
+730
-730
File diff suppressed because it is too large
Load Diff
+624
-625
File diff suppressed because it is too large
Load Diff
+663
-663
File diff suppressed because it is too large
Load Diff
+929
-929
File diff suppressed because it is too large
Load Diff
+574
-574
File diff suppressed because it is too large
Load Diff
@@ -234,8 +234,11 @@ bool Connection::createDefaultConfig(const std::string& path)
|
||||
file << "exportdir=" << dataDir << "\n";
|
||||
file << "experimentalfeatures=1\n";
|
||||
file << "developerencryptwallet=1\n";
|
||||
file << "addnode=195.201.20.230\n";
|
||||
file << "addnode=195.201.137.219\n";
|
||||
file << "addnode=node.dragonx.is\n";
|
||||
file << "addnode=node1.dragonx.is\n";
|
||||
file << "addnode=node2.dragonx.is\n";
|
||||
file << "addnode=node3.dragonx.is\n";
|
||||
file << "addnode=node4.dragonx.is\n";
|
||||
|
||||
file.close();
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// rpc_client.cpp — JSON-RPC client over HTTPS using libcurl.
|
||||
// All calls are blocking; run on RPCWorker threads, never on main thread.
|
||||
|
||||
#include "rpc_client.h"
|
||||
#include "../config/version.h"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// rpc_worker.cpp — Background work queue. Executes WorkFn on its own thread,
|
||||
// returns MainCb callbacks drained each frame on the main thread.
|
||||
|
||||
#include "rpc_worker.h"
|
||||
#include <cstdio>
|
||||
|
||||
@@ -300,7 +300,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
cjkCfg.GlyphRanges = cjkRanges;
|
||||
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
|
||||
|
||||
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
|
||||
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
|
||||
if (mergeResult) {
|
||||
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
|
||||
g_noto_cjk_subset_size, name);
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: WARNING — CJK merge FAILED for %s (size=%u)\n",
|
||||
name, g_noto_cjk_subset_size);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
||||
|
||||
@@ -93,6 +93,9 @@ static bool sp_gradient_background = false;
|
||||
// Low-spec mode
|
||||
static bool sp_low_spec_mode = false;
|
||||
|
||||
// Reduce motion (accessibility)
|
||||
static bool sp_reduce_motion = false;
|
||||
|
||||
// Font scale (user accessibility, 1.0–3.0)
|
||||
static float sp_font_scale = 1.0f;
|
||||
|
||||
@@ -179,6 +182,7 @@ static void loadSettingsPageState(config::Settings* settings) {
|
||||
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
||||
sp_low_spec_mode = settings->getLowSpecMode();
|
||||
effects::setLowSpecMode(sp_low_spec_mode);
|
||||
sp_reduce_motion = settings->getReduceMotion();
|
||||
sp_font_scale = settings->getFontScale();
|
||||
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
|
||||
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
||||
@@ -232,6 +236,7 @@ static void saveSettingsPageState(config::Settings* settings) {
|
||||
settings->setScanlineEnabled(sp_scanline_enabled);
|
||||
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
||||
settings->setLowSpecMode(sp_low_spec_mode);
|
||||
settings->setReduceMotion(sp_reduce_motion);
|
||||
settings->setFontScale(sp_font_scale);
|
||||
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
||||
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
||||
@@ -672,6 +677,12 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
|
||||
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) {
|
||||
saveSettingsPageState(app->settings());
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
|
||||
|
||||
ImGui::BeginDisabled(sp_low_spec_mode);
|
||||
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
@@ -702,7 +713,7 @@ void RenderSettingsPage(App* app) {
|
||||
{
|
||||
char blur_fmt[16];
|
||||
if (sp_blur_amount < 0.01f)
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
|
||||
else
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
|
||||
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||||
@@ -724,7 +735,7 @@ void RenderSettingsPage(App* app) {
|
||||
{
|
||||
char noise_fmt[16];
|
||||
if (sp_noise_opacity < 0.01f)
|
||||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
||||
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
|
||||
else
|
||||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
|
||||
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
||||
@@ -962,6 +973,11 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
|
||||
|
||||
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) {
|
||||
saveSettingsPageState(app->settings());
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
|
||||
|
||||
ImGui::BeginDisabled(sp_low_spec_mode);
|
||||
|
||||
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
|
||||
@@ -985,7 +1001,7 @@ void RenderSettingsPage(App* app) {
|
||||
{
|
||||
char blur_fmt[16];
|
||||
if (sp_blur_amount < 0.01f)
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
|
||||
else
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
|
||||
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||||
@@ -1003,7 +1019,7 @@ void RenderSettingsPage(App* app) {
|
||||
{
|
||||
char noise_fmt[16];
|
||||
if (sp_noise_opacity < 0.01f)
|
||||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
||||
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
|
||||
else
|
||||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
|
||||
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
||||
@@ -1570,7 +1586,7 @@ void RenderSettingsPage(App* app) {
|
||||
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
|
||||
|
||||
int timeout = app->settings()->getAutoLockTimeout();
|
||||
const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" };
|
||||
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
|
||||
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
|
||||
int selTimeout = 0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
|
||||
+8
-4
@@ -380,7 +380,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
// collapsed: when true, sidebar is in icon-only mode (narrow width).
|
||||
// The caller can toggle collapsed via a reference if a toggle button is desired.
|
||||
inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight,
|
||||
SidebarStatus& status, bool& collapsed)
|
||||
SidebarStatus& status, bool& collapsed, bool locked = false)
|
||||
{
|
||||
using namespace material;
|
||||
bool changed = false;
|
||||
@@ -610,8 +610,12 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
|
||||
// Click detection
|
||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
// Click detection — block pages that require unlock when locked
|
||||
bool pageNeedsUnlock = locked &&
|
||||
item.page != NavPage::Console &&
|
||||
item.page != NavPage::Peers &&
|
||||
item.page != NavPage::Settings;
|
||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
||||
current = item.page;
|
||||
changed = true;
|
||||
}
|
||||
@@ -620,7 +624,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
float iconS = iconHalfSize; // icon half-size in pixels
|
||||
float iconCY = cursor.y + btnH * 0.5f;
|
||||
float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f;
|
||||
ImU32 textCol = selected ? Primary() : OnSurfaceMedium();
|
||||
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
|
||||
|
||||
if (showLabels) {
|
||||
// Measure total width of icon + gap + label, then center.
|
||||
|
||||
@@ -144,7 +144,7 @@ void BackupWalletDialog::render(App* app)
|
||||
s_status = statusMsg;
|
||||
s_backing_up = false;
|
||||
if (success) {
|
||||
Notifications::instance().success(TR("backup_created"));
|
||||
Notifications::instance().success(TR("backup_created"), 5.0f);
|
||||
} else {
|
||||
Notifications::instance().warning(statusMsg);
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Lerp displayed balances toward actual values
|
||||
{
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
float speed = kBalanceLerpSpeed;
|
||||
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
|
||||
auto lerp = [](double& disp, double target, float dt, float spd) {
|
||||
double diff = target - disp;
|
||||
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
||||
@@ -1278,7 +1278,7 @@ static void UpdateBalanceLerp(App* app) {
|
||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
||||
const auto& state = app->state();
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
float speed = kBalanceLerpSpeed;
|
||||
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
|
||||
auto lerp = [](double& disp, double target, float dt, float spd) {
|
||||
double diff = target - disp;
|
||||
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// console_tab.cpp — Interactive RPC console with command history,
|
||||
// tab completion, daemon log display, and color-coded output.
|
||||
|
||||
#include "console_tab.h"
|
||||
#include "../material/colors.h"
|
||||
|
||||
@@ -213,7 +213,7 @@ void ExportAllKeysDialog::render(App* app)
|
||||
s_exporting = false;
|
||||
if (writeOk) {
|
||||
s_status = "Exported to: " + filepath;
|
||||
Notifications::instance().success(TR("export_keys_success"));
|
||||
Notifications::instance().success(TR("export_keys_success"), 5.0f);
|
||||
} else {
|
||||
s_status = "Failed to write file";
|
||||
Notifications::instance().error("Failed to save key file");
|
||||
|
||||
@@ -142,7 +142,7 @@ void ExportTransactionsDialog::render(App* app)
|
||||
|
||||
s_status = "Exported " + std::to_string(state.transactions.size()) +
|
||||
" transactions to: " + filepath;
|
||||
Notifications::instance().success(TR("export_tx_success"));
|
||||
Notifications::instance().success(TR("export_tx_success"), 5.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +307,9 @@ void ImportKeyDialog::render(App* app)
|
||||
|
||||
// Import keys on worker thread to avoid freezing UI
|
||||
bool rescan = s_rescan;
|
||||
int rescanHeight = s_rescan_height;
|
||||
if (app->worker()) {
|
||||
app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb {
|
||||
app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
|
||||
int imported = 0;
|
||||
int failed = 0;
|
||||
|
||||
@@ -317,10 +318,22 @@ void ImportKeyDialog::render(App* app)
|
||||
|
||||
try {
|
||||
if (keyType == "z-spending") {
|
||||
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
|
||||
// z_importkey "key" "yes"|"no" startheight
|
||||
if (rescan && rescanHeight > 0) {
|
||||
rpc->call("z_importkey", {key, "yes", rescanHeight});
|
||||
} else {
|
||||
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
|
||||
}
|
||||
imported++;
|
||||
} else if (keyType == "t-privkey") {
|
||||
rpc->call("importprivkey", {key, "", rescan});
|
||||
// importprivkey does not accept a startheight;
|
||||
// import without rescan, then rescanblockchain
|
||||
// from the requested height afterward.
|
||||
if (rescan && rescanHeight > 0) {
|
||||
rpc->call("importprivkey", {key, "", false});
|
||||
} else {
|
||||
rpc->call("importprivkey", {key, "", rescan});
|
||||
}
|
||||
imported++;
|
||||
} else {
|
||||
failed++;
|
||||
@@ -329,6 +342,17 @@ void ImportKeyDialog::render(App* app)
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// If t-keys were imported without rescan (because a
|
||||
// specific start height was requested), trigger a
|
||||
// single rescanblockchain from that height now.
|
||||
if (rescan && rescanHeight > 0 && imported > 0) {
|
||||
try {
|
||||
rpc->call("rescanblockchain", {rescanHeight});
|
||||
} catch (...) {
|
||||
// rescan failure is non-fatal; user can retry
|
||||
}
|
||||
}
|
||||
|
||||
return [imported, failed]() {
|
||||
s_imported_keys = imported;
|
||||
@@ -339,7 +363,7 @@ void ImportKeyDialog::render(App* app)
|
||||
imported, failed);
|
||||
s_status = buf;
|
||||
if (imported > 0) {
|
||||
Notifications::instance().success(TR("import_key_success"));
|
||||
Notifications::instance().success(TR("import_key_success"), 5.0f);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -237,6 +237,26 @@ void RenderMarketTab(App* app)
|
||||
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
|
||||
}
|
||||
|
||||
// ---- STALENESS INDICATOR ----
|
||||
{
|
||||
auto fetchTime = market.last_fetch_time;
|
||||
if (fetchTime.time_since_epoch().count() > 0) {
|
||||
auto elapsed = std::chrono::steady_clock::now() - fetchTime;
|
||||
int ageSecs = (int)std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
|
||||
bool stale = ageSecs > 300; // 5 minutes
|
||||
if (ageSecs < 60)
|
||||
snprintf(buf, sizeof(buf), "%s %ds %s", stale ? ICON_MD_WARNING : "", ageSecs, TR("ago"));
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%s %dm %s", stale ? ICON_MD_WARNING : "", ageSecs / 60, TR("ago"));
|
||||
ImFont* staleFont = capFont;
|
||||
ImU32 staleCol = stale ? Warning() : WithAlpha(OnSurface(), 100);
|
||||
float staleY = statsY + ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
|
||||
ImVec2 staleSz = staleFont->CalcTextSizeA(staleFont->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(staleFont, staleFont->LegacySize,
|
||||
ImVec2(cardMin.x + Layout::spacingLg(), staleY), staleCol, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TRADE BUTTON (top-right of card) ----
|
||||
if (!currentExchange.pairs.empty()) {
|
||||
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
|
||||
|
||||
@@ -43,6 +43,132 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
|
||||
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
|
||||
static int s_earnings_filter = 0;
|
||||
|
||||
// Thread benchmark state
|
||||
struct ThreadBenchmark {
|
||||
enum class Phase { Idle, Starting, WarmingUp, Measuring, Advancing, CoolingDown, Done };
|
||||
Phase phase = Phase::Idle;
|
||||
|
||||
std::vector<int> candidates;
|
||||
int current_index = 0;
|
||||
|
||||
struct Result {
|
||||
int threads;
|
||||
double hashrate;
|
||||
};
|
||||
std::vector<Result> results;
|
||||
|
||||
float phase_timer = 0.0f;
|
||||
|
||||
// Warmup: wait at least MIN then check for hashrate stability; cap at MAX.
|
||||
// Laptops need 90s+ for thermal throttling to fully manifest.
|
||||
static constexpr float MIN_WARMUP_SECS = 90.0f;
|
||||
static constexpr float MAX_WARMUP_SECS = 300.0f;
|
||||
static constexpr float MEASURE_SECS = 30.0f;
|
||||
static constexpr float COOLDOWN_SECS = 5.0f;
|
||||
|
||||
// Stability detection — compare rolling 10s hashrate windows.
|
||||
// Require STABLE_WINDOWS_NEEDED consecutive stable readings.
|
||||
static constexpr float STABILITY_WINDOW_SECS = 10.0f;
|
||||
static constexpr float STABILITY_THRESHOLD = 0.05f; // 5% change → stable
|
||||
static constexpr int STABLE_WINDOWS_NEEDED = 3;
|
||||
double prev_window_avg = 0.0;
|
||||
double window_sum = 0.0;
|
||||
int window_samples = 0;
|
||||
float window_timer = 0.0f;
|
||||
int consecutive_stable = 0; // count of consecutive stable windows
|
||||
|
||||
// Measurement: average-based (sustained performance, not peak burst)
|
||||
double measure_sum = 0.0;
|
||||
int measure_samples = 0;
|
||||
|
||||
int optimal_threads = 0;
|
||||
double optimal_hashrate = 0.0;
|
||||
bool was_pool_running = false;
|
||||
int prev_threads = 0;
|
||||
|
||||
// Track actual warmup durations for better time estimates
|
||||
float total_warmup_secs = 0.0f;
|
||||
|
||||
void reset() {
|
||||
phase = Phase::Idle;
|
||||
candidates.clear();
|
||||
current_index = 0;
|
||||
results.clear();
|
||||
phase_timer = 0.0f;
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
measure_sum = 0.0;
|
||||
measure_samples = 0;
|
||||
optimal_threads = 0;
|
||||
optimal_hashrate = 0.0;
|
||||
was_pool_running = false;
|
||||
prev_threads = 0;
|
||||
total_warmup_secs = 0.0f;
|
||||
}
|
||||
|
||||
void buildCandidates(int max_threads) {
|
||||
candidates.clear();
|
||||
// Start at half the cores — lower counts are rarely optimal and
|
||||
// testing them first would waste time warming up the CPU before
|
||||
// reaching the thread counts that actually matter.
|
||||
int start = std::max(1, max_threads / 2);
|
||||
for (int t = start; t <= max_threads; t++)
|
||||
candidates.push_back(t);
|
||||
}
|
||||
|
||||
/// Average warmup duration based on tests completed so far
|
||||
float avgWarmupSecs() const {
|
||||
if (current_index > 0)
|
||||
return total_warmup_secs / (float)current_index;
|
||||
return (MIN_WARMUP_SECS + MAX_WARMUP_SECS) * 0.5f; // initial estimate
|
||||
}
|
||||
|
||||
/// Estimated seconds per test (uses observed warmup average)
|
||||
float perTestSecs() const {
|
||||
return avgWarmupSecs() + MEASURE_SECS;
|
||||
}
|
||||
|
||||
float totalEstimatedSecs() const {
|
||||
int n = (int)candidates.size();
|
||||
if (n <= 0) return 0.0f;
|
||||
// Completed tests use actual time; remaining use estimate
|
||||
float completed_time = total_warmup_secs
|
||||
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
int remaining = n - current_index;
|
||||
float remaining_time = (float)remaining * (avgWarmupSecs() + MEASURE_SECS)
|
||||
+ (float)std::max(0, remaining - 1) * COOLDOWN_SECS;
|
||||
return completed_time + remaining_time;
|
||||
}
|
||||
|
||||
float elapsedSecs() const {
|
||||
float completed = total_warmup_secs
|
||||
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
return completed + phase_timer;
|
||||
}
|
||||
|
||||
float progress() const {
|
||||
float total = totalEstimatedSecs();
|
||||
return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f;
|
||||
}
|
||||
|
||||
void resetStabilityTracking() {
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
}
|
||||
};
|
||||
static ThreadBenchmark s_benchmark;
|
||||
|
||||
bool IsMiningBenchmarkActive() {
|
||||
return s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||
}
|
||||
|
||||
// Pool mode state
|
||||
static bool s_pool_mode = false;
|
||||
static char s_pool_url[256] = "pool.dragonx.is:3433";
|
||||
@@ -162,9 +288,16 @@ static void RenderMiningTabContent(App* app)
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Skip during benchmark — the benchmark controls thread counts directly
|
||||
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !IsMiningBenchmarkActive()) {
|
||||
if (s_pool_mode && state.pool_mining.xmrig_running) {
|
||||
// Use the requested thread count (available immediately) rather
|
||||
// than threads_active from the xmrig API which lags during restarts.
|
||||
int reqThreads = app->getXmrigRequestedThreads();
|
||||
if (reqThreads > 0)
|
||||
s_selected_threads = std::min(reqThreads, max_threads);
|
||||
else if (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);
|
||||
}
|
||||
@@ -239,6 +372,135 @@ static void RenderMiningTabContent(App* app)
|
||||
? state.pool_mining.xmrig_running
|
||||
: (mining.generate || state.pool_mining.xmrig_running);
|
||||
|
||||
// ================================================================
|
||||
// Thread Benchmark state machine — runs pool mining at each candidate
|
||||
// thread count to find the optimal setting for this CPU.
|
||||
// ================================================================
|
||||
if (s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done) {
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
s_benchmark.phase_timer += dt;
|
||||
|
||||
switch (s_benchmark.phase) {
|
||||
case ThreadBenchmark::Phase::Starting:
|
||||
// Start pool mining at current candidate
|
||||
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
|
||||
int t = s_benchmark.candidates[s_benchmark.current_index];
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(t);
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::WarmingUp;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
s_benchmark.resetStabilityTracking();
|
||||
s_benchmark.measure_sum = 0.0;
|
||||
s_benchmark.measure_samples = 0;
|
||||
} else {
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::WarmingUp: {
|
||||
// Adaptive warmup: wait for hashrate to stabilize (thermal steady state).
|
||||
// After MIN_WARMUP (90s), compare rolling 10s hashrate windows.
|
||||
// Require 3 consecutive windows within 5% to confirm equilibrium.
|
||||
// Laptops can take 2-3+ minutes for thermal throttling to fully
|
||||
// manifest, so a single stable window isn't sufficient.
|
||||
bool past_min = s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS;
|
||||
bool past_max = s_benchmark.phase_timer >= ThreadBenchmark::MAX_WARMUP_SECS;
|
||||
|
||||
// Accumulate samples into current window
|
||||
if (state.pool_mining.hashrate_10s > 0.0) {
|
||||
s_benchmark.window_sum += state.pool_mining.hashrate_10s;
|
||||
s_benchmark.window_samples++;
|
||||
}
|
||||
s_benchmark.window_timer += dt;
|
||||
|
||||
bool stable = false;
|
||||
if (past_min && s_benchmark.window_timer >= ThreadBenchmark::STABILITY_WINDOW_SECS
|
||||
&& s_benchmark.window_samples > 0) {
|
||||
double current_avg = s_benchmark.window_sum / s_benchmark.window_samples;
|
||||
if (s_benchmark.prev_window_avg > 0.0) {
|
||||
double change = std::abs(current_avg - s_benchmark.prev_window_avg)
|
||||
/ s_benchmark.prev_window_avg;
|
||||
if (change < ThreadBenchmark::STABILITY_THRESHOLD)
|
||||
s_benchmark.consecutive_stable++;
|
||||
else
|
||||
s_benchmark.consecutive_stable = 0; // reset on instability
|
||||
if (s_benchmark.consecutive_stable >= ThreadBenchmark::STABLE_WINDOWS_NEEDED)
|
||||
stable = true;
|
||||
}
|
||||
// Shift window
|
||||
s_benchmark.prev_window_avg = current_avg;
|
||||
s_benchmark.window_sum = 0.0;
|
||||
s_benchmark.window_samples = 0;
|
||||
s_benchmark.window_timer = 0.0f;
|
||||
}
|
||||
|
||||
if (stable || past_max) {
|
||||
s_benchmark.total_warmup_secs += s_benchmark.phase_timer;
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Measuring;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
s_benchmark.measure_sum = 0.0;
|
||||
s_benchmark.measure_samples = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ThreadBenchmark::Phase::Measuring:
|
||||
// Sample average hashrate — reflects sustained (thermally throttled) performance
|
||||
if (state.pool_mining.hashrate_10s > 0.0) {
|
||||
s_benchmark.measure_sum += state.pool_mining.hashrate_10s;
|
||||
s_benchmark.measure_samples++;
|
||||
}
|
||||
if (s_benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) {
|
||||
int t = s_benchmark.candidates[s_benchmark.current_index];
|
||||
double avg = (s_benchmark.measure_samples > 0)
|
||||
? s_benchmark.measure_sum / s_benchmark.measure_samples
|
||||
: 0.0;
|
||||
s_benchmark.results.push_back({t, avg});
|
||||
if (avg > s_benchmark.optimal_hashrate) {
|
||||
s_benchmark.optimal_hashrate = avg;
|
||||
s_benchmark.optimal_threads = t;
|
||||
}
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Advancing;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::Advancing:
|
||||
app->stopPoolMining();
|
||||
s_benchmark.current_index++;
|
||||
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
|
||||
// Cool down before next test to reduce thermal throttling bias
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::CoolingDown;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
} else {
|
||||
// Done — apply optimal thread count
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
if (s_benchmark.optimal_threads > 0) {
|
||||
s_selected_threads = s_benchmark.optimal_threads;
|
||||
app->settings()->setPoolThreads(s_selected_threads);
|
||||
app->settings()->save();
|
||||
}
|
||||
// Restart mining if it was running before, using optimal count
|
||||
if (s_benchmark.was_pool_running && s_benchmark.optimal_threads > 0) {
|
||||
app->startPoolMining(s_benchmark.optimal_threads);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::CoolingDown:
|
||||
// Idle pause — let CPU temps drop before starting next test
|
||||
if (s_benchmark.phase_timer >= ThreadBenchmark::COOLDOWN_SECS) {
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Proportional section budget — ensures all content fits without
|
||||
// scrolling at the minimum window size (1024×775).
|
||||
@@ -936,6 +1198,41 @@ static void RenderMiningTabContent(App* app)
|
||||
idleRightEdge = sBtnX - 4.0f * dp;
|
||||
}
|
||||
|
||||
// GPU-aware idle toggle (to the left, when idle is on)
|
||||
// When ON (default): GPU utilization >= 10% counts as "not idle"
|
||||
// When OFF: unrestricted mode, only keyboard/mouse input matters
|
||||
if (idleOn) {
|
||||
bool gpuAware = app->settings()->getIdleGpuAware();
|
||||
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
|
||||
float gBtnX = idleRightEdge - btnSz;
|
||||
float gBtnY = btnY;
|
||||
|
||||
if (gpuAware) {
|
||||
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
|
||||
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
||||
}
|
||||
|
||||
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
|
||||
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
|
||||
dl->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
|
||||
gIcoCol, gpuIcon);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
|
||||
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
app->settings()->setIdleGpuAware(!gpuAware);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", gpuAware
|
||||
? TR("mining_idle_gpu_on_tooltip")
|
||||
: TR("mining_idle_gpu_off_tooltip"));
|
||||
}
|
||||
idleRightEdge = gBtnX - 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; };
|
||||
@@ -1076,6 +1373,175 @@ static void RenderMiningTabContent(App* app)
|
||||
ImGui::SetCursorScreenPos(savedCur);
|
||||
}
|
||||
|
||||
// --- Thread Benchmark button / progress (left of idle toggle) ---
|
||||
{
|
||||
ImVec2 benchSavedCur = ImGui::GetCursorScreenPos();
|
||||
bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||
bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done;
|
||||
ImFont* icoFont = Type().iconSmall();
|
||||
|
||||
if (benchRunning) {
|
||||
// Show progress bar + current test info
|
||||
float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp);
|
||||
float barH = 4.0f * dp;
|
||||
float barX = idleRightEdge - barW;
|
||||
float barY = curY + headerH - barH - 2.0f * dp;
|
||||
|
||||
// Progress bar track
|
||||
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||
WithAlpha(OnSurface(), 30), barH * 0.5f);
|
||||
// Progress bar fill
|
||||
float pct = s_benchmark.progress();
|
||||
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH),
|
||||
Primary(), barH * 0.5f);
|
||||
|
||||
// Status text above bar
|
||||
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
|
||||
? s_benchmark.candidates[s_benchmark.current_index] : 0;
|
||||
// Estimated remaining time (uses observed warmup for better accuracy)
|
||||
int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index;
|
||||
float elapsed_in_phase = s_benchmark.phase_timer;
|
||||
float phase_total;
|
||||
if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp)
|
||||
phase_total = s_benchmark.avgWarmupSecs(); // adaptive estimate
|
||||
else if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
|
||||
phase_total = ThreadBenchmark::COOLDOWN_SECS;
|
||||
else
|
||||
phase_total = ThreadBenchmark::MEASURE_SECS;
|
||||
float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase);
|
||||
// Remaining tests after current each need warmup + measure + cooldown
|
||||
float est_secs = remaining_in_current
|
||||
+ (remaining_tests - 1) * (s_benchmark.avgWarmupSecs() + ThreadBenchmark::MEASURE_SECS + ThreadBenchmark::COOLDOWN_SECS);
|
||||
int est_min = (int)(est_secs / 60.0f);
|
||||
int est_sec = (int)est_secs % 60;
|
||||
const char* phase_label;
|
||||
if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
|
||||
phase_label = TR("mining_benchmark_cooling");
|
||||
else if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp
|
||||
&& s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS)
|
||||
phase_label = TR("mining_benchmark_stabilizing");
|
||||
else
|
||||
phase_label = TR("mining_benchmark_testing");
|
||||
if (est_min > 0)
|
||||
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds",
|
||||
phase_label,
|
||||
s_benchmark.current_index + 1,
|
||||
(int)s_benchmark.candidates.size(), ct, est_min, est_sec);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds",
|
||||
phase_label,
|
||||
s_benchmark.current_index + 1,
|
||||
(int)s_benchmark.candidates.size(), ct, est_sec);
|
||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
|
||||
OnSurfaceMedium(), buf);
|
||||
|
||||
// Cancel button (small X)
|
||||
float cancelSz = icoFont->LegacySize + 4.0f * dp;
|
||||
float cancelX = barX - cancelSz - 4.0f * dp;
|
||||
float cancelY = curY + (headerH - cancelSz) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY));
|
||||
ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
app->stopPoolMining();
|
||||
if (s_benchmark.was_pool_running)
|
||||
app->startPoolMining(s_benchmark.prev_threads);
|
||||
s_benchmark.reset();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", TR("mining_benchmark_cancel"));
|
||||
}
|
||||
const char* cancelIcon = ICON_MD_CLOSE;
|
||||
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
|
||||
dl->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f,
|
||||
cancelY + (cancelSz - cIcoSz.y) * 0.5f),
|
||||
OnSurfaceMedium(), cancelIcon);
|
||||
|
||||
idleRightEdge = cancelX - 4.0f * dp;
|
||||
|
||||
} else if (benchDone && s_benchmark.optimal_threads > 0) {
|
||||
// Show result briefly, then reset on next click
|
||||
snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)",
|
||||
TR("mining_benchmark_result"),
|
||||
s_benchmark.optimal_threads, s_benchmark.optimal_hashrate);
|
||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
float txtX = idleRightEdge - txtSz.x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f),
|
||||
WithAlpha(Success(), 220), buf);
|
||||
|
||||
// Dismiss button
|
||||
float dismissSz = icoFont->LegacySize + 4.0f * dp;
|
||||
float dismissX = txtX - dismissSz - 4.0f * dp;
|
||||
float dismissY = curY + (headerH - dismissSz) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY));
|
||||
ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz));
|
||||
if (ImGui::IsItemClicked())
|
||||
s_benchmark.reset();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss"));
|
||||
}
|
||||
const char* okIcon = ICON_MD_CHECK;
|
||||
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
|
||||
dl->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f,
|
||||
dismissY + (dismissSz - oIcoSz.y) * 0.5f),
|
||||
WithAlpha(Success(), 200), okIcon);
|
||||
|
||||
idleRightEdge = dismissX - 4.0f * dp;
|
||||
|
||||
} else if (s_pool_mode) {
|
||||
// Show benchmark button (only in pool mode)
|
||||
float btnSz = icoFont->LegacySize + 8.0f * dp;
|
||||
float btnX = idleRightEdge - btnSz;
|
||||
float btnY = curY + (headerH - btnSz) * 0.5f;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
|
||||
bool benchHovered = ImGui::IsItemHovered();
|
||||
bool benchClicked = ImGui::IsItemClicked();
|
||||
|
||||
// Hover highlight
|
||||
if (benchHovered) {
|
||||
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
||||
StateHover(), btnSz * 0.5f);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip"));
|
||||
}
|
||||
|
||||
const char* benchIcon = ICON_MD_SPEED;
|
||||
ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon);
|
||||
dl->AddText(icoFont, icoFont->LegacySize,
|
||||
ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f,
|
||||
btnY + (btnSz - bIcoSz.y) * 0.5f),
|
||||
OnSurfaceMedium(), benchIcon);
|
||||
|
||||
if (benchClicked) {
|
||||
// Require a wallet address for pool mining
|
||||
std::string worker(s_pool_worker);
|
||||
if (!worker.empty()) {
|
||||
s_benchmark.reset();
|
||||
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
||||
s_benchmark.prev_threads = s_selected_threads;
|
||||
s_benchmark.buildCandidates(max_threads);
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||
// Stop any active solo mining first
|
||||
if (mining.generate)
|
||||
app->stopMining();
|
||||
}
|
||||
}
|
||||
|
||||
idleRightEdge = btnX - 4.0f * dp;
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(benchSavedCur);
|
||||
}
|
||||
|
||||
// Active mining indicator (left of idle toggle)
|
||||
if (mining.generate) {
|
||||
float pulse = effects::isLowSpecMode()
|
||||
@@ -1115,11 +1581,13 @@ static void RenderMiningTabContent(App* app)
|
||||
}
|
||||
|
||||
// Show pointer cursor when hovering the thread grid
|
||||
if (hovered_thread > 0)
|
||||
bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||
if (hovered_thread > 0 && !benchActive)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
|
||||
// Drag-to-select logic
|
||||
if (ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||
// Drag-to-select logic (disabled during benchmark)
|
||||
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||
// Begin drag
|
||||
s_drag_active = true;
|
||||
s_drag_anchor_thread = hovered_thread;
|
||||
|
||||
@@ -15,5 +15,11 @@ namespace ui {
|
||||
*/
|
||||
void RenderMiningTab(App* app);
|
||||
|
||||
/**
|
||||
* @brief Returns true when the thread benchmark is actively running.
|
||||
* Used by idle mining to avoid interfering with measurements.
|
||||
*/
|
||||
bool IsMiningBenchmarkActive();
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
#include "receive_tab.h"
|
||||
#include "send_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../data/wallet_state.h"
|
||||
#include "../../ui/widgets/qr_code.h"
|
||||
@@ -45,7 +47,7 @@ static std::string TrId(const char* key, const char* id) {
|
||||
// ============================================================================
|
||||
static int s_selected_address_idx = -1;
|
||||
static double s_request_amount = 0.0;
|
||||
static char s_request_memo[256] = "";
|
||||
static char s_request_memo[512] = "";
|
||||
static double s_request_usd_amount = 0.0;
|
||||
static bool s_request_usd_mode = false;
|
||||
static std::string s_cached_qr_data;
|
||||
@@ -76,16 +78,9 @@ static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40)
|
||||
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());
|
||||
static void OpenExplorerURL(App* app, const std::string& address) {
|
||||
std::string url = app->settings()->getAddressExplorerUrl() + address;
|
||||
dragonx::util::Platform::openUrl(url);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -978,7 +973,7 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
|
||||
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
|
||||
OpenExplorerURL(selected.address);
|
||||
OpenExplorerURL(app, selected.address);
|
||||
}
|
||||
ImGui::PopStyleVar(); // FrameBorderSize
|
||||
ImGui::PopStyleColor(4);
|
||||
|
||||
@@ -804,7 +804,7 @@ void RenderSendConfirmPopup(App* app) {
|
||||
s_tx_status = TR("send_tx_sent");
|
||||
s_result_txid = result;
|
||||
s_status_success = true;
|
||||
Notifications::instance().success(TR("send_tx_success"));
|
||||
Notifications::instance().success(TR("send_tx_success"), 5.0f);
|
||||
s_to_address[0] = '\0';
|
||||
s_amount = 0.0;
|
||||
s_memo[0] = '\0';
|
||||
|
||||
@@ -256,6 +256,19 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["settings_quick_unlock_pin"] = "Quick-unlock PIN";
|
||||
strings_["settings_pin_active"] = "PIN";
|
||||
strings_["settings_encrypt_first_pin"] = "Encrypt wallet first to enable PIN";
|
||||
strings_["incorrect_pin"] = "Incorrect PIN";
|
||||
strings_["incorrect_passphrase"] = "Incorrect passphrase";
|
||||
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
|
||||
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
|
||||
strings_["force_quit"] = "Force Quit";
|
||||
strings_["force_quit_warning"] = "This will immediately kill the daemon without a clean shutdown. May require a blockchain resync.";
|
||||
strings_["force_quit_confirm_title"] = "Force Quit?";
|
||||
strings_["force_quit_confirm_msg"] = "This will immediately kill the daemon without a clean shutdown.\nThis may corrupt the blockchain index and require a resync.";
|
||||
strings_["force_quit_yes"] = "Force Quit";
|
||||
strings_["reduce_motion"] = "Reduce Motion";
|
||||
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
|
||||
strings_["ago"] = "ago";
|
||||
strings_["wizard_daemon_start_failed"] = "Failed to start daemon \xe2\x80\x94 it will be retried automatically";
|
||||
strings_["settings_data_dir"] = "Data Dir:";
|
||||
strings_["settings_wallet_size_label"] = "Wallet Size:";
|
||||
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
|
||||
@@ -272,6 +285,13 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
|
||||
strings_["settings_configure_explorer"] = "Configure external block explorer links";
|
||||
strings_["settings_auto_lock"] = "AUTO-LOCK";
|
||||
strings_["timeout_off"] = "Off";
|
||||
strings_["timeout_1min"] = "1 min";
|
||||
strings_["timeout_5min"] = "5 min";
|
||||
strings_["timeout_15min"] = "15 min";
|
||||
strings_["timeout_30min"] = "30 min";
|
||||
strings_["timeout_1hour"] = "1 hour";
|
||||
strings_["slider_off"] = "Off";
|
||||
strings_["settings_wallet_file_size"] = "Wallet file size: %s";
|
||||
strings_["settings_wallet_location"] = "Wallet location: %s";
|
||||
strings_["settings_rpc_note"] = "Note: Connection settings are usually auto-detected from DRAGONX.conf";
|
||||
@@ -845,6 +865,8 @@ void I18n::loadBuiltinEnglish()
|
||||
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_gpu_on_tooltip"] = "GPU-aware: ON\nGPU activity (video, games) prevents idle mining\nClick for unrestricted mode";
|
||||
strings_["mining_idle_gpu_off_tooltip"] = "Unrestricted: ON\nOnly keyboard/mouse input determines idle state\nClick to enable GPU-aware detection";
|
||||
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";
|
||||
@@ -866,6 +888,13 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mining_recent_payouts"] = "RECENT POOL PAYOUTS";
|
||||
strings_["mining_remove"] = "Remove";
|
||||
strings_["mining_reset_defaults"] = "Reset Defaults";
|
||||
strings_["mining_benchmark_tooltip"] = "Find optimal thread count for this CPU";
|
||||
strings_["mining_benchmark_testing"] = "Testing";
|
||||
strings_["mining_benchmark_cooling"] = "Cooling";
|
||||
strings_["mining_benchmark_stabilizing"] = "Stabilizing";
|
||||
strings_["mining_benchmark_cancel"] = "Cancel benchmark";
|
||||
strings_["mining_benchmark_result"] = "Optimal";
|
||||
strings_["mining_benchmark_dismiss"] = "Dismiss";
|
||||
strings_["mining_save_payout_address"] = "Save payout address";
|
||||
strings_["mining_save_pool_url"] = "Save pool URL";
|
||||
strings_["mining_saved_addresses"] = "Saved Addresses:";
|
||||
|
||||
@@ -688,5 +688,106 @@ int Platform::getSystemIdleSeconds()
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GPU utilization detection
|
||||
// ============================================================================
|
||||
|
||||
int Platform::getGpuUtilization()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Windows: read GPU utilization via SetupAPI / D3DKMT
|
||||
// Not all GPUs expose this; return -1 if unavailable.
|
||||
// Use a popen fallback: nvidia-smi for NVIDIA, or return -1.
|
||||
static bool s_tried_nvidia = false;
|
||||
static bool s_has_nvidia = false;
|
||||
if (!s_tried_nvidia) {
|
||||
s_tried_nvidia = true;
|
||||
FILE* f = _popen("where nvidia-smi 2>nul", "r");
|
||||
if (f) {
|
||||
char buf[256];
|
||||
s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr);
|
||||
_pclose(f);
|
||||
}
|
||||
}
|
||||
if (s_has_nvidia) {
|
||||
FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r");
|
||||
if (f) {
|
||||
char buf[64];
|
||||
int util = -1;
|
||||
if (fgets(buf, sizeof(buf), f)) {
|
||||
util = atoi(buf);
|
||||
if (util < 0 || util > 100) util = -1;
|
||||
}
|
||||
_pclose(f);
|
||||
return util;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
#elif defined(__APPLE__)
|
||||
return -1;
|
||||
#else
|
||||
// Linux: try multiple GPU sysfs paths
|
||||
|
||||
// AMD: /sys/class/drm/card*/device/gpu_busy_percent
|
||||
{
|
||||
// Try card0 through card3
|
||||
char path[128];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/gpu_busy_percent", i);
|
||||
std::ifstream ifs(path);
|
||||
if (ifs.is_open()) {
|
||||
int val = -1;
|
||||
ifs >> val;
|
||||
if (val >= 0 && val <= 100)
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NVIDIA: nvidia-smi (binary may exist even without sysfs)
|
||||
{
|
||||
static bool s_tried = false;
|
||||
static bool s_has_nvidia_smi = false;
|
||||
if (!s_tried) {
|
||||
s_tried = true;
|
||||
FILE* f = popen("which nvidia-smi 2>/dev/null", "r");
|
||||
if (f) {
|
||||
char buf[256];
|
||||
s_has_nvidia_smi = (fgets(buf, sizeof(buf), f) != nullptr);
|
||||
pclose(f);
|
||||
}
|
||||
}
|
||||
if (s_has_nvidia_smi) {
|
||||
FILE* f = popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "r");
|
||||
if (f) {
|
||||
char buf[64];
|
||||
int util = -1;
|
||||
if (fgets(buf, sizeof(buf), f)) {
|
||||
util = atoi(buf);
|
||||
if (util < 0 || util > 100) util = -1;
|
||||
}
|
||||
pclose(f);
|
||||
return util;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Intel: compare current vs max freq as a rough proxy
|
||||
{
|
||||
std::ifstream curF("/sys/class/drm/card0/gt_cur_freq_mhz");
|
||||
std::ifstream maxF("/sys/class/drm/card0/gt_max_freq_mhz");
|
||||
if (curF.is_open() && maxF.is_open()) {
|
||||
int cur = 0, mx = 0;
|
||||
curF >> cur;
|
||||
maxF >> mx;
|
||||
if (mx > 0)
|
||||
return std::min(100, (cur * 100) / mx);
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -131,6 +131,14 @@ public:
|
||||
* @return Seconds since last user input, or 0 on failure
|
||||
*/
|
||||
static int getSystemIdleSeconds();
|
||||
|
||||
/**
|
||||
* @brief Get GPU utilization percentage (0–100).
|
||||
* Linux: reads sysfs for AMD, /proc for NVIDIA.
|
||||
* Windows: queries PDH GPU engine counters.
|
||||
* @return GPU busy percent, or -1 if unavailable.
|
||||
*/
|
||||
static int getGpuUtilization();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user