19 Commits

Author SHA1 Message Date
d7bc5c638a fix: remove D3D11 debug layer flag that prevented startup on user machines
DRAGONX_DEBUG was defined unconditionally, causing D3D11CreateDevice() to
request the debug layer via D3D11_CREATE_DEVICE_DEBUG. This layer is only
available on machines with the Windows SDK or Graphics Tools installed,
so the call fails with DXGI_ERROR_SDK_COMPONENT_MISSING on regular user
machines — causing the app to silently exit.
2026-03-12 00:13:27 -05:00
39f193a264 fix xmrig bundling issues 2026-03-11 21:14:03 -05:00
4023af9466 fix scrolling bug 2026-03-11 03:15:31 -05:00
853e3e0f17 build: Linux release outputs binaries zip + AppImage, bundle sapling params
- Linux --linux-release now produces both ObsidianDragon-Linux-x64.zip
  (raw binaries) and ObsidianDragon.AppImage (single-file)
- Windows --win-release keeps standalone exe alongside zip with binaries
- Bundle sapling-spend.params and sapling-output.params in Linux release
2026-03-11 01:38:59 -05:00
1bcfcd1c73 ui: add idle delay combo to mining tab
Add inline combo box (30s/1m/2m/5m/10m) next to the idle mining
toggle so users can choose how long to wait before idle mining starts.
2026-03-11 01:38:48 -05:00
6bb35bd3e1 ui: reorganize settings page with collapsible sections
- Rename APPEARANCE section to THEME & LANGUAGE
- Move font scale slider out of effects into main section
- Collapse visual effects into "Advanced Effects..." toggle
- Collapse wallet tools into "Tools & Actions..." toggle
- Remove redundant Tools & Actions divider/section from wallet card
- Add i18n strings: theme_language, advanced_effects, tools_actions
2026-03-11 01:38:40 -05:00
2c5a658ea5 feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
2026-03-11 00:40:50 -05:00
f416ff3d09 Add mine-when-idle, default banlist, and console parsing improvements
Mine-when-idle:
- Auto-start/stop mining based on system idle time detection
- Platform::getSystemIdleSeconds() via XScreenSaver (Linux) / GetLastInputInfo (Win)
- Settings: mine_when_idle toggle + configurable delay (30s–10m)
- Settings page UI with checkbox and delay combo

Console tab:
- Shell-like argument parsing with quote and JSON bracket support
- Pass JSON objects/arrays directly as RPC params
- Fix selection indices when lines are evicted from buffer

Connection & status bar:
- Reduce RPC connect timeout to 1s for localhost fast-fail
- Fast retry timer on daemon startup and external daemon detection
- Show pool mining hashrate in status bar; sidebar badge reflects pool state

UI polish:
- Add logo to About card in settings; expose logo dimensions on App
- Header title offset-y support; adjust content-area margins
- Fix banned peers row cursor position (rawRowPosB.x)

Branding:
- Update copyright to "DragonX Developers" in RC and About section
- Replace logo/icon assets with updated versions

Misc:
- setup.sh: checkout dragonx branch before pulling
- Remove stale prebuilt-binaries/xmrig/.gitkeep
2026-03-07 13:42:31 -06:00
b3a0ce29ed fix: Windows identity, async address creation, mining UI, and chart artifacts
Windows identity:
- Add VERSIONINFO resource (.rc) with ObsidianDragon file description
- Embed application manifest for DPI awareness and shell identity
- Patch libwinpthread/libpthread to remove competing VERSIONINFO
- Set AppUserModelID and HWND property store to override Task Manager cache
- Link patched pthread libs to eliminate "POSIX WinThreads" description

Address creation (+New button):
- Move z_getnewaddress/getnewaddress off UI thread to async worker
- Inject new address into state immediately for instant UI selection
- Trigger background refresh for balance updates

Mining tab:
- Add pool mining dropdown with saved URLs/workers and bookmarks
- Add solo mining log panel from daemon output with chart/log toggle
- Fix toggle button cursor (render after InputTextMultiline)
- Auto-restart miner on pool config change
- Migrate default pool URL to include stratum port

Transactions:
- Sort pending (0-conf) transactions to top of history
- Fall back to timereceived when timestamp is missing

Shutdown:
- Replace blocking sleep_for calls with 100ms polling loops
- Check shutting_down_ flag throughout daemon restart/bootstrap flows
- Reduce daemon stop timeout from 30s to 10s

Other:
- Fix market chart fill artifact (single concave polygon vs per-segment quads)
- Add bootstrap checksum verification state display
- Rename daemon client identifier to ObsidianDragon
2026-03-05 22:43:27 -06:00
e2265b0bdf improve diagnostics, security UX, and network tab refresh
Diagnostics & logging:
- add verbose logging system (VERBOSE_LOGF) with toggle in Settings
- forward app-level log messages to Console tab for in-UI visibility
- add detailed connection attempt logging (attempt #, daemon state,
  config paths, auth failures, port owner identification)
- detect HTTP 401 auth failures and show actionable error messages
- identify port owner process (PID + name) on both Linux and Windows
- demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF
- persist verbose_logging preference in settings.json
- link iphlpapi on Windows for GetExtendedTcpTable

Security & encryption:
- update local encryption state immediately after encryptwallet RPC
  so Settings reflects the change before daemon restarts
- show notifications for encrypt success/failure and PIN skip
- use dedicated RPC client for z_importwallet during decrypt flow
  to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh)
- force full state refresh (addresses, transactions, peers) after
  successful wallet import

Network tab:
- redesign peers refresh button as glass-panel with icon + label,
  matching the mining button style
- add spinning arc animation while peer data is loading
  (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo)
- prevent double-click spam during refresh
- add refresh-button size to ui.toml

Other:
- use fast_rpc_ for rescan polling to avoid blocking on main rpc_
- enable DRAGONX_DEBUG in all build configs (was debug-only)
- setup.sh: pull latest xmrig-hac when repo already exists
2026-03-05 05:26:04 -06:00
9368b945e0 fix text shifting in status bar from font scale changes 2026-03-05 01:29:03 -06:00
eda19ef6d5 improved font scaling text and window adjustment, added ctrl + scroll hotkey for font scaling 2026-03-05 01:22:20 -06:00
7d48936f30 refresh network info instantly when switching to network tab 2026-03-04 15:16:32 -06:00
bc2ef4ffaf feat: RPC caching, background decrypt import, fast-lane peers, mining fix
RPC client:
- Add call() overload with per-call timeout parameter
- z_exportwallet uses 300s, z_importwallet uses 1200s timeout

Decrypt wallet (app_security.cpp, app.cpp):
- Show per-step and overall elapsed timers during decrypt flow
- Reduce dialog to 5 steps; close before key import begins
- Run z_importwallet on detached background thread
- Add pulsing "Importing keys..." status bar indicator
- Report success/failure via notifications instead of dialog

RPC caching (app_network.cpp, app.h):
- Cache z_viewtransaction results in viewtx_cache_ across refresh cycles
- Skip RPC calls for already-cached txids (biggest perf win)
- Build confirmed_tx_cache_ for deeply-confirmed transactions
- Clear all caches on disconnect
- Remove unused refreshTransactions() dead code

Peers (app_network.cpp, peers_tab.cpp):
- Route refreshPeerInfo() through fast_worker_ to avoid head-of-line blocking
- Replace footer "Refresh Peers" button with ICON_MD_REFRESH in toggle header
- Refresh button triggers both peer list and full blockchain data refresh

Mining (mining_tab.cpp):
- Allow pool mining toggle when blockchain is not synced
- Pool mining only needs xmrig, not local daemon sync
2026-03-04 15:12:24 -06:00
0da1657b12 Rename hush→dragonx across wallet codebase
- Rename RESOURCE_HUSHD/HUSH_CLI/HUSH_TX to RESOURCE_DRAGONXD/DRAGONX_CLI/DRAGONX_TX
- Remove unused .bat resource constants (DRAGONXD_BAT, DRAGONX_CLI_BAT)
- Update INCBIN symbols: g_hushd_exe → g_dragonxd_exe, etc.
- Update daemon search paths, removing hush-arrakis-chain fallbacks
- Update process detection (Windows findProcessByName, Linux /proc/comm, macOS pgrep)
- Update build.sh: embed dragonxd.exe/dragonx-cli.exe/dragonx-tx.exe
- Overhaul setup.sh: fix binary names, release paths, add -j passthrough
- Update getDaemonPath/needsDaemonExtraction/hasDaemonAvailable for new names
2026-03-04 03:17:32 -06:00
0d30ebc8e8 setup script improvements, automatically clone xmrig-hac and build for multiple platforms 2026-03-03 01:47:44 -06:00
c778e03f37 update links 2026-03-03 01:20:03 -06:00
0a3f68f1eb change release output names 2026-02-28 15:28:40 -06:00
1a5c4e8744 feat: blockchain rescan via daemon restart + status bar progress
- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
2026-02-28 15:06:35 -06:00
18 changed files with 510 additions and 244 deletions

View File

@@ -4,7 +4,7 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(ObsidianDragon project(ObsidianDragon
VERSION 1.0.0 VERSION 1.0.1
LANGUAGES C CXX LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet" DESCRIPTION "DragonX Cryptocurrency Wallet"
) )
@@ -373,12 +373,14 @@ endif()
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe) # Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
if(WIN32) if(WIN32)
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico") set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
# Generate manifest with version from project()
configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest.in
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest
@ONLY
)
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest") set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
# Version numbers for the VERSIONINFO resource block # Generate .rc with version from project()
set(DRAGONX_VER_MAJOR 1)
set(DRAGONX_VER_MINOR 0)
set(DRAGONX_VER_PATCH 0)
set(DRAGONX_VERSION "1.0.0")
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc ${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc
${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc
@@ -387,6 +389,13 @@ if(WIN32)
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc) set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
endif() endif()
# Generate version.h from the single project(VERSION ...) declaration
configure_file(
${CMAKE_SOURCE_DIR}/src/config/version.h.in
${CMAKE_SOURCE_DIR}/src/config/version.h
@ONLY
)
# Generate INCBIN font embedding source with absolute paths to .ttf files # Generate INCBIN font embedding source with absolute paths to .ttf files
configure_file( configure_file(
${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in ${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in

113
build.sh
View File

@@ -5,7 +5,7 @@
# #
# Usage: # Usage:
# ./build.sh # Dev build (Linux, debug-friendly) # ./build.sh # Dev build (Linux, debug-friendly)
# ./build.sh --linux-release # Linux release + AppImage # ./build.sh --linux-release # Linux release (zip + AppImage)
# ./build.sh --win-release # Windows cross-compile (mingw-w64) # ./build.sh --win-release # Windows cross-compile (mingw-w64)
# ./build.sh --mac-release # macOS .app bundle + DMG # ./build.sh --mac-release # macOS .app bundle + DMG
# ./build.sh --linux-release --win-release # Multiple targets # ./build.sh --linux-release --win-release # Multiple targets
@@ -51,7 +51,7 @@ DragonX Wallet — Unified Build Script
Usage: $0 [options] Usage: $0 [options]
Targets (at least one required, or none for dev build): Targets (at least one required, or none for dev build):
--linux-release Linux release build + AppImage -> release/linux/ --linux-release Linux release (zip + AppImage) -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/ --win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/ --mac-release macOS .app bundle + DMG -> release/mac/
@@ -71,7 +71,7 @@ Cross-compiling from Linux:
Examples: Examples:
$0 # Quick dev build (Linux) $0 # Quick dev build (Linux)
$0 --linux-release # Linux release + AppImage $0 --linux-release # Linux release (zip + AppImage)
$0 --win-release # Windows cross-compile $0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross) $0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --clean --linux-release --win-release # Clean + both $0 --clean --linux-release --win-release # Clean + both
@@ -218,7 +218,7 @@ build_dev() {
} }
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: LINUX — build + strip + bundle daemon + AppImage # RELEASE: LINUX — build + strip + bundle daemon + zip + AppImage
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
build_release_linux() { build_release_linux() {
header "Release: Linux x86_64" header "Release: Linux x86_64"
@@ -249,17 +249,41 @@ build_release_linux() {
# ── Bundle daemon ──────────────────────────────────────────────────────── # ── Bundle daemon ────────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build" bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
# ── Package: release/linux/ ────────────────────────────────────────────── # ── Package: release/linux/ ──────────────────────────────────────────────
rm -rf "$out" rm -rf "$out"
mkdir -p "$out" mkdir -p "$out"
cp bin/ObsidianDragon "$out/" local DIST="ObsidianDragon-Linux-x64"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$out/" local dist_dir="$out/$DIST"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$out/" mkdir -p "$dist_dir"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$out/"
cp -r bin/res "$out/" 2>/dev/null || true
# ── AppImage ───────────────────────────────────────────────────────────── cp bin/ObsidianDragon "$dist_dir/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
# ── AppImage (single-file) ───────────────────────────────────────────────
info "Creating AppImage ..." info "Creating AppImage ..."
local APPDIR="$bd/AppDir" local APPDIR="$bd/AppDir"
rm -rf "$APPDIR" rm -rf "$APPDIR"
@@ -271,10 +295,16 @@ build_release_linux() {
cp bin/ObsidianDragon "$APPDIR/usr/bin/" cp bin/ObsidianDragon "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
# Daemon inside AppImage [[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/" [[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/" # Daemon data files must be alongside the daemon binary (usr/bin/)
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/share/ObsidianDragon/" # because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry # Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK' cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
@@ -348,32 +378,11 @@ APPRUN
local ARCH local ARCH
ARCH=$(uname -m) ARCH=$(uname -m)
local IMG_NAME="ObsidianDragon-${ARCH}.AppImage"
cd "$bd" cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "$IMG_NAME" 2>/dev/null && { ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${ARCH}.AppImage" 2>/dev/null && {
cp "$IMG_NAME" "$out/" cp "ObsidianDragon-${ARCH}.AppImage" "$out/ObsidianDragon.AppImage"
# Rename to match Windows convention: ObsidianDragon.AppImage
mv "$out/$IMG_NAME" "$out/ObsidianDragon.AppImage"
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))" info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
} || warn "AppImage creation failed (appimagetool issue) — raw binary still in release/linux/" } || warn "AppImage creation failed — binaries zip still in release/linux/"
# Clean up: keep only AppImage + zip in release/linux/
if [[ -f "$out/ObsidianDragon.AppImage" ]]; then
# AppImage succeeded — remove everything except AppImage
find "$out" -maxdepth 1 -type f ! -name 'ObsidianDragon.AppImage' -delete
rm -rf "$out/res" 2>/dev/null
# Create zip matching Windows naming convention
local DIST="ObsidianDragon-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "$out/ObsidianDragon.AppImage" "$dist_dir/"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
fi
info "Linux release artifacts: $out/" info "Linux release artifacts: $out/"
ls -lh "$out/" ls -lh "$out/"
@@ -601,31 +610,25 @@ HDR
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/" [[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done done
cat > "$dist_dir/README.txt" <<'README' # Bundle Sapling params + asmap for the zip distribution
DragonX Wallet - Windows Edition # (The single-file exe has these embedded via INCBIN, but the zip
================================ # needs them on disk so the daemon can find them in its work dir.)
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
SINGLE-FILE DISTRIBUTION cp -r bin/res "$dist_dir/" 2>/dev/null || true
========================
This wallet is a true single-file executable with all resources embedded.
Just run ObsidianDragon.exe — no additional files needed!
On first run, the wallet will automatically extract: # ── Single-file exe (all resources embedded) ────────────────────────────
- Sapling parameters to %APPDATA%\ZcashParams\
- asmap.dat to %APPDATA%\Hush\DRAGONX\
For support: https://git.dragonx.is/dragonx/ObsidianDragon
README
# Copy single-file exe to release dir
cp bin/ObsidianDragon.exe "$out/" cp bin/ObsidianDragon.exe "$out/"
info "Single-file exe: $out/ObsidianDragon.exe ($(du -h "$out/ObsidianDragon.exe" | cut -f1))"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST") (cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))" info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
# Clean up: keep .zip + single-file exe, remove loose directory
rm -rf "$dist_dir"
fi fi
rm -rf "$dist_dir"
info "Windows release artifacts: $out/" info "Windows release artifacts: $out/"
ls -lh "$out/" ls -lh "$out/"

View File

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

View File

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

View File

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

View File

@@ -627,8 +627,13 @@ XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac" XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
# Clean previous prebuilt xmrig binaries so we always rebuild # Clean previous prebuilt xmrig binaries so we always rebuild
# Only clean the binary for the platform(s) we are actually building,
# otherwise a plain ./setup.sh deletes xmrig.exe without rebuilding it.
if ! $CHECK_ONLY; then if ! $CHECK_ONLY; then
rm -f "$XMRIG_PREBUILT/xmrig" "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true rm -f "$XMRIG_PREBUILT/xmrig" 2>/dev/null || true
if $SETUP_WIN; then
rm -f "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true
fi
fi fi
# Helper: clone xmrig-hac if not present # Helper: clone xmrig-hac if not present

View File

@@ -1855,10 +1855,34 @@ bool App::startEmbeddedDaemon()
} }
DEBUG_LOGF("Sapling params extracted successfully\n"); DEBUG_LOGF("Sapling params extracted successfully\n");
} else { } else {
daemon_status_ = "Sapling parameters not found. They should be in: " + rpc::Connection::getSaplingParamsDir(); // Fallback: check for params bundled alongside the executable
DEBUG_LOGF("Sapling params not found and no embedded resources available!\n"); // (zip distributions bundle sapling-*.params next to the binary)
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str()); namespace fs = std::filesystem;
return false; std::string exe_dir = util::Platform::getExecutableDirectory();
std::string daemon_dir = resources::getDaemonDirectory();
const char* paramFiles[] = { "sapling-spend.params", "sapling-output.params", "asmap.dat" };
bool copied = false;
if (!exe_dir.empty()) {
std::error_code ec;
fs::create_directories(daemon_dir, ec);
for (const char* name : paramFiles) {
fs::path src = fs::path(exe_dir) / name;
fs::path dst = fs::path(daemon_dir) / name;
if (fs::exists(src) && !fs::exists(dst)) {
DEBUG_LOGF("Copying bundled %s from exe dir to %s\n", name, daemon_dir.c_str());
fs::copy_file(src, dst, ec);
if (!ec) copied = true;
}
}
}
if (copied && rpc::Connection::verifySaplingParams()) {
DEBUG_LOGF("Sapling params copied from exe directory successfully\n");
} else {
daemon_status_ = "Sapling parameters not found. They should be in: " + rpc::Connection::getSaplingParamsDir();
DEBUG_LOGF("Sapling params not found and no embedded resources available!\n");
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
return false;
}
} }
} }

View File

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

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

@@ -0,0 +1,31 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "@PROJECT_VERSION@"
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"

View File

@@ -466,6 +466,18 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
dup2(pipefd[1], STDERR_FILENO); dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]); close(pipefd[1]);
// Detach from controlling terminal's stdin to prevent SIGTTIN/SIGTTOU
// when running in a new process group (setpgid below).
int devnull = open("/dev/null", O_RDONLY);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
close(devnull);
}
// Ignore job-control signals that a background process group may receive
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
// New process group so we can kill the whole group // New process group so we can kill the whole group
setpgid(0, 0); setpgid(0, 0);

View File

@@ -419,12 +419,20 @@ int main(int argc, char* argv[])
if (!g_single_instance.tryLock()) { if (!g_single_instance.tryLock()) {
fprintf(stderr, "Another instance of ObsidianDragon is already running.\n"); fprintf(stderr, "Another instance of ObsidianDragon is already running.\n");
DEBUG_LOGF("Please close the existing instance first.\n"); DEBUG_LOGF("Please close the existing instance first.\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.",
L"ObsidianDragon", MB_OK | MB_ICONINFORMATION);
#endif
return 1; return 1;
} }
// Initialize SDL // Initialize SDL
if (!InitSDL()) { if (!InitSDL()) {
fprintf(stderr, "Failed to initialize SDL!\n"); fprintf(stderr, "Failed to initialize SDL!\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Failed to initialize SDL. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
return 1; return 1;
} }
@@ -495,6 +503,8 @@ int main(int argc, char* argv[])
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
if (!nativeHwnd) { if (!nativeHwnd) {
fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError()); fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError());
MessageBoxW(nullptr, L"Failed to create window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
SDL_Quit(); SDL_Quit();
return 1; return 1;
} }
@@ -534,6 +544,8 @@ int main(int argc, char* argv[])
SDL_DestroyProperties(createProps); SDL_DestroyProperties(createProps);
if (window == nullptr) { if (window == nullptr) {
fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError()); fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError());
MessageBoxW(nullptr, L"Failed to create SDL window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
DestroyWindow(nativeHwnd); DestroyWindow(nativeHwnd);
SDL_Quit(); SDL_Quit();
return 1; return 1;
@@ -560,6 +572,8 @@ int main(int argc, char* argv[])
dragonx::platform::DX11Context dx; dragonx::platform::DX11Context dx;
if (!dx.init(window)) { if (!dx.init(window)) {
fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n"); fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n");
MessageBoxW(nullptr, L"Failed to initialize DirectX 11.\nPlease ensure your graphics drivers are up to date.\n\nCheck the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Graphics Error", MB_OK | MB_ICONERROR);
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
SDL_Quit(); SDL_Quit();
return 1; return 1;
@@ -635,6 +649,8 @@ int main(int argc, char* argv[])
// Initialize ImGui with DX11 backend // Initialize ImGui with DX11 backend
if (!InitImGui(window, dx)) { if (!InitImGui(window, dx)) {
fprintf(stderr, "Failed to initialize ImGui!\n"); fprintf(stderr, "Failed to initialize ImGui!\n");
MessageBoxW(nullptr, L"Failed to initialize ImGui. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
Shutdown(window, dx); Shutdown(window, dx);
return 1; return 1;
} }
@@ -808,6 +824,10 @@ int main(int argc, char* argv[])
dragonx::App app; dragonx::App app;
if (!app.init()) { if (!app.init()) {
fprintf(stderr, "Failed to initialize application!\n"); fprintf(stderr, "Failed to initialize application!\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Failed to initialize application. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
#ifdef DRAGONX_USE_DX11 #ifdef DRAGONX_USE_DX11
Shutdown(window, dx); Shutdown(window, dx);
#else #else

View File

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

View File

@@ -509,6 +509,18 @@ std::string getXmrigPath()
return xmrigName; return xmrigName;
} }
// Check alongside the wallet executable (zip / AppImage distributions)
{
std::string exeDir = util::Platform::getExecutableDirectory();
if (!exeDir.empty()) {
std::string exeDirPath = exeDir + "/" + xmrigName;
if (std::filesystem::exists(exeDirPath)) {
DEBUG_LOGF("[INFO] getXmrigPath: found alongside wallet at %s\n", exeDirPath.c_str());
return exeDirPath;
}
}
}
// Check development paths // Check development paths
#ifdef _WIN32 #ifdef _WIN32
// Not applicable for cross-compiled Windows builds // Not applicable for cross-compiled Windows builds

View File

@@ -757,15 +757,26 @@ inline void ApplySmoothScroll(float speed = 12.0f)
s.current = actualY; s.current = actualY;
} }
// Capture mouse wheel when hovered, but not when a popup (combo dropdown // Capture mouse wheel when the cursor is inside this child window.
// etc.) is open — let the popup handle its own scrolling exclusively. // Use a direct position check instead of IsWindowHovered() which can
bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); // return false when an ActiveId exists (focused input, held button) or
if (!popupOpen && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) { // when tooltip/popup windows from row hovers interfere with hover
float wheel = ImGui::GetIO().MouseWheel; // routing. Still skip when a real popup (combo dropdown, context menu)
if (wheel != 0.0f) { // is open so the popup handles its own scrolling.
float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f; {
s.target -= wheel * step; bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
s.target = ImClamp(s.target, 0.0f, scrollMaxY); ImVec2 mpos = ImGui::GetIO().MousePos;
ImVec2 wmin = win->Pos;
ImVec2 wmax = ImVec2(wmin.x + win->Size.x, wmin.y + win->Size.y);
bool inside = (mpos.x >= wmin.x && mpos.x < wmax.x &&
mpos.y >= wmin.y && mpos.y < wmax.y);
if (!popupOpen && inside) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f;
s.target -= wheel * step;
s.target = ImClamp(s.target, 0.0f, scrollMaxY);
}
} }
} }

View File

@@ -122,6 +122,8 @@ static bool sp_verbose_logging = false;
static std::set<std::string> sp_debug_categories; static std::set<std::string> sp_debug_categories;
static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted
static bool sp_debug_expanded = false; // collapsible card state static bool sp_debug_expanded = false; // collapsible card state
static bool sp_effects_expanded = false; // "Advanced Effects..." toggle
static bool sp_tools_expanded = false; // "Tools & Actions..." toggle
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
// (APPEARANCE card now uses ChannelsSplit like all other cards) // (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -377,11 +379,11 @@ void RenderSettingsPage(App* app) {
ImFont* sub1 = Type().subtitle1(); ImFont* sub1 = Type().subtitle1();
// ==================================================================== // ====================================================================
// APPEARANCE — card (draw-first approach; avoids ChannelsSplit which // THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit
// breaks BeginCombo popup rendering in some ImGui versions) // which breaks BeginCombo popup rendering in some ImGui versions)
// ==================================================================== // ====================================================================
{ {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("appearance")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMin = ImGui::GetCursorScreenPos();
@@ -561,9 +563,59 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Visual Effects (checkboxes on one row, Quality+Blur paired) --- // --- Font Scale slider (always visible) ---
{ {
ImGui::PushFont(body2); ImGui::PushFont(body2);
ImGui::TextUnformatted(TR("font_scale"));
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW);
ImGui::SetNextItemWidth(fontSliderW);
sp_font_scale = Layout::userFontScale();
float prev_font_scale = sp_font_scale;
{
char fs_fmt[16];
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale);
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
ImGuiSliderFlags_AlwaysClamp);
}
sp_font_scale = std::max(1.0f, std::min(1.5f,
std::round(sp_font_scale * 20.0f) / 20.0f));
if (sp_font_scale != prev_font_scale)
Layout::setUserFontScaleVisual(sp_font_scale);
if (ImGui::IsItemDeactivatedAfterEdit()) {
Layout::setUserFontScale(sp_font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Advanced Effects... ---
{
const char* arrow = sp_effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
sp_effects_expanded = !sp_effects_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
if (sp_effects_expanded) {
ImGui::PushFont(body2);
// Checkbox row: Low-spec | Console scanline | Theme effects | Gradient background // Checkbox row: Low-spec | Console scanline | Theme effects | Gradient background
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) { if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) {
@@ -603,7 +655,6 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
// Simple background is lightweight — always interactive, even in low-spec mode
ImGui::SameLine(0, Layout::spacingLg()); ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &sp_gradient_background)) { if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &sp_gradient_background)) {
schema::SkinManager::instance().setGradientMode(sp_gradient_background); schema::SkinManager::instance().setGradientMode(sp_gradient_background);
@@ -635,12 +686,10 @@ void RenderSettingsPage(App* app) {
float baseX = ImGui::GetCursorScreenPos().x; float baseX = ImGui::GetCursorScreenPos().x;
float rightX = baseX + ctrlW + Layout::spacingLg(); float rightX = baseX + ctrlW + Layout::spacingLg();
// Acrylic label + slider (left column)
ImGui::TextUnformatted(TR("acrylic")); ImGui::TextUnformatted(TR("acrylic"));
float row1Y = ImGui::GetCursorScreenPos().y; float row1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
// Build display format: "Off" at zero, percentage otherwise
char blur_fmt[16]; char blur_fmt[16];
if (sp_blur_amount < 0.01f) if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off"); snprintf(blur_fmt, sizeof(blur_fmt), "Off");
@@ -648,7 +697,6 @@ void RenderSettingsPage(App* app) {
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f); 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, if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) { ImGuiSliderFlags_AlwaysClamp)) {
// Snap to off when dragged near 0%
if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f; if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f;
sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_acrylic_enabled = (sp_blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
@@ -658,7 +706,6 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
float afterRow1Y = ImGui::GetCursorScreenPos().y; float afterRow1Y = ImGui::GetCursorScreenPos().y;
// Noise label + slider (right column, same row)
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y; float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH)); ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH));
ImGui::TextUnformatted(TR("noise")); ImGui::TextUnformatted(TR("noise"));
@@ -678,10 +725,8 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
// Reset cursor to left column, past row 1
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y)); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
// Row 2: UI Opacity + Window Opacity (labels above)
ImGui::TextUnformatted(TR("ui_opacity")); ImGui::TextUnformatted(TR("ui_opacity"));
float row2Y = ImGui::GetCursorScreenPos().y; float row2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
@@ -697,7 +742,6 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
float afterRow2Y = ImGui::GetCursorScreenPos().y; float afterRow2Y = ImGui::GetCursorScreenPos().y;
// Window label + slider (right column, same row)
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH)); ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
ImGui::TextUnformatted(TR("window_opacity")); ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y)); ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y));
@@ -712,12 +756,11 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
// Reset cursor to left column, past row 2
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y)); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
ImGui::EndDisabled(); // low-spec ImGui::EndDisabled(); // low-spec
ImGui::PopFont(); ImGui::PopFont();
} } // sp_effects_expanded
} else { } else {
// ============================================================ // ============================================================
// Narrow: stacked combos + 2-column effects (original layout) // Narrow: stacked combos + 2-column effects (original layout)
@@ -810,11 +853,62 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Visual Effects (checkboxes + controls) --- // --- Font Scale slider (always visible) ---
{ {
ImGui::PushFont(body2); ImGui::PushFont(body2);
ImGui::TextUnformatted(TR("font_scale"));
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2);
ImGui::SetNextItemWidth(fontSliderW);
sp_font_scale = Layout::userFontScale();
float prev_font_scale = sp_font_scale;
{
char fs_fmt[16];
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale);
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
ImGuiSliderFlags_AlwaysClamp);
}
sp_font_scale = std::max(1.0f, std::min(1.5f,
std::round(sp_font_scale * 20.0f) / 20.0f));
if (sp_font_scale != prev_font_scale)
Layout::setUserFontScaleVisual(sp_font_scale);
if (ImGui::IsItemDeactivatedAfterEdit()) {
Layout::setUserFontScale(sp_font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Advanced Effects... ---
{
const char* arrow = sp_effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
float narrowContentW = availWidth - pad * 2;
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) {
sp_effects_expanded = !sp_effects_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
if (sp_effects_expanded) {
ImGui::PushFont(body2);
// Checkbox row 1: Low-spec
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) { if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) {
effects::setLowSpecMode(sp_low_spec_mode); effects::setLowSpecMode(sp_low_spec_mode);
if (sp_low_spec_mode) { if (sp_low_spec_mode) {
@@ -852,7 +946,6 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
// Simple background is lightweight — always interactive, even in low-spec mode
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &sp_gradient_background)) { if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &sp_gradient_background)) {
schema::SkinManager::instance().setGradientMode(sp_gradient_background); schema::SkinManager::instance().setGradientMode(sp_gradient_background);
saveSettingsPageState(app->settings()); saveSettingsPageState(app->settings());
@@ -861,7 +954,6 @@ void RenderSettingsPage(App* app) {
ImGui::BeginDisabled(sp_low_spec_mode); ImGui::BeginDisabled(sp_low_spec_mode);
// Checkbox row 2: Console scanline | Theme effects
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) { if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
ConsoleTab::s_scanline_enabled = sp_scanline_enabled; ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
app->settings()->setScanlineEnabled(sp_scanline_enabled); app->settings()->setScanlineEnabled(sp_scanline_enabled);
@@ -876,9 +968,8 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
// Acrylic blur slider (label above)
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
contentW); availWidth - pad * 2.0f);
ImGui::TextUnformatted(TR("acrylic")); ImGui::TextUnformatted(TR("acrylic"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -897,7 +988,6 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
// Noise opacity slider (label above)
ImGui::TextUnformatted(TR("noise")); ImGui::TextUnformatted(TR("noise"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -914,7 +1004,6 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
// UI Opacity slider (label above)
ImGui::TextUnformatted(TR("ui_opacity")); ImGui::TextUnformatted(TR("ui_opacity"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -928,7 +1017,6 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
// Window Opacity slider (label above)
ImGui::TextUnformatted(TR("window_opacity")); ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -943,44 +1031,7 @@ void RenderSettingsPage(App* app) {
ImGui::EndDisabled(); // low-spec ImGui::EndDisabled(); // low-spec
ImGui::PopFont(); ImGui::PopFont();
} } // sp_effects_expanded
}
// ============================================================
// Font Scale slider (always enabled, not affected by low-spec)
// ============================================================
{
ImGui::PushFont(body2);
ImGui::Spacing();
ImGui::TextUnformatted(TR("font_scale"));
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2);
ImGui::SetNextItemWidth(fontSliderW);
// Sync from Layout so Alt+scroll hotkey changes are reflected
sp_font_scale = Layout::userFontScale();
float prev_font_scale = sp_font_scale;
{
char fs_fmt[16];
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale);
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
ImGuiSliderFlags_AlwaysClamp);
}
// Snap to 0.05 increments for consistent stepping.
// Visual scaling uses FontScaleMain (no atlas rebuild),
// atlas rebuild is deferred to slider release for crisp text.
sp_font_scale = std::max(1.0f, std::min(1.5f,
std::round(sp_font_scale * 20.0f) / 20.0f));
if (sp_font_scale != prev_font_scale) {
// While dragging: update layout scale without atlas rebuild
Layout::setUserFontScaleVisual(sp_font_scale);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
// On release: rebuild font atlas at final size
Layout::setUserFontScale(sp_font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
} }
// Bottom padding // Bottom padding
@@ -1000,7 +1051,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, gap)); ImGui::Dummy(ImVec2(0, gap));
// ==================================================================== // ====================================================================
// WALLET — card (Keys, Backup, Tools, Maintenance, Node/RPC) // WALLET — card (privacy/daemon toggles + collapsible tools)
// ==================================================================== // ====================================================================
{ {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
@@ -1013,56 +1064,6 @@ void RenderSettingsPage(App* app) {
ImGui::Indent(pad); ImGui::Indent(pad);
float contentW = availWidth - pad * 2; float contentW = availWidth - pad * 2;
float btnSpacing = Layout::spacingMd();
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
// Calculate button width that fits available space
// 6 buttons total: on wide screens 3+3, on narrow screens 2+2+2
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
// Clamp to reasonable size
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
// Row 1 — Tools & Actions
{
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
AddressBookDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
ValidateAddressDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
RequestPaymentDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment"));
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
sp_confirm_clear_ztx = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
}
// Thin divider
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{
float divAlpha = S.drawElement("components.settings-page", "section-divider-alpha").opacity;
if (divAlpha <= 0.0f) divAlpha = 0.08f;
ImU32 baseDivCol = S.resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 20));
ImU32 divCol = material::ScaleAlpha(baseDivCol, divAlpha / 0.08f);
ImVec2 p = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(p.x, p.y), ImVec2(p.x + contentW, p.y), divCol);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit // Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
{ {
@@ -1111,47 +1112,94 @@ void RenderSettingsPage(App* app) {
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
} }
// Mine when idle — checkbox + delay combo ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Tools & Actions... ---
{ {
if (ImGui::Checkbox(TrId("mine_when_idle", "mine_idle").c_str(), &sp_mine_when_idle)) { const char* arrow = sp_tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
saveSettingsPageState(app->settings()); ImGui::PushFont(body2);
} ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
if (ImGui::IsItemHovered()) ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::SetTooltip("%s", TR("tt_mine_idle")); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
if (sp_mine_when_idle) { ImVec2 hdrPos = ImGui::GetCursorScreenPos();
ImGui::SameLine(0, Layout::spacingMd()); if (ImGui::Button("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
ImGui::AlignTextToFramePadding(); sp_tools_expanded = !sp_tools_expanded;
ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "%s", TR("settings_idle_after"));
ImGui::SameLine(0, Layout::spacingSm());
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == sp_mine_idle_delay) { previewLabel = d.label; break; }
} }
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
ImGui::SetNextItemWidth(schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f)); dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions"));
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) { ImFont* iconFont = Type().iconSmall();
for (const auto& d : delays) { if (!iconFont) iconFont = body2;
bool selected = (d.seconds == sp_mine_idle_delay); float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
if (ImGui::Selectable(d.label, selected)) { dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
sp_mine_idle_delay = d.seconds;
saveSettingsPageState(app->settings());
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
} }
ImGui::PopStyleColor(3);
ImGui::PopFont();
} }
// Bottom row — Keys & Data left-aligned, Setup Wizard right-aligned if (sp_tools_expanded) {
float btnSpacing = Layout::spacingMd();
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
AddressBookDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
ValidateAddressDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
RequestPaymentDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment"));
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
sp_confirm_clear_ztx = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
}
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// BACKUP & DATA — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
float contentW = availWidth - pad * 2;
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
{ {
const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")}; const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")};
const char* t1[] = { const char* t1[] = {
@@ -1165,13 +1213,12 @@ void RenderSettingsPage(App* app) {
float sp = Layout::spacingSm(); float sp = Layout::spacingSm();
ImFont* btnFont = S.resolveFont("button"); ImFont* btnFont = S.resolveFont("button");
// Measure natural widths
float btnPadX = btnPad * 2; float btnPadX = btnPad * 2;
float naturalW = 0; float naturalW = 0;
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX; naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX; float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
float totalW = naturalW + wizW + sp * 6; // 5 gaps between data btns + 1 gap before wizard float totalW = naturalW + wizW + sp * 6;
float scale = (totalW > contentW) ? contentW / totalW : 1.0f; float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale); if (scale < 1.0f) ImGui::SetWindowFontScale(scale);

View File

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

View File

@@ -153,10 +153,15 @@ void RenderMiningTab(App* app)
s_pool_mode = app->settings()->getPoolMode(); s_pool_mode = app->settings()->getPoolMode();
strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1); strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1);
strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1); strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1);
s_pool_state_loaded = true;
}
// If pool worker is empty or placeholder, default to user's first address // Default pool worker to user's first shielded address once addresses are available
{
static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker); std::string workerStr(s_pool_worker);
if (workerStr.empty() || workerStr == "x") { if (!s_pool_worker_defaulted && !state.addresses.empty() &&
(workerStr.empty() || workerStr == "x")) {
std::string defaultAddr; std::string defaultAddr;
for (const auto& addr : state.addresses) { for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) { if (addr.type == "shielded" && !addr.address.empty()) {
@@ -175,9 +180,10 @@ void RenderMiningTab(App* app)
if (!defaultAddr.empty()) { if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
} }
s_pool_worker_defaulted = true;
} }
s_pool_state_loaded = true;
} }
// Persist pool settings when dirty and no field is active // Persist pool settings when dirty and no field is active
@@ -866,9 +872,42 @@ void RenderMiningTab(App* app)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip")); ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
} }
ImGui::SetCursorScreenPos(savedCur);
idleRightEdge = btnX - 4.0f * dp; idleRightEdge = btnX - 4.0f * dp;
// Idle delay combo (to the left of the icon when enabled)
if (idleOn) {
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
ImGui::SetCursorScreenPos(savedCur);
} }
// Active mining indicator (left of idle toggle) // Active mining indicator (left of idle toggle)

View File

@@ -174,6 +174,9 @@ void I18n::loadBuiltinEnglish()
// Settings sections // Settings sections
strings_["appearance"] = "APPEARANCE"; strings_["appearance"] = "APPEARANCE";
strings_["theme_language"] = "THEME & LANGUAGE";
strings_["advanced_effects"] = "Advanced Effects...";
strings_["tools_actions"] = "Tools & Actions...";
strings_["wallet"] = "WALLET"; strings_["wallet"] = "WALLET";
strings_["node_security"] = "NODE & SECURITY"; strings_["node_security"] = "NODE & SECURITY";
strings_["node"] = "NODE"; strings_["node"] = "NODE";