Compare commits
138
Commits
229373e937
...
70fa1ec3bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70fa1ec3bf | ||
|
|
f6440739a5 | ||
|
|
c625a4e31a | ||
|
|
fc85297636 | ||
|
|
95795b1581 | ||
|
|
f9cce04010 | ||
|
|
2624f3dd2e | ||
|
|
d95703a762 | ||
|
|
3a1169018a | ||
|
|
fca3e830fa | ||
|
|
cec6c54137 | ||
|
|
690c4230b3 | ||
|
|
93f7e9ec68 | ||
|
|
d9c05d194e | ||
|
|
e18669daee | ||
|
|
8f95089add | ||
|
|
4e8a39f90e | ||
|
|
2da58a2bfb | ||
|
|
820c5da040 | ||
|
|
923aa4dba3 | ||
|
|
8b5d239f53 | ||
|
|
9b07586b25 | ||
|
|
76f2f3cfe0 | ||
|
|
1fc7f6e22e | ||
|
|
db1b214cc4 | ||
|
|
b5270fa995 | ||
|
|
b4ddf5b146 | ||
|
|
3e1b60b0f5 | ||
|
|
edb1e53380 | ||
|
|
e7ee96aba8 | ||
|
|
dcda46fe2b | ||
|
|
0ffe95d254 | ||
|
|
06a7c8f354 | ||
|
|
e41361dfab | ||
|
|
6f255327a7 | ||
|
|
e9d729a0b4 | ||
|
|
e77e64494e | ||
|
|
76fb423df6 | ||
|
|
bb5874355d | ||
|
|
dab714e0ad | ||
|
|
a93cb7d7cb | ||
|
|
d05d01413c | ||
|
|
3d9c7c99d3 | ||
|
|
a0eb994798 | ||
|
|
72555d5dc0 | ||
|
|
ee798c0117 | ||
|
|
3050920486 | ||
|
|
70fbb4d2fa | ||
|
|
596fe537d6 | ||
|
|
50e5dba689 | ||
|
|
7819d22c0b | ||
|
|
ff95b1bd7b | ||
|
|
af471626c2 | ||
|
|
fb7fb3dee1 | ||
|
|
e9e436f77c | ||
|
|
928c2cbdfe | ||
|
|
70478beb3e | ||
|
|
b3f74e43ac | ||
|
|
3d60de49fe | ||
|
|
2bcf1e6df3 | ||
|
|
b5249fd353 | ||
|
|
37281e8aff | ||
|
|
e278971f72 | ||
|
|
5c36cd0757 | ||
|
|
25c56249eb | ||
|
|
deefd3b845 | ||
|
|
a642e37d9d | ||
|
|
1b4515feab | ||
|
|
02df7c1e83 | ||
|
|
3d78b248bb | ||
|
|
eb820dce21 | ||
|
|
9b4c9b805a | ||
|
|
293533694e | ||
|
|
dd2b49b0cd | ||
|
|
8cf1d20dee | ||
|
|
86efba3933 | ||
|
|
7e013006eb | ||
|
|
4059259a8d | ||
|
|
5c2adc6b0c | ||
|
|
cc507f2c4a | ||
|
|
3110c83665 | ||
|
|
bf384b53ff | ||
|
|
dc26c2952a | ||
|
|
ec8b500a42 | ||
|
|
78770f689e | ||
|
|
97e5a9a4aa | ||
|
|
d188fd9d5f | ||
|
|
54c9e2ba87 | ||
|
|
93d93cfc45 | ||
|
|
62e7ede907 | ||
|
|
a6464fbcf8 | ||
|
|
936070b552 | ||
|
|
b962638198 | ||
|
|
e594ac8c40 | ||
|
|
b7bec1e719 | ||
|
|
4a5497181b | ||
|
|
c818a015a1 | ||
|
|
a36df94b03 | ||
|
|
1683968388 | ||
|
|
b5e72300a3 | ||
|
|
27d11ebe6d | ||
|
|
f169b50e8f | ||
|
|
f84adb206f | ||
|
|
cb6765ae5b | ||
|
|
3bab0ed528 | ||
|
|
2174d880c2 | ||
|
|
424aeb11a6 | ||
|
|
9e991a9c2f | ||
|
|
db49cc8f5b | ||
|
|
335a25d07a | ||
|
|
c997dee9bf | ||
|
|
bf156d0ee9 | ||
|
|
9ce5feeb6a | ||
|
|
ea5749772f | ||
|
|
c198721d54 | ||
|
|
3f0abddf10 | ||
|
|
a97059fd53 | ||
|
|
e906ca58f4 | ||
|
|
95902788f2 | ||
|
|
1fc82addc9 | ||
|
|
096a43ba84 | ||
|
|
3717b231f8 | ||
|
|
6d4ec52331 | ||
|
|
ec075f3db4 | ||
|
|
a5015cfcf2 | ||
|
|
e07abc2e98 | ||
|
|
08b478ed3f | ||
|
|
859405261b | ||
|
|
1b7fcc53ba | ||
|
|
d8a2646747 | ||
|
|
b937364b7c | ||
|
|
648ee2bfdc | ||
|
|
7dd7916b3d | ||
|
|
31ad95bd22 | ||
|
|
10f98ef3fa | ||
|
|
af06b8bf0d | ||
|
|
ffe63d3129 | ||
|
|
99e6444f0d |
@@ -41,3 +41,9 @@ asmap.dat
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
# Local-only archive of superseded lite-wallet design/planning docs (untracked)
|
||||||
|
docs/_archive/
|
||||||
|
|
||||||
|
# ed25519 release-signing keys — the secret key must NEVER be committed
|
||||||
|
*.ed25519.key
|
||||||
|
*.ed25519.pub.b64
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
ObsidianDragon is a portable, full-node GUI wallet for DragonX (DRGX), written in C++17 using SDL3 + Dear ImGui (immediate-mode). It drives a `dragonxd` full node over JSON-RPC and can embed/extract the daemon itself. A separate **Lite** variant (`ObsidianDragonLite`) drops the full node and instead talks to an external lite-wallet backend library.
|
||||||
|
|
||||||
|
## Build & run
|
||||||
|
|
||||||
|
`build.sh` is the single entry point for all builds. `setup.sh` (repo root) installs/validates dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh # Dev build (native, no packaging) -> build/linux/bin/ObsidianDragon
|
||||||
|
./build.sh --lite # Dev build of the Lite variant -> build/linux/bin/ObsidianDragonLite
|
||||||
|
./build.sh --clean # Wipe the build dir first
|
||||||
|
./build.sh --linux-release # Release zip + AppImage -> release/linux/
|
||||||
|
./build.sh --win-release # Windows cross-compile (mingw-w64) -> release/windows/
|
||||||
|
./build.sh --mac-release # macOS .app bundle + DMG
|
||||||
|
./setup.sh --check # Report missing build deps without installing
|
||||||
|
```
|
||||||
|
|
||||||
|
Dev builds use `build/linux/` (or `build/mac/`). To re-build incrementally without re-running CMake config: `cmake --build build/linux -j$(nproc)`.
|
||||||
|
|
||||||
|
The wallet connects to the daemon using credentials in `~/.hush/DRAGONX/DRAGONX.conf` (`rpcuser`/`rpcpassword`/`rpcport`). It searches for `dragonxd`/`dragonx-cli` binaries in the **executable's own directory first**, so dropping custom node builds next to the wallet binary overrides the bundled ones.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Tests live in `tests/test_phase4.cpp` — a single large translation unit using a custom assertion harness (`EXPECT_TRUE`/`EXPECT_EQ`/`EXPECT_NEAR` macros, one `main()`, exit code = failure count). `include(CTest)` enables `BUILD_TESTING=ON` by default, so the `ObsidianDragonTests` executable is built alongside the app.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd build/linux && ctest --output-on-failure # run the suite
|
||||||
|
./build/linux/bin/ObsidianDragonTests # run the binary directly (same thing)
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no per-test filtering — it is one binary that runs every assertion. The suite exercises the services layer, lite-wallet bridge, and pure helpers (parsers, formatters, model classes) without launching the GUI. Fixtures are under `tests/fixtures/` (path injected as `DRAGONX_TEST_FIXTURE_DIR`).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Entry & main loop.** `src/main.cpp` owns SDL3 window creation, ImGui/OpenGL(or DX11 on Windows) setup, and the frame loop. The `App` class is the central controller; because it is large it is split across four files that all implement the same class:
|
||||||
|
- `src/app.cpp` — core lifecycle, the per-frame `render()`, tab dispatch
|
||||||
|
- `src/app_network.cpp` — RPC orchestration, sync, peers, daemon lifecycle
|
||||||
|
- `src/app_security.cpp` — encryption, PIN/lock screen, key import/export, backup
|
||||||
|
- `src/app_wizard.cpp` — first-run wizard
|
||||||
|
|
||||||
|
**RPC.** All daemon calls go through `src/rpc/` (`rpc_client`, `connection`, `rpc_worker`). **Never block the main/UI thread with synchronous network I/O — dispatch through `RPCWorker`** (async). `rpc/types.h` holds the shared DTOs.
|
||||||
|
|
||||||
|
**Services** (`src/services/`) hold the non-UI state machines that the `App` owns: `NetworkRefreshService` + `RefreshScheduler` (polling/refresh of balance, peers, txs on intervals) and the `WalletSecurity*` controller/workflow stack (encryption & unlock flows).
|
||||||
|
|
||||||
|
**Data model** (`src/data/`): `WalletState`, `address_book`, `transaction_history_cache`, `exchange_info`. UI reads from these.
|
||||||
|
|
||||||
|
**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `material/` is the design-system layer (the live helpers `color_theme`, `colors`, `type`/`typography`, `draw_helpers`, `layout`, `project_icons`, `components/buttons`), `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic).
|
||||||
|
|
||||||
|
**Lite wallet** (`src/wallet/`): the bridge to an external `litelib_*` C-ABI backend. `lite_client_bridge` loads the backend (via direct `litelib_*` externs in `linkedSdxl()`) and owns each Rust string through `lite_owned_string` (copy-before-free / free-once). On top sit `lite_connection_service`, `lite_sync_service`, `lite_result_parsers`, `lite_wallet_gateway`, `lite_wallet_state_mapper`, and `lite_wallet_lifecycle_service`, all driven by `lite_wallet_controller`. The real frontend entry points are `lite_wallet_lifecycle_ui_adapter` and `lite_wallet_server_selection_adapter` (used by `src/ui/pages/settings_page.cpp`); everything else is reachable through them. (The prebuilt-backend symbol check for `DRAGONX_ENABLE_LITE_BACKEND` is done in CMake against the symbols inventory — see below — not in C++.)
|
||||||
|
|
||||||
|
> ⚠️ **Do not regrow the `_plan`/`_batch` churn.** This directory previously held ~160 dead `lite_wallet_*_plan` / `*_batch*_receipt_custody_acceptance_confirmation_archive_handoff_*` files (filenames up to 250 chars) — auto-generated scaffolding that never reached the shipping binary. They were deleted. When extending lite-wallet behavior, **edit the named service/bridge/runtime files in place**; never add another "promotion/receipt/custody/handoff/stewardship" wrapper layer. `scripts/check-source-hygiene.sh` (wired as a `.git/hooks/pre-commit` hook) blocks >80-char filenames and chained churn-token names — run it in CI too.
|
||||||
|
|
||||||
|
**Chat** (`src/chat/chat_protocol.cpp`): experimental HushChat protocol, compiled in only when `DRAGONX_ENABLE_CHAT=ON`.
|
||||||
|
|
||||||
|
## Build variants & feature gating
|
||||||
|
|
||||||
|
Variants are selected with CMake options (set by `build.sh` flags), surfaced to C++ as compile definitions:
|
||||||
|
- `DRAGONX_BUILD_LITE` (`--lite`) → `DRAGONX_LITE_BUILD` define; renames the app to `ObsidianDragonLite` and excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).
|
||||||
|
- `DRAGONX_ENABLE_LITE_BACKEND` → links a real external lite backend. Requires `--lite`, link mode `imported`, ABI `sdxl-c-v1`, and a symbols inventory file (built by `scripts/build-lite-backend-artifact.sh`); CMake hard-fails if any required `litelib_*` symbol is missing.
|
||||||
|
- `DRAGONX_ENABLE_CHAT` → `DRAGONX_ENABLE_CHAT` define gating the chat module.
|
||||||
|
|
||||||
|
Guard full-node-only code paths with `#if DRAGONX_LITE_BUILD` / chat code with `DRAGONX_ENABLE_CHAT`.
|
||||||
|
|
||||||
|
## Lite wallet status
|
||||||
|
|
||||||
|
The Lite variant is **functionally complete and runtime-verified on Linux + Windows** (work lives on branch `cleanup/lite-plan-churn`, **local-only — not pushed yet**):
|
||||||
|
- **Implemented:** lifecycle (create/open/restore + auto-open on startup), sync, refresh, send / shield / import / export / seed, persistence (the backend does *not* auto-save after sync/send/shield — the controller triggers `save` at those points), and passphrase **encryption** (encrypt/unlock/lock/decrypt + Settings UI + send-time & startup unlock; the backend locks immediately on `encrypt`). All controller-tested against the fake backend (`tests/fake_lite_backend.h`) and smoke-verified against the real SDXL backend via `tools/lite_smoke` (incl. a full sync). GUI is wired end-to-end with lite-appropriate wording; the full-node RPC connect loop / wizard / daemon strings are gated out of lite (lite "online" is derived from `lite_wallet_->walletOpen()`, not RPC).
|
||||||
|
- **Packaging:** `./build.sh --lite-backend --linux-release` (zip + AppImage, **verified**) and `--win-release` (cross-compiled `.exe`, **verified**; first build the Windows backend artifact with `scripts/build-lite-backend-artifact.sh --platform windows`). macOS `--lite-backend --mac-release` is **wired but not yet verified on this Linux box** (needs macOS/osxcross): the `.app`/launcher/rpath/`CFBundleExecutable` follow `ObsidianDragonLite`, full-node assets are skipped, and the lite variant gets its own `CFBundleName` ("DragonX Wallet Lite"), bundle id (`is.hush.dragonx.lite`), and DMG name so it can coexist with the full-node app. All variants correctly exclude full-node assets.
|
||||||
|
- **Rollout / kill-switch (implemented):** `wallet/lite_rollout_policy.{h,cpp}` is a pure, fail-open gate (local-only, no network) feeding `LiteWalletLifecycleService::availability()` (new `RolloutDisabled` reason). Inputs: the emergency env var `DRAGONX_LITE_KILL_SWITCH` (absolute — not even `force_on` bypasses it); a `lite_rollout` setting (`auto`/`force_on`/`force_off`); and an optional **locally-cached** manifest at `<config-dir>/lite_rollout.json` (`global_enabled`, `min_version`/`max_version`, `blocked_versions`, `rollout_permille`, `message`) keyed for staged rollout on a hashed, never-transmitted per-install id. A signed remote fetcher can populate that cache later without touching the policy. Resolved in `App::rebuildLiteWallet()`; the disable message surfaces via the lifecycle status. Unit-tested + runtime-verified (env / manifest / control).
|
||||||
|
- **Remaining (M5b):** verify the wired macOS `--lite` packaging on a Mac/osxcross, CI backend-artifact build + signing.
|
||||||
|
- **To publish:** rename branch → `feat/lite-wallet`, base the PR on `dev` (the full-node UX is already there), and handle the dormant gated-OFF HushChat content bundled in commit `af06b8b`.
|
||||||
|
|
||||||
|
The detailed milestone plan and design history (the v2 plan, backend artifact/ABI/signing design docs, the v1 plan, chat specs, etc.) are kept **untracked** under `docs/_archive/`.
|
||||||
|
|
||||||
|
## Miner updater (xmrig)
|
||||||
|
|
||||||
|
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live.
|
||||||
|
|
||||||
|
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **C++17.** Match the surrounding code's style per file.
|
||||||
|
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs.
|
||||||
|
- **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code.
|
||||||
|
- **i18n:** user-facing strings are translated via `src/util/i18n`; translation JSON lives in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`, English fallback in code). Translation/font helper scripts are in `scripts/` (`gen_*.py`, CJK subset tooling).
|
||||||
|
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.
|
||||||
+278
-6
@@ -15,13 +15,19 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.2.0
|
VERSION 1.3.0
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
|
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
|
||||||
set(DRAGONX_VERSION_SUFFIX "-rc1")
|
set(DRAGONX_VERSION_SUFFIX "")
|
||||||
|
|
||||||
|
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
|
||||||
|
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
|
||||||
|
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
|
||||||
|
set(DRAGONX_LITE_VERSION "1.0.0")
|
||||||
|
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
||||||
|
|
||||||
# C++17 standard
|
# C++17 standard
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
@@ -36,6 +42,130 @@ endif()
|
|||||||
# Options
|
# Options
|
||||||
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
||||||
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
||||||
|
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
|
||||||
|
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
|
||||||
|
option(DRAGONX_ENABLE_CHAT "Enable experimental HushChat protocol/UI integration" OFF)
|
||||||
|
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
|
||||||
|
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
|
||||||
|
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
|
||||||
|
set(DRAGONX_LITE_BACKEND_LINK_MODE "imported" CACHE STRING "Lite backend link mode; Phase 1 supports imported only")
|
||||||
|
set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
|
||||||
|
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
|
||||||
|
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
|
||||||
|
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
|
||||||
|
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
|
||||||
|
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
|
||||||
|
litelib_wallet_exists
|
||||||
|
litelib_initialize_new
|
||||||
|
litelib_initialize_new_from_phrase
|
||||||
|
litelib_initialize_existing
|
||||||
|
litelib_execute
|
||||||
|
litelib_rust_free_string
|
||||||
|
litelib_check_server_online
|
||||||
|
litelib_shutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
if(DRAGONX_BUILD_LITE)
|
||||||
|
set(DRAGONX_APP_NAME "ObsidianDragonLite")
|
||||||
|
set(DRAGONX_BINARY_NAME "ObsidianDragonLite")
|
||||||
|
# NOTE: do NOT FORCE-write DRAGONX_ENABLE_EMBEDDED_DAEMON=OFF into the cache here. A forced
|
||||||
|
# cache write persists into a later full-node reconfigure of the same build dir and silently
|
||||||
|
# disables the embedded daemon — the binary still embeds/extracts, but isUsingEmbeddedDaemon()
|
||||||
|
# returns false, so it "unpacks dragonxd but never starts" (the 1.3.0 regression). It is also
|
||||||
|
# redundant: makeWalletCapabilities() already forces the embedded-daemon capability off for any
|
||||||
|
# lite build via `fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
|
||||||
|
# regardless of this flag. build.sh sets the flag explicitly per variant to defeat stale caches.
|
||||||
|
set(DRAGONX_APP_VERSION "${DRAGONX_LITE_VERSION}")
|
||||||
|
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_LITE_VERSION_SUFFIX}")
|
||||||
|
else()
|
||||||
|
set(DRAGONX_APP_NAME "ObsidianDragon")
|
||||||
|
set(DRAGONX_BINARY_NAME "ObsidianDragon")
|
||||||
|
set(DRAGONX_APP_VERSION "${PROJECT_VERSION}")
|
||||||
|
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_VERSION_SUFFIX}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Split the active version into numeric components for the generated header + Windows VERSIONINFO.
|
||||||
|
string(REPLACE "." ";" _dragonx_ver_parts "${DRAGONX_APP_VERSION}")
|
||||||
|
list(GET _dragonx_ver_parts 0 DRAGONX_APP_VERSION_MAJOR)
|
||||||
|
list(GET _dragonx_ver_parts 1 DRAGONX_APP_VERSION_MINOR)
|
||||||
|
list(GET _dragonx_ver_parts 2 DRAGONX_APP_VERSION_PATCH)
|
||||||
|
|
||||||
|
set(DRAGONX_LITE_BACKEND_READY OFF)
|
||||||
|
if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||||
|
if(NOT DRAGONX_BUILD_LITE)
|
||||||
|
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND is only supported with DRAGONX_BUILD_LITE=ON")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_LINK_MODE STREQUAL "imported")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LINK_MODE currently supports only 'imported'; runtime dynamic loading is a later bridge-runtime phase")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_ABI STREQUAL "sdxl-c-v1")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_ABI must be sdxl-c-v1")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_LIBRARY)
|
||||||
|
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_LIBRARY to point at an SDXL-compatible artifact")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LIBRARY does not exist: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_SYMBOLS_FILE)
|
||||||
|
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_SYMBOLS_FILE generated by scripts/build-lite-backend-artifact.sh")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE does not exist: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
endif()
|
||||||
|
file(STRINGS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}" DRAGONX_LITE_BACKEND_SYMBOL_LINES)
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_SYMBOL_LINES)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is empty: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
endif()
|
||||||
|
foreach(DRAGONX_LITE_REQUIRED_SYMBOL IN LISTS DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS)
|
||||||
|
list(FIND DRAGONX_LITE_BACKEND_SYMBOL_LINES "${DRAGONX_LITE_REQUIRED_SYMBOL}" DRAGONX_LITE_SYMBOL_INDEX)
|
||||||
|
if(DRAGONX_LITE_SYMBOL_INDEX EQUAL -1)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is missing required symbol: ${DRAGONX_LITE_REQUIRED_SYMBOL}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
|
endif()
|
||||||
|
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
|
||||||
|
endif()
|
||||||
|
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
|
||||||
|
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
|
||||||
|
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
|
||||||
|
endif()
|
||||||
|
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
|
||||||
|
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
|
||||||
|
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
|
||||||
|
endif()
|
||||||
|
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
|
||||||
|
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
||||||
|
)
|
||||||
|
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||||
|
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||||
|
endif()
|
||||||
|
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
set(DRAGONX_LITE_BACKEND_READY ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|
||||||
@@ -280,6 +410,21 @@ set(APP_SOURCES
|
|||||||
src/services/wallet_security_controller.cpp
|
src/services/wallet_security_controller.cpp
|
||||||
src/services/wallet_security_workflow.cpp
|
src/services/wallet_security_workflow.cpp
|
||||||
src/services/wallet_security_workflow_executor.cpp
|
src/services/wallet_security_workflow_executor.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_diagnostics.cpp
|
||||||
|
src/wallet/lite_wallet_controller.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
src/wallet/lite_sync_service.cpp
|
||||||
|
src/wallet/lite_wallet_gateway.cpp
|
||||||
|
src/wallet/lite_wallet_state_mapper.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_server_selection_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_server_lifecycle_readiness.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_service.cpp
|
||||||
src/data/wallet_state.cpp
|
src/data/wallet_state.cpp
|
||||||
src/data/transaction_history_cache.cpp
|
src/data/transaction_history_cache.cpp
|
||||||
src/ui/theme.cpp
|
src/ui/theme.cpp
|
||||||
@@ -290,6 +435,7 @@ set(APP_SOURCES
|
|||||||
src/ui/notifications.cpp
|
src/ui/notifications.cpp
|
||||||
src/ui/windows/main_window.cpp
|
src/ui/windows/main_window.cpp
|
||||||
src/ui/windows/balance_tab.cpp
|
src/ui/windows/balance_tab.cpp
|
||||||
|
src/ui/windows/balance_components.cpp
|
||||||
src/ui/windows/balance_address_list.cpp
|
src/ui/windows/balance_address_list.cpp
|
||||||
src/ui/windows/balance_recent_tx.cpp
|
src/ui/windows/balance_recent_tx.cpp
|
||||||
src/ui/windows/balance_tab_helpers.cpp
|
src/ui/windows/balance_tab_helpers.cpp
|
||||||
@@ -297,10 +443,16 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/receive_tab.cpp
|
src/ui/windows/receive_tab.cpp
|
||||||
src/ui/windows/transactions_tab.cpp
|
src/ui/windows/transactions_tab.cpp
|
||||||
src/ui/windows/mining_tab.cpp
|
src/ui/windows/mining_tab.cpp
|
||||||
|
src/ui/windows/mining_earnings.cpp
|
||||||
|
src/ui/windows/mining_stats.cpp
|
||||||
|
src/ui/windows/mining_controls.cpp
|
||||||
|
src/ui/windows/mining_mode_toggle.cpp
|
||||||
src/ui/windows/mining_benchmark.cpp
|
src/ui/windows/mining_benchmark.cpp
|
||||||
src/ui/windows/mining_pool_panel.cpp
|
src/ui/windows/mining_pool_panel.cpp
|
||||||
src/ui/windows/mining_tab_helpers.cpp
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
src/ui/windows/peers_tab.cpp
|
src/ui/windows/peers_tab.cpp
|
||||||
|
src/ui/windows/network_tab.cpp
|
||||||
|
src/ui/windows/lite_console_tab.cpp
|
||||||
src/ui/windows/explorer_tab.cpp
|
src/ui/windows/explorer_tab.cpp
|
||||||
src/ui/windows/market_tab.cpp
|
src/ui/windows/market_tab.cpp
|
||||||
src/ui/windows/console_tab.cpp
|
src/ui/windows/console_tab.cpp
|
||||||
@@ -333,9 +485,11 @@ set(APP_SOURCES
|
|||||||
src/util/logger.cpp
|
src/util/logger.cpp
|
||||||
src/util/async_task_manager.cpp
|
src/util/async_task_manager.cpp
|
||||||
src/util/amount_format.cpp
|
src/util/amount_format.cpp
|
||||||
|
src/util/address_validation.cpp
|
||||||
src/util/base64.cpp
|
src/util/base64.cpp
|
||||||
src/util/single_instance.cpp
|
src/util/single_instance.cpp
|
||||||
src/util/i18n.cpp
|
src/util/i18n.cpp
|
||||||
|
src/util/text_format.cpp
|
||||||
src/util/platform.cpp
|
src/util/platform.cpp
|
||||||
src/util/payment_uri.cpp
|
src/util/payment_uri.cpp
|
||||||
src/util/texture_loader.cpp
|
src/util/texture_loader.cpp
|
||||||
@@ -345,6 +499,9 @@ set(APP_SOURCES
|
|||||||
src/daemon/lifecycle_adapters.cpp
|
src/daemon/lifecycle_adapters.cpp
|
||||||
src/daemon/xmrig_manager.cpp
|
src/daemon/xmrig_manager.cpp
|
||||||
src/util/bootstrap.cpp
|
src/util/bootstrap.cpp
|
||||||
|
src/util/lite_server_probe.cpp
|
||||||
|
src/util/xmrig_updater.cpp
|
||||||
|
src/util/xmrig_updater_core.cpp
|
||||||
src/util/secure_vault.cpp
|
src/util/secure_vault.cpp
|
||||||
src/ui/effects/framebuffer.cpp
|
src/ui/effects/framebuffer.cpp
|
||||||
src/ui/effects/blur_shader.cpp
|
src/ui/effects/blur_shader.cpp
|
||||||
@@ -380,6 +537,21 @@ set(APP_HEADERS
|
|||||||
src/services/wallet_security_controller.h
|
src/services/wallet_security_controller.h
|
||||||
src/services/wallet_security_workflow.h
|
src/services/wallet_security_workflow.h
|
||||||
src/services/wallet_security_workflow_executor.h
|
src/services/wallet_security_workflow_executor.h
|
||||||
|
src/wallet/wallet_capabilities.h
|
||||||
|
src/wallet/wallet_backend.h
|
||||||
|
src/wallet/lite_owned_string.h
|
||||||
|
src/wallet/lite_rollout_policy.h
|
||||||
|
src/wallet/lite_client_bridge.h
|
||||||
|
src/wallet/lite_connection_service.h
|
||||||
|
src/wallet/lite_result_parsers.h
|
||||||
|
src/wallet/lite_sync_service.h
|
||||||
|
src/wallet/lite_wallet_gateway.h
|
||||||
|
src/wallet/lite_wallet_state_mapper.h
|
||||||
|
src/wallet/lite_wallet_lifecycle_ui_adapter.h
|
||||||
|
src/wallet/lite_wallet_server_selection_adapter.h
|
||||||
|
src/wallet/lite_wallet_server_lifecycle_readiness.h
|
||||||
|
src/wallet/lite_wallet_lifecycle_service.h
|
||||||
|
src/chat/chat_protocol.h
|
||||||
src/config/version.h
|
src/config/version.h
|
||||||
src/data/wallet_state.h
|
src/data/wallet_state.h
|
||||||
src/data/transaction_history_cache.h
|
src/data/transaction_history_cache.h
|
||||||
@@ -479,10 +651,12 @@ 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
|
# Generate version values from the single project(VERSION ...) declaration.
|
||||||
|
# Keep the build-specific app name in the build tree so full/lite configures do
|
||||||
|
# not rewrite a tracked source header.
|
||||||
configure_file(
|
configure_file(
|
||||||
${CMAKE_SOURCE_DIR}/src/config/version.h.in
|
${CMAKE_SOURCE_DIR}/src/config/version.h.in
|
||||||
${CMAKE_SOURCE_DIR}/src/config/version.h
|
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
|
||||||
@ONLY
|
@ONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -518,6 +692,8 @@ add_executable(ObsidianDragon
|
|||||||
${WIN_RC_FILE}
|
${WIN_RC_FILE}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}")
|
||||||
|
|
||||||
target_include_directories(ObsidianDragon PRIVATE
|
target_include_directories(ObsidianDragon PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
${CMAKE_SOURCE_DIR}/src/embedded
|
${CMAKE_SOURCE_DIR}/src/embedded
|
||||||
@@ -542,6 +718,34 @@ target_link_libraries(ObsidianDragon PRIVATE
|
|||||||
${SODIUM_LIBRARY}
|
${SODIUM_LIBRARY}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(DRAGONX_LITE_BACKEND_READY)
|
||||||
|
target_link_libraries(ObsidianDragon PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
|
||||||
|
|
||||||
|
# Real-backend smoke tool (only built when a real lite backend is linked).
|
||||||
|
add_executable(lite_smoke
|
||||||
|
tools/lite_smoke.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(lite_smoke PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
target_compile_definitions(lite_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
|
||||||
|
target_link_libraries(lite_smoke PRIVATE
|
||||||
|
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Platform-specific settings
|
# Platform-specific settings
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
|
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
|
||||||
@@ -570,6 +774,10 @@ endif()
|
|||||||
# Compile definitions
|
# Compile definitions
|
||||||
target_compile_definitions(ObsidianDragon PRIVATE
|
target_compile_definitions(ObsidianDragon PRIVATE
|
||||||
DRAGONX_DEBUG
|
DRAGONX_DEBUG
|
||||||
|
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
|
||||||
|
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
|
||||||
|
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
|
||||||
|
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
|
||||||
)
|
)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
||||||
@@ -577,6 +785,25 @@ else()
|
|||||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
add_executable(HushChatFixtureCheck
|
||||||
|
tools/hushchat_fixture_check.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(HushChatFixtureCheck PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(HushChatFixtureCheck PRIVATE
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(HushChatFixtureCheck PRIVATE
|
||||||
|
DRAGONX_ENABLE_CHAT=0
|
||||||
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Copy resources
|
# Copy resources
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -727,7 +954,7 @@ install(TARGETS ObsidianDragon
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
||||||
DESTINATION share/ObsidianDragon
|
DESTINATION share/${DRAGONX_BINARY_NAME}
|
||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -743,6 +970,21 @@ if(BUILD_TESTING)
|
|||||||
src/services/wallet_security_controller.cpp
|
src/services/wallet_security_controller.cpp
|
||||||
src/services/wallet_security_workflow.cpp
|
src/services/wallet_security_workflow.cpp
|
||||||
src/services/wallet_security_workflow_executor.cpp
|
src/services/wallet_security_workflow_executor.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_diagnostics.cpp
|
||||||
|
src/wallet/lite_wallet_controller.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
src/wallet/lite_sync_service.cpp
|
||||||
|
src/wallet/lite_wallet_gateway.cpp
|
||||||
|
src/wallet/lite_wallet_state_mapper.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_server_selection_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_server_lifecycle_readiness.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_service.cpp
|
||||||
src/ui/explorer/explorer_block_cache.cpp
|
src/ui/explorer/explorer_block_cache.cpp
|
||||||
src/ui/windows/balance_address_list.cpp
|
src/ui/windows/balance_address_list.cpp
|
||||||
src/ui/windows/balance_recent_tx.cpp
|
src/ui/windows/balance_recent_tx.cpp
|
||||||
@@ -754,14 +996,22 @@ if(BUILD_TESTING)
|
|||||||
src/ui/windows/mining_tab_helpers.cpp
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
src/util/payment_uri.cpp
|
src/util/payment_uri.cpp
|
||||||
src/util/amount_format.cpp
|
src/util/amount_format.cpp
|
||||||
|
src/util/address_validation.cpp
|
||||||
|
src/util/i18n.cpp
|
||||||
|
src/util/text_format.cpp
|
||||||
src/data/wallet_state.cpp
|
src/data/wallet_state.cpp
|
||||||
src/data/transaction_history_cache.cpp
|
src/data/transaction_history_cache.cpp
|
||||||
src/daemon/lifecycle_adapters.cpp
|
src/daemon/lifecycle_adapters.cpp
|
||||||
src/rpc/connection.cpp
|
src/rpc/connection.cpp
|
||||||
|
src/config/settings.cpp
|
||||||
src/resources/embedded_resources.cpp
|
src/resources/embedded_resources.cpp
|
||||||
src/util/secure_vault.cpp
|
src/util/secure_vault.cpp
|
||||||
src/util/platform.cpp
|
src/util/platform.cpp
|
||||||
src/util/logger.cpp
|
src/util/logger.cpp
|
||||||
|
src/util/lite_server_probe.cpp
|
||||||
|
src/util/xmrig_updater.cpp
|
||||||
|
src/util/xmrig_updater_core.cpp
|
||||||
|
${MINIZ_SOURCES}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(ObsidianDragonTests PRIVATE
|
target_include_directories(ObsidianDragonTests PRIVATE
|
||||||
@@ -771,14 +1021,29 @@ if(BUILD_TESTING)
|
|||||||
${CMAKE_BINARY_DIR}/generated
|
${CMAKE_BINARY_DIR}/generated
|
||||||
${IMGUI_DIR}
|
${IMGUI_DIR}
|
||||||
${SODIUM_INCLUDE_DIR}
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
${CURL_INCLUDE_DIRS}
|
||||||
|
${MINIZ_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(ObsidianDragonTests PRIVATE
|
target_link_libraries(ObsidianDragonTests PRIVATE
|
||||||
nlohmann_json::nlohmann_json
|
nlohmann_json::nlohmann_json
|
||||||
sqlite3_amalgamation
|
sqlite3_amalgamation
|
||||||
${SODIUM_LIBRARY}
|
${SODIUM_LIBRARY}
|
||||||
|
${CURL_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(ObsidianDragonTests PRIVATE
|
||||||
|
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
|
||||||
|
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
|
||||||
|
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
|
||||||
|
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
|
||||||
|
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(DRAGONX_LITE_BACKEND_READY)
|
||||||
|
target_link_libraries(ObsidianDragonTests PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
|
||||||
|
endif()
|
||||||
|
|
||||||
if(UNIX)
|
if(UNIX)
|
||||||
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
|
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
|
||||||
endif()
|
endif()
|
||||||
@@ -792,10 +1057,17 @@ endif()
|
|||||||
|
|
||||||
message(STATUS "")
|
message(STATUS "")
|
||||||
message(STATUS "DragonX ImGui Wallet Configuration:")
|
message(STATUS "DragonX ImGui Wallet Configuration:")
|
||||||
message(STATUS " Version: ${PROJECT_VERSION}")
|
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
|
||||||
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
|
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
|
||||||
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
|
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
|
||||||
message(STATUS " ImGui dir: ${IMGUI_DIR}")
|
message(STATUS " ImGui dir: ${IMGUI_DIR}")
|
||||||
message(STATUS " SDL3 found: ${SDL3_FOUND}")
|
message(STATUS " SDL3 found: ${SDL3_FOUND}")
|
||||||
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
|
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
|
||||||
|
message(STATUS " Lite build: ${DRAGONX_BUILD_LITE}")
|
||||||
|
message(STATUS " Lite requested: ${DRAGONX_ENABLE_LITE_BACKEND}")
|
||||||
|
message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
|
||||||
|
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
|
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
|
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
|
||||||
message(STATUS "")
|
message(STATUS "")
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
VERSION="1.2.0-rc1"
|
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
|
||||||
|
# parsing — see the APP_BASENAME block below. Placeholder until then.
|
||||||
|
VERSION=""
|
||||||
|
|
||||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -41,6 +43,8 @@ DO_DEV=false
|
|||||||
DO_LINUX=false
|
DO_LINUX=false
|
||||||
DO_WIN=false
|
DO_WIN=false
|
||||||
DO_MAC=false
|
DO_MAC=false
|
||||||
|
DO_LITE=false
|
||||||
|
DO_LITE_BACKEND=false
|
||||||
CLEAN=false
|
CLEAN=false
|
||||||
BUILD_TYPE="Release"
|
BUILD_TYPE="Release"
|
||||||
|
|
||||||
@@ -54,6 +58,10 @@ Targets (at least one required, or none for dev build):
|
|||||||
--linux-release Linux release (zip + 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/
|
||||||
|
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
|
||||||
|
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
|
||||||
|
(auto-discovers build/lite-backend/<platform>/; build it with
|
||||||
|
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
|
||||||
|
|
||||||
Build trees are stored under build/{linux,windows,mac}/
|
Build trees are stored under build/{linux,windows,mac}/
|
||||||
|
|
||||||
@@ -74,6 +82,7 @@ Examples:
|
|||||||
$0 --linux-release # Linux release (zip + 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 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
|
||||||
$0 --clean --linux-release --win-release # Clean + both
|
$0 --clean --linux-release --win-release # Clean + both
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
@@ -85,6 +94,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--linux-release) DO_LINUX=true; shift ;;
|
--linux-release) DO_LINUX=true; shift ;;
|
||||||
--win-release) DO_WIN=true; shift ;;
|
--win-release) DO_WIN=true; shift ;;
|
||||||
--mac-release) DO_MAC=true; shift ;;
|
--mac-release) DO_MAC=true; shift ;;
|
||||||
|
--lite) DO_LITE=true; shift ;;
|
||||||
|
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
|
||||||
-c|--clean) CLEAN=true; shift ;;
|
-c|--clean) CLEAN=true; shift ;;
|
||||||
-d|--debug) BUILD_TYPE="Debug"; shift ;;
|
-d|--debug) BUILD_TYPE="Debug"; shift ;;
|
||||||
-j) JOBS="$2"; shift 2 ;;
|
-j) JOBS="$2"; shift 2 ;;
|
||||||
@@ -98,6 +109,92 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
|
|||||||
DO_DEV=true
|
DO_DEV=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
APP_BASENAME="ObsidianDragon"
|
||||||
|
CMAKE_LITE_ARGS=()
|
||||||
|
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
|
||||||
|
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
|
||||||
|
if $DO_LITE; then
|
||||||
|
APP_BASENAME="ObsidianDragonLite"
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
|
||||||
|
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
|
||||||
|
info "Lite mode enabled: building ${APP_BASENAME}"
|
||||||
|
else
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
|
||||||
|
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
|
||||||
|
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
|
||||||
|
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
|
||||||
|
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||||
|
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||||
|
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
||||||
|
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||||
|
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
|
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
|
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
|
if $DO_LITE; then
|
||||||
|
VERSION="${_lite_ver}${_lite_suffix}"
|
||||||
|
else
|
||||||
|
VERSION="${_full_ver}${_full_suffix}"
|
||||||
|
fi
|
||||||
|
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
|
||||||
|
info "Release version: ${VERSION} (${APP_BASENAME})"
|
||||||
|
|
||||||
|
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
|
||||||
|
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
|
||||||
|
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
|
||||||
|
# override the directory with DRAGONX_LITE_BACKEND_DIR.
|
||||||
|
if $DO_LITE_BACKEND; then
|
||||||
|
# Artifact platform follows the cross target when exactly one non-host release is requested,
|
||||||
|
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) lb_platform="linux" ;;
|
||||||
|
Darwin) lb_platform="macos" ;;
|
||||||
|
*) lb_platform="linux" ;;
|
||||||
|
esac
|
||||||
|
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
|
||||||
|
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
|
||||||
|
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
|
||||||
|
lb_lib=""
|
||||||
|
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
|
||||||
|
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
|
||||||
|
done
|
||||||
|
lb_symbols="$lb_dir/lite-backend-symbols.txt"
|
||||||
|
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
|
||||||
|
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
|
||||||
|
err "Lite backend artifact not found under: $lb_dir"
|
||||||
|
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
|
||||||
|
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CMAKE_LITE_ARGS+=(
|
||||||
|
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
|
||||||
|
)
|
||||||
|
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
|
||||||
|
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
|
||||||
|
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
|
||||||
|
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
|
||||||
|
if [[ "$lb_platform" == "windows" ]]; then
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
|
||||||
|
fi
|
||||||
|
info "Lite backend enabled ($lb_platform): $lb_lib"
|
||||||
|
else
|
||||||
|
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
|
||||||
|
# (which would then fail the BUILD_LITE=OFF guard in CMake).
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
|
||||||
|
fi
|
||||||
|
|
||||||
|
should_bundle_full_node_assets() {
|
||||||
|
! $DO_LITE
|
||||||
|
}
|
||||||
|
|
||||||
# ── Helper: find resource files ──────────────────────────────────────────────
|
# ── Helper: find resource files ──────────────────────────────────────────────
|
||||||
find_sapling_params() {
|
find_sapling_params() {
|
||||||
local dirs=(
|
local dirs=(
|
||||||
@@ -215,13 +312,14 @@ build_dev() {
|
|||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=ON
|
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
|
||||||
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
|
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -242,42 +340,51 @@ build_release_linux() {
|
|||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=ON
|
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
||||||
|
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip bin/ObsidianDragon
|
strip "bin/${APP_BASENAME}"
|
||||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||||
|
|
||||||
# ── Bundle daemon ────────────────────────────────────────────────────────
|
if should_bundle_full_node_assets; then
|
||||||
|
# ── 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 ────────────────────────────────────────────────
|
# ── Bundle Sapling params ────────────────────────────────────────────
|
||||||
SAPLING_SPEND="" SAPLING_OUTPUT=""
|
SAPLING_SPEND="" SAPLING_OUTPUT=""
|
||||||
find_sapling_params && {
|
find_sapling_params && {
|
||||||
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
|
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
|
||||||
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
|
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
|
||||||
info "Bundled Sapling params"
|
info "Bundled Sapling params"
|
||||||
} || warn "Sapling params not found — not bundled"
|
} || warn "Sapling params not found — not bundled"
|
||||||
|
else
|
||||||
|
info "Lite mode: skipping daemon and Sapling/asmap bundling"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Package: release/linux/ ──────────────────────────────────────────────
|
# ── Package: release/linux/ ──────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
|
||||||
|
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
|
||||||
|
|
||||||
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
|
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
|
|
||||||
cp bin/ObsidianDragon "$dist_dir/"
|
cp "bin/${APP_BASENAME}" "$dist_dir/"
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
|
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
|
||||||
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
|
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
|
||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$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-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
||||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||||
|
fi
|
||||||
# Bundle xmrig for mining support
|
# Bundle xmrig for mining support
|
||||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
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"
|
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||||
@@ -299,9 +406,10 @@ build_release_linux() {
|
|||||||
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
|
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
|
||||||
"$APPDIR/usr/share/ObsidianDragon/res"
|
"$APPDIR/usr/share/ObsidianDragon/res"
|
||||||
|
|
||||||
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
|
cp "bin/${APP_BASENAME}" "$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
|
||||||
|
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
[[ -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/)
|
# Daemon data files must be alongside the daemon binary (usr/bin/)
|
||||||
@@ -309,17 +417,18 @@ build_release_linux() {
|
|||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
[[ -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-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
||||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||||
|
fi
|
||||||
# Bundle xmrig for mining support
|
# Bundle xmrig for mining support
|
||||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
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"; }
|
[[ -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
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=DragonX Wallet
|
Name=DragonX Wallet
|
||||||
Comment=DragonX Cryptocurrency Wallet
|
Comment=DragonX Cryptocurrency Wallet
|
||||||
Exec=ObsidianDragon
|
Exec=${APP_BASENAME}
|
||||||
Icon=ObsidianDragon
|
Icon=ObsidianDragon
|
||||||
Categories=Finance;Network;
|
Categories=Finance;Network;
|
||||||
Terminal=false
|
Terminal=false
|
||||||
@@ -350,14 +459,14 @@ SVG
|
|||||||
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
|
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
|
||||||
|
|
||||||
# AppRun
|
# AppRun
|
||||||
cat > "$APPDIR/AppRun" <<'APPRUN'
|
cat > "$APPDIR/AppRun" <<APPRUN
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
SELF=$(readlink -f "$0")
|
SELF=\$(readlink -f "\$0")
|
||||||
HERE=${SELF%/*}
|
HERE=\${SELF%/*}
|
||||||
export DRAGONX_RES_PATH="${HERE}/usr/share/ObsidianDragon/res"
|
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
|
||||||
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
|
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
|
||||||
cd "${HERE}/usr/share/ObsidianDragon"
|
cd "\${HERE}/usr/share/ObsidianDragon"
|
||||||
exec "${HERE}/usr/bin/ObsidianDragon" "$@"
|
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
|
||||||
APPRUN
|
APPRUN
|
||||||
chmod +x "$APPDIR/AppRun"
|
chmod +x "$APPDIR/AppRun"
|
||||||
|
|
||||||
@@ -386,9 +495,9 @@ APPRUN
|
|||||||
local ARCH
|
local ARCH
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
cd "$bd"
|
cd "$bd"
|
||||||
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
||||||
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
|
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
|
||||||
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
|
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))"
|
||||||
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
||||||
|
|
||||||
info "Linux release artifacts: $out/"
|
info "Linux release artifacts: $out/"
|
||||||
@@ -497,6 +606,7 @@ HDR
|
|||||||
|
|
||||||
# ── Daemon binaries ──────────────────────────────────────────────
|
# ── Daemon binaries ──────────────────────────────────────────────
|
||||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
|
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
|
||||||
info "Embedding daemon binaries ..."
|
info "Embedding daemon binaries ..."
|
||||||
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
|
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
|
||||||
@@ -514,9 +624,30 @@ HDR
|
|||||||
else
|
else
|
||||||
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
|
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
info "Lite mode: skipping embedded daemon binaries"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||||
|
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
|
||||||
|
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
|
||||||
|
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
|
||||||
|
# no miner ("xmrig binary not found" at runtime).
|
||||||
|
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
|
||||||
|
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
|
||||||
|
local _xtmp; _xtmp=$(mktemp -d)
|
||||||
|
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
|
||||||
|
# non-zero if a pattern matches nothing).
|
||||||
|
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
|
||||||
|
if [[ -f "$_xtmp/xmrig.exe" ]]; then
|
||||||
|
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
|
||||||
|
info " Extracted xmrig.exe from $(basename "$_xz")"
|
||||||
|
fi
|
||||||
|
rm -rf "$_xtmp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
|
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
|
||||||
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
|
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
|
||||||
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||||
@@ -599,24 +730,27 @@ HDR
|
|||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||||
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
||||||
|
|
||||||
# ── Package: release/windows/ ────────────────────────────────────────────
|
# ── Package: release/windows/ ────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
|
||||||
|
|
||||||
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
|
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
cp bin/ObsidianDragon.exe "$dist_dir/"
|
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
|
||||||
|
|
||||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||||
done
|
done
|
||||||
@@ -627,6 +761,9 @@ HDR
|
|||||||
for f in sapling-spend.params sapling-output.params asmap.dat; do
|
for f in sapling-spend.params sapling-output.params asmap.dat; do
|
||||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
|
||||||
|
fi
|
||||||
|
|
||||||
# Bundle xmrig for mining support
|
# Bundle xmrig for mining support
|
||||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||||
@@ -635,8 +772,8 @@ HDR
|
|||||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
# ── Single-file exe (all resources embedded) ────────────────────────────
|
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||||
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
|
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
|
||||||
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
|
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))"
|
||||||
|
|
||||||
# ── Zip ──────────────────────────────────────────────────────────────────
|
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||||
if command -v zip &>/dev/null; then
|
if command -v zip &>/dev/null; then
|
||||||
@@ -818,7 +955,8 @@ TOOLCHAIN
|
|||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
else
|
else
|
||||||
# Build libsodium as universal if needed
|
# Build libsodium as universal if needed
|
||||||
local need_sodium=false
|
local need_sodium=false
|
||||||
@@ -844,39 +982,40 @@ TOOLCHAIN
|
|||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
|
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
|
||||||
|
|
||||||
# Strip — use osxcross strip for cross-builds
|
# Strip — use osxcross strip for cross-builds
|
||||||
if $IS_CROSS; then
|
if $IS_CROSS; then
|
||||||
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
||||||
if [[ -x "$STRIP_CMD" ]]; then
|
if [[ -x "$STRIP_CMD" ]]; then
|
||||||
info "Stripping (osxcross) ..."
|
info "Stripping (osxcross) ..."
|
||||||
"$STRIP_CMD" bin/ObsidianDragon
|
"$STRIP_CMD" "bin/${APP_BASENAME}"
|
||||||
else
|
else
|
||||||
warn "osxcross strip not found at $STRIP_CMD — skipping"
|
warn "osxcross strip not found at $STRIP_CMD — skipping"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip bin/ObsidianDragon
|
strip "bin/${APP_BASENAME}"
|
||||||
# Verify universal binary
|
# Verify universal binary
|
||||||
if command -v lipo &>/dev/null; then
|
if command -v lipo &>/dev/null; then
|
||||||
info "Architecture info:"
|
info "Architecture info:"
|
||||||
lipo -info bin/ObsidianDragon
|
lipo -info "bin/${APP_BASENAME}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||||
|
|
||||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
rm -rf "$out"
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
local APP="$out/ObsidianDragon.app"
|
local APP="$out/${APP_BASENAME}.app"
|
||||||
local CONTENTS="$APP/Contents"
|
local CONTENTS="$APP/Contents"
|
||||||
local MACOS="$CONTENTS/MacOS"
|
local MACOS="$CONTENTS/MacOS"
|
||||||
local RESOURCES="$CONTENTS/Resources"
|
local RESOURCES="$CONTENTS/Resources"
|
||||||
@@ -885,12 +1024,13 @@ TOOLCHAIN
|
|||||||
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
|
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
|
||||||
|
|
||||||
# Main binary
|
# Main binary
|
||||||
cp bin/ObsidianDragon "$MACOS/"
|
cp "bin/${APP_BASENAME}" "$MACOS/"
|
||||||
chmod +x "$MACOS/ObsidianDragon"
|
chmod +x "$MACOS/${APP_BASENAME}"
|
||||||
|
|
||||||
# Resources
|
# Resources
|
||||||
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
|
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
|
||||||
|
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
# Daemon binaries (macOS native, from dragonxd-mac/)
|
# Daemon binaries (macOS native, from dragonxd-mac/)
|
||||||
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
|
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
|
||||||
if [[ -d "$daemon_dir" ]]; then
|
if [[ -d "$daemon_dir" ]]; then
|
||||||
@@ -919,6 +1059,9 @@ TOOLCHAIN
|
|||||||
else
|
else
|
||||||
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||||
|
fi
|
||||||
|
|
||||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||||
@@ -930,11 +1073,13 @@ TOOLCHAIN
|
|||||||
warn "xmrig not found — mining unavailable in .app"
|
warn "xmrig not found — mining unavailable in .app"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if should_bundle_full_node_assets; then
|
||||||
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
|
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
|
||||||
find_asmap 2>/dev/null && {
|
find_asmap 2>/dev/null && {
|
||||||
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
|
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
|
||||||
info " Bundled asmap.dat"
|
info " Bundled asmap.dat"
|
||||||
}
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# Bundle SDL3 dylib
|
# Bundle SDL3 dylib
|
||||||
local sdl_dylib=""
|
local sdl_dylib=""
|
||||||
@@ -954,26 +1099,34 @@ TOOLCHAIN
|
|||||||
# Fix the rpath so the binary finds SDL3 in Frameworks/
|
# Fix the rpath so the binary finds SDL3 in Frameworks/
|
||||||
if $IS_CROSS; then
|
if $IS_CROSS; then
|
||||||
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
|
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
|
||||||
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
|
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
|
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
info " Bundled $sdl_name"
|
info " Bundled $sdl_name"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Launcher script (ensures working dir + dylib path)
|
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
|
||||||
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
|
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
|
||||||
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
|
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
|
||||||
|
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
|
||||||
export DYLD_LIBRARY_PATH="$DIR/../Frameworks:$DYLD_LIBRARY_PATH"
|
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
|
||||||
export DRAGONX_RES_PATH="$DIR/../Resources/res"
|
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
|
||||||
cd "$DIR/../Resources"
|
cd "\$DIR/../Resources"
|
||||||
exec "$DIR/ObsidianDragon.bin" "$@"
|
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
|
||||||
LAUNCH
|
LAUNCH
|
||||||
chmod +x "$MACOS/ObsidianDragon"
|
chmod +x "$MACOS/${APP_BASENAME}"
|
||||||
|
|
||||||
# Info.plist
|
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
|
||||||
|
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
|
||||||
|
local APP_DISPLAY_NAME="DragonX Wallet"
|
||||||
|
local APP_BUNDLE_ID="is.hush.dragonx"
|
||||||
|
if $DO_LITE; then
|
||||||
|
APP_DISPLAY_NAME="DragonX Wallet Lite"
|
||||||
|
APP_BUNDLE_ID="is.hush.dragonx.lite"
|
||||||
|
fi
|
||||||
cat > "$CONTENTS/Info.plist" <<PLIST
|
cat > "$CONTENTS/Info.plist" <<PLIST
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
@@ -981,17 +1134,17 @@ LAUNCH
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>DragonX Wallet</string>
|
<string>${APP_DISPLAY_NAME}</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>DragonX Wallet</string>
|
<string>${APP_DISPLAY_NAME}</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>is.hush.dragonx</string>
|
<string>${APP_BUNDLE_ID}</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>${VERSION}</string>
|
<string>${VERSION}</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>${VERSION}</string>
|
<string>${VERSION}</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>ObsidianDragon</string>
|
<string>${APP_BASENAME}</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>ObsidianDragon</string>
|
<string>ObsidianDragon</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
@@ -1053,25 +1206,27 @@ PLIST
|
|||||||
info ".app bundle created: $APP"
|
info ".app bundle created: $APP"
|
||||||
|
|
||||||
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
||||||
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||||
if command -v zip &>/dev/null; then
|
if command -v zip &>/dev/null; then
|
||||||
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
|
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
|
||||||
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||||
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
local DMG_BASENAME="DragonX_Wallet"
|
||||||
|
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||||
|
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||||
|
|
||||||
if command -v create-dmg &>/dev/null; then
|
if command -v create-dmg &>/dev/null; then
|
||||||
# create-dmg (works on macOS; also available on Linux via npm)
|
# create-dmg (works on macOS; also available on Linux via npm)
|
||||||
info "Creating DMG with create-dmg ..."
|
info "Creating DMG with create-dmg ..."
|
||||||
create-dmg \
|
create-dmg \
|
||||||
--volname "DragonX Wallet" \
|
--volname "${APP_DISPLAY_NAME}" \
|
||||||
--volicon "$RESOURCES/ObsidianDragon.icns" \
|
--volicon "$RESOURCES/ObsidianDragon.icns" \
|
||||||
--window-pos 200 120 \
|
--window-pos 200 120 \
|
||||||
--window-size 600 400 \
|
--window-size 600 400 \
|
||||||
--icon-size 100 \
|
--icon-size 100 \
|
||||||
--icon "ObsidianDragon.app" 150 190 \
|
--icon "${APP_BASENAME}.app" 150 190 \
|
||||||
--app-drop-link 450 190 \
|
--app-drop-link 450 190 \
|
||||||
--no-internet-enable \
|
--no-internet-enable \
|
||||||
"$out/$DMG_NAME" \
|
"$out/$DMG_NAME" \
|
||||||
@@ -1086,7 +1241,7 @@ PLIST
|
|||||||
mkdir -p "$staging"
|
mkdir -p "$staging"
|
||||||
cp -a "$APP" "$staging/"
|
cp -a "$APP" "$staging/"
|
||||||
ln -s /Applications "$staging/Applications"
|
ln -s /Applications "$staging/Applications"
|
||||||
hdiutil create -volname "DragonX Wallet" \
|
hdiutil create -volname "${APP_DISPLAY_NAME}" \
|
||||||
-srcfolder "$staging" \
|
-srcfolder "$staging" \
|
||||||
-ov -format UDZO \
|
-ov -format UDZO \
|
||||||
"$out/$DMG_NAME" 2>/dev/null && {
|
"$out/$DMG_NAME" 2>/dev/null && {
|
||||||
@@ -1102,7 +1257,7 @@ PLIST
|
|||||||
cp -a "$APP" "$staging/"
|
cp -a "$APP" "$staging/"
|
||||||
# Can't create a real symlink to /Applications in an ISO, but the .app
|
# Can't create a real symlink to /Applications in an ISO, but the .app
|
||||||
# is the important part — users drag it to Applications manually.
|
# is the important part — users drag it to Applications manually.
|
||||||
genisoimage -V "DragonX Wallet" \
|
genisoimage -V "${APP_DISPLAY_NAME}" \
|
||||||
-D -R -apple -no-pad \
|
-D -R -apple -no-pad \
|
||||||
-o "$out/$DMG_NAME" \
|
-o "$out/$DMG_NAME" \
|
||||||
"$staging" 2>/dev/null && {
|
"$staging" 2>/dev/null && {
|
||||||
|
|||||||
@@ -1,671 +0,0 @@
|
|||||||
# Codebase Cleanup Audit
|
|
||||||
|
|
||||||
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This audit covers architecture, threading, UI/layout code, build/resource integration, RPC/daemon behavior, and general cleanup opportunities. It is a static maintainability audit, not a runtime performance profile or security penetration test.
|
|
||||||
|
|
||||||
Evidence was gathered from source-tree scans, largest-file metrics, targeted code inspection, and focused architecture/UI/build review passes.
|
|
||||||
|
|
||||||
## Snapshot
|
|
||||||
|
|
||||||
- `src/` contains about 77,639 handwritten C++ source/header lines, excluding generated language outputs.
|
|
||||||
- Generated language headers previously accounted for about 34,675 source-tree lines under `src/embedded/lang_*.h`; Phase 4 moved those outputs to `build/generated/embedded/`.
|
|
||||||
- Largest handwritten files are concentrated in UI and application orchestration:
|
|
||||||
- `src/ui/windows/balance_tab.cpp`: 3,434 lines
|
|
||||||
- `src/app.cpp`: 3,097 lines
|
|
||||||
- `src/ui/windows/mining_tab.cpp`: 2,544 lines
|
|
||||||
- `src/ui/pages/settings_page.cpp`: 2,092 lines
|
|
||||||
- `src/main.cpp`: 2,014 lines
|
|
||||||
- `src/app_security.cpp`: 1,918 lines
|
|
||||||
- `src/app_network.cpp`: 1,912 lines
|
|
||||||
- `src/ui/windows/console_tab.cpp`: 1,572 lines
|
|
||||||
- `src/app_wizard.cpp`: 1,378 lines
|
|
||||||
- `src/daemon/embedded_daemon.cpp`: 1,213 lines
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
The codebase has a solid functional foundation: RPC work is already separated from UI-thread callbacks, the schema-driven UI system is useful, and embedded resource handling is well established. The largest opportunities are mostly structural rather than feature bugs.
|
|
||||||
|
|
||||||
The main cleanup theme is ownership. `App` currently coordinates lifecycle, rendering, daemon state, RPC refreshes, wallet security, dialogs, shutdown, mining, and first-run flow. That centralization makes features easy to wire in the short term, but it also pushes thread lifetime, error propagation, UI state, and background polling into a few very large files.
|
|
||||||
|
|
||||||
The second theme is consistency. Dialogs mostly use Material overlay helpers, but some still use raw ImGui modals. Many UI dimensions are schema-driven, but dialog/button/icon sizes still have scattered literals. RPC auth exists in more than one place, generated resources live partly in source and partly in build output, and error handling ranges from user-visible reporting to silent `catch (...)` blocks.
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
### Phase 1 Completed On 2026-04-27
|
|
||||||
|
|
||||||
- Replaced time-seeded `std::rand()` RPC credential generation in `src/rpc/connection.cpp` with libsodium-backed random generation.
|
|
||||||
- Replaced direct UI `system()` explorer/about launch calls with `util::Platform::openUrl()`.
|
|
||||||
- Reworked `util::Platform::openUrl()` and `openFolder()` so macOS/Linux launchers use `posix_spawnp()` arguments instead of shell-built command strings, and folder creation uses `std::filesystem::create_directories()`.
|
|
||||||
- Added URL scheme validation for platform URL opens.
|
|
||||||
- Added CMake visibility for missing `xxd` and Python theme expansion dependencies.
|
|
||||||
- Added `xxd` and Python checks/package coverage to `setup.sh`.
|
|
||||||
- Added shared `App::sendStopCommandSafely()` logging helper and routed repeated daemon `stop` RPC calls through it.
|
|
||||||
- Confirmed `libs/incbin.h` is already represented in `THIRD_PARTY_LICENSES`, so no additional license entry was needed.
|
|
||||||
- Verified with staged builds after logical batches, ending with `cd build && make -j$(nproc)` successfully linking `bin/ObsidianDragon`.
|
|
||||||
|
|
||||||
Remaining high-priority follow-ups from this audit include the larger `App`/security/refresh service boundaries and tests around the newly centralized runtime behavior.
|
|
||||||
|
|
||||||
### Phase 2 Completed On 2026-04-27
|
|
||||||
|
|
||||||
- Added schema-backed dialog layout tokens in `res/themes/ui.toml` and central accessors in `src/ui/layout.h` for common dialog widths, form width, action width/gap, max height ratio, and compact bottom alignment.
|
|
||||||
- Registered the `dialog` UI schema section in `src/ui/schema/ui_schema.cpp` so those tokens are available through the existing schema cache.
|
|
||||||
- Extended `material::BeginOverlayDialog()` with an optional ID suffix and added shared overlay action/footer helpers in `src/ui/material/draw_helpers.h`.
|
|
||||||
- Migrated address book Add/Edit address dialogs in `src/ui/windows/address_book_dialog.cpp` from raw `ImGui::BeginPopupModal()` usage to `material::BeginOverlayDialog()` with one shared form/action renderer.
|
|
||||||
- Added `src/ui/material/project_icons.h` as the wallet icon registry and moved pickaxe-specific font rendering behind that helper.
|
|
||||||
- Kept `AddressLabelDialog::drawIconByName()` and `iconGlyphForName()` as compatibility wrappers for existing call sites.
|
|
||||||
- Verified with `cd build && make -j$(nproc)` successfully linking `bin/ObsidianDragon`; diagnostics were clean on Phase 2 touched files, `git diff --check` passed, and scans found no remaining Add/Edit raw modal usage or local address-label icon arrays.
|
|
||||||
|
|
||||||
### Phase 3 Completed On 2026-04-27
|
|
||||||
|
|
||||||
- Added `src/util/async_task_manager.h/.cpp` as a named task owner with cancellation tokens, completed-task reaping, and join-on-shutdown behavior.
|
|
||||||
- Routed App-owned daemon maintenance, wizard daemon stop/check, encryption daemon restart, and decrypt restart/import background work through `AsyncTaskManager` instead of detached App threads.
|
|
||||||
- Kept `Bootstrap`'s worker joinable so its existing destructor cancellation can join the download/extract thread instead of losing ownership through `detach()`.
|
|
||||||
- Added `src/daemon/daemon_controller.h/.cpp` as the first daemon ownership boundary; it now owns `EmbeddedDaemon`, syncs settings into the daemon, and centralizes start/stop calls while `App` keeps a non-owning bridge pointer for low-churn follow-up migration.
|
|
||||||
- Extended `rpc::ConnectionConfig` with auth-source tracking and `use_tls`, parsed `rpctls`/`rpcssl`-style config flags, and centralized `.cookie` auth retry construction in `Connection::buildCookieAuthConfig()`.
|
|
||||||
- Updated main, fast-lane, temporary stop, wizard stop, and decrypt-import RPC clients to pass the TLS flag to `RPCClient`.
|
|
||||||
- Added a one-per-session runtime warning and Settings-page warning when a non-localhost RPC host is configured without TLS.
|
|
||||||
- Verified after each logical batch with `cmake .. && make -j$(nproc)` or `make -j$(nproc)` successfully linking `bin/ObsidianDragon`; diagnostics were clean on Phase 3 touched files, `git diff --check` passed, and scans found no remaining App-owned detached background tasks or App-level manual `.cookie` fallback.
|
|
||||||
|
|
||||||
### Phase 4 Completed On 2026-04-27
|
|
||||||
|
|
||||||
- Added `src/services/refresh_scheduler.h/.cpp` and moved refresh interval/timer policy out of `App`, while leaving RPC refresh bodies behavior-preserving in the existing app/network methods.
|
|
||||||
- Replaced App refresh timer fields with `services::RefreshScheduler`, including page-specific refresh policy, wallet mutation refresh marking, transaction-age throttling, OPID polling cadence, price refresh cadence, and fast mining/rescan ticks.
|
|
||||||
- Moved generated language headers from tracked `src/embedded/lang_*.h` files into `${CMAKE_BINARY_DIR}/generated/embedded/`, keeping the existing `embedded/lang_*.h` include strings working through the generated include directory.
|
|
||||||
- Added CTest infrastructure and `ObsidianDragonTests` with focused coverage for connection config parsing, cookie fallback and plaintext/TLS checks, payment URI parsing, fixed amount formatting, spendable wallet address filtering, and scheduler behavior.
|
|
||||||
- Added `src/util/amount_format.h/.cpp` for shared fixed-decimal amount formatting and used it in transaction send payload construction.
|
|
||||||
- Added pure spendability helpers in `src/data/wallet_state.h/.cpp` and routed Send-tab source address selection through them.
|
|
||||||
- Split low-risk helpers out of large UI tabs: balance helper formatting/drawing code, mining formatting/estimate/thread helpers, and the console RPC command reference registry.
|
|
||||||
- Verified focused tests with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`, then verified the UI split batch with `cd build && cmake .. && make -j$(nproc)` successfully linking `bin/ObsidianDragon`.
|
|
||||||
|
|
||||||
### Phase 5 Completed On 2026-04-27
|
|
||||||
|
|
||||||
- Added `src/services/wallet_security_controller.h/.cpp` and moved deferred wizard encryption/PIN state, connection retry throttling, PIN validation, and secure clearing behind a wallet-security boundary while keeping the existing RPC/UI behavior in `App`.
|
|
||||||
- Added `src/services/network_refresh_service.h/.cpp` as the fuller refresh boundary around `RefreshScheduler`, named refresh jobs, in-flight job guards, and explicit queue-pressure skips for core, address, transaction, mining, and peer refreshes.
|
|
||||||
- Extended `DaemonController` with shutdown/external-daemon policy decisions and routed `App::beginShutdown()` through that boundary.
|
|
||||||
- Grouped Settings-page static UI state into `SettingsPageState` and first-run wizard static UI state into `WizardUiState`, preserving existing file-local behavior while making state ownership explicit.
|
|
||||||
- Continued low-risk renderer splitting with console layout helpers, balance recent-transaction visual/amount helpers, and mining active-state/thread clamp helpers.
|
|
||||||
- Expanded focused tests for daemon shutdown policy, wallet security transitions, network refresh job guards, renderer helpers, and generated resource fallback behavior.
|
|
||||||
- Documented the `src/config/version.h` policy: it remains committed source for editor/release tooling compatibility, and CMake regenerates it from `src/config/version.h.in` during configure.
|
|
||||||
- Verified Phase 5 batches with repeated `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure` runs.
|
|
||||||
|
|
||||||
### Phase 6 Completed On 2026-04-27
|
|
||||||
|
|
||||||
- Expanded `WalletSecurityController` with mockable RPC and secure-vault gateways for deferred encryption, unlock, export, import, key classification, import messaging, decrypt export naming, and secure handoff behavior.
|
|
||||||
- Routed wallet encryption, deferred encryption retry, unlock, and key import/export classification through the wallet-security service boundary while preserving existing App-visible behavior.
|
|
||||||
- Extended `DaemonController` with lifecycle decisions for manual restart, rescan, blockchain-data deletion, and bootstrap daemon stop sequencing, then routed the corresponding App flows through those decisions.
|
|
||||||
- Promoted `NetworkRefreshService` to dispatch-ticket ownership with queue-depth telemetry, queue-pressure skips, in-flight skips, completion stats, cancellation, and stale-callback detection for named refresh jobs.
|
|
||||||
- Continued renderer splitting with stable modules for recent transaction presentation, pool-worker default selection, and console output filtering.
|
|
||||||
- Removed Settings-page compatibility aliases so the page now uses `SettingsPageState` fields directly.
|
|
||||||
- Expanded focused integration-style tests with mock wallet-security RPC/vault collaborators, daemon lifecycle policy checks, refresh dispatch telemetry/stale-callback edges, and UI helper coverage.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 7 completed on 2026-04-27:
|
|
||||||
- `DaemonController` now owns lifecycle execution ordering for restart, rescan, blockchain-data deletion, and bootstrap stops through mockable `LifecycleRuntime` and `LifecycleTaskContext` collaborators.
|
|
||||||
- App removed the temporary non-owning `embedded_daemon_` bridge; daemon state/output reads now route through `DaemonController` wrappers or explicit controller access.
|
|
||||||
- Added `WalletSecurityWorkflow` for decrypt/export/import dialog phase, step, import-active state, and wallet file planning, so the dialog state can be tested without constructing `App`.
|
|
||||||
- `NetworkRefreshService` now owns enqueue/callback wrapping for named refresh jobs, including queue-depth sampling, queue-pressure skips, stale callback suppression, and UI-thread callback handoff.
|
|
||||||
- Core, address, transaction, mining, peer, and encryption refresh jobs now use the service enqueue wrapper instead of app-local dispatch-ticket boilerplate.
|
|
||||||
- Split additional UI helpers into `balance_address_list`, `mining_benchmark`, and `console_input_model`, with focused tests for filtering/sorting, benchmark state estimates, history navigation, and autocomplete.
|
|
||||||
- Expanded `ObsidianDragonTests` with mock daemon lifecycle execution tests, wallet workflow tests, refresh enqueue/stale-callback tests, and the new UI module coverage.
|
|
||||||
- Verified after each logical batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 8 completed on 2026-04-27:
|
|
||||||
- Added `WalletSecurityWorkflowExecutor` with mockable RPC, import, file, daemon, and secure-vault cleanup gateways; decrypt wallet unlock/export/backup/restart/import/cleanup sequencing now runs through that executor instead of nested App-local orchestration.
|
|
||||||
- Completed named refresh enqueue coverage by routing price refresh through `NetworkRefreshService::enqueue(Job::Price)` and connect-time `getinfo`/`getwalletinfo` prefetch through `Job::ConnectionInit`.
|
|
||||||
- Added daemon lifecycle collaborators for async/immediate task contexts and blockchain-data cleanup, reducing App-specific lifecycle runtime code where the extracted pieces are directly testable.
|
|
||||||
- Continued large-view reduction with balance address-row layout/USD helpers, mining benchmark transition and pool saved/default helpers, and console command parse/result classification helpers.
|
|
||||||
- Expanded `ObsidianDragonTests` for workflow executor edges, ConnectionInit enqueue coverage, daemon lifecycle adapters, and the new UI helper/model behavior.
|
|
||||||
- Verified each logical Phase 8 batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 9 completed on 2026-04-27:
|
|
||||||
- Added typed refresh result models, JSON parsers, and service-owned apply contracts on `NetworkRefreshService` for connection info, wallet encryption, core balance/sync, mining, peers, and price refreshes.
|
|
||||||
- Routed `App` refresh callbacks through those contracts for connect-time prefetch, warmup info application, core refresh, encryption refresh, mining refresh, peer refresh, and price refresh while preserving the existing RPC call ordering.
|
|
||||||
- Expanded `ObsidianDragonTests` with focused refresh result parsing/application coverage for balance/sync state, connection metadata, wallet lock state, mining history, peer lists, banned peers, and price history.
|
|
||||||
- Documented remaining intentional process-wide ImGui state and legacy compatibility wrappers in `docs/ui-static-state.md`, and linked that policy from `docs/codebase-overview.md`.
|
|
||||||
- Verified the Phase 9 implementation batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 10 completed on 2026-04-27:
|
|
||||||
- Added `NetworkRefreshService::AddressRefreshResult` and routed address snapshot application through `applyAddressRefreshResult()` while preserving App-owned RPC collection and dirty-flag behavior.
|
|
||||||
- Added service-owned transaction view-cache models plus `TransactionRefreshResult`/`TransactionCacheUpdate` so transaction refresh callbacks now apply `WalletState::transactions`, `last_tx_update`, `last_tx_block_height_`, `viewtx_cache_`, `send_txids_`, and confirmed transaction caches through one cache-update contract.
|
|
||||||
- Moved z-viewtransaction output parsing and outgoing-send enrichment into `NetworkRefreshService` helpers, leaving only the RPC call order and per-cycle throttling in `App`.
|
|
||||||
- Evaluated the remaining decrypt workflow orchestration and kept the nested UI step handoffs App-owned because those boundaries still carry progress updates, worker-thread callback ordering, and shutdown/token cancellation semantics.
|
|
||||||
- Did not touch high-churn tab/dialog statics in this phase; `docs/ui-static-state.md` remains the policy for future behavior-adjacent UI state changes.
|
|
||||||
- Expanded `ObsidianDragonTests` with address/transaction applicator coverage, view-transaction enrichment/cache-update tests, stale callback cancellation coverage, additional remote/TLS config parsing, and shutdown-cancellation lifecycle coverage.
|
|
||||||
- Verified the Phase 10 implementation batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 11 completed on 2026-04-27:
|
|
||||||
- Revisited the decrypt restart/import flow and kept the remaining UI handoff choreography App-owned because each boundary still corresponds to an explicit progress step, cancellation check, or background import transition. No partial executor move was made.
|
|
||||||
- Moved additional address refresh construction into `NetworkRefreshService` helpers for shielded-address validation results, transparent address parsing, and unspent-output balance application while preserving the existing daemon RPC call order in `App::refreshAddressData()`.
|
|
||||||
- Moved transaction pre-refresh snapshot construction and list parsing helpers into `NetworkRefreshService`, including shielded address snapshots, fully enriched txid snapshots, transparent transaction parsing, shielded receive parsing, and final transaction sorting. `App::refreshTransactionData()` still owns RPC call order and per-cycle `z_viewtransaction` throttling.
|
|
||||||
- Expanded `ObsidianDragonTests` with focused coverage for the new address/transaction snapshot helpers, worker callback ordering across independent refresh jobs, and reconnect-style stale transaction callbacks.
|
|
||||||
- Did not migrate additional tab/dialog statics because Phase 11 did not touch those views for behavior changes.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 12 completed on 2026-04-27:
|
|
||||||
- Added a shared ordered mock refresh RPC fixture in `ObsidianDragonTests` so service-level refresh collectors can assert exact daemon method ordering and parameters.
|
|
||||||
- Introduced `NetworkRefreshService::RefreshRpcGateway` plus `collectAddressRefreshResult()` and `collectTransactionRefreshResult()`; address and transaction refresh RPC collection now lives behind the service boundary while preserving the same daemon call order and per-cycle `z_viewtransaction` cap.
|
|
||||||
- Routed `App::refreshAddressData()` and `App::refreshTransactionData()` through a small `RPCClient` adapter for those collectors, keeping App focused on enqueueing and applying the returned results.
|
|
||||||
- Expanded refresh lifecycle coverage for ordered callbacks, reconnect-style stale transaction callbacks, and collector ordering around address validation, z-balance fallback, shielded receive polling, cached viewtransaction entries, fresh `z_viewtransaction`, and `gettransaction` enrichment.
|
|
||||||
- Revisited decrypt restart/import orchestration again and made no extra move because the remaining boundaries are still the explicit progress/cancellation/import handoff points.
|
|
||||||
- Did not migrate additional tab/dialog statics because Phase 12 did not touch those views for behavior changes.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 13 completed on 2026-04-27:
|
|
||||||
- Added service-owned core and peer refresh collectors behind `NetworkRefreshService::RefreshRpcGateway`, moving the fixed `z_gettotalbalance`/`getblockchaininfo` and `getpeerinfo`/`listbanned` RPC bodies out of `App`.
|
|
||||||
- Extended the ordered mock RPC fixture tests to assert the exact core and peer daemon call order, empty parameter arrays, parsed result values, and partial-failure behavior where the second RPC still runs after the first fails.
|
|
||||||
- Kept warmup `getinfo`, mining refresh, connection init, and price fetch orchestration App-owned because those paths either have UI status handoffs, cadence-specific behavior, or non-RPC HTTP/callback details that were not part of this small collector move.
|
|
||||||
- Did not add new reconnect/shutdown lifecycle tests because Phase 13 did not change async cancellation, daemon restart, or worker lifecycle ownership.
|
|
||||||
- Revisited decrypt restart/import orchestration and high-churn UI state migrations by scope only; no behavior work touched those complete progress/cancellation or tab/dialog seams.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 14 completed on 2026-04-27:
|
|
||||||
- Added `NetworkRefreshService::collectMiningRefreshResult()` behind `RefreshRpcGateway`; the service now owns the ordered `getlocalsolps` plus optional `getmininginfo` RPC collection while `App` still owns the fast/slow cadence decision and daemon-memory snapshot.
|
|
||||||
- Added `NetworkRefreshService::collectConnectionInitResult()` so the initial `getinfo` then `getwalletinfo` prefetch is service-owned while `App::onConnected()` keeps the high-priority enqueue and `encryption_state_prefetched_` lock-screen timing flag.
|
|
||||||
- Extended ordered mock RPC tests for mining slow ticks, mining fast-only ticks, mining partial failure, connection-init success, and connection-init wallet-info prefetch after `getinfo` failure.
|
|
||||||
- Did not add reconnect/shutdown lifecycle tests because Phase 14 did not change worker callback ownership, shutdown, reconnect, daemon restart, or cancellation behavior.
|
|
||||||
- Did not touch decrypt orchestration or high-churn UI state because no complete behavior-preserving step in those areas was part of the batch.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 15 completed on 2026-04-27:
|
|
||||||
- Added `NetworkRefreshService::WarmupPollResult` and `collectWarmupPollResult()` behind `RefreshRpcGateway`; the service now owns the single warmup `getinfo` call and preserves either parsed connection info or the raw RPC error string.
|
|
||||||
- Routed the warmup branch of `App::refreshCoreData()` through the new collector while keeping UI status translation, daemon block-height decoration, `connection_status_`, and the `refreshData()` transition App-owned.
|
|
||||||
- Extended ordered mock RPC tests for warmup success and warmup failure so the `getinfo` call, empty params, parsed ready state, and error propagation are directly covered.
|
|
||||||
- Reviewed price HTTP fetching and kept it App-owned because the current worker callback, libcurl setup, HTTP-status handling, logging, and parse/apply handoff are clearer in one place; only JSON response parsing remains service-owned.
|
|
||||||
- Left command-style RPC actions App-owned because Phase 15 did not include behavior changes for send, address creation, mining toggles, ban operations, or import/export commands.
|
|
||||||
- Did not add reconnect/shutdown lifecycle tests because worker callback ownership, shutdown, reconnect, daemon restart, and cancellation behavior did not change.
|
|
||||||
- Did not touch decrypt orchestration or high-churn UI state because no complete behavior-preserving step in those areas was part of the batch.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 16 completed on 2026-04-27:
|
|
||||||
- Added `NetworkRefreshService::PriceHttpResponse`, `PriceHttpResult`, and `parsePriceHttpResponse()` so libcurl transport status, HTTP status, parse success, and failure messages are represented as a focused service-owned boundary.
|
|
||||||
- Kept libcurl initialization, request execution, worker callback ownership, successful price logging, and UI-thread price application in `App::refreshPrice()`.
|
|
||||||
- Expanded focused tests for successful price HTTP parsing, HTTP non-200 failures, transport failures, and unrecognized response bodies.
|
|
||||||
- Kept command-style RPC actions App-owned because Phase 16 did not include a complete behavior change for send, address creation, mining toggles, ban operations, or import/export commands.
|
|
||||||
- Did not add reconnect/shutdown lifecycle tests because callback ownership, cancellation, shutdown, reconnect, and daemon restart behavior did not change.
|
|
||||||
- Did not touch decrypt orchestration or high-churn UI state because no behavior work touched those workflows.
|
|
||||||
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 17 completed on 2026-04-27:
|
|
||||||
- Reviewed command-style RPC actions for send, address creation, mining toggles, ban operations, and key import/export; kept them App-owned because no complete behavior change created a focused extraction boundary with tests.
|
|
||||||
- Reviewed lifecycle, reconnect, shutdown, and daemon restart ownership; no new lifecycle tests were added because callback ownership, cancellation, shutdown, reconnect, and daemon restart behavior did not change.
|
|
||||||
- Reviewed decrypt and high-churn UI-state seams by scope; no changes were made because Phase 17 did not directly touch those workflows for behavior.
|
|
||||||
- Kept refresh and price boundaries stable because the current service collectors/result helpers already cover the testable seams introduced in prior phases, and no new smaller behavior seam appeared.
|
|
||||||
- Verified the stable-boundary decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 18 completed on 2026-04-27:
|
|
||||||
- Re-reviewed the remaining command-style RPC actions (`setgenerate`, `setban`, new address creation, key import, send) and kept them App-owned because Phase 18 did not include a real behavior change that supplied a focused extraction boundary and tests.
|
|
||||||
- Re-reviewed reconnect, shutdown, daemon restart, and callback ownership; no lifecycle tests were added because those ownership and cancellation behaviors did not change.
|
|
||||||
- Re-reviewed decrypt and high-churn UI-state seams by scope; no code changes were made because those workflows were not directly touched for behavior.
|
|
||||||
- Preserved the stable refresh and price boundaries introduced in prior phases because no new behavior made a smaller tested seam useful.
|
|
||||||
- Verified the feature-driven cleanup decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 19 completed on 2026-04-27:
|
|
||||||
- Treated the audit as feature-scoped maintenance rather than an extraction-only phase and made no source changes because no real behavior change created a new focused test seam.
|
|
||||||
- Re-reviewed command-style RPC actions (`setgenerate`, `setban`, new address creation, key import, send) and kept them App-owned because their current boundaries are command-specific and tied to immediate UI/app-state updates.
|
|
||||||
- Re-reviewed lifecycle, reconnect, shutdown, daemon restart, decrypt, and high-churn UI-state seams; no tests or migrations were added because those behaviors did not change directly.
|
|
||||||
- Preserved the stable refresh and price service boundaries because the existing collectors/result helpers already cover the testable seams created by earlier work.
|
|
||||||
- Verified the maintenance decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 20 completed on 2026-04-27:
|
|
||||||
- Moved the cleanup audit into maintenance mode and avoided additional extraction-only work because no concrete behavior change created a smaller tested seam.
|
|
||||||
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
|
||||||
- Added no lifecycle, decrypt, or UI-state tests because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
|
|
||||||
- Preserved stable refresh and price service boundaries because no new behavior exposed a better tested boundary.
|
|
||||||
- Verified maintenance mode with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 21 completed on 2026-04-27:
|
|
||||||
- Sustained maintenance mode and avoided extraction-only work because no concrete feature change created a focused tested seam.
|
|
||||||
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
|
||||||
- Added no lifecycle, decrypt, or UI-state coverage because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
|
|
||||||
- Preserved stable refresh and price service boundaries because no new behavior exposed a smaller tested seam.
|
|
||||||
- Verified sustained maintenance mode with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Phase 22 completed on 2026-04-27:
|
|
||||||
- Continued sustained maintenance mode and avoided extraction-only work because no concrete feature change created a focused tested seam.
|
|
||||||
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
|
||||||
- Added no lifecycle, decrypt, or UI-state coverage because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
|
|
||||||
- Preserved stable refresh and price service boundaries because no new behavior exposed a smaller tested seam.
|
|
||||||
- Verified the maintenance checkpoint with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
|
||||||
|
|
||||||
Remaining Phase 23 follow-ups:
|
|
||||||
- Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
|
|
||||||
- Keep command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
|
||||||
- Add lifecycle, decrypt, or UI-state coverage only when directly changing callback ownership, cancellation, daemon restart, decrypt flow, or UI state behavior.
|
|
||||||
- Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
|
||||||
|
|
||||||
## High-Priority Findings
|
|
||||||
|
|
||||||
### 1. `App` Is Carrying Too Many Responsibilities
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- `src/app.h`, `src/app.cpp`, `src/app_network.cpp`, `src/app_security.cpp`, and `src/app_wizard.cpp` form a broad partial-class style application layer.
|
|
||||||
- `src/app.cpp` is 3,141 lines, with lifecycle, rendering, daemon/RPC startup, shutdown, modal rendering, and restart flows.
|
|
||||||
- `src/app_network.cpp` is 1,926 lines and contains many refresh, polling, send, mining, market, peer, and address metadata paths.
|
|
||||||
- `src/app_security.cpp` is 1,819 lines and combines wallet encryption, PIN handling, secure vault, import/export, and restart flows.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Changes in one workflow can easily affect unrelated lifecycle or UI behavior.
|
|
||||||
- It is hard to unit-test isolated flows without instantiating most of the app.
|
|
||||||
- Shutdown and async callback ownership are difficult to reason about because many subsystems capture or mutate `App` state directly.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Extract a `DaemonController` for embedded/external daemon ownership, restart, port ownership, and shutdown sequencing.
|
|
||||||
- Extract a `WalletSecurityController` for PIN, encryption, secure vault, and key import/export flow state.
|
|
||||||
- Extract a `NetworkRefreshService` or `WalletRefreshScheduler` for balance/address/transaction/mining/market polling.
|
|
||||||
- Keep `App` focused on initialization, top-level navigation, frame dispatch, and service composition.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 3: add the first `DaemonController` boundary around `EmbeddedDaemon` ownership, settings sync, and start/stop behavior. Follow-up work should move more restart/shutdown sequencing and external-daemon policy into the controller.
|
|
||||||
|
|
||||||
### 2. Background Thread Lifetime Is Not Centrally Owned
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- Detached threads appear in `src/app.cpp`, `src/app_security.cpp`, `src/app_wizard.cpp`, `src/util/bootstrap.cpp`, and `src/main.cpp`.
|
|
||||||
- Several lambdas capture `this` and then call back after waits or daemon operations.
|
|
||||||
- Managed thread members also exist (`shutdown_thread_`, `daemon_restart_thread_`, `wizard_stop_thread_`, monitor threads), but ownership patterns vary by subsystem.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Detached work can outlive the `App` object or UI state it touches.
|
|
||||||
- Shutdown order is harder to guarantee.
|
|
||||||
- Cancellation is inconsistent, especially across daemon restart, wallet import, bootstrap download, and PIN unlock flows.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Introduce an `AsyncTaskManager` owned by `App`, with named tasks, cancellation flags/tokens, and join-on-shutdown behavior.
|
|
||||||
- Replace new detached threads with submitted tasks and explicit cancellation.
|
|
||||||
- Require background callbacks to hop through the existing UI callback queue before touching UI state.
|
|
||||||
- Add shutdown assertions or logging for tasks that fail to stop promptly.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 3: convert App-owned daemon restart/import/wizard background work to `AsyncTaskManager` and keep `Bootstrap`'s worker joinable. Follow-up work should move remaining subsystem monitor/watchdog policies behind explicit owners where useful.
|
|
||||||
|
|
||||||
### 3. RPC Credential Generation And Remote RPC Handling Need Hardening
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- Before Phase 1, `src/rpc/connection.cpp` generated default `rpcuser`/`rpcpassword` with `std::srand(std::time(nullptr))` and `std::rand()`.
|
|
||||||
- Phase 1 replaced that path with libsodium-backed random generation.
|
|
||||||
- Before Phase 3, `src/rpc/rpc_client.cpp` always constructed an `http://host:port/` URL.
|
|
||||||
- Before Phase 3, `.cookie` auth handling appeared in `src/rpc/connection.cpp` and also as a 401 fallback in `src/app_network.cpp`.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- `std::rand()` is not appropriate for credentials.
|
|
||||||
- HTTP is acceptable for strict localhost daemon RPC, but remote hosts can expose credentials unless the UI explicitly warns or supports TLS.
|
|
||||||
- Duplicate auth fallback paths make it harder to reason about precedence and failures.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Completed in Phase 1: generate RPC credentials with libsodium-backed random generation.
|
|
||||||
- Completed in Phase 3: centralize `.cookie` retry construction in `Connection` after config-password failure.
|
|
||||||
- Completed in Phase 3: add a `use_tls` transport flag to `ConnectionConfig` and pass it through `RPCClient` connections.
|
|
||||||
- Completed in Phase 3: warn at runtime and in Settings when a non-localhost RPC host uses plaintext HTTP.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 1 and Phase 3: replace `std::rand()` credential generation, centralize cookie fallback, and add remote plaintext/TLS handling.
|
|
||||||
|
|
||||||
### 4. Shell Launching Uses `system()` In UI And Platform Paths
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- Before Phase 1, `src/util/platform.cpp` used `system()` for URL/folder opening and directory creation on some platforms.
|
|
||||||
- Before Phase 1, `src/ui/windows/transactions_tab.cpp`, `src/ui/windows/transaction_details_dialog.cpp`, and `src/ui/windows/about_dialog.cpp` called `system()` directly.
|
|
||||||
- Phase 1 routed these UI calls through `util::Platform::openUrl()` and removed shell-built launcher strings from `Platform` on macOS/Linux.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Shell invocation increases quoting and command-injection risk.
|
|
||||||
- Direct calls bypass the existing platform abstraction.
|
|
||||||
- Behavior varies by shell and desktop environment.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Route all URL/file/folder launch behavior through `Platform` helpers.
|
|
||||||
- Replace shell strings with platform APIs where possible: `ShellExecute` on Windows, `open` or `xdg-open` via `fork/exec` or `posix_spawn` on Unix-like systems.
|
|
||||||
- Validate URL schemes before opening external links.
|
|
||||||
- Remove direct `system()` calls from UI windows.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 1: update About, transaction details, and transaction tab actions to use `Platform::openUrl()` and safer platform launcher behavior.
|
|
||||||
|
|
||||||
## Medium-Priority Findings
|
|
||||||
|
|
||||||
### 5. Silent And Broad Exception Handling Is Common
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- Empty or near-empty `catch (...)` blocks appear in `src/app.cpp`, `src/app_network.cpp`, `src/app_security.cpp`, `src/app_wizard.cpp`, `src/rpc/rpc_worker.cpp`, `src/rpc/rpc_client.cpp`, `src/main.cpp`, and several UI files.
|
|
||||||
- Repeated `try { rpc_->call("stop"); } catch (...) {}` patterns appear in daemon stop flows.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Failures vanish from logs and UI, making support and diagnosis difficult.
|
|
||||||
- Some swallowed failures are acceptable during best-effort shutdown, but the code does not consistently explain that intent.
|
|
||||||
- Repeated patterns invite copy-paste drift.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Add small helpers such as `stopDaemonSafely(context)` and `parseFloatOrDefault(value, fallback, context)`.
|
|
||||||
- Log best-effort failures with enough context to debug without spamming normal shutdown.
|
|
||||||
- Prefer typed catches where the thrown type is known.
|
|
||||||
- For RPC work, propagate categorized failures to the notification system when they affect user-visible state.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 1: replace repeated daemon stop catch blocks with a single helper that logs at debug/verbose level.
|
|
||||||
|
|
||||||
### 6. UI Dialogs Have Mixed Modal Systems And Repeated Layout Literals
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- Most dialogs use `Material::BeginOverlayDialog()`, but `src/ui/windows/address_book_dialog.cpp` still uses raw `ImGui::BeginPopupModal()` for Add/Edit address modals.
|
|
||||||
- Dialog widths such as `480.0f`, `500.0f`, `620.0f`, and `660.0f` appear across UI files.
|
|
||||||
- Button widths and padding literals such as `140.0f`, `160.0f`, and `24.0f` are repeated.
|
|
||||||
- Icon font sizes are hardcoded in `src/ui/material/typography.cpp` as 14, 18, 24, and 40 px families.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- The schema system already solves this class of problem, but not all dimensions use it yet.
|
|
||||||
- Modal behavior diverges when some dialogs bypass overlay scrim/input blocking helpers.
|
|
||||||
- Theme and density changes require hunting through many files.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Add schema-backed dialog tokens under `res/themes/ui.toml`, such as `globals.dialog.width-default`, `width-lg`, `min-width`, and `max-height-ratio`.
|
|
||||||
- Add schema-backed action widths under a shared `button` or `globals.button-sizes` section.
|
|
||||||
- Move icon size selection into schema or a single icon token helper.
|
|
||||||
- Migrate address book Add/Edit dialogs to `BeginOverlayDialog()` and extract one shared form renderer.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 2: refactor the address book Add/Edit dialog because it had duplicated modal form code and was still outside the overlay helper path.
|
|
||||||
|
|
||||||
### 7. UI State Uses Many Static Locals And File-Scoped Globals
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- `src/ui/pages/settings_page.cpp` has many `sp_*` static variables for page state.
|
|
||||||
- `src/app_wizard.cpp` uses local statics for first-run appearance and daemon checks.
|
|
||||||
- Theme/color/effect modules use static state for current theme and effect initialization.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Static state is hard to reset in tests.
|
|
||||||
- It makes multi-window or future session-reset behavior harder.
|
|
||||||
- Initialization order and stale state can become subtle bugs after hot reloads or account switching.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Introduce `SettingsPageState` and `WizardState` structures owned by `App` or a UI state container.
|
|
||||||
- Keep global/static state only for immutable constants or explicit process-wide singletons.
|
|
||||||
- Add reset/apply methods for state structures to support theme reload and test setup.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Move settings-page statics into a single struct without changing behavior. This is mostly mechanical and improves readability.
|
|
||||||
|
|
||||||
### 8. Build And Generated Resource Lifecycle Is Split Across Source, Build, And Scripts
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- `src/config/version.h` is generated from `src/config/version.h.in` but the generated file is present in source control.
|
|
||||||
- CMake generates `src/embedded/lang_*.h` from `res/lang/*.json` with `xxd`.
|
|
||||||
- `setup.sh` does not appear to check for `xxd`.
|
|
||||||
- `find_package(Python3 QUIET COMPONENTS Interpreter)` is used for theme expansion; CMake falls back when Python is unavailable.
|
|
||||||
- Font `OBJECT_DEPENDS` are listed manually in `CMakeLists.txt`.
|
|
||||||
- `src/resources/embedded_resources.cpp` conditionally includes `embedded_data.h`, which is produced by release/build scripts for some platforms.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Generated files under `src/` make the source tree noisier and can cause stale diffs.
|
|
||||||
- Missing tools fail late or silently reduce build output quality.
|
|
||||||
- Resource embedding behavior differs by platform without one obvious matrix.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Generate language headers under `build/generated/embedded/` and include that directory instead of writing under `src/embedded/`.
|
|
||||||
- Decide whether `src/config/version.h` should be generated-only or committed, then enforce that decision with `.gitignore` and documentation.
|
|
||||||
- Add explicit `xxd` checks to `setup.sh` and CMake configure output.
|
|
||||||
- Make the Python theme-expansion fallback a warning, not a quiet behavior change.
|
|
||||||
- Replace manual font `OBJECT_DEPENDS` with a generated list or CMake glob configured for the font directory.
|
|
||||||
- Document the embedded resource matrix by platform and build type.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 1: add `xxd` detection and make missing Python theme expansion noisy.
|
|
||||||
|
|
||||||
### 9. Polling And Refresh Flow Would Benefit From A Scheduler Boundary
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- `src/app_network.cpp` coordinates many refresh paths and timings.
|
|
||||||
- `src/app.cpp` also contains timed daemon/mining/rescan update logic.
|
|
||||||
- `src/rpc/rpc_worker.cpp` already provides a worker queue, but refresh orchestration is still spread through app-level methods.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- Polling order, throttle behavior, and cancellation are difficult to audit.
|
|
||||||
- Adding one more refresh path can accidentally affect responsiveness or RPC queue pressure.
|
|
||||||
- Console fast-lane RPC is a good pattern, but regular refresh batches still need a clear scheduler owner.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Add a `PollingScheduler` or `RefreshScheduler` that owns timer intervals, dependencies, cancellation, and rate limiting.
|
|
||||||
- Model refreshes as named jobs with last-run time, minimum interval, and in-flight state.
|
|
||||||
- Keep `RPCWorker` as execution plumbing while moving scheduling decisions out of `App`.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 4: added `RefreshScheduler` for refresh timers, page intervals, transaction age throttling, OPID cadence, price refresh, and fast tick behavior.
|
|
||||||
- Completed in Phase 7 and Phase 8: `NetworkRefreshService` now owns enqueue/callback wrapping for core, address, transaction, mining, peer, encryption, price, and connection-init jobs, including queue pressure and stale callback suppression.
|
|
||||||
|
|
||||||
### 10. Large UI Tabs Need Feature-Sliced Components
|
|
||||||
|
|
||||||
Evidence:
|
|
||||||
- `src/ui/windows/balance_tab.cpp` is 3,562 lines.
|
|
||||||
- `src/ui/windows/mining_tab.cpp` is 2,842 lines.
|
|
||||||
- `src/ui/windows/console_tab.cpp` is 1,861 lines.
|
|
||||||
- Several of these files combine state management, drawing, filtering, formatting, and modal coordination.
|
|
||||||
|
|
||||||
Why it matters:
|
|
||||||
- UI changes become difficult to review because unrelated drawing and state logic share one file.
|
|
||||||
- Reusable patterns stay local and get reimplemented elsewhere.
|
|
||||||
- Testing smaller formatting/state helpers is harder when they are buried in render functions.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Split large tabs by feature area: panels, table/list renderers, local state structs, formatting helpers, and action handlers.
|
|
||||||
- Keep ImGui draw calls close to the view but move data preparation and command decisions into smaller helpers.
|
|
||||||
- Avoid introducing inheritance-heavy UI abstractions; simple namespaces and structs should be enough.
|
|
||||||
|
|
||||||
Suggested first slice:
|
|
||||||
- Completed in Phase 4 through Phase 8: extracted low-risk balance helpers, balance address-list/recent-transaction models, mining benchmark/pool helpers, console command/input/output models, and additional address-row/mining/console execution helpers. Remaining work should continue moving draw-heavy action glue only when it lowers review/test cost.
|
|
||||||
|
|
||||||
## Low-Priority And Quick-Win Findings
|
|
||||||
|
|
||||||
### 11. Test Infrastructure Is Missing Or Not Obvious
|
|
||||||
|
|
||||||
Before Phase 4, no test target was visible in the workspace snapshot. Phase 4 added a small CTest executable covering connection config parsing, `.cookie` fallback priority, plaintext/TLS detection, wallet state filtering, amount formatting, payment URI parsing, and scheduler behavior.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Completed in Phase 4: add a small CTest target with a lightweight assertion harness for pure utility/RPC/scheduler helpers.
|
|
||||||
- Consider migrating to Catch2 or GoogleTest if the test suite grows beyond a few focused files.
|
|
||||||
- Add mocks for `RPCClient` and daemon process state once service boundaries exist.
|
|
||||||
|
|
||||||
### 12. Translation Key Usage Is Inconsistent
|
|
||||||
|
|
||||||
`TR()` and `TrId()` coexist in places such as Settings. This is workable, but it increases typo risk and makes missing translations harder to audit.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Define constants for high-traffic translation keys.
|
|
||||||
- Prefer one translation call style inside each module.
|
|
||||||
- Add a script that compares referenced keys against `res/lang/*.json`.
|
|
||||||
|
|
||||||
### 13. Project Icon Handling Needs One Registry
|
|
||||||
|
|
||||||
Material icons are referenced directly in many files, while the pickaxe icon uses a special one-glyph font path. The special case is now documented in `docs/codebase-overview.md`, but a code-level registry would make future icon work cleaner.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Completed in Phase 2: add `src/ui/material/project_icons.h` for app-specific icon names.
|
|
||||||
- Completed in Phase 2: encapsulate pickaxe rendering behind one helper, so feature code does not need to know which font supplies it.
|
|
||||||
|
|
||||||
### 14. Release/Portability Docs Can Be Closer To The Real Build
|
|
||||||
|
|
||||||
`build.sh` contains much more platform-specific behavior than the README currently explains. AppImage dependency bundling, macOS universal build defaults, Windows MinGW assumptions, embedded daemon/resource behavior, and third-party license sync deserve their own build notes.
|
|
||||||
|
|
||||||
Recommended cleanup:
|
|
||||||
- Add a focused `docs/build-and-release.md`.
|
|
||||||
- Document development build vs release packaging per platform.
|
|
||||||
- Add a checklist for third-party license updates, including `libs/incbin.h`.
|
|
||||||
|
|
||||||
## Suggested Roadmap
|
|
||||||
|
|
||||||
### Phase 1: Small High-Value Fixes
|
|
||||||
|
|
||||||
- [x] Replace weak RPC credential generation in `src/rpc/connection.cpp`.
|
|
||||||
- [x] Route direct URL/open actions away from raw `system()` calls.
|
|
||||||
- [x] Add `xxd` checks and noisy Python theme-expansion warnings.
|
|
||||||
- [x] Confirm missing third-party license entry for INCBIN is not needed because `THIRD_PARTY_LICENSES` already includes it.
|
|
||||||
- [x] Extract repeated daemon stop try/catch blocks into a helper with contextual logging.
|
|
||||||
|
|
||||||
### Phase 2: UI Consistency Pass
|
|
||||||
|
|
||||||
- [x] Move common dialog sizes and action button widths into `res/themes/ui.toml` or existing layout helpers.
|
|
||||||
- [x] Migrate address book Add/Edit dialogs to `BeginOverlayDialog()`.
|
|
||||||
- [x] Add a shared dialog content/footer helper.
|
|
||||||
- [x] Add an icon registry and hide the pickaxe font special case behind it.
|
|
||||||
|
|
||||||
### Phase 3: Ownership And Runtime Boundaries
|
|
||||||
|
|
||||||
- [x] Introduce `AsyncTaskManager` for background work.
|
|
||||||
- [x] Extract `DaemonController` from `App`.
|
|
||||||
- [x] Consolidate RPC auth and connection fallback behavior.
|
|
||||||
- [x] Add warning/TLS configuration for non-localhost RPC.
|
|
||||||
|
|
||||||
### Phase 4: Larger Maintainability Work
|
|
||||||
|
|
||||||
- [x] Extract `NetworkRefreshService` or `RefreshScheduler`.
|
|
||||||
- [x] Split `balance_tab.cpp`, `mining_tab.cpp`, and `console_tab.cpp` into smaller feature files where safe.
|
|
||||||
- [x] Move generated language headers into `build/generated`.
|
|
||||||
- [x] Add focused unit tests for connection config, URI parsing, amount formatting, wallet address spendability filtering, and scheduler behavior.
|
|
||||||
|
|
||||||
### Phase 5: Service Boundaries And Test Depth
|
|
||||||
|
|
||||||
- [x] Move wallet encryption, PIN, secure vault, key import/export, and restart flow state toward a `WalletSecurityController`.
|
|
||||||
- [x] Move more daemon restart/shutdown policy and external-daemon behavior from `App` into `DaemonController`.
|
|
||||||
- [x] Evolve `RefreshScheduler` into a fuller network refresh service by modeling named refresh jobs, in-flight state, and RPC queue pressure explicitly.
|
|
||||||
- [x] Split larger UI feature renderers after the low-risk helper extractions, starting with balance address/recent transaction panels and mining benchmark/pool panels.
|
|
||||||
- [x] Move settings/wizard static UI state into explicit state structs.
|
|
||||||
- [x] Add deeper tests around daemon/RPC service boundaries, scheduler edge cases, wallet security state transitions, and generated resource behavior.
|
|
||||||
- [x] Decide and document whether `src/config/version.h` is committed source or generated-only output.
|
|
||||||
|
|
||||||
### Phase 6: Deeper Service Extraction And Integration Tests
|
|
||||||
|
|
||||||
- [x] Move wallet-security RPC orchestration, secure-vault handoffs, key import/export state, and daemon restart sequencing behind service interfaces that can be exercised without constructing the full UI `App`.
|
|
||||||
- [x] Move daemon restart/rescan/bootstrap shutdown sequencing and external-daemon ownership policy into `DaemonController` with mockable collaborators.
|
|
||||||
- [x] Promote `NetworkRefreshService` from refresh policy/guard owner to dispatcher owner for named RPC jobs, queue-pressure telemetry, and stale-callback handling.
|
|
||||||
- [x] Split large UI files into stable feature modules, especially balance address-list/recent-transaction rendering, mining benchmark/pool rendering, and console output/input rendering.
|
|
||||||
- [x] Replace remaining compatibility aliases around extracted UI state with direct state-struct use.
|
|
||||||
- [x] Add integration tests with mock RPC/daemon collaborators for wallet security, daemon lifecycle, and refresh dispatch edges.
|
|
||||||
|
|
||||||
### Phase 7: Service-Owned Execution And UI Module Finish
|
|
||||||
|
|
||||||
- [x] Move async daemon lifecycle execution, restart delays, rescan flags, blockchain-data deletion, bootstrap stops, and external-daemon ownership enforcement deeper into `DaemonController` with mock daemon/filesystem/task collaborators.
|
|
||||||
- [x] Extract wallet decrypt/export/import dialog workflow state and secure-vault restart handoffs into a wallet-security workflow service that can be tested without constructing `App`.
|
|
||||||
- [x] Promote `NetworkRefreshService` from dispatch-ticket/telemetry owner to the enqueue/callback wrapper for named RPC refresh jobs, including stale callback suppression and queue-pressure reporting at the service boundary.
|
|
||||||
- [x] Continue splitting balance address-list rendering, mining benchmark controls, and console input/history/autocomplete into stable modules with tests.
|
|
||||||
- [x] Remove temporary App compatibility bridges around daemon ownership and any remaining extracted UI state once direct service/state use is complete.
|
|
||||||
|
|
||||||
### Phase 8: Workflow Executors And Large-View Reduction
|
|
||||||
|
|
||||||
- [x] Move wallet decrypt import/restart RPC orchestration out of `App`'s nested lambdas into a workflow executor with mock RPC/daemon/vault/file collaborators.
|
|
||||||
- [x] Split concrete daemon lifecycle runtime responsibilities into smaller adapters only where it lowers App coupling without adding indirection for its own sake.
|
|
||||||
- [x] Complete named refresh enqueue coverage for price/market refresh and connect-time one-off polling.
|
|
||||||
- [x] Continue feature-slicing large ImGui views by extracting address-row, mining benchmark/pool, and console command execution helper/model seams.
|
|
||||||
- [x] Add focused tests around the new workflow, refresh, daemon lifecycle, and UI helper seams.
|
|
||||||
|
|
||||||
### Phase 9: Typed Refresh Results And State Cleanup
|
|
||||||
|
|
||||||
- [x] Evolve refresh jobs toward typed result models and service-owned result application contracts for refresh paths that still require complex app-local snapshots.
|
|
||||||
- [x] Review remaining static UI state and compatibility glue, then move it to explicit state structs or document what is intentionally process-wide.
|
|
||||||
- [x] Reviewed draw-heavy ImGui bodies and deferred additional slicing to Phase 10 unless adjacent to a required behavior-preserving change.
|
|
||||||
- [x] Add focused tests around refresh result application, reconnect/stale-callback edges, and any state-struct migrations.
|
|
||||||
|
|
||||||
### Phase 10: Transaction Refresh And State Follow-Through
|
|
||||||
|
|
||||||
- [x] Evaluate typed address/transaction refresh result models and cache-update applicators for the refresh paths that still build large App-local snapshots.
|
|
||||||
- [x] Reduce decrypt workflow orchestration in `App` further only where executor-owned operations can preserve progress reporting and cancellation behavior; Phase 10 evaluated the remaining chain and deferred extra movement because the current App handoffs are the progress/cancellation boundary.
|
|
||||||
- [x] Convert remaining high-churn tab/dialog statics to explicit state structs as those views are touched, following `docs/ui-static-state.md`; Phase 10 did not touch those views for behavior changes.
|
|
||||||
- [x] Add stronger reconnect/stale-callback, remote/TLS RPC, and shutdown-cancellation tests.
|
|
||||||
|
|
||||||
### Phase 11: Workflow And UI State Hardening
|
|
||||||
|
|
||||||
- [x] Revisit decrypt restart/import orchestration for one complete executor-owned step if progress and cancellation can stay explicit.
|
|
||||||
- [x] Continue high-churn tab/dialog state-struct migrations only when those views are touched for real behavior changes.
|
|
||||||
- [x] Move more address/transaction RPC snapshot construction into testable helpers if it reduces App coupling without duplicating daemon call ordering.
|
|
||||||
- [x] Keep broadening refresh/RPC lifecycle tests around realistic worker callback ordering and reconnect races.
|
|
||||||
|
|
||||||
### Phase 12: Integration Fixtures And Remaining Orchestration Edges
|
|
||||||
|
|
||||||
- [x] Add shared mock RPC fixtures only if they let tests assert daemon call ordering directly instead of duplicating production sequencing in test setup.
|
|
||||||
- [x] Move more refresh body construction out of `App` only where call ordering remains obvious and covered by tests.
|
|
||||||
- [x] Revisit decrypt restart/import orchestration only if a complete progress/cancellation step can be executor-owned end to end.
|
|
||||||
- [x] Continue UI state-struct migrations opportunistically when behavior work touches those tabs/dialogs.
|
|
||||||
|
|
||||||
### Phase 13: Ordered Collectors And Lifecycle Coverage
|
|
||||||
|
|
||||||
- [x] Extend ordered mock RPC coverage to additional refresh collectors only when those collectors move beyond App-owned call bodies.
|
|
||||||
- [x] Keep refresh call ordering explicit in tests whenever daemon call sequencing moves into a service.
|
|
||||||
- [x] Broaden reconnect/shutdown lifecycle coverage around async refresh cancellation only when those flows are touched.
|
|
||||||
- [x] Continue decrypt and UI-state hardening opportunistically at complete, behavior-preserving seams.
|
|
||||||
|
|
||||||
### Phase 14: Mining And Connection Edge Review
|
|
||||||
|
|
||||||
- [x] Move mining refresh collection only if the fast/slow polling cadence remains explicit and covered by ordered mock RPC tests.
|
|
||||||
- [x] Move connection-init prefetch only if lock-screen timing and `getinfo`/`getwalletinfo` ordering remain obvious and tested.
|
|
||||||
- [x] Add lifecycle coverage only when changing worker callback, shutdown, reconnect, or daemon restart ownership.
|
|
||||||
- [x] Continue decrypt and UI-state work only when a complete behavior-preserving step is touched.
|
|
||||||
|
|
||||||
### Phase 15: Remaining App-Owned Edges
|
|
||||||
|
|
||||||
- [x] Review warmup status polling only if the UI status/progress handoff can stay explicit and covered.
|
|
||||||
- [x] Review price HTTP fetching only if network parsing, callback behavior, and failure handling become easier to test without hiding libcurl details.
|
|
||||||
- [x] Leave command-style RPC actions App-owned unless a complete behavior change creates a focused extraction boundary.
|
|
||||||
- [x] Add lifecycle, decrypt, or UI-state tests only when touching those behaviors directly.
|
|
||||||
|
|
||||||
### Phase 16: Price And Command Boundary Review
|
|
||||||
|
|
||||||
- [x] Revisit price HTTP fetching only if libcurl status/error behavior can be represented as a focused testable boundary.
|
|
||||||
- [x] Keep send, address creation, mining toggle, ban, and import/export command RPC actions App-owned unless a complete behavior change justifies extraction.
|
|
||||||
- [x] Add reconnect/shutdown lifecycle coverage only when callback ownership or cancellation behavior changes.
|
|
||||||
- [x] Continue decrypt and UI-state work only when directly touching those workflows for behavior.
|
|
||||||
|
|
||||||
### Phase 17: Command And Lifecycle Guardrails
|
|
||||||
|
|
||||||
- [x] Review command-style RPC actions only when a complete behavior change creates a focused extraction boundary.
|
|
||||||
- [x] Add reconnect/shutdown lifecycle coverage only when callback ownership, cancellation, shutdown, reconnect, or daemon restart behavior changes.
|
|
||||||
- [x] Continue decrypt and UI-state work only when directly touching those workflows for behavior.
|
|
||||||
- [x] Keep existing refresh and price boundaries stable unless new behavior creates a smaller tested seam.
|
|
||||||
|
|
||||||
### Phase 18: Feature-Driven Cleanup Only
|
|
||||||
|
|
||||||
- [x] Move command-style RPC actions only when a real behavior change supplies a focused extraction boundary and tests.
|
|
||||||
- [x] Add lifecycle tests only when callback ownership, cancellation, shutdown, reconnect, or daemon restart behavior changes.
|
|
||||||
- [x] Continue decrypt and UI-state cleanup only when those workflows are touched for behavior.
|
|
||||||
- [x] Preserve stable refresh and price boundaries unless new behavior makes a smaller tested seam useful.
|
|
||||||
|
|
||||||
### Phase 19: Feature-Scoped Maintenance
|
|
||||||
|
|
||||||
- [x] Prefer feature-scoped maintenance over additional extraction-only phases.
|
|
||||||
- [x] Move command-style RPC actions only with a complete behavior change and focused tests.
|
|
||||||
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
|
||||||
- [x] Preserve existing refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
|
||||||
|
|
||||||
### Phase 20: Maintenance Mode
|
|
||||||
|
|
||||||
- [x] Convert the cleanup roadmap from extraction phases to maintenance-mode guidance.
|
|
||||||
- [x] Keep command-style RPC actions App-owned unless a complete feature change supplies focused tests.
|
|
||||||
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
|
||||||
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
|
||||||
|
|
||||||
### Phase 21: Sustained Maintenance Mode
|
|
||||||
|
|
||||||
- [x] Continue maintenance-mode cleanup only when concrete feature work creates a focused tested seam.
|
|
||||||
- [x] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
|
|
||||||
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
|
||||||
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
|
||||||
|
|
||||||
### Phase 22: Maintenance Checkpoint
|
|
||||||
|
|
||||||
- [x] Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
|
|
||||||
- [x] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
|
|
||||||
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
|
||||||
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
|
||||||
|
|
||||||
### Phase 23: Maintenance Checkpoint
|
|
||||||
|
|
||||||
- [ ] Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
|
|
||||||
- [ ] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
|
|
||||||
- [ ] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
|
||||||
- [ ] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
|
||||||
|
|
||||||
## Residual Risk
|
|
||||||
|
|
||||||
Several findings are maintainability risks rather than confirmed user-facing bugs. Remote/plaintext RPC handling now warns and has a TLS config path, and Phase 10 added focused parsing tests, but real daemon TLS setups still deserve manual validation.
|
|
||||||
|
|
||||||
The safest next step is Phase 23: continue sustained maintenance mode, preserving tested boundaries and tying future cleanup to concrete feature work.
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Codebase Overview
|
|
||||||
|
|
||||||
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
ObsidianDragon is a Dear ImGui full-node wallet for DragonX (DRGX). It manages an embedded or external `dragonxd`, renders a schema-driven desktop UI, and provides shielded transactions, mining, market data, address management, explorer tools, and bootstrap download support.
|
|
||||||
|
|
||||||
## Runtime Architecture
|
|
||||||
|
|
||||||
- `src/main.cpp` initializes SDL3, graphics backends, ImGui, and the main loop.
|
|
||||||
- `src/app.cpp` owns application lifecycle, navigation, rendering, dialogs, and daemon/RPC startup.
|
|
||||||
- `src/app_network.cpp` contains refresh and transaction flows, including balance, address, transaction, mining, market, peer, and address metadata updates.
|
|
||||||
- `src/app_security.cpp` covers wallet encryption, PIN unlock, auto-lock, and secure vault integration.
|
|
||||||
- `src/app_wizard.cpp` handles first-run setup and bootstrap/encryption flow.
|
|
||||||
|
|
||||||
The UI thread renders ImGui and drains callback queues. RPC work runs through `RPCWorker`, which posts worker-thread RPC calls and returns UI-thread callbacks. Console commands can use a separate fast-lane RPC client/worker so they do not queue behind regular refresh batches.
|
|
||||||
|
|
||||||
`NetworkRefreshService` owns refresh timing, named job enqueue/callback guards, typed refresh result models, and service-owned result applicators for connection, core balance/sync, encryption, mining, peers, price, address snapshots, and transaction cache updates. Warmup polling, connection-init, core, mining, peer, address, and transaction refresh collection now run through a testable `RefreshRpcGateway`, and price HTTP response evaluation has a service-owned status/error/result model. `App` keeps enqueueing, UI timing decisions, cadence decisions, libcurl execution, and application handoff code for those paths.
|
|
||||||
|
|
||||||
## Source Map
|
|
||||||
|
|
||||||
| Path | Role |
|
|
||||||
|------|------|
|
|
||||||
| `src/config/` | Settings JSON persistence and generated `version.h` |
|
|
||||||
| `src/data/` | Wallet state, address book, exchange info |
|
|
||||||
| `src/rpc/` | libcurl JSON-RPC client, connection config, worker queue |
|
|
||||||
| `src/daemon/` | Embedded `dragonxd` manager and xmrig pool miner manager |
|
|
||||||
| `src/ui/windows/` | Main tabs and modal dialogs |
|
|
||||||
| `src/ui/pages/` | Page-style screens such as Settings |
|
|
||||||
| `src/ui/schema/` | TOML schema loader, skin manager, color resolver |
|
|
||||||
| `src/ui/material/` | Material-style typography, layout, drawing, components |
|
|
||||||
| `src/ui/effects/` | Acrylic, blur, noise, theme effects, low-spec fallback |
|
|
||||||
| `src/resources/` | Embedded resource extraction for params, daemon assets, themes, images |
|
|
||||||
| `src/platform/` | Windows DX11 and backdrop helpers |
|
|
||||||
| `src/util/` | i18n, logging, platform paths, bootstrap, vault, URI/base64/texture utilities |
|
|
||||||
|
|
||||||
## Build And Resources
|
|
||||||
|
|
||||||
- CMake uses C++17 and outputs `build/bin/ObsidianDragon`.
|
|
||||||
- Version comes from `project(... VERSION 1.2.0)` plus `DRAGONX_VERSION_SUFFIX=-rc1`.
|
|
||||||
- `src/config/version.h` is intentionally committed source for editor/release tooling compatibility, but CMake regenerates it from `src/config/version.h.in` during configure. Version bumps should update the CMake version/suffix and commit the regenerated header with the template if it changes.
|
|
||||||
- SDL3 is found from the system first, then fetched by CMake if unavailable.
|
|
||||||
- nlohmann/json and toml++ are fetched with CMake FetchContent.
|
|
||||||
- libcurl is system-provided on Linux/macOS and fetched statically for Windows.
|
|
||||||
- libsodium is system-provided on Linux or local under `libs/libsodium/`, `libs/libsodium-win/`, or `libs/libsodium-mac/`.
|
|
||||||
- Fonts are embedded with INCBIN: Ubuntu, Material Icons, a one-glyph MDI pickaxe subset, and Noto CJK subset.
|
|
||||||
- `res/themes/ui.toml` is embedded as a fallback and expanded into build themes with `scripts/expand_themes.py`.
|
|
||||||
- `res/default_banlist.txt` is embedded into `build/generated/default_banlist_embedded.h`.
|
|
||||||
|
|
||||||
## RPC And Daemon Notes
|
|
||||||
|
|
||||||
- Default RPC port is `21769`.
|
|
||||||
- `RPCClient` uses local HTTP with Basic auth. TLS is not assumed for localhost daemon RPC.
|
|
||||||
- DragonX daemon config paths:
|
|
||||||
- Linux: `~/.hush/DRAGONX/DRAGONX.conf`
|
|
||||||
- Windows: `%APPDATA%/Hush/DRAGONX/DRAGONX.conf`
|
|
||||||
- macOS: `~/Library/Application Support/Hush/DRAGONX/DRAGONX.conf`
|
|
||||||
- `Connection::autoDetectConfig()` creates missing config files, appends `exportdir`, `experimentalfeatures=1`, and `developerencryptwallet=1`, and falls back to `.cookie` auth if no `rpcpassword` is configured.
|
|
||||||
- The embedded daemon detects an external daemon on the RPC port and connects to it instead of taking ownership.
|
|
||||||
- Chain args include TLS-only mode, adaptive `-dbcache`, DragonX asset parameters, node seeds `node.dragonx.is` through `node4.dragonx.is`, and optional `-maxconnections=<n>` from Settings.
|
|
||||||
|
|
||||||
## UI And Data Notes
|
|
||||||
|
|
||||||
- Sidebar navigation is driven by `NavPage`: Overview, Send, Receive, History, Mining, Market, Console, Network, Explorer, Settings.
|
|
||||||
- Explorer lives in `src/ui/windows/explorer_tab.cpp`; Settings uses `src/ui/pages/settings_page.cpp`.
|
|
||||||
- Address labels, icons, favorites, hidden state, and manual ordering are persisted in Settings, especially `address_meta`.
|
|
||||||
- `AddressInfo::has_spending_key` tracks view-only shielded addresses; send flows filter or reject non-spendable z-addresses.
|
|
||||||
- The pickaxe icon is not a normal `ICON_MD_*` glyph. Use `AddressLabelDialog::drawIconByName()` or `Typography::pickaxeFontForSize()` for that special case.
|
|
||||||
- Remaining process-wide ImGui state and legacy compatibility wrappers are documented in `docs/ui-static-state.md`.
|
|
||||||
- Warmup, connection-init, core, mining, peer, address, and transaction refreshes use typed `NetworkRefreshService` result contracts plus gateway-backed collectors. Price refresh keeps libcurl setup/execution and callback ownership in App, while `NetworkRefreshService` owns JSON parsing plus HTTP status/error result evaluation. App still owns warmup status handoffs, mining fast/slow cadence, and command-style RPC actions. Transaction application updates `WalletState`, `viewtx_cache_`, `send_txids_`, the confirmed transaction cache, and block-height markers as one cache-update operation.
|
|
||||||
|
|
||||||
## Remaining Work
|
|
||||||
|
|
||||||
- Investigate `todo.md`: determine whether DragonX/Komodo wallet storage supports a safe compaction or consolidation workflow for wallets with too many addresses.
|
|
||||||
- Phase 23 should continue sustained maintenance mode: keep refresh and price boundaries stable, and revisit command/lifecycle/decrypt/UI-state seams only when concrete feature work creates a focused tested boundary.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# UI Static State Policy
|
|
||||||
|
|
||||||
The wallet currently runs one `App` instance inside one ImGui context. Some UI state is still file-static because it represents process-wide tab or modal state that must persist across frames. Phase 9 reviewed these remaining cases and treats them as intentional until the owning view is next refactored.
|
|
||||||
|
|
||||||
## Intentional Process-Wide State
|
|
||||||
|
|
||||||
- Modal payload state in `src/ui/windows/*_dialog*` files owns one open dialog instance at a time. These statics are acceptable for singleton dialogs, but new async workflow state should live in a service or App-owned controller.
|
|
||||||
- Tab-local form, filter, and selection state in views such as Send, Receive, Market, Mining, Peers, Transactions, and Explorer persists user input across frames. When those views are touched for behavior changes, prefer an explicit `*TabState` struct over adding more independent statics.
|
|
||||||
- Material/theme/effect singletons hold process-wide rendering caches or current theme state. They should remain resettable only through their owning theme/effect APIs.
|
|
||||||
- Compatibility glue remains where external call sites still rely on legacy names: `App::setCurrentTab()`/`getCurrentTab()`, `WalletState` balance aliases, layout `k*` accessors, and icon helper wrappers. Remove these only after all call sites have moved to the newer names.
|
|
||||||
|
|
||||||
## Rules For New UI Code
|
|
||||||
|
|
||||||
- Add new mutable UI state to an existing explicit state struct when one exists.
|
|
||||||
- Use file-static state only for singleton UI surfaces that cannot have multiple instances in the current app model.
|
|
||||||
- Keep long-running workflow progress outside render functions and expose it through services or App-owned controllers.
|
|
||||||
- Document any new compatibility wrapper with the call sites it protects and delete it when the migration is complete.
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<assemblyIdentity
|
<assemblyIdentity
|
||||||
type="win32"
|
type="win32"
|
||||||
name="DragonX.ObsidianDragon.Wallet"
|
name="DragonX.ObsidianDragon.Wallet"
|
||||||
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
|
version="@DRAGONX_APP_VERSION_MAJOR@.@DRAGONX_APP_VERSION_MINOR@.@DRAGONX_APP_VERSION_PATCH@.0"
|
||||||
processorArchitecture="amd64"
|
processorArchitecture="amd64"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
#include <winver.h>
|
#include <winver.h>
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
FILEVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
|
||||||
PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
PRODUCTVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
|
||||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
FILEFLAGS 0x0L
|
FILEFLAGS 0x0L
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
@@ -32,13 +32,13 @@ BEGIN
|
|||||||
BLOCK "040904B0" // US-English, Unicode
|
BLOCK "040904B0" // US-English, Unicode
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "DragonX Developers\0"
|
VALUE "CompanyName", "DragonX Developers\0"
|
||||||
VALUE "FileDescription", "ObsidianDragon Wallet\0"
|
VALUE "FileDescription", "@DRAGONX_APP_NAME@ Wallet\0"
|
||||||
VALUE "FileVersion", "@PROJECT_VERSION@\0"
|
VALUE "FileVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
|
||||||
VALUE "InternalName", "ObsidianDragon\0"
|
VALUE "InternalName", "@DRAGONX_APP_NAME@\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", "@DRAGONX_APP_NAME@.exe\0"
|
||||||
VALUE "ProductName", "ObsidianDragon\0"
|
VALUE "ProductName", "@DRAGONX_APP_NAME@\0"
|
||||||
VALUE "ProductVersion", "@PROJECT_VERSION@\0"
|
VALUE "ProductVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
Executable
+769
@@ -0,0 +1,769 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
ABI_VERSION="sdxl-c-v1"
|
||||||
|
LINK_MODE="imported"
|
||||||
|
BACKEND_DIR="$PROJECT_ROOT/external/SilentDragonXLite/lib"
|
||||||
|
BACKEND_SOURCE_DIR=""
|
||||||
|
BUILD_BACKEND_DIR=""
|
||||||
|
BACKEND_DEPENDENCY_DIR=""
|
||||||
|
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=false
|
||||||
|
OUT_DIR="$PROJECT_ROOT/build/lite-backend"
|
||||||
|
PLATFORM=""
|
||||||
|
RUST_TARGET=""
|
||||||
|
CARGO_TARGET_DIR_VALUE="${CARGO_TARGET_DIR:-}"
|
||||||
|
ARTIFACT_PATH=""
|
||||||
|
BUILD_ARTIFACT=true
|
||||||
|
BUILDER="${DRAGONX_LITE_BACKEND_BUILDER:-local}"
|
||||||
|
JOBS="${JOBS:-}"
|
||||||
|
SOURCE_DATE_EPOCH_VALUE="${SOURCE_DATE_EPOCH:-}"
|
||||||
|
REPRODUCIBLE=false
|
||||||
|
SIGNATURE_REQUIRED=false
|
||||||
|
SIGNATURE_FILE=""
|
||||||
|
SIGNATURE_FORMAT=""
|
||||||
|
SIGNATURE_VERIFICATION_TOOL=""
|
||||||
|
SIGNATURE_VERIFICATION_COMMAND=""
|
||||||
|
SIGNATURE_KEY_FINGERPRINT=""
|
||||||
|
SIGNATURE_CERTIFICATE_IDENTITY=""
|
||||||
|
SIGNATURE_CERTIFICATE_ISSUER=""
|
||||||
|
SIGNATURE_TRANSPARENCY_LOG_URL=""
|
||||||
|
SIGNATURE_VERIFIED_SHA256=""
|
||||||
|
SIGNATURE_POLICY_NAME="dragonx-lite-backend-signature-policy-v1"
|
||||||
|
SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE=true
|
||||||
|
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
|
||||||
|
SIGNATURE_METADATA_PROVIDED=false
|
||||||
|
SIGNATURE_VERIFICATION_PERFORMED=false
|
||||||
|
SIGNATURE_VERIFICATION_STATUS="not-provided"
|
||||||
|
SIGNATURE_FILE_SHA256=""
|
||||||
|
|
||||||
|
REQUIRED_SYMBOLS=(
|
||||||
|
litelib_wallet_exists
|
||||||
|
litelib_initialize_new
|
||||||
|
litelib_initialize_new_from_phrase
|
||||||
|
litelib_initialize_existing
|
||||||
|
litelib_execute
|
||||||
|
litelib_rust_free_string
|
||||||
|
litelib_check_server_online
|
||||||
|
litelib_shutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
EXTRA_CARGO_ARGS=()
|
||||||
|
EXTRA_REMAP_PATH_PREFIXES=()
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Build or inventory the SDXL-compatible lite backend artifact.
|
||||||
|
|
||||||
|
Usage: $0 [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--platform linux|windows|macos Artifact platform. Defaults to host platform.
|
||||||
|
--rust-target TRIPLE Cargo target triple for cross builds.
|
||||||
|
--cargo-target-dir PATH Isolated Cargo target directory for clean builds.
|
||||||
|
--backend-dir PATH SilentDragonXLite/lib source directory.
|
||||||
|
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
|
||||||
|
--out-dir PATH Output directory for copied artifact and metadata.
|
||||||
|
--artifact PATH Inventory an existing artifact instead of building.
|
||||||
|
--no-build Do not run cargo; requires --artifact.
|
||||||
|
--reproducible Add deterministic Rust path remaps for clean builds.
|
||||||
|
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
|
||||||
|
--builder NAME Redacted builder/provenance label. Default: local.
|
||||||
|
--signature-required Fail if verified signature metadata is not supplied.
|
||||||
|
--signature-file PATH Existing sidecar signature file to record.
|
||||||
|
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
|
||||||
|
--signature-verification-tool T Verification tool and version used by the release builder.
|
||||||
|
--signature-verification-command C
|
||||||
|
Verification command already run by the release builder.
|
||||||
|
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
|
||||||
|
--signature-certificate-identity ID
|
||||||
|
Reviewed certificate identity, when applicable.
|
||||||
|
--signature-certificate-issuer I
|
||||||
|
Reviewed certificate issuer, when applicable.
|
||||||
|
--signature-transparency-log-url URL
|
||||||
|
Transparency log entry, when applicable.
|
||||||
|
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
|
||||||
|
-j, --jobs N Cargo parallel jobs.
|
||||||
|
--cargo-arg ARG Extra argument forwarded to cargo build.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
<out>/<platform>/<artifact>
|
||||||
|
<out>/<platform>/lite-backend-symbols.txt
|
||||||
|
<out>/<platform>/lite-backend-artifact-manifest.json
|
||||||
|
|
||||||
|
The script captures symbols, checksums, and optional read-only signature
|
||||||
|
verification metadata only. It does not load the library, resolve function
|
||||||
|
pointers, call SDXL, sign, upload, or publish artifacts.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { printf '[lite-backend] %s\n' "$*"; }
|
||||||
|
warn() { printf '[lite-backend] warning: %s\n' "$*" >&2; }
|
||||||
|
die() { printf '[lite-backend] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
absolute_path() {
|
||||||
|
local path="$1"
|
||||||
|
if [[ "$path" = /* ]]; then
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
else
|
||||||
|
printf '%s/%s\n' "$PWD" "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
host_platform() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) printf 'linux\n' ;;
|
||||||
|
Darwin) printf 'macos\n' ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) printf 'windows\n' ;;
|
||||||
|
*) die "unsupported host platform: $(uname -s)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_platform() {
|
||||||
|
case "$1" in
|
||||||
|
linux|Linux) printf 'linux\n' ;;
|
||||||
|
windows|win|Win|Windows) printf 'windows\n' ;;
|
||||||
|
macos|mac|darwin|Darwin) printf 'macos\n' ;;
|
||||||
|
"") host_platform ;;
|
||||||
|
*) die "unsupported platform: $1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--platform)
|
||||||
|
[[ $# -ge 2 ]] || die "--platform requires a value"
|
||||||
|
PLATFORM="$(normalize_platform "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--rust-target)
|
||||||
|
[[ $# -ge 2 ]] || die "--rust-target requires a value"
|
||||||
|
RUST_TARGET="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cargo-target-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--cargo-target-dir requires a value"
|
||||||
|
CARGO_TARGET_DIR_VALUE="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--backend-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--backend-dir requires a value"
|
||||||
|
BACKEND_DIR="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--silentdragonxlitelib-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--silentdragonxlitelib-dir requires a value"
|
||||||
|
BACKEND_DEPENDENCY_DIR="$(absolute_path "$2")"
|
||||||
|
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=true
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--out-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--out-dir requires a value"
|
||||||
|
OUT_DIR="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--artifact)
|
||||||
|
[[ $# -ge 2 ]] || die "--artifact requires a value"
|
||||||
|
ARTIFACT_PATH="$(absolute_path "$2")"
|
||||||
|
BUILD_ARTIFACT=false
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-build)
|
||||||
|
BUILD_ARTIFACT=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--reproducible)
|
||||||
|
REPRODUCIBLE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--remap-path-prefix)
|
||||||
|
[[ $# -ge 2 ]] || die "--remap-path-prefix requires FROM=TO"
|
||||||
|
[[ "$2" == *=* ]] || die "--remap-path-prefix requires FROM=TO"
|
||||||
|
EXTRA_REMAP_PATH_PREFIXES+=("$2")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--builder)
|
||||||
|
[[ $# -ge 2 ]] || die "--builder requires a value"
|
||||||
|
BUILDER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-required)
|
||||||
|
SIGNATURE_REQUIRED=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--signature-file|--signature-path)
|
||||||
|
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||||
|
SIGNATURE_FILE="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-format)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-format requires a value"
|
||||||
|
SIGNATURE_FORMAT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-verification-tool|--signature-tool)
|
||||||
|
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||||
|
SIGNATURE_VERIFICATION_TOOL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-verification-command)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
|
||||||
|
SIGNATURE_VERIFICATION_COMMAND="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-key-fingerprint)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
|
||||||
|
SIGNATURE_KEY_FINGERPRINT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-certificate-identity)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
|
||||||
|
SIGNATURE_CERTIFICATE_IDENTITY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-certificate-issuer)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
|
||||||
|
SIGNATURE_CERTIFICATE_ISSUER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-transparency-log-url)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
|
||||||
|
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-verified-sha256)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
|
||||||
|
SIGNATURE_VERIFIED_SHA256="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-j|--jobs)
|
||||||
|
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
||||||
|
JOBS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cargo-arg)
|
||||||
|
[[ $# -ge 2 ]] || die "--cargo-arg requires a value"
|
||||||
|
EXTRA_CARGO_ARGS+=("$2")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) die "unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
PLATFORM="$(normalize_platform "$PLATFORM")"
|
||||||
|
BACKEND_SOURCE_DIR="$BACKEND_DIR"
|
||||||
|
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
|
||||||
|
|
||||||
|
if [[ "$PLATFORM" == "windows" && -z "$RUST_TARGET" ]]; then
|
||||||
|
RUST_TARGET="x86_64-pc-windows-gnu"
|
||||||
|
fi
|
||||||
|
if [[ "$PLATFORM" == "macos" && -z "$RUST_TARGET" && "$(host_platform)" != "macos" ]]; then
|
||||||
|
die "macOS artifacts require --rust-target when not running on macOS"
|
||||||
|
fi
|
||||||
|
if [[ "$BUILD_ARTIFACT" == false && -z "$ARTIFACT_PATH" ]]; then
|
||||||
|
die "--no-build requires --artifact"
|
||||||
|
fi
|
||||||
|
|
||||||
|
backend_dependency_path_from_cargo() {
|
||||||
|
local cargo_toml="$1"
|
||||||
|
awk '
|
||||||
|
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
|
||||||
|
original = $0
|
||||||
|
path = $0
|
||||||
|
sub(/.*path[[:space:]]*=[[:space:]]*"/, "", path)
|
||||||
|
sub(/".*/, "", path)
|
||||||
|
if (path != original) print path
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$cargo_toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical_dependency_path() {
|
||||||
|
local path="$1"
|
||||||
|
if [[ -d "$path" ]]; then
|
||||||
|
(cd "$path" && pwd -P)
|
||||||
|
else
|
||||||
|
absolute_path "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_backend_dependency_source() {
|
||||||
|
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] || return
|
||||||
|
|
||||||
|
if [[ ! -f "$BACKEND_DEPENDENCY_DIR/Cargo.toml" ]]; then
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
|
||||||
|
die "Cargo.toml not found in $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
warn "Cargo.toml not found in silentdragonxlitelib source: $BACKEND_DEPENDENCY_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*"silentdragonxlitelib"' "$BACKEND_DEPENDENCY_DIR/Cargo.toml"; then
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
|
||||||
|
die "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
warn "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_backend_source() {
|
||||||
|
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
|
||||||
|
|
||||||
|
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == false ]]; then
|
||||||
|
if [[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]]; then
|
||||||
|
local configured_dependency_path
|
||||||
|
configured_dependency_path="$(backend_dependency_path_from_cargo "$BACKEND_SOURCE_DIR/Cargo.toml")"
|
||||||
|
if [[ -n "$configured_dependency_path" ]]; then
|
||||||
|
if [[ "$configured_dependency_path" = /* ]]; then
|
||||||
|
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$configured_dependency_path")"
|
||||||
|
warn "backend Cargo.toml uses an absolute silentdragonxlitelib path; use --silentdragonxlitelib-dir for portable builders"
|
||||||
|
else
|
||||||
|
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$BACKEND_SOURCE_DIR/$configured_dependency_path")"
|
||||||
|
info "using relative silentdragonxlitelib dependency at $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
validate_backend_dependency_source
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BACKEND_SOURCE_DIR"
|
||||||
|
validate_backend_dependency_source
|
||||||
|
[[ "$BACKEND_DEPENDENCY_DIR" != *\"* ]] || die "--silentdragonxlitelib-dir path cannot contain a double quote"
|
||||||
|
|
||||||
|
local prepared_root="$OUT_DIR/.prepared-backend/$PLATFORM"
|
||||||
|
[[ "$prepared_root" == */.prepared-backend/* ]] || die "refusing unsafe prepared backend path: $prepared_root"
|
||||||
|
rm -rf "$prepared_root"
|
||||||
|
mkdir -p "$prepared_root"
|
||||||
|
|
||||||
|
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
||||||
|
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||||
|
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||||
|
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||||
|
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
|
||||||
|
|
||||||
|
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
|
||||||
|
awk -v replacement="$replacement" '
|
||||||
|
BEGIN { replaced = 0 }
|
||||||
|
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
|
||||||
|
print replacement
|
||||||
|
replaced = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
END { if (replaced != 1) exit 42 }
|
||||||
|
' "$BACKEND_SOURCE_DIR/Cargo.toml" > "$prepared_root/Cargo.toml" \
|
||||||
|
|| die "failed to prepare backend Cargo.toml with portable silentdragonxlitelib path"
|
||||||
|
|
||||||
|
BUILD_BACKEND_DIR="$prepared_root"
|
||||||
|
info "prepared backend source at $BUILD_BACKEND_DIR with silentdragonxlitelib from $BACKEND_DEPENDENCY_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_backend_source
|
||||||
|
|
||||||
|
artifact_kind() {
|
||||||
|
local name="${1##*/}"
|
||||||
|
case "$name" in
|
||||||
|
*.a|*.lib) printf 'static-library\n' ;;
|
||||||
|
*.so|*.dylib|*.dll) printf 'shared-library\n' ;;
|
||||||
|
*) printf 'unknown\n' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cargo_output_candidates() {
|
||||||
|
local cargo_target_root="$BUILD_BACKEND_DIR/target"
|
||||||
|
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
|
||||||
|
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local base="$cargo_target_root/release"
|
||||||
|
if [[ -n "$RUST_TARGET" ]]; then
|
||||||
|
base="$cargo_target_root/$RUST_TARGET/release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$PLATFORM" in
|
||||||
|
linux)
|
||||||
|
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.so"
|
||||||
|
;;
|
||||||
|
windows)
|
||||||
|
printf '%s\n' "$base/silentdragonxlite.lib" "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.dll"
|
||||||
|
;;
|
||||||
|
macos)
|
||||||
|
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.dylib" "$base/silentdragonxlite.dylib"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
source_revision_for() {
|
||||||
|
local dir="$1"
|
||||||
|
local revision_file
|
||||||
|
for revision_file in "$dir/DRAGONX_SOURCE_REVISION" "$dir/../DRAGONX_SOURCE_REVISION"; do
|
||||||
|
if [[ -f "$revision_file" ]]; then
|
||||||
|
sed -n '1p' "$revision_file"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
git -C "$dir" rev-parse HEAD 2>/dev/null || printf 'unknown'
|
||||||
|
else
|
||||||
|
printf 'unknown'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
default_source_date_epoch() {
|
||||||
|
if git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
git -C "$PROJECT_ROOT" log -1 --format=%ct 2>/dev/null || printf '0'
|
||||||
|
else
|
||||||
|
printf '0'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
append_rustflag() {
|
||||||
|
local rustflag="$1"
|
||||||
|
if [[ -n "${RUSTFLAGS:-}" ]]; then
|
||||||
|
export RUSTFLAGS="${RUSTFLAGS} ${rustflag}"
|
||||||
|
else
|
||||||
|
export RUSTFLAGS="$rustflag"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
append_rust_path_remap() {
|
||||||
|
local from_path="$1"
|
||||||
|
local to_path="$2"
|
||||||
|
[[ -n "$from_path" && -n "$to_path" ]] || return
|
||||||
|
append_rustflag "--remap-path-prefix=${from_path}=${to_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_reproducible_rustflags() {
|
||||||
|
local cargo_target_root="$BUILD_BACKEND_DIR/target"
|
||||||
|
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
|
||||||
|
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
append_rust_path_remap "$PROJECT_ROOT" "/dragonx-project"
|
||||||
|
append_rust_path_remap "$BACKEND_SOURCE_DIR" "/dragonx-lite-backend"
|
||||||
|
if [[ "$BUILD_BACKEND_DIR" != "$BACKEND_SOURCE_DIR" ]]; then
|
||||||
|
append_rust_path_remap "$BUILD_BACKEND_DIR" "/dragonx-lite-backend"
|
||||||
|
fi
|
||||||
|
append_rust_path_remap "$BACKEND_DEPENDENCY_DIR" "/dragonx-lite-backend-dependency"
|
||||||
|
for path_remap in "${EXTRA_REMAP_PATH_PREFIXES[@]}"; do
|
||||||
|
append_rustflag "--remap-path-prefix=${path_remap}"
|
||||||
|
done
|
||||||
|
|
||||||
|
local cargo_home="${CARGO_HOME:-}"
|
||||||
|
if [[ -z "$cargo_home" && -n "${HOME:-}" ]]; then
|
||||||
|
cargo_home="$HOME/.cargo"
|
||||||
|
fi
|
||||||
|
if [[ -n "$cargo_home" && -d "$cargo_home" ]]; then
|
||||||
|
append_rust_path_remap "$cargo_home" "/cargo-home"
|
||||||
|
fi
|
||||||
|
append_rust_path_remap "$cargo_target_root" "/dragonx-lite-cargo-target"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_with_cargo() {
|
||||||
|
command -v cargo >/dev/null 2>&1 || die "cargo was not found"
|
||||||
|
[[ -f "$BUILD_BACKEND_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BUILD_BACKEND_DIR"
|
||||||
|
|
||||||
|
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
|
||||||
|
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export CARGO_INCREMENTAL=0
|
||||||
|
export SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH_VALUE"
|
||||||
|
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
|
||||||
|
export CARGO_TARGET_DIR="$CARGO_TARGET_DIR_VALUE"
|
||||||
|
fi
|
||||||
|
if [[ "$REPRODUCIBLE" == true ]]; then
|
||||||
|
apply_reproducible_rustflags
|
||||||
|
fi
|
||||||
|
if [[ "$PLATFORM" == "windows" && -d "$BUILD_BACKEND_DIR/libsodium-mingw" ]]; then
|
||||||
|
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cargo_cmd=(cargo build --locked --lib --release)
|
||||||
|
if [[ -n "$RUST_TARGET" ]]; then
|
||||||
|
cargo_cmd+=(--target "$RUST_TARGET")
|
||||||
|
fi
|
||||||
|
if [[ -n "$JOBS" ]]; then
|
||||||
|
cargo_cmd+=(-j "$JOBS")
|
||||||
|
fi
|
||||||
|
cargo_cmd+=("${EXTRA_CARGO_ARGS[@]}")
|
||||||
|
|
||||||
|
info "building backend in $BUILD_BACKEND_DIR"
|
||||||
|
(cd "$BUILD_BACKEND_DIR" && "${cargo_cmd[@]}")
|
||||||
|
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
ARTIFACT_PATH="$candidate"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done < <(cargo_output_candidates)
|
||||||
|
|
||||||
|
die "cargo finished, but no expected backend artifact was found under $BUILD_BACKEND_DIR/target"
|
||||||
|
}
|
||||||
|
|
||||||
|
select_nm_tool() {
|
||||||
|
if [[ "$PLATFORM" == "windows" ]] && command -v x86_64-w64-mingw32-nm >/dev/null 2>&1; then
|
||||||
|
printf 'x86_64-w64-mingw32-nm\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v llvm-nm >/dev/null 2>&1; then
|
||||||
|
printf 'llvm-nm\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v nm >/dev/null 2>&1; then
|
||||||
|
printf 'nm\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
die "no symbol inventory tool found; install nm, llvm-nm, or x86_64-w64-mingw32-nm"
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_sha256() {
|
||||||
|
local file="$1"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$file" | awk '{print $1}'
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$file" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
die "sha256sum or shasum is required"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value//\\/\\\\}"
|
||||||
|
value="${value//\"/\\\"}"
|
||||||
|
value="${value//$'\n'/\\n}"
|
||||||
|
value="${value//$'\r'/}"
|
||||||
|
value="${value//$'\t'/\\t}"
|
||||||
|
printf '"%s"' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
json_array() {
|
||||||
|
local first=true
|
||||||
|
printf '['
|
||||||
|
for value in "$@"; do
|
||||||
|
if [[ "$first" == true ]]; then
|
||||||
|
first=false
|
||||||
|
else
|
||||||
|
printf ','
|
||||||
|
fi
|
||||||
|
json_escape "$value"
|
||||||
|
done
|
||||||
|
printf ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
json_array_from_file() {
|
||||||
|
local file="$1"
|
||||||
|
local values=()
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
mapfile -t values < "$file"
|
||||||
|
fi
|
||||||
|
json_array "${values[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
signature_metadata_requested() {
|
||||||
|
[[ "$SIGNATURE_REQUIRED" == true || \
|
||||||
|
-n "$SIGNATURE_FILE" || \
|
||||||
|
-n "$SIGNATURE_FORMAT" || \
|
||||||
|
-n "$SIGNATURE_VERIFICATION_TOOL" || \
|
||||||
|
-n "$SIGNATURE_VERIFICATION_COMMAND" || \
|
||||||
|
-n "$SIGNATURE_KEY_FINGERPRINT" || \
|
||||||
|
-n "$SIGNATURE_CERTIFICATE_IDENTITY" || \
|
||||||
|
-n "$SIGNATURE_CERTIFICATE_ISSUER" || \
|
||||||
|
-n "$SIGNATURE_TRANSPARENCY_LOG_URL" || \
|
||||||
|
-n "$SIGNATURE_VERIFIED_SHA256" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_signature_metadata() {
|
||||||
|
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
|
||||||
|
if [[ "$SIGNATURE_REQUIRED" == true ]]; then
|
||||||
|
SIGNATURE_REQUIRED_MANIFEST_VALUE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! signature_metadata_requested; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$SIGNATURE_FILE" ]] || die "signature metadata requires --signature-file"
|
||||||
|
[[ -f "$SIGNATURE_FILE" ]] || die "signature file does not exist: $SIGNATURE_FILE"
|
||||||
|
[[ -n "$SIGNATURE_FORMAT" ]] || die "signature metadata requires --signature-format"
|
||||||
|
case "$SIGNATURE_FORMAT" in
|
||||||
|
minisign|gpg|sigstore|external|other) ;;
|
||||||
|
*) die "unsupported --signature-format: $SIGNATURE_FORMAT" ;;
|
||||||
|
esac
|
||||||
|
[[ -n "$SIGNATURE_VERIFICATION_TOOL" ]] || die "signature metadata requires --signature-verification-tool"
|
||||||
|
[[ -n "$SIGNATURE_VERIFIED_SHA256" ]] || die "signature metadata requires --signature-verified-sha256"
|
||||||
|
[[ "$SIGNATURE_VERIFIED_SHA256" == "$SHA256_DIGEST" ]] || die "signature verified SHA-256 does not match artifact SHA-256"
|
||||||
|
if [[ -z "$SIGNATURE_KEY_FINGERPRINT" && -z "$SIGNATURE_CERTIFICATE_IDENTITY" ]]; then
|
||||||
|
die "signature metadata requires --signature-key-fingerprint or --signature-certificate-identity"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIGNATURE_METADATA_PROVIDED=true
|
||||||
|
SIGNATURE_VERIFICATION_PERFORMED=true
|
||||||
|
SIGNATURE_VERIFICATION_STATUS="verified"
|
||||||
|
SIGNATURE_FILE_SHA256="$(compute_sha256 "$SIGNATURE_FILE")"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true ]]; then
|
||||||
|
build_with_cargo
|
||||||
|
fi
|
||||||
|
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
|
||||||
|
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$ARTIFACT_PATH" ]] || die "artifact not found: $ARTIFACT_PATH"
|
||||||
|
|
||||||
|
KIND="$(artifact_kind "$ARTIFACT_PATH")"
|
||||||
|
[[ "$KIND" != "unknown" ]] || die "artifact kind is unsupported: $ARTIFACT_PATH"
|
||||||
|
|
||||||
|
PLATFORM_OUT_DIR="$OUT_DIR/$PLATFORM"
|
||||||
|
mkdir -p "$PLATFORM_OUT_DIR"
|
||||||
|
|
||||||
|
ARTIFACT_NAME="$(basename "$ARTIFACT_PATH")"
|
||||||
|
ARTIFACT_OUTPUT="$PLATFORM_OUT_DIR/$ARTIFACT_NAME"
|
||||||
|
if [[ "$(absolute_path "$ARTIFACT_PATH")" != "$(absolute_path "$ARTIFACT_OUTPUT")" ]]; then
|
||||||
|
cp -p "$ARTIFACT_PATH" "$ARTIFACT_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.txt"
|
||||||
|
RAW_SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.raw.txt"
|
||||||
|
NM_TOOL="$(select_nm_tool)"
|
||||||
|
|
||||||
|
info "capturing exported symbols with $NM_TOOL"
|
||||||
|
if ! "$NM_TOOL" -g --defined-only "$ARTIFACT_OUTPUT" > "$RAW_SYMBOLS_FILE" 2> "$PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"; then
|
||||||
|
die "symbol inventory failed; see $PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"
|
||||||
|
fi
|
||||||
|
awk '{print $NF}' "$RAW_SYMBOLS_FILE" \
|
||||||
|
| sed 's/^_//' \
|
||||||
|
| grep -E '^(litelib_[A-Za-z0-9_]*|blake3_PW)$' \
|
||||||
|
| sort -u > "$SYMBOLS_FILE" || true
|
||||||
|
|
||||||
|
[[ -s "$SYMBOLS_FILE" ]] || die "no SDXL C ABI symbols were found in $ARTIFACT_OUTPUT"
|
||||||
|
|
||||||
|
MISSING_SYMBOLS=()
|
||||||
|
for required in "${REQUIRED_SYMBOLS[@]}"; do
|
||||||
|
if ! grep -Fxq "$required" "$SYMBOLS_FILE"; then
|
||||||
|
MISSING_SYMBOLS+=("$required")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ ${#MISSING_SYMBOLS[@]} -ne 0 ]]; then
|
||||||
|
printf '%s\n' "${MISSING_SYMBOLS[@]}" > "$PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
|
||||||
|
die "artifact is missing required symbols; see $PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA256_DIGEST="$(compute_sha256 "$ARTIFACT_OUTPUT")"
|
||||||
|
validate_signature_metadata
|
||||||
|
ARTIFACT_SIZE_BYTES="$(wc -c < "$ARTIFACT_OUTPUT" | tr -d ' ')"
|
||||||
|
PROJECT_REVISION="$(source_revision_for "$PROJECT_ROOT")"
|
||||||
|
BACKEND_REVISION="$(source_revision_for "$BACKEND_SOURCE_DIR")"
|
||||||
|
BACKEND_DEPENDENCY_REVISION=""
|
||||||
|
if [[ -n "$BACKEND_DEPENDENCY_DIR" ]]; then
|
||||||
|
BACKEND_DEPENDENCY_REVISION="$(source_revision_for "$BACKEND_DEPENDENCY_DIR")"
|
||||||
|
fi
|
||||||
|
ARTIFACT_SET_ID="$PLATFORM-${SHA256_DIGEST:0:16}"
|
||||||
|
REPRODUCIBLE_MANIFEST_VALUE=false
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true && "$REPRODUCIBLE" == true ]]; then
|
||||||
|
REPRODUCIBLE_MANIFEST_VALUE=true
|
||||||
|
fi
|
||||||
|
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=false
|
||||||
|
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
|
||||||
|
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=true
|
||||||
|
fi
|
||||||
|
FILE_DESCRIPTION="unknown"
|
||||||
|
if command -v file >/dev/null 2>&1; then
|
||||||
|
FILE_DESCRIPTION="$(file -b "$ARTIFACT_OUTPUT")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
|
||||||
|
{
|
||||||
|
printf '{\n'
|
||||||
|
printf ' "schema": "dragonx.lite.backend-artifact.v1",\n'
|
||||||
|
printf ' "generated_by": "scripts/build-lite-backend-artifact.sh",\n'
|
||||||
|
printf ' "read_only_inventory": true,\n'
|
||||||
|
printf ' "artifact_mutation_requested": false,\n'
|
||||||
|
printf ' "upload_requested": false,\n'
|
||||||
|
printf ' "signing_requested": false,\n'
|
||||||
|
printf ' "publication_requested": false,\n'
|
||||||
|
printf ' "signature_verification": {\n'
|
||||||
|
printf ' "policy_name": '; json_escape "$SIGNATURE_POLICY_NAME"; printf ',\n'
|
||||||
|
printf ' "policy_defined": %s,\n' "$SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE"
|
||||||
|
printf ' "required_for_release": %s,\n' "$SIGNATURE_REQUIRED_MANIFEST_VALUE"
|
||||||
|
printf ' "metadata_read_only": true,\n'
|
||||||
|
printf ' "metadata_provided": %s,\n' "$SIGNATURE_METADATA_PROVIDED"
|
||||||
|
printf ' "verification_performed": %s,\n' "$SIGNATURE_VERIFICATION_PERFORMED"
|
||||||
|
printf ' "verification_status": '; json_escape "$SIGNATURE_VERIFICATION_STATUS"; printf ',\n'
|
||||||
|
printf ' "signature_format": '; json_escape "$SIGNATURE_FORMAT"; printf ',\n'
|
||||||
|
printf ' "signature_path": '; json_escape "$SIGNATURE_FILE"; printf ',\n'
|
||||||
|
printf ' "signature_file_sha256": '; json_escape "$SIGNATURE_FILE_SHA256"; printf ',\n'
|
||||||
|
printf ' "verification_tool": '; json_escape "$SIGNATURE_VERIFICATION_TOOL"; printf ',\n'
|
||||||
|
printf ' "verification_command": '; json_escape "$SIGNATURE_VERIFICATION_COMMAND"; printf ',\n'
|
||||||
|
printf ' "key_fingerprint": '; json_escape "$SIGNATURE_KEY_FINGERPRINT"; printf ',\n'
|
||||||
|
printf ' "certificate_identity": '; json_escape "$SIGNATURE_CERTIFICATE_IDENTITY"; printf ',\n'
|
||||||
|
printf ' "certificate_issuer": '; json_escape "$SIGNATURE_CERTIFICATE_ISSUER"; printf ',\n'
|
||||||
|
printf ' "transparency_log_url": '; json_escape "$SIGNATURE_TRANSPARENCY_LOG_URL"; printf ',\n'
|
||||||
|
printf ' "verified_artifact_sha256": '; json_escape "$SIGNATURE_VERIFIED_SHA256"; printf '\n'
|
||||||
|
printf ' },\n'
|
||||||
|
printf ' "abi_version": '; json_escape "$ABI_VERSION"; printf ',\n'
|
||||||
|
printf ' "link_mode": '; json_escape "$LINK_MODE"; printf ',\n'
|
||||||
|
printf ' "platform": '; json_escape "$PLATFORM"; printf ',\n'
|
||||||
|
printf ' "rust_target": '; json_escape "$RUST_TARGET"; printf ',\n'
|
||||||
|
printf ' "artifact": {\n'
|
||||||
|
printf ' "path": '; json_escape "$ARTIFACT_OUTPUT"; printf ',\n'
|
||||||
|
printf ' "kind": '; json_escape "$KIND"; printf ',\n'
|
||||||
|
printf ' "size_bytes": %s,\n' "$ARTIFACT_SIZE_BYTES"
|
||||||
|
printf ' "sha256": '; json_escape "$SHA256_DIGEST"; printf ',\n'
|
||||||
|
printf ' "file_description": '; json_escape "$FILE_DESCRIPTION"; printf '\n'
|
||||||
|
printf ' },\n'
|
||||||
|
printf ' "symbol_inventory": {\n'
|
||||||
|
printf ' "tool": '; json_escape "$NM_TOOL"; printf ',\n'
|
||||||
|
printf ' "symbols_path": '; json_escape "$SYMBOLS_FILE"; printf ',\n'
|
||||||
|
printf ' "raw_symbols_path": '; json_escape "$RAW_SYMBOLS_FILE"; printf ',\n'
|
||||||
|
printf ' "required_symbols": '; json_array "${REQUIRED_SYMBOLS[@]}"; printf ',\n'
|
||||||
|
printf ' "exported_symbols": '; json_array_from_file "$SYMBOLS_FILE"; printf ',\n'
|
||||||
|
printf ' "missing_required_symbols": []\n'
|
||||||
|
printf ' },\n'
|
||||||
|
printf ' "provenance": {\n'
|
||||||
|
printf ' "owner_ready": true,\n'
|
||||||
|
printf ' "metadata_provided": true,\n'
|
||||||
|
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
||||||
|
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
||||||
|
printf ' "portable_dependency_override": %s,\n' "$PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE"
|
||||||
|
printf ' "silentdragonxlitelib_source": '; json_escape "$BACKEND_DEPENDENCY_DIR"; printf ',\n'
|
||||||
|
printf ' "builder": '; json_escape "$BUILDER"; printf ',\n'
|
||||||
|
printf ' "source_revision": '; json_escape "$BACKEND_REVISION"; printf ',\n'
|
||||||
|
printf ' "silentdragonxlitelib_revision": '; json_escape "$BACKEND_DEPENDENCY_REVISION"; printf ',\n'
|
||||||
|
printf ' "project_revision": '; json_escape "$PROJECT_REVISION"; printf ',\n'
|
||||||
|
printf ' "artifact_set_id": '; json_escape "$ARTIFACT_SET_ID"; printf ',\n'
|
||||||
|
printf ' "source_date_epoch": '; json_escape "$SOURCE_DATE_EPOCH_VALUE"; printf ',\n'
|
||||||
|
printf ' "reproducible": %s,\n' "$REPRODUCIBLE_MANIFEST_VALUE"
|
||||||
|
printf ' "redacted": true\n'
|
||||||
|
printf ' }\n'
|
||||||
|
printf '}\n'
|
||||||
|
} > "$MANIFEST_FILE"
|
||||||
|
|
||||||
|
info "artifact: $ARTIFACT_OUTPUT"
|
||||||
|
info "symbols: $SYMBOLS_FILE"
|
||||||
|
info "manifest: $MANIFEST_FILE"
|
||||||
|
info "sha256: $SHA256_DIGEST"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
CMake configure example:
|
||||||
|
cmake -S "$PROJECT_ROOT" -B "$PROJECT_ROOT/build/lite" \\
|
||||||
|
-DDRAGONX_BUILD_LITE=ON \\
|
||||||
|
-DDRAGONX_ENABLE_LITE_BACKEND=ON \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_LIBRARY="$ARTIFACT_OUTPUT" \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE="$SYMBOLS_FILE" \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_MANIFEST="$MANIFEST_FILE" \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_LINK_MODE=$LINK_MODE \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_ABI=$ABI_VERSION
|
||||||
|
EOF
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Source-tree hygiene guard.
|
||||||
|
#
|
||||||
|
# Blocks two failure modes that an AI coding session previously introduced in
|
||||||
|
# src/wallet/ (the lite-wallet "_plan"/"_batch" churn): pathologically long
|
||||||
|
# filenames (which also break the Windows MAX_PATH 260-char limit during the
|
||||||
|
# cross-build) and the runaway "receipt/custody/handoff/stewardship" naming
|
||||||
|
# explosion where each session wrapped the previous artifact in one more layer.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/check-source-hygiene.sh # check working-tree src/
|
||||||
|
# scripts/check-source-hygiene.sh --staged # check staged files (pre-commit)
|
||||||
|
#
|
||||||
|
# Install as a git pre-commit hook:
|
||||||
|
# ln -sf ../../scripts/check-source-hygiene.sh .git/hooks/pre-commit
|
||||||
|
# # (the hook invokes it with --staged automatically when named pre-commit)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MAX_LEN=80
|
||||||
|
# Naming-explosion tokens. Two or more chained in one basename is the smell.
|
||||||
|
CHURN_RE='receipt|custody|handoff|stewardship|promotion_activation|acceptance_confirmation|archive_handoff|post_closure'
|
||||||
|
|
||||||
|
mode="${1:-}"
|
||||||
|
if [[ "$mode" == "--staged" || "$(basename "$0")" == "pre-commit" ]]; then
|
||||||
|
mapfile -t files < <(git diff --cached --name-only --diff-filter=AR | grep -E '\.(cpp|h|hpp|cc)$' || true)
|
||||||
|
else
|
||||||
|
mapfile -t files < <(git ls-files 'src/**/*.cpp' 'src/**/*.h' 2>/dev/null; \
|
||||||
|
find src -type f \( -name '*.cpp' -o -name '*.h' \) 2>/dev/null)
|
||||||
|
# de-dup
|
||||||
|
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u)
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ -z "$f" ]] && continue
|
||||||
|
base="$(basename "$f")"
|
||||||
|
len=${#base}
|
||||||
|
if (( len > MAX_LEN )); then
|
||||||
|
echo "✗ filename too long ($len > $MAX_LEN chars): $f" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
# count distinct churn tokens in the basename ( || true: grep exits 1 on no match)
|
||||||
|
n=$(printf '%s' "$base" | grep -oE "$CHURN_RE" | sort -u | wc -l || true)
|
||||||
|
if (( n >= 2 )); then
|
||||||
|
echo "✗ runaway naming pattern ($n churn tokens) — refactor in place, don't add a layer: $f" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( fail )); then
|
||||||
|
echo "" >&2
|
||||||
|
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "source hygiene OK (${#files[@]} files checked)"
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign DRG-XMRig release archives for the wallet's in-app updater (opt-in ed25519 signatures).
|
||||||
|
#
|
||||||
|
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||||
|
# key pinned in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64). For each archive
|
||||||
|
# <name>.zip this produces <name>.zip.sig holding the base64 of the raw 64-byte ed25519 signature —
|
||||||
|
# upload that .sig next to the .zip as a release asset.
|
||||||
|
#
|
||||||
|
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
|
||||||
|
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
||||||
|
# (verified by the wallet's unit tests + an interop check).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sign-xmrig-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||||
|
# scripts/sign-xmrig-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||||
|
# scripts/sign-xmrig-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||||
|
#
|
||||||
|
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||||
|
# kXmrigSignaturePublicKeyBase64 in src/util/xmrig_updater.h.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
|
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||||
|
|
||||||
|
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
|
||||||
|
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||||
|
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||||
|
|
||||||
|
cmd="${1:-}"; shift || true
|
||||||
|
case "$cmd" in
|
||||||
|
keygen)
|
||||||
|
prefix="${1:-drg-xmrig}"
|
||||||
|
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
|
||||||
|
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
|
||||||
|
chmod 600 "$prefix.ed25519.key"
|
||||||
|
pub="$(pubkey_b64 "$prefix.ed25519.key")"
|
||||||
|
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
|
||||||
|
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
|
||||||
|
echo "public key : $prefix.ed25519.pub.b64"
|
||||||
|
echo
|
||||||
|
echo "Pin this in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64):"
|
||||||
|
echo " $pub"
|
||||||
|
;;
|
||||||
|
pubkey)
|
||||||
|
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||||
|
pubkey_b64 "$1"
|
||||||
|
;;
|
||||||
|
sign)
|
||||||
|
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||||
|
key="$1"; shift
|
||||||
|
[ -f "$key" ] || die "no such key: $key"
|
||||||
|
for f in "$@"; do
|
||||||
|
[ -f "$f" ] || die "no such file: $f"
|
||||||
|
raw="$(mktemp)"
|
||||||
|
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||||
|
openssl base64 -A -in "$raw" > "$f.sig"
|
||||||
|
printf '\n' >> "$f.sig"
|
||||||
|
rm -f "$raw"
|
||||||
|
echo "signed: $f -> $f.sig"
|
||||||
|
done
|
||||||
|
echo "Upload each .sig as a release asset next to its archive."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -442,6 +442,53 @@ copy_daemon_data() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Stale-daemon guard ───────────────────────────────────────────────────────
|
||||||
|
# A prebuilt daemon binary is only rebuilt on its platform's flag (--win/--mac),
|
||||||
|
# and build.sh merely BUNDLES whatever binary already exists — so a daemon left
|
||||||
|
# over from an old source revision silently ships in the wallet (e.g. the Network
|
||||||
|
# tab once reported v1.0.1 while the source was v1.0.2). These helpers compare the
|
||||||
|
# version baked into a prebuilt binary against the dragonx source and flag drift.
|
||||||
|
STALE_DAEMON=0
|
||||||
|
|
||||||
|
# MAJOR.MINOR.REVISION from the checked-out dragonx source (empty if unavailable).
|
||||||
|
dragonx_source_version() {
|
||||||
|
local hdr="$DRAGONX_SRC/src/clientversion.h"
|
||||||
|
[[ -f "$hdr" ]] || return 1
|
||||||
|
local maj min rev
|
||||||
|
maj=$(awk '/#define[ \t]+CLIENT_VERSION_MAJOR/{print $3; exit}' "$hdr")
|
||||||
|
min=$(awk '/#define[ \t]+CLIENT_VERSION_MINOR/{print $3; exit}' "$hdr")
|
||||||
|
rev=$(awk '/#define[ \t]+CLIENT_VERSION_REVISION/{print $3; exit}' "$hdr")
|
||||||
|
[[ -n "$maj" && -n "$min" && -n "$rev" ]] || return 1
|
||||||
|
printf '%s.%s.%s' "$maj" "$min" "$rev"
|
||||||
|
}
|
||||||
|
|
||||||
|
# vX.Y.Z baked into a built daemon binary (the daemon embeds "vX.Y.Z-<githash>").
|
||||||
|
# Uses grep -a so no `strings`/binutils dependency is required.
|
||||||
|
dragonx_binary_version() {
|
||||||
|
local bin="$1"
|
||||||
|
[[ -f "$bin" ]] || return 1
|
||||||
|
LC_ALL=C grep -aoE 'v[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{6,}' "$bin" 2>/dev/null \
|
||||||
|
| head -1 | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compare a prebuilt daemon against the source; warn (and set STALE_DAEMON) on drift.
|
||||||
|
# $1 = label, $2 = binary path, $3 = rebuild flag(s) (e.g. "--win", "" for Linux)
|
||||||
|
daemon_version_guard() {
|
||||||
|
local label="$1" bin="$2" rebuild_hint="$3"
|
||||||
|
[[ -f "$bin" ]] || return 0
|
||||||
|
local src bv
|
||||||
|
src=$(dragonx_source_version) || return 0 # no source checked out → can't compare
|
||||||
|
bv=$(dragonx_binary_version "$bin")
|
||||||
|
[[ -n "$bv" ]] || return 0 # couldn't read the binary's version
|
||||||
|
if [[ "$bv" == "$src" ]]; then
|
||||||
|
ok " $label daemon is v$bv (matches dragonx source)"
|
||||||
|
else
|
||||||
|
warn " $label daemon is v$bv but dragonx source is v$src — STALE"
|
||||||
|
warn " rebuild so the wallet ships the current daemon: ./setup.sh${rebuild_hint:+ $rebuild_hint}"
|
||||||
|
STALE_DAEMON=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Linux daemon ─────────────────────────────────────────────────────────────
|
# ── Linux daemon ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Skip Linux daemon build if only cross-compile targets were requested
|
# Skip Linux daemon build if only cross-compile targets were requested
|
||||||
@@ -461,11 +508,13 @@ fi
|
|||||||
if $CHECK_ONLY; then
|
if $CHECK_ONLY; then
|
||||||
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
|
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
|
||||||
ok "dragonxd daemon (Linux) present"
|
ok "dragonxd daemon (Linux) present"
|
||||||
|
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
|
||||||
else
|
else
|
||||||
miss "dragonxd daemon (Linux) not built"
|
miss "dragonxd daemon (Linux) not built"
|
||||||
fi
|
fi
|
||||||
elif $SKIP_LINUX_DAEMON; then
|
elif $SKIP_LINUX_DAEMON; then
|
||||||
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
|
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
|
||||||
|
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
|
||||||
else
|
else
|
||||||
clone_dragonx_if_needed
|
clone_dragonx_if_needed
|
||||||
|
|
||||||
@@ -500,9 +549,11 @@ fi
|
|||||||
|
|
||||||
if ! $SETUP_WIN; then
|
if ! $SETUP_WIN; then
|
||||||
skip "dragonxd (Windows) — use --win to cross-compile"
|
skip "dragonxd (Windows) — use --win to cross-compile"
|
||||||
|
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
|
||||||
elif $CHECK_ONLY; then
|
elif $CHECK_ONLY; then
|
||||||
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
|
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
|
||||||
ok "dragonxd daemon (Windows) present"
|
ok "dragonxd daemon (Windows) present"
|
||||||
|
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
|
||||||
else
|
else
|
||||||
miss "dragonxd daemon (Windows) not built"
|
miss "dragonxd daemon (Windows) not built"
|
||||||
fi
|
fi
|
||||||
@@ -558,9 +609,11 @@ fi
|
|||||||
|
|
||||||
if ! $SETUP_MAC; then
|
if ! $SETUP_MAC; then
|
||||||
skip "dragonxd (macOS) — use --mac to cross-compile"
|
skip "dragonxd (macOS) — use --mac to cross-compile"
|
||||||
|
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
|
||||||
elif $CHECK_ONLY; then
|
elif $CHECK_ONLY; then
|
||||||
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
|
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
|
||||||
ok "dragonxd daemon (macOS) present"
|
ok "dragonxd daemon (macOS) present"
|
||||||
|
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
|
||||||
else
|
else
|
||||||
miss "dragonxd daemon (macOS) not built"
|
miss "dragonxd daemon (macOS) not built"
|
||||||
fi
|
fi
|
||||||
@@ -623,6 +676,14 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prominent reminder if any prebuilt daemon drifted from the source — these are bundled verbatim
|
||||||
|
# by build.sh, so a stale binary ships in the wallet (and shows an old version in the Network tab).
|
||||||
|
if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||||
|
warn "One or more prebuilt daemons are OLDER than the dragonx source (see above)."
|
||||||
|
warn "build.sh bundles them as-is, so rebuild the stale platform(s) before releasing:"
|
||||||
|
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||||
header "xmrig-hac Mining Binary"
|
header "xmrig-hac Mining Binary"
|
||||||
|
|
||||||
|
|||||||
+702
-54
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
|||||||
#include "services/wallet_security_controller.h"
|
#include "services/wallet_security_controller.h"
|
||||||
#include "services/wallet_security_workflow.h"
|
#include "services/wallet_security_workflow.h"
|
||||||
#include "util/async_task_manager.h"
|
#include "util/async_task_manager.h"
|
||||||
|
#include "wallet/wallet_capabilities.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/console_tab.h"
|
#include "ui/windows/console_tab.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -32,6 +33,7 @@ namespace dragonx {
|
|||||||
namespace config { class Settings; }
|
namespace config { class Settings; }
|
||||||
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
|
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
|
||||||
namespace util { class Bootstrap; class SecureVault; }
|
namespace util { class Bootstrap; class SecureVault; }
|
||||||
|
namespace wallet { class LiteWalletController; }
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -130,6 +132,13 @@ public:
|
|||||||
* @brief Whether we are in the shutdown phase
|
* @brief Whether we are in the shutdown phase
|
||||||
*/
|
*/
|
||||||
bool isShuttingDown() const { return shutting_down_; }
|
bool isShuttingDown() const { return shutting_down_; }
|
||||||
|
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
|
||||||
|
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
|
||||||
|
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||||||
|
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
||||||
|
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
|
||||||
|
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
|
||||||
|
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
|
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
|
||||||
@@ -146,6 +155,15 @@ public:
|
|||||||
rpc::RPCClient* rpc() { return rpc_.get(); }
|
rpc::RPCClient* rpc() { return rpc_.get(); }
|
||||||
rpc::RPCWorker* worker() { return worker_.get(); }
|
rpc::RPCWorker* worker() { return worker_.get(); }
|
||||||
config::Settings* settings() { return settings_.get(); }
|
config::Settings* settings() { return settings_.get(); }
|
||||||
|
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||||
|
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||||
|
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
|
||||||
|
const std::string& liteOpenError() const { return lite_open_error_; }
|
||||||
|
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
|
||||||
|
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
|
||||||
|
// (Re)build the lite controller from current settings so a changed lite-server selection
|
||||||
|
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
||||||
|
void rebuildLiteWallet(bool force = false);
|
||||||
WalletState& state() { return state_; }
|
WalletState& state() { return state_; }
|
||||||
const WalletState& state() const { return state_; }
|
const WalletState& state() const { return state_; }
|
||||||
const WalletState& getWalletState() const { return state_; }
|
const WalletState& getWalletState() const { return state_; }
|
||||||
@@ -181,6 +199,10 @@ public:
|
|||||||
int getXmrigRequestedThreads() const {
|
int getXmrigRequestedThreads() const {
|
||||||
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
||||||
}
|
}
|
||||||
|
// True while the pool miner process is live — used to refuse replacing the binary under it.
|
||||||
|
bool isPoolMinerRunning() const {
|
||||||
|
return xmrig_manager_ && xmrig_manager_->isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
// Mine-when-idle state query
|
// Mine-when-idle state query
|
||||||
bool isIdleMiningActive() const { return idle_mining_active_; }
|
bool isIdleMiningActive() const { return idle_mining_active_; }
|
||||||
@@ -235,6 +257,13 @@ public:
|
|||||||
double amount, double fee, const std::string& memo,
|
double amount, double fee, const std::string& memo,
|
||||||
std::function<void(bool success, const std::string& result)> callback);
|
std::function<void(bool success, const std::string& result)> callback);
|
||||||
|
|
||||||
|
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
|
||||||
|
// auto-shield) with the shared opid poller so its eventual success/failure is
|
||||||
|
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
|
||||||
|
// richer pending-send path internally; this is for operations with no optimistic
|
||||||
|
// transaction row of their own.
|
||||||
|
void trackOperation(const std::string& opid);
|
||||||
|
|
||||||
// Force refresh
|
// Force refresh
|
||||||
void refreshNow();
|
void refreshNow();
|
||||||
void refreshMiningInfo();
|
void refreshMiningInfo();
|
||||||
@@ -274,8 +303,8 @@ public:
|
|||||||
bool startEmbeddedDaemon();
|
bool startEmbeddedDaemon();
|
||||||
void stopEmbeddedDaemon();
|
void stopEmbeddedDaemon();
|
||||||
bool isEmbeddedDaemonRunning() const;
|
bool isEmbeddedDaemonRunning() const;
|
||||||
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
|
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
|
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||||||
void rescanBlockchain(); // restart daemon with -rescan flag
|
void rescanBlockchain(); // restart daemon with -rescan flag
|
||||||
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
||||||
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
||||||
@@ -360,6 +389,13 @@ public:
|
|||||||
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||||
std::string transactionSendProgressText() const;
|
std::string transactionSendProgressText() const;
|
||||||
std::string transactionRefreshProgressText() const;
|
std::string transactionRefreshProgressText() const;
|
||||||
|
|
||||||
|
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
|
||||||
|
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
|
||||||
|
// something the user copied afterwards). Only a hash of the secret is retained, never the
|
||||||
|
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||||
|
void copySecretToClipboard(const std::string& secret);
|
||||||
|
void pumpSecretClipboardClear();
|
||||||
bool isTransactionRefreshInProgress() const {
|
bool isTransactionRefreshInProgress() const {
|
||||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||||
}
|
}
|
||||||
@@ -379,6 +415,10 @@ private:
|
|||||||
const std::string& txid);
|
const std::string& txid);
|
||||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||||
bool restoreBalances);
|
bool restoreBalances);
|
||||||
|
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
|
||||||
|
// reaches a terminal status. Returns true if a callback was registered (and fired).
|
||||||
|
bool invokeSendResultCallback(const std::string& opid, bool ok,
|
||||||
|
const std::string& result);
|
||||||
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
||||||
std::string transactionHistoryCacheWalletIdentity() const;
|
std::string transactionHistoryCacheWalletIdentity() const;
|
||||||
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||||
@@ -404,6 +444,23 @@ private:
|
|||||||
rpc::ConnectionConfig saved_config_;
|
rpc::ConnectionConfig saved_config_;
|
||||||
|
|
||||||
std::unique_ptr<config::Settings> settings_;
|
std::unique_ptr<config::Settings> settings_;
|
||||||
|
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
|
||||||
|
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
|
||||||
|
// controller's async broadcast result arrives). Only one lite send runs at a time.
|
||||||
|
std::function<void(bool, const std::string&)> lite_send_callback_;
|
||||||
|
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
|
||||||
|
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
|
||||||
|
bool lite_autoopen_done_ = false;
|
||||||
|
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
|
||||||
|
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
|
||||||
|
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
|
||||||
|
std::string lite_open_error_;
|
||||||
|
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||||
|
bool lite_firstrun_dismissed_ = false;
|
||||||
|
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||||||
|
bool lite_unlock_prompt_ = false;
|
||||||
|
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
|
||||||
|
bool lite_startup_lock_checked_ = false;
|
||||||
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||||
util::AsyncTaskManager async_tasks_;
|
util::AsyncTaskManager async_tasks_;
|
||||||
@@ -439,7 +496,7 @@ private:
|
|||||||
bool show_address_book_ = false;
|
bool show_address_book_ = false;
|
||||||
|
|
||||||
// Embedded daemon state
|
// Embedded daemon state
|
||||||
bool use_embedded_daemon_ = true;
|
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||||||
std::string daemon_status_;
|
std::string daemon_status_;
|
||||||
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
||||||
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||||||
@@ -457,6 +514,15 @@ private:
|
|||||||
std::string connection_status_ = "Disconnected";
|
std::string connection_status_ = "Disconnected";
|
||||||
bool connection_in_progress_ = false;
|
bool connection_in_progress_ = false;
|
||||||
bool remote_rpc_plaintext_warning_shown_ = false;
|
bool remote_rpc_plaintext_warning_shown_ = false;
|
||||||
|
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
|
||||||
|
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
|
||||||
|
int daemon_wait_attempts_ = 0;
|
||||||
|
bool daemon_start_error_shown_ = false;
|
||||||
|
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
||||||
|
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
|
||||||
|
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
||||||
|
std::uint64_t clipboard_secret_hash_ = 0;
|
||||||
|
double clipboard_clear_deadline_ = 0.0;
|
||||||
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
||||||
|
|
||||||
// Current page (sidebar navigation)
|
// Current page (sidebar navigation)
|
||||||
@@ -509,6 +575,10 @@ private:
|
|||||||
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
||||||
std::size_t shielded_history_scan_cursor_ = 0;
|
std::size_t shielded_history_scan_cursor_ = 0;
|
||||||
bool shielded_history_scan_pending_ = false;
|
bool shielded_history_scan_pending_ = false;
|
||||||
|
// False until the first full shielded-history scan finishes. Drives the History tab's
|
||||||
|
// "Loading older history…" progress so the user knows transactions are still streaming in
|
||||||
|
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
|
||||||
|
bool initial_history_scan_complete_ = false;
|
||||||
std::unordered_map<std::string, int> shielded_history_scan_heights_;
|
std::unordered_map<std::string, int> shielded_history_scan_heights_;
|
||||||
|
|
||||||
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
||||||
@@ -529,7 +599,14 @@ private:
|
|||||||
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
||||||
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
||||||
bool rescan_status_poll_in_progress_ = false;
|
bool rescan_status_poll_in_progress_ = false;
|
||||||
|
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
|
||||||
|
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
|
||||||
|
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
|
||||||
|
bool rescan_confirmed_active_ = false;
|
||||||
bool opid_poll_in_progress_ = false;
|
bool opid_poll_in_progress_ = false;
|
||||||
|
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
||||||
|
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
||||||
|
int consecutive_core_failures_ = 0;
|
||||||
|
|
||||||
// Pending z_sendmany operation tracking
|
// Pending z_sendmany operation tracking
|
||||||
bool send_progress_active_ = false;
|
bool send_progress_active_ = false;
|
||||||
@@ -543,6 +620,10 @@ private:
|
|||||||
std::int64_t timestamp = 0;
|
std::int64_t timestamp = 0;
|
||||||
};
|
};
|
||||||
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
||||||
|
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
|
||||||
|
// user isn't told "sent successfully" before the tx is actually built/broadcast.
|
||||||
|
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
|
||||||
|
pending_send_callbacks_;
|
||||||
// Txids from completed z_sendmany operations.
|
// Txids from completed z_sendmany operations.
|
||||||
// Ensures shielded sends are discoverable by z_viewtransaction
|
// Ensures shielded sends are discoverable by z_viewtransaction
|
||||||
// even when they don't appear in listtransactions or
|
// even when they don't appear in listtransactions or
|
||||||
@@ -627,6 +708,8 @@ private:
|
|||||||
// Private methods - rendering
|
// Private methods - rendering
|
||||||
void renderStatusBar();
|
void renderStatusBar();
|
||||||
void renderAboutDialog();
|
void renderAboutDialog();
|
||||||
|
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||||
|
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||||
void renderImportKeyDialog();
|
void renderImportKeyDialog();
|
||||||
void renderExportKeyDialog();
|
void renderExportKeyDialog();
|
||||||
void renderBackupDialog();
|
void renderBackupDialog();
|
||||||
@@ -642,6 +725,16 @@ private:
|
|||||||
void tryConnect();
|
void tryConnect();
|
||||||
void onConnected();
|
void onConnected();
|
||||||
void onDisconnected(const std::string& reason);
|
void onDisconnected(const std::string& reason);
|
||||||
|
// Set the "node is initializing" UI state (status line + overlay description) from the
|
||||||
|
// embedded/external daemon's launch state and its own console output (current phase + block
|
||||||
|
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
|
||||||
|
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
|
||||||
|
// false when the daemon is merely launching (not bound yet). Returns the status title.
|
||||||
|
std::string applyDaemonInitStatus(bool reachableButBusy);
|
||||||
|
// Tear down a connection that died mid-session (daemon crash / restart / dropped
|
||||||
|
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
|
||||||
|
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
|
||||||
|
void handleLostConnection(const std::string& reason);
|
||||||
void applyDefaultBanlist();
|
void applyDefaultBanlist();
|
||||||
|
|
||||||
// Private methods - data refresh
|
// Private methods - data refresh
|
||||||
|
|||||||
+473
-107
@@ -34,6 +34,7 @@
|
|||||||
#include "rpc/rpc_worker.h"
|
#include "rpc/rpc_worker.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
#include "config/settings.h"
|
#include "config/settings.h"
|
||||||
|
#include "wallet/lite_wallet_controller.h" // lite send/new-address routing
|
||||||
#include "daemon/daemon_controller.h"
|
#include "daemon/daemon_controller.h"
|
||||||
#include "daemon/embedded_daemon.h"
|
#include "daemon/embedded_daemon.h"
|
||||||
#include "daemon/xmrig_manager.h"
|
#include "daemon/xmrig_manager.h"
|
||||||
@@ -60,6 +61,12 @@ using NetworkRefreshService = services::NetworkRefreshService;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
bool isPageEnabledForBuild(ui::NavPage page)
|
||||||
|
{
|
||||||
|
return wallet::isUiSurfaceAvailable(
|
||||||
|
wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
|
||||||
|
}
|
||||||
|
|
||||||
std::string unencryptedTransactionHistoryCacheKey(const std::string& walletIdentity)
|
std::string unencryptedTransactionHistoryCacheKey(const std::string& walletIdentity)
|
||||||
{
|
{
|
||||||
return std::string("obsidian-dragon-unencrypted-tx-cache-v1:") +
|
return std::string("obsidian-dragon-unencrypted-tx-cache-v1:") +
|
||||||
@@ -115,6 +122,19 @@ std::size_t shieldedReceiveScanBudget(ui::NavPage page)
|
|||||||
return page == ui::NavPage::History ? 8u : 4u;
|
return page == ui::NavPage::History ? 8u : 4u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How far the tip may drift past an address's last shielded scan before we re-scan it. A full pass
|
||||||
|
// scans ~budget addresses per refresh cycle (≈96 per block of wall time), so a wallet with many
|
||||||
|
// z-addresses takes several blocks to scan fully. With a strict (tolerance 0) "scanned at tip"
|
||||||
|
// check, new blocks arriving mid-pass would invalidate already-scanned addresses and the pass would
|
||||||
|
// never complete — leaving transactions_dirty_ (and its "refreshing history" banner + send-progress
|
||||||
|
// gate) stuck on forever. Scaling the tolerance with the address count lets the pass complete while
|
||||||
|
// keeping shielded-receive latency minimal for small wallets; it's capped for pathological sizes.
|
||||||
|
int shieldedScanTipTolerance(std::size_t shieldedAddressCount)
|
||||||
|
{
|
||||||
|
int t = 2 + static_cast<int>(shieldedAddressCount / 96);
|
||||||
|
return std::min(t, 50);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -151,12 +171,30 @@ static WarmupText translateWarmup(const std::string& raw)
|
|||||||
return {raw.c_str(), ""};
|
return {raw.c_str(), ""};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phrases dragonxd prints to its console while initializing, in the order translateWarmup()
|
||||||
|
// understands them. The most recent matching console line tells us which stage the node is in
|
||||||
|
// even when the RPC probe just times out (no -28 reply to read).
|
||||||
|
static const char* const kDaemonInitPhases[] = {
|
||||||
|
"Rescanning", "Rewinding", "Activating", "Verifying", "Loading", "Pruning",
|
||||||
|
};
|
||||||
|
|
||||||
|
// How many consecutive "RPC port busy but no config" connect attempts to wait through before
|
||||||
|
// warning the user that whatever owns the port isn't a usable DragonX node. The core retry runs
|
||||||
|
// roughly every few seconds, so this is on the order of ~20s — long enough for a real daemon to
|
||||||
|
// write its config, short enough not to leave the user guessing.
|
||||||
|
static constexpr int kDaemonWaitWarnAttempts = 4;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Connection Management
|
// Connection Management
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void App::tryConnect()
|
void App::tryConnect()
|
||||||
{
|
{
|
||||||
|
// Lite builds have no full node / RPC daemon, so never run the RPC connection state machine
|
||||||
|
// (it would just fail every tick). The lite controller drives the wallet; "online" status is
|
||||||
|
// derived from it each frame in App::update(), which also gates the wallet UI (isConnected()).
|
||||||
|
if (isLiteBuild()) return;
|
||||||
|
|
||||||
if (connection_in_progress_) return;
|
if (connection_in_progress_) return;
|
||||||
|
|
||||||
static int connect_attempt = 0;
|
static int connect_attempt = 0;
|
||||||
@@ -174,38 +212,59 @@ void App::tryConnect()
|
|||||||
VERBOSE_LOGF("[connect #%d] No valid config — DRAGONX.conf missing or no rpcuser/rpcpassword (looked at: %s)\n",
|
VERBOSE_LOGF("[connect #%d] No valid config — DRAGONX.conf missing or no rpcuser/rpcpassword (looked at: %s)\n",
|
||||||
connect_attempt, confPath.c_str());
|
connect_attempt, confPath.c_str());
|
||||||
|
|
||||||
// If we already know an external daemon is on the port, just wait
|
// Re-evaluate the RPC port LIVE rather than trusting a latched "external daemon detected"
|
||||||
// for the config file to appear (the daemon creates it on first run).
|
// flag: EmbeddedDaemon::start() sets that latch whenever the port was busy at a prior
|
||||||
if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) {
|
// attempt and then never re-checks it, so a stale socket (or a transient squatter that has
|
||||||
|
// since died) would strand us forever "waiting for config". If the port is genuinely busy,
|
||||||
|
// a real daemon writes its config shortly and we keep waiting; if it's free, we must start
|
||||||
|
// our own.
|
||||||
|
const bool portInUse = daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||||
|
if (portInUse) {
|
||||||
connection_status_ = TR("sb_waiting_config");
|
connection_status_ = TR("sb_waiting_config");
|
||||||
VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt);
|
VERBOSE_LOGF("[connect #%d] RPC port in use but no config yet — waiting for the daemon to write it\n",
|
||||||
|
connect_attempt);
|
||||||
|
// After a bounded wait with no config appearing, whatever owns the port is not a usable
|
||||||
|
// DragonX node (a foreign process, or a stuck/half-dead daemon). Say so once, with the
|
||||||
|
// action, instead of leaving the user on a silent "waiting" spinner forever.
|
||||||
|
if (++daemon_wait_attempts_ == kDaemonWaitWarnAttempts) {
|
||||||
|
ui::Notifications::instance().warning(TR("daemon_port_busy_warn"), 20.0f);
|
||||||
|
}
|
||||||
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
||||||
services::RefreshScheduler::kCoreDefault - 1.0f);
|
services::RefreshScheduler::kCoreDefault - 1.0f);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
daemon_wait_attempts_ = 0; // port is free — clear the bounded-wait counter
|
||||||
|
|
||||||
connection_status_ = TR("sb_no_conf");
|
connection_status_ = TR("sb_no_conf");
|
||||||
|
|
||||||
// Try to start embedded daemon if enabled
|
// Port is free → start our own embedded daemon (if enabled).
|
||||||
if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) {
|
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
||||||
connection_status_ = TR("sb_starting_daemon");
|
connection_status_ = TR("sb_starting_daemon");
|
||||||
if (startEmbeddedDaemon()) {
|
if (startEmbeddedDaemon()) {
|
||||||
// Will retry connection after daemon starts
|
// Will retry connection after daemon starts
|
||||||
VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt);
|
VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt);
|
||||||
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
||||||
services::RefreshScheduler::kCoreDefault - 1.0f);
|
services::RefreshScheduler::kCoreDefault - 1.0f);
|
||||||
} else if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) {
|
} else {
|
||||||
connection_status_ = TR("sb_waiting_config");
|
// The daemon couldn't be started (binary not found, Sapling params missing, spawn
|
||||||
VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt);
|
// failure, …). Surface the actual reason instead of leaving the status stuck on
|
||||||
|
// "Starting dragonxd…": connection_status_ for the overlay/status bar, plus a
|
||||||
|
// one-time sticky notification with the full, actionable detail.
|
||||||
|
std::string detail = daemon_controller_ ? daemon_controller_->lastError() : std::string();
|
||||||
|
VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n",
|
||||||
|
connect_attempt, detail.empty() ? "(none)" : detail.c_str(),
|
||||||
|
daemon::EmbeddedDaemon::findDaemonBinary().c_str());
|
||||||
|
connection_status_ = TR("sb_daemon_start_failed");
|
||||||
|
if (!daemon_start_error_shown_) {
|
||||||
|
daemon_start_error_shown_ = true;
|
||||||
|
ui::Notifications::instance().error(
|
||||||
|
detail.empty() ? std::string(TR("sb_daemon_start_failed")) : detail, 30.0f);
|
||||||
|
}
|
||||||
|
// Keep retrying: a missing binary/params can be fixed without a restart.
|
||||||
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
||||||
services::RefreshScheduler::kCoreDefault - 1.0f);
|
services::RefreshScheduler::kCoreDefault - 1.0f);
|
||||||
} else {
|
|
||||||
VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n",
|
|
||||||
connect_attempt,
|
|
||||||
daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)",
|
|
||||||
daemon::EmbeddedDaemon::findDaemonBinary().c_str());
|
|
||||||
}
|
}
|
||||||
} else if (!use_embedded_daemon_) {
|
} else if (!isUsingEmbeddedDaemon()) {
|
||||||
VERBOSE_LOGF("[connect #%d] Embedded daemon disabled (using external). No config found at %s\n",
|
VERBOSE_LOGF("[connect #%d] Embedded daemon disabled (using external). No config found at %s\n",
|
||||||
connect_attempt, confPath.c_str());
|
connect_attempt, confPath.c_str());
|
||||||
}
|
}
|
||||||
@@ -254,9 +313,24 @@ void App::tryConnect()
|
|||||||
externalDetected ? "yes" : "no",
|
externalDetected ? "yes" : "no",
|
||||||
daemon_controller_->crashCount(),
|
daemon_controller_->crashCount(),
|
||||||
daemon_controller_->lastError().empty() ? "(none)" : daemon_controller_->lastError().c_str());
|
daemon_controller_->lastError().empty() ? "(none)" : daemon_controller_->lastError().c_str());
|
||||||
|
|
||||||
|
// The embedded daemon can spawn successfully and then exit immediately (a missing runtime
|
||||||
|
// DLL, wrong architecture, corrupt binary, datadir lock, …). The crash monitor records a
|
||||||
|
// detailed reason (translated exit code + launch command + debug.log tail) in lastError(),
|
||||||
|
// but it runs on a background thread and was never shown — so the wallet looked like it was
|
||||||
|
// "stuck connecting" while the node silently died-and-respawned. Surface each new crash once.
|
||||||
|
const int crashes = daemon_controller_->crashCount();
|
||||||
|
if (crashes > daemon_last_seen_crashes_) {
|
||||||
|
daemon_last_seen_crashes_ = crashes;
|
||||||
|
const std::string detail = daemon_controller_->lastError();
|
||||||
|
if (!detail.empty()) {
|
||||||
|
connection_status_ = TR("sb_daemon_start_failed");
|
||||||
|
ui::Notifications::instance().error(detail, 30.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
VERBOSE_LOGF("[connect #%d] No embedded daemon object (use_embedded=%s)\n",
|
VERBOSE_LOGF("[connect #%d] No embedded daemon object (use_embedded=%s)\n",
|
||||||
attempt, use_embedded_daemon_ ? "yes" : "no");
|
attempt, isUsingEmbeddedDaemon() ? "yes" : "no");
|
||||||
}
|
}
|
||||||
|
|
||||||
worker_->post([this, config, daemonStarting, externalDetected, attempt]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, config, daemonStarting, externalDetected, attempt]() -> rpc::RPCWorker::MainCb {
|
||||||
@@ -332,21 +406,25 @@ void App::tryConnect()
|
|||||||
"Restart the daemon or correct the credentials.");
|
"Restart the daemon or correct the credentials.");
|
||||||
} else if (daemonStarting) {
|
} else if (daemonStarting) {
|
||||||
state_.connected = false;
|
state_.connected = false;
|
||||||
// Show the actual RPC error alongside the waiting message so
|
// The daemon is launched but RPC isn't answering yet. A *timeout* means it
|
||||||
// auth mismatches and timeouts aren't silently hidden.
|
// connected but the node is busy initializing (loading the block index, etc.);
|
||||||
if (!connectErr.empty()) {
|
// a connect refusal means it hasn't bound the RPC port yet. Either way, show a
|
||||||
char buf[256]; snprintf(buf, sizeof(buf), TR("sb_waiting_daemon_err"), connectErr.c_str());
|
// clear "node initializing" overlay (status + phase + block height from the
|
||||||
connection_status_ = buf;
|
// daemon's own console output) instead of a bare technical error.
|
||||||
} else {
|
const bool reachableButBusy = connectErr.find("Timeout") != std::string::npos;
|
||||||
connection_status_ = TR("sb_waiting_daemon");
|
applyDaemonInitStatus(reachableButBusy);
|
||||||
}
|
|
||||||
VERBOSE_LOGF("[connect #%d] RPC connection failed (%s) — daemon still starting, will retry...\n",
|
VERBOSE_LOGF("[connect #%d] RPC connection failed (%s) — daemon still starting, will retry...\n",
|
||||||
attempt, connectErr.c_str());
|
attempt, connectErr.c_str());
|
||||||
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core,
|
||||||
services::RefreshScheduler::kCoreDefault - 1.0f);
|
services::RefreshScheduler::kCoreDefault - 1.0f);
|
||||||
} else if (externalDetected) {
|
} else if (externalDetected) {
|
||||||
state_.connected = false;
|
state_.connected = false;
|
||||||
if (!connectErr.empty()) {
|
// An external daemon is on the RPC port but not answering. A timeout means it's
|
||||||
|
// up and busy initializing; surface that as the init overlay (we can't read its
|
||||||
|
// console since we didn't launch it, so no phase line — just a clear message).
|
||||||
|
if (connectErr.find("Timeout") != std::string::npos) {
|
||||||
|
applyDaemonInitStatus(/*reachableButBusy=*/true);
|
||||||
|
} else if (!connectErr.empty()) {
|
||||||
char buf[256]; snprintf(buf, sizeof(buf), TR("sb_connecting_err"), connectErr.c_str());
|
char buf[256]; snprintf(buf, sizeof(buf), TR("sb_connecting_err"), connectErr.c_str());
|
||||||
connection_status_ = buf;
|
connection_status_ = buf;
|
||||||
} else {
|
} else {
|
||||||
@@ -360,7 +438,7 @@ void App::tryConnect()
|
|||||||
onDisconnected("Connection failed");
|
onDisconnected("Connection failed");
|
||||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
|
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
|
||||||
|
|
||||||
if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) {
|
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
||||||
// Prevent infinite crash-restart loop
|
// Prevent infinite crash-restart loop
|
||||||
if (daemon_controller_ && daemon_controller_->crashCount() >= 3) {
|
if (daemon_controller_ && daemon_controller_->crashCount() >= 3) {
|
||||||
{ char buf[128]; snprintf(buf, sizeof(buf), TR("sb_daemon_crashed"), daemon_controller_->crashCount());
|
{ char buf[128]; snprintf(buf, sizeof(buf), TR("sb_daemon_crashed"), daemon_controller_->crashCount());
|
||||||
@@ -380,7 +458,7 @@ void App::tryConnect()
|
|||||||
daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)");
|
daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!use_embedded_daemon_) {
|
} else if (!isUsingEmbeddedDaemon()) {
|
||||||
VERBOSE_LOGF("[connect #%d] Embedded daemon disabled — external daemon at %s:%s not responding\n",
|
VERBOSE_LOGF("[connect #%d] Embedded daemon disabled — external daemon at %s:%s not responding\n",
|
||||||
attempt, config.host.c_str(), config.port.c_str());
|
attempt, config.host.c_str(), config.port.c_str());
|
||||||
} else {
|
} else {
|
||||||
@@ -396,6 +474,10 @@ void App::tryConnect()
|
|||||||
void App::onConnected()
|
void App::onConnected()
|
||||||
{
|
{
|
||||||
state_.connected = true;
|
state_.connected = true;
|
||||||
|
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
|
||||||
|
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
|
||||||
|
daemon_start_error_shown_ = false;
|
||||||
|
daemon_last_seen_crashes_ = 0; // (onConnected resets the daemon's crash count too)
|
||||||
connection_status_ = TR("connected");
|
connection_status_ = TR("connected");
|
||||||
|
|
||||||
// Reset crash counter on successful connection
|
// Reset crash counter on successful connection
|
||||||
@@ -481,6 +563,12 @@ void App::onDisconnected(const std::string& reason)
|
|||||||
last_tx_block_height_ = -1;
|
last_tx_block_height_ = -1;
|
||||||
pending_opids_.clear();
|
pending_opids_.clear();
|
||||||
pending_send_info_.clear();
|
pending_send_info_.clear();
|
||||||
|
// Resolve any deferred send callbacks so their UI doesn't spin forever on disconnect.
|
||||||
|
for (auto& entry : pending_send_callbacks_) {
|
||||||
|
if (entry.second) entry.second(false, reason);
|
||||||
|
}
|
||||||
|
pending_send_callbacks_.clear();
|
||||||
|
consecutive_core_failures_ = 0;
|
||||||
send_progress_active_ = false;
|
send_progress_active_ = false;
|
||||||
send_submissions_in_flight_ = 0;
|
send_submissions_in_flight_ = 0;
|
||||||
network_refresh_.resetJobs();
|
network_refresh_.resetJobs();
|
||||||
@@ -489,15 +577,72 @@ void App::onDisconnected(const std::string& reason)
|
|||||||
address_validation_cache_dirty_ = true;
|
address_validation_cache_dirty_ = true;
|
||||||
resetTransactionHistoryCacheSession();
|
resetTransactionHistoryCacheSession();
|
||||||
|
|
||||||
// Tear down the fast-lane connection
|
// Tear down the fast-lane connection. Signal abort first so a fast-lane call blocked in
|
||||||
|
// curl_easy_perform unblocks and stop()'s join() returns promptly (no UI freeze).
|
||||||
|
if (fast_rpc_) fast_rpc_->requestAbort();
|
||||||
if (fast_worker_) {
|
if (fast_worker_) {
|
||||||
fast_worker_->stop();
|
fast_worker_->stop();
|
||||||
|
// Drop the stopped worker so onConnected recreates and starts a fresh one. Keeping a
|
||||||
|
// stopped-but-present worker would defeat onConnected's `if (!fast_worker_)` guard, leaving
|
||||||
|
// the fast lane dead for the rest of the session — which silently stalls the opid poll
|
||||||
|
// (its post() never runs, so a completed send spins on "Waiting for operation").
|
||||||
|
fast_worker_.reset();
|
||||||
}
|
}
|
||||||
if (fast_rpc_) {
|
if (fast_rpc_) {
|
||||||
fast_rpc_->disconnect();
|
fast_rpc_->disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string App::applyDaemonInitStatus(bool reachableButBusy)
|
||||||
|
{
|
||||||
|
state_.daemon_initializing = true;
|
||||||
|
|
||||||
|
// Find the most recent console line that names an init phase, so we can tell the user exactly
|
||||||
|
// what the node is doing (loading the block index, verifying, activating best chain, …).
|
||||||
|
std::string phaseLine;
|
||||||
|
if (daemon_controller_) {
|
||||||
|
const auto lines = daemon_controller_->recentLines(40);
|
||||||
|
for (auto it = lines.rbegin(); it != lines.rend() && phaseLine.empty(); ++it) {
|
||||||
|
for (const char* phase : kDaemonInitPhases) {
|
||||||
|
if (it->find(phase) != std::string::npos) { phaseLine = *it; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WarmupText wt;
|
||||||
|
if (!phaseLine.empty()) {
|
||||||
|
wt = translateWarmup(phaseLine);
|
||||||
|
} else if (reachableButBusy) {
|
||||||
|
// The probe connected but got no RPC reply within the timeout: the node is up but busy
|
||||||
|
// initializing (it isn't printing a recognizable phase, or we didn't launch it).
|
||||||
|
wt = {"Starting DragonX node…",
|
||||||
|
"The node is reachable but still initializing and isn't answering yet. "
|
||||||
|
"This is normal after an update or on first launch — it can take a few minutes."};
|
||||||
|
} else {
|
||||||
|
// The daemon is launching but hasn't bound its RPC port yet.
|
||||||
|
wt = {"Starting DragonX node…",
|
||||||
|
"Launching dragonxd and waiting for it to come online…"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string title = wt.title;
|
||||||
|
const int h = daemon_controller_ ? daemon_controller_->lastBlockHeight() : -1;
|
||||||
|
if (h > 0) title += " (Block " + std::to_string(h) + ")";
|
||||||
|
|
||||||
|
state_.warmup_status = title;
|
||||||
|
state_.warmup_description = wt.description ? wt.description : "";
|
||||||
|
connection_status_ = title;
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::handleLostConnection(const std::string& reason)
|
||||||
|
{
|
||||||
|
DEBUG_LOGF("[Connection] %s — tearing down for reconnect\n", reason.c_str());
|
||||||
|
// Flip the main client's connected_ flag so update()'s else-branch re-enters
|
||||||
|
// tryConnect(). onDisconnected() alone only tears down the fast lane.
|
||||||
|
if (rpc_) rpc_->disconnect();
|
||||||
|
onDisconnected(reason);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Data Refresh — Tab-Aware Prioritized System
|
// Data Refresh — Tab-Aware Prioritized System
|
||||||
//
|
//
|
||||||
@@ -524,7 +669,14 @@ App::RefreshIntervals App::getIntervalsForPage(ui::NavPage page)
|
|||||||
|
|
||||||
void App::applyRefreshPolicy(ui::NavPage page)
|
void App::applyRefreshPolicy(ui::NavPage page)
|
||||||
{
|
{
|
||||||
network_refresh_.setIntervals(getIntervalsForPage(page));
|
// While the daemon is syncing, override the per-tab cadence with the low-impact sync profile so
|
||||||
|
// the wallet stops contending for the daemon's cs_main lock (frequent getpeerinfo / per-block
|
||||||
|
// transaction scans / balance polls slow block connection). This makes every tab sync as fast
|
||||||
|
// as the Console tab does today. Reverts to the per-tab profile once sync finishes.
|
||||||
|
refresh_policy_syncing_ = state_.sync.syncing;
|
||||||
|
network_refresh_.setIntervals(refresh_policy_syncing_
|
||||||
|
? services::RefreshScheduler::kSyncProfile
|
||||||
|
: getIntervalsForPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::currentPageNeedsWalletDataRefresh() const
|
bool App::currentPageNeedsWalletDataRefresh() const
|
||||||
@@ -545,13 +697,18 @@ bool App::shouldRunWalletTransactionRefresh() const
|
|||||||
|
|
||||||
void App::setCurrentPage(ui::NavPage page)
|
void App::setCurrentPage(ui::NavPage page)
|
||||||
{
|
{
|
||||||
|
if (!isPageEnabledForBuild(page)) {
|
||||||
|
page = ui::NavPage::Overview;
|
||||||
|
}
|
||||||
if (page == current_page_) return;
|
if (page == current_page_) return;
|
||||||
current_page_ = page;
|
current_page_ = page;
|
||||||
applyRefreshPolicy(page);
|
applyRefreshPolicy(page);
|
||||||
using RefreshTimer = services::NetworkRefreshService::Timer;
|
using RefreshTimer = services::NetworkRefreshService::Timer;
|
||||||
|
|
||||||
// Immediate refresh for the incoming tab's priority data
|
// Immediate refresh for the incoming tab's priority data. Gate on ACTUAL RPC connectivity
|
||||||
if (state_.connected && !state_.isLocked()) {
|
// (not state_.connected, which is the lite "online" proxy) — lite has no RPC daemon and the
|
||||||
|
// lite controller refreshes wallet data itself, so these full-node RPC polls must not fire.
|
||||||
|
if (rpc_ && rpc_->isConnected() && !state_.isLocked()) {
|
||||||
using NP = ui::NavPage;
|
using NP = ui::NavPage;
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case NP::Overview:
|
case NP::Overview:
|
||||||
@@ -584,6 +741,11 @@ void App::setCurrentPage(ui::NavPage page)
|
|||||||
|
|
||||||
bool App::shouldRefreshTransactions() const
|
bool App::shouldRefreshTransactions() const
|
||||||
{
|
{
|
||||||
|
// NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new
|
||||||
|
// block arrived (currentBlocks != last_tx_block_height_), the history was never fetched, or
|
||||||
|
// something marked it dirty (tab entry, a send, a reorg, etc.). The Transactions timer only
|
||||||
|
// controls how often this CHECK runs; between blocks the lightweight recent-poll
|
||||||
|
// (shouldRefreshRecentTransactions / TxAge) handles mempool + unconfirmed deltas instead.
|
||||||
const int currentBlocks = state_.sync.blocks;
|
const int currentBlocks = state_.sync.blocks;
|
||||||
return network_refresh_.shouldRefreshTransactions(last_tx_block_height_,
|
return network_refresh_.shouldRefreshTransactions(last_tx_block_height_,
|
||||||
currentBlocks,
|
currentBlocks,
|
||||||
@@ -713,6 +875,27 @@ void App::removePendingSendTransactions(const std::vector<std::string>& opids,
|
|||||||
state_.last_tx_update = std::time(nullptr);
|
state_.last_tx_update = std::time(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::trackOperation(const std::string& opid)
|
||||||
|
{
|
||||||
|
if (opid.empty()) return;
|
||||||
|
// Touched only from the main thread (sendTransaction's MainCb and the opid poller's
|
||||||
|
// MainCb both run via drainResults()), so no locking is needed.
|
||||||
|
if (std::find(pending_opids_.begin(), pending_opids_.end(), opid) != pending_opids_.end())
|
||||||
|
return;
|
||||||
|
pending_opids_.push_back(opid);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::invokeSendResultCallback(const std::string& opid, bool ok,
|
||||||
|
const std::string& result)
|
||||||
|
{
|
||||||
|
auto it = pending_send_callbacks_.find(opid);
|
||||||
|
if (it == pending_send_callbacks_.end()) return false;
|
||||||
|
auto cb = std::move(it->second);
|
||||||
|
pending_send_callbacks_.erase(it);
|
||||||
|
if (cb) cb(ok, result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances)
|
void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances)
|
||||||
{
|
{
|
||||||
for (const auto& [opid, pending] : pending_send_info_) {
|
for (const auto& [opid, pending] : pending_send_info_) {
|
||||||
@@ -792,6 +975,7 @@ void App::invalidateShieldedHistoryScanProgress(bool persistCache)
|
|||||||
{
|
{
|
||||||
shielded_history_scan_cursor_ = 0;
|
shielded_history_scan_cursor_ = 0;
|
||||||
shielded_history_scan_pending_ = false;
|
shielded_history_scan_pending_ = false;
|
||||||
|
initial_history_scan_complete_ = false; // a full re-scan is coming — show load progress again
|
||||||
shielded_history_scan_heights_.clear();
|
shielded_history_scan_heights_.clear();
|
||||||
if (persistCache) storeTransactionHistoryCacheIfAvailable();
|
if (persistCache) storeTransactionHistoryCacheIfAvailable();
|
||||||
}
|
}
|
||||||
@@ -925,7 +1109,9 @@ void App::refreshData()
|
|||||||
else if (!addressRefreshNeeded && walletDataPage && shouldRefreshRecentTransactions())
|
else if (!addressRefreshNeeded && walletDataPage && shouldRefreshRecentTransactions())
|
||||||
refreshRecentTransactionData();
|
refreshRecentTransactionData();
|
||||||
|
|
||||||
if (current_page_ == ui::NavPage::Peers)
|
// Always refresh peers here — refreshData() only runs on one-shot transitions (connect,
|
||||||
|
// warmup-complete, unlock), so this populates the status-bar peer count immediately on open
|
||||||
|
// regardless of the active tab. Ongoing updates come from the periodic Peers timer.
|
||||||
refreshPeerInfo();
|
refreshPeerInfo();
|
||||||
|
|
||||||
if (!state_.encryption_state_known &&
|
if (!state_.encryption_state_known &&
|
||||||
@@ -988,14 +1174,36 @@ void App::refreshCoreData()
|
|||||||
? fast_rpc_.get() : rpc_.get();
|
? fast_rpc_.get() : rpc_.get();
|
||||||
if (!w || !rpc) return;
|
if (!w || !rpc) return;
|
||||||
ui::NavPage tracePage = current_page_;
|
ui::NavPage tracePage = current_page_;
|
||||||
|
// Skip the balance call while syncing (it's incomplete anyway and takes the wallet lock +
|
||||||
|
// cs_main). Captured on the main thread to avoid reading state_ off the worker thread.
|
||||||
|
const bool includeBalance = !state_.sync.syncing;
|
||||||
|
|
||||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage]() -> rpc::RPCWorker::MainCb {
|
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc, tracePage, includeBalance]() -> rpc::RPCWorker::MainCb {
|
||||||
AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh"));
|
AppRefreshRpcGateway refreshRpc(*rpc, traceSource(tracePage, "Core refresh"));
|
||||||
auto result = NetworkRefreshService::collectCoreRefreshResult(refreshRpc);
|
auto result = NetworkRefreshService::collectCoreRefreshResult(refreshRpc, includeBalance);
|
||||||
return [this, result]() {
|
return [this, result]() {
|
||||||
try {
|
try {
|
||||||
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
|
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
|
||||||
applyPendingSendBalanceDeltas(true);
|
applyPendingSendBalanceDeltas(true);
|
||||||
|
|
||||||
|
// Mid-session connection-loss detection. During normal operation, both core
|
||||||
|
// RPCs failing together means the daemon connection is dead (a busy daemon
|
||||||
|
// fails them individually, not both at once). Warmup is excluded — both fail
|
||||||
|
// with -28 there legitimately, and counting it would cause a reconnect loop.
|
||||||
|
constexpr int kCoreFailuresBeforeDisconnect = 3;
|
||||||
|
if (!state_.warming_up) {
|
||||||
|
if (!result.balanceOk && !result.blockchainOk) {
|
||||||
|
if (++consecutive_core_failures_ >= kCoreFailuresBeforeDisconnect &&
|
||||||
|
state_.connected) {
|
||||||
|
consecutive_core_failures_ = 0;
|
||||||
|
handleLostConnection("Lost connection to daemon");
|
||||||
|
return; // state torn down — skip the rest of this callback
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consecutive_core_failures_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-shield transparent funds if enabled
|
// Auto-shield transparent funds if enabled
|
||||||
if (result.balanceOk && settings_ && settings_->getAutoShield() &&
|
if (result.balanceOk && settings_ && settings_->getAutoShield() &&
|
||||||
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||||
@@ -1007,20 +1215,38 @@ void App::refreshCoreData()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!targetZAddr.empty() && rpc_) {
|
if (!targetZAddr.empty() && worker_) {
|
||||||
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||||
state_.transparent_balance, targetZAddr.c_str());
|
state_.transparent_balance, targetZAddr.c_str());
|
||||||
rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50,
|
// Use the user-configured fee, formatted fixed-decimal so the daemon's
|
||||||
[this](const json& result) {
|
// ParseFixedPoint accepts it (a small double would serialize to "5e-05").
|
||||||
if (result.contains("opid")) {
|
const std::string feeStr =
|
||||||
DEBUG_LOGF("[AutoShield] Started: %s\n",
|
util::formatAmountFixed(settings_ ? settings_->getDefaultFee() : 0.0001);
|
||||||
result["opid"].get<std::string>().c_str());
|
// This callback runs on the UI thread (drainResults). Build/broadcast
|
||||||
|
// on the worker thread — never block the UI with synchronous RPC.
|
||||||
|
worker_->post([this, targetZAddr, feeStr]() -> rpc::RPCWorker::MainCb {
|
||||||
|
std::string opid;
|
||||||
|
std::string err;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Auto-shield / z_shieldcoinbase");
|
||||||
|
auto result = rpc_->call("z_shieldcoinbase",
|
||||||
|
{std::string("*"), targetZAddr, feeStr, 50});
|
||||||
|
opid = result.value("opid", "");
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
err = e.what();
|
||||||
}
|
}
|
||||||
|
return [this, opid, err]() {
|
||||||
auto_shield_pending_ = false;
|
auto_shield_pending_ = false;
|
||||||
},
|
if (!err.empty()) {
|
||||||
[this](const std::string& err) {
|
|
||||||
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||||
auto_shield_pending_ = false;
|
return;
|
||||||
|
}
|
||||||
|
if (!opid.empty()) {
|
||||||
|
DEBUG_LOGF("[AutoShield] Started: %s\n", opid.c_str());
|
||||||
|
// Surface the async result + refresh balances on completion.
|
||||||
|
trackOperation(opid);
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
auto_shield_pending_ = false;
|
auto_shield_pending_ = false;
|
||||||
@@ -1102,6 +1328,8 @@ void App::refreshTransactionData()
|
|||||||
transactionSnapshot.shieldedScanHeights = shielded_history_scan_heights_;
|
transactionSnapshot.shieldedScanHeights = shielded_history_scan_heights_;
|
||||||
transactionSnapshot.shieldedScanStartIndex = shielded_history_scan_cursor_;
|
transactionSnapshot.shieldedScanStartIndex = shielded_history_scan_cursor_;
|
||||||
transactionSnapshot.maxShieldedReceiveScans = shieldedReceiveScanBudget(current_page_);
|
transactionSnapshot.maxShieldedReceiveScans = shieldedReceiveScanBudget(current_page_);
|
||||||
|
transactionSnapshot.shieldedScanTipTolerance =
|
||||||
|
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
|
||||||
ui::NavPage tracePage = current_page_;
|
ui::NavPage tracePage = current_page_;
|
||||||
|
|
||||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
|
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
|
||||||
@@ -1128,6 +1356,7 @@ void App::refreshTransactionData()
|
|||||||
storeTransactionHistoryCacheIfAvailable();
|
storeTransactionHistoryCacheIfAvailable();
|
||||||
shielded_history_scan_cursor_ = nextShieldedScanStartIndex;
|
shielded_history_scan_cursor_ = nextShieldedScanStartIndex;
|
||||||
shielded_history_scan_pending_ = !shieldedScanComplete;
|
shielded_history_scan_pending_ = !shieldedScanComplete;
|
||||||
|
if (shieldedScanComplete) initial_history_scan_complete_ = true;
|
||||||
transactions_dirty_ = !shieldedScanComplete;
|
transactions_dirty_ = !shieldedScanComplete;
|
||||||
maybeFinishTransactionSendProgress();
|
maybeFinishTransactionSendProgress();
|
||||||
};
|
};
|
||||||
@@ -1150,6 +1379,8 @@ void App::refreshRecentTransactionData()
|
|||||||
transactionSnapshot.shieldedScanHeights = shielded_history_scan_heights_;
|
transactionSnapshot.shieldedScanHeights = shielded_history_scan_heights_;
|
||||||
transactionSnapshot.shieldedScanStartIndex = shielded_history_scan_cursor_;
|
transactionSnapshot.shieldedScanStartIndex = shielded_history_scan_cursor_;
|
||||||
transactionSnapshot.maxShieldedReceiveScans = 1;
|
transactionSnapshot.maxShieldedReceiveScans = 1;
|
||||||
|
transactionSnapshot.shieldedScanTipTolerance =
|
||||||
|
shieldedScanTipTolerance(transactionSnapshot.shieldedAddresses.size());
|
||||||
ui::NavPage tracePage = current_page_;
|
ui::NavPage tracePage = current_page_;
|
||||||
|
|
||||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
|
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks,
|
||||||
@@ -1251,6 +1482,12 @@ void App::refreshMiningInfo()
|
|||||||
bool doSlowRefresh = (mining_slow_counter_++ % 5 == 0);
|
bool doSlowRefresh = (mining_slow_counter_++ % 5 == 0);
|
||||||
bool includeLocalHashrate = state_.mining.generate || current_page_ == ui::NavPage::Mining;
|
bool includeLocalHashrate = state_.mining.generate || current_page_ == ui::NavPage::Mining;
|
||||||
if (!includeLocalHashrate && !doSlowRefresh) return;
|
if (!includeLocalHashrate && !doSlowRefresh) return;
|
||||||
|
|
||||||
|
// While syncing, don't poll getmininginfo (another cs_main contender) unless the user is
|
||||||
|
// actually on the Mining tab or mining — mining stats are irrelevant mid-sync, and this keeps
|
||||||
|
// the sync throttle complete (the mining poll runs off the separate 1s Fast timer, so the
|
||||||
|
// sync-profile intervals don't otherwise cover it).
|
||||||
|
if (state_.sync.syncing && !includeLocalHashrate) return;
|
||||||
ui::NavPage tracePage = current_page_;
|
ui::NavPage tracePage = current_page_;
|
||||||
|
|
||||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Mining, *w,
|
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Mining, *w,
|
||||||
@@ -1372,6 +1609,11 @@ void App::refreshMarketData()
|
|||||||
|
|
||||||
void App::startMining(int threads)
|
void App::startMining(int threads)
|
||||||
{
|
{
|
||||||
|
if (!supportsSoloMining()) {
|
||||||
|
(void)threads;
|
||||||
|
ui::Notifications::instance().warning("Solo mining is unavailable in lite build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!state_.connected || !rpc_ || !worker_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
|
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
|
||||||
|
|
||||||
@@ -1401,6 +1643,7 @@ void App::startMining(int threads)
|
|||||||
|
|
||||||
void App::stopMining()
|
void App::stopMining()
|
||||||
{
|
{
|
||||||
|
if (!supportsSoloMining()) return;
|
||||||
if (!state_.connected || !rpc_ || !worker_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
|
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
|
||||||
|
|
||||||
@@ -1426,6 +1669,12 @@ void App::stopMining()
|
|||||||
|
|
||||||
void App::startPoolMining(int threads)
|
void App::startPoolMining(int threads)
|
||||||
{
|
{
|
||||||
|
if (!supportsPoolMining()) {
|
||||||
|
(void)threads;
|
||||||
|
ui::Notifications::instance().warning("Pool mining is unavailable in this build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!xmrig_manager_)
|
if (!xmrig_manager_)
|
||||||
xmrig_manager_ = std::make_unique<daemon::XmrigManager>();
|
xmrig_manager_ = std::make_unique<daemon::XmrigManager>();
|
||||||
|
|
||||||
@@ -1505,28 +1754,58 @@ void App::stopPoolMining()
|
|||||||
|
|
||||||
void App::banPeer(const std::string& ip, int duration_seconds)
|
void App::banPeer(const std::string& ip, int duration_seconds)
|
||||||
{
|
{
|
||||||
if (!state_.connected || !rpc_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
// Run on the worker thread — these are called straight from the Peers tab's ImGui
|
||||||
rpc_->setBan(ip, "add", [this](const json&) {
|
// handlers, and rpc_->call() blocks on synchronous curl under curl_mutex_.
|
||||||
refreshPeerInfo();
|
worker_->post([this, ip, duration_seconds]() -> rpc::RPCWorker::MainCb {
|
||||||
}, nullptr, duration_seconds);
|
std::string err;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Peers / Ban");
|
||||||
|
rpc_->call("setban", {ip, "add", duration_seconds});
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
err = e.what();
|
||||||
|
}
|
||||||
|
return [this, err]() {
|
||||||
|
if (!err.empty()) ui::Notifications::instance().error("Ban failed: " + err);
|
||||||
|
else refreshPeerInfo();
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::unbanPeer(const std::string& ip)
|
void App::unbanPeer(const std::string& ip)
|
||||||
{
|
{
|
||||||
if (!state_.connected || !rpc_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
worker_->post([this, ip]() -> rpc::RPCWorker::MainCb {
|
||||||
rpc_->setBan(ip, "remove", [this](const json&) {
|
std::string err;
|
||||||
refreshPeerInfo();
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Peers / Unban");
|
||||||
|
rpc_->call("setban", {ip, "remove"});
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
err = e.what();
|
||||||
|
}
|
||||||
|
return [this, err]() {
|
||||||
|
if (!err.empty()) ui::Notifications::instance().error("Unban failed: " + err);
|
||||||
|
else refreshPeerInfo();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::clearBans()
|
void App::clearBans()
|
||||||
{
|
{
|
||||||
if (!state_.connected || !rpc_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||||
rpc_->clearBanned([this](const json&) {
|
std::string err;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Peers / Clear bans");
|
||||||
|
rpc_->call("clearbanned", nlohmann::json::array());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
err = e.what();
|
||||||
|
}
|
||||||
|
return [this, err]() {
|
||||||
|
if (!err.empty()) { ui::Notifications::instance().error("Clear bans failed: " + err); return; }
|
||||||
state_.banned_peers.clear();
|
state_.banned_peers.clear();
|
||||||
|
refreshPeerInfo();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1588,6 +1867,23 @@ void App::applyDefaultBanlist()
|
|||||||
|
|
||||||
void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
||||||
{
|
{
|
||||||
|
// Lite build: derive locally via the controller (fast, no network). The backend auto-saves
|
||||||
|
// new addresses; the next lite refresh lists it with a balance.
|
||||||
|
if (lite_wallet_) {
|
||||||
|
const auto result = lite_wallet_->newAddress(/*shielded*/ true);
|
||||||
|
if (result.ok) {
|
||||||
|
AddressInfo info;
|
||||||
|
info.address = result.address;
|
||||||
|
info.type = "shielded";
|
||||||
|
info.balance = 0.0;
|
||||||
|
state_.z_addresses.push_back(info);
|
||||||
|
state_.addresses.push_back(info);
|
||||||
|
address_list_dirty_ = true;
|
||||||
|
}
|
||||||
|
if (callback) callback(result.ok ? result.address : std::string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!state_.connected || !rpc_ || !worker_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
|
||||||
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
|
||||||
@@ -1619,6 +1915,22 @@ void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
|||||||
|
|
||||||
void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
||||||
{
|
{
|
||||||
|
// Lite build: derive locally via the controller (see createNewZAddress).
|
||||||
|
if (lite_wallet_) {
|
||||||
|
const auto result = lite_wallet_->newAddress(/*shielded*/ false);
|
||||||
|
if (result.ok) {
|
||||||
|
AddressInfo info;
|
||||||
|
info.address = result.address;
|
||||||
|
info.type = "transparent";
|
||||||
|
info.balance = 0.0;
|
||||||
|
state_.t_addresses.push_back(info);
|
||||||
|
state_.addresses.push_back(info);
|
||||||
|
address_list_dirty_ = true;
|
||||||
|
}
|
||||||
|
if (callback) callback(result.ok ? result.address : std::string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!state_.connected || !rpc_ || !worker_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
|
||||||
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
|
||||||
@@ -1757,15 +2069,26 @@ bool App::isMiningAddress(const std::string& addr) const
|
|||||||
|
|
||||||
void App::setMiningAddress(const std::string& addr, bool mining)
|
void App::setMiningAddress(const std::string& addr, bool mining)
|
||||||
{
|
{
|
||||||
if (settings_) {
|
if (!settings_) return;
|
||||||
settings_->setMiningAddress(addr, mining);
|
settings_->setMiningAddress(addr, mining);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
invalidateShieldedHistoryScanProgress(true);
|
|
||||||
transactions_dirty_ = true;
|
// "mined" vs "receive" is a pure function of the LOCAL mining-address set — the daemon knows
|
||||||
last_tx_block_height_ = -1;
|
// nothing about it, so there is NO need to re-scan the chain. Relabel the affected rows in the
|
||||||
network_refresh_.markDue(services::NetworkRefreshService::Timer::Transactions);
|
// in-memory history directly and persist them to the (SQLite) history cache. This is instant,
|
||||||
|
// with no daemon round-trip; the History tab's display cache rebuilds on the type change.
|
||||||
|
const auto miningAddrs = settings_->getMiningAddresses();
|
||||||
|
bool changed = false;
|
||||||
|
for (auto& tx : state_.transactions) {
|
||||||
|
if (tx.address.empty() || (tx.type != "receive" && tx.type != "mined")) continue;
|
||||||
|
std::string newType = miningAddrs.count(tx.address) ? "mined" : "receive";
|
||||||
|
if (tx.type != newType) {
|
||||||
|
tx.type = std::move(newType);
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changed) storeTransactionHistoryCacheIfAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
void App::invalidateAddressValidationCache()
|
void App::invalidateAddressValidationCache()
|
||||||
{
|
{
|
||||||
@@ -1780,33 +2103,35 @@ void App::invalidateAddressValidationCache()
|
|||||||
|
|
||||||
void App::exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback)
|
void App::exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback)
|
||||||
{
|
{
|
||||||
if (!state_.connected || !rpc_) {
|
if (!state_.connected || !rpc_ || !worker_) {
|
||||||
if (callback) callback("");
|
if (callback) callback("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto keyKind = services::WalletSecurityController::classifyAddress(address);
|
const bool shielded = services::WalletSecurityController::classifyAddress(address)
|
||||||
if (keyKind == services::WalletSecurityController::KeyKind::Shielded) {
|
== services::WalletSecurityController::KeyKind::Shielded;
|
||||||
// Z-address: use z_exportkey
|
const char* method = shielded ? "z_exportkey" : "dumpprivkey";
|
||||||
|
// Run on the worker thread — z_exportkey/dumpprivkey block on synchronous curl and
|
||||||
|
// are invoked straight from the export dialog (UI thread).
|
||||||
|
worker_->post([this, method, address, callback]() -> rpc::RPCWorker::MainCb {
|
||||||
|
std::string key;
|
||||||
|
std::string err;
|
||||||
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Settings / Export private key");
|
rpc::RPCClient::TraceScope trace("Settings / Export private key");
|
||||||
rpc_->z_exportKey(address, [callback](const json& result) {
|
key = rpc_->call(method, {address}).get<std::string>();
|
||||||
if (callback) callback(result.get<std::string>());
|
} catch (const std::exception& e) {
|
||||||
}, [callback](const std::string& error) {
|
err = e.what();
|
||||||
DEBUG_LOGF("Export z-key error: %s\n", error.c_str());
|
|
||||||
ui::Notifications::instance().error("Key export failed: " + error);
|
|
||||||
if (callback) callback("");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// T-address: use dumpprivkey
|
|
||||||
rpc::RPCClient::TraceScope trace("Settings / Export private key");
|
|
||||||
rpc_->dumpPrivKey(address, [callback](const json& result) {
|
|
||||||
if (callback) callback(result.get<std::string>());
|
|
||||||
}, [callback](const std::string& error) {
|
|
||||||
DEBUG_LOGF("Export t-key error: %s\n", error.c_str());
|
|
||||||
ui::Notifications::instance().error("Key export failed: " + error);
|
|
||||||
if (callback) callback("");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return [callback, key, err]() {
|
||||||
|
if (!err.empty()) {
|
||||||
|
DEBUG_LOGF("Export key error: %s\n", err.c_str());
|
||||||
|
ui::Notifications::instance().error("Key export failed: " + err);
|
||||||
|
if (callback) callback("");
|
||||||
|
} else if (callback) {
|
||||||
|
callback(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::exportAllKeys(std::function<void(const std::string&)> callback)
|
void App::exportAllKeys(std::function<void(const std::string&)> callback)
|
||||||
@@ -1860,34 +2185,36 @@ void App::exportAllKeys(std::function<void(const std::string&)> callback)
|
|||||||
|
|
||||||
void App::importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback)
|
void App::importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback)
|
||||||
{
|
{
|
||||||
if (!state_.connected || !rpc_) {
|
if (!state_.connected || !rpc_ || !worker_) {
|
||||||
if (callback) callback(false, "Not connected");
|
if (callback) callback(false, "Not connected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto keyKind = services::WalletSecurityController::classifyPrivateKey(key);
|
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
|
||||||
|
== services::WalletSecurityController::KeyKind::Shielded;
|
||||||
if (keyKind == services::WalletSecurityController::KeyKind::Shielded) {
|
// Run on the worker thread — import requests a full rescan (rescan=true), so the
|
||||||
|
// synchronous curl call can take many seconds; never block the UI thread on it.
|
||||||
|
worker_->post([this, key, shielded, callback]() -> rpc::RPCWorker::MainCb {
|
||||||
|
std::string err;
|
||||||
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
||||||
rpc_->z_importKey(key, true, [this, callback](const json& result) {
|
if (shielded) rpc_->call("z_importkey", {key, "yes"}); // rescan
|
||||||
invalidateAddressValidationCache();
|
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan
|
||||||
refreshAddresses();
|
} catch (const std::exception& e) {
|
||||||
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
err = e.what();
|
||||||
services::WalletSecurityController::KeyKind::Shielded));
|
|
||||||
}, [callback](const std::string& error) {
|
|
||||||
if (callback) callback(false, error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
|
||||||
rpc_->importPrivKey(key, true, [this, callback](const json& result) {
|
|
||||||
invalidateAddressValidationCache();
|
|
||||||
refreshAddresses();
|
|
||||||
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
|
||||||
services::WalletSecurityController::KeyKind::Transparent));
|
|
||||||
}, [callback](const std::string& error) {
|
|
||||||
if (callback) callback(false, error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return [this, shielded, err, callback]() {
|
||||||
|
if (!err.empty()) {
|
||||||
|
if (callback) callback(false, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invalidateAddressValidationCache();
|
||||||
|
refreshAddresses();
|
||||||
|
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
||||||
|
shielded ? services::WalletSecurityController::KeyKind::Shielded
|
||||||
|
: services::WalletSecurityController::KeyKind::Transparent));
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback)
|
void App::backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback)
|
||||||
@@ -1927,11 +2254,38 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
double amount, double fee, const std::string& memo,
|
double amount, double fee, const std::string& memo,
|
||||||
std::function<void(bool success, const std::string& result)> callback)
|
std::function<void(bool success, const std::string& result)> callback)
|
||||||
{
|
{
|
||||||
|
// Lite build: route to the controller's async broadcast. `from`/`fee` are ignored — the
|
||||||
|
// backend selects inputs and adds the network fee itself. The result (txid/error) is
|
||||||
|
// delivered to `callback` from update() once takeBroadcastResult() yields it.
|
||||||
|
if (lite_wallet_) {
|
||||||
|
wallet::LiteSendRequest req;
|
||||||
|
wallet::LiteSendRecipient recipient;
|
||||||
|
recipient.address = to;
|
||||||
|
recipient.amountZatoshis = static_cast<std::uint64_t>(std::llround(amount * 100000000.0));
|
||||||
|
recipient.memo = memo;
|
||||||
|
req.recipients.push_back(std::move(recipient));
|
||||||
|
if (!lite_wallet_->sendTransaction(req)) {
|
||||||
|
if (callback) callback(false, "A send is already in progress, or no wallet is open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lite_send_callback_ = std::move(callback); // delivered from update()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!state_.connected || !rpc_) {
|
if (!state_.connected || !rpc_) {
|
||||||
if (callback) callback(false, "Not connected");
|
if (callback) callback(false, "Not connected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single-flight guard: a rapid double-click (or any second caller) must not issue two
|
||||||
|
// z_sendmany calls before the first returns its opid. The send form already guards this in the
|
||||||
|
// UI, but the controller entry point must not depend on that. (send_submissions_in_flight_ is
|
||||||
|
// main-thread only: ++ here, -- in the worker's main-thread result callback.)
|
||||||
|
if (send_submissions_in_flight_ > 0) {
|
||||||
|
if (callback) callback(false, "A transaction is already being submitted — please wait.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check that we have the spending key for the from address
|
// Check that we have the spending key for the from address
|
||||||
if (!from.empty() && from[0] == 'z') {
|
if (!from.empty() && from[0] == 'z') {
|
||||||
bool spendable = false;
|
bool spendable = false;
|
||||||
@@ -1966,12 +2320,18 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
|
|
||||||
send_progress_active_ = true;
|
send_progress_active_ = true;
|
||||||
++send_submissions_in_flight_;
|
++send_submissions_in_flight_;
|
||||||
worker_->post([this, from, to, amount, memo, recipients, callback]() -> rpc::RPCWorker::MainCb {
|
// z_sendmany signature is (fromaddress, amounts, minconf, fee). The fee MUST be a JSON number:
|
||||||
|
// the daemon reads it with get_real(), so a string is rejected ("JSON value is not a number as
|
||||||
|
// expected"). get_real() parses the double directly and accepts any number notation (incl. the
|
||||||
|
// "5e-05" form of a small fee), so passing the raw double is correct for every fee value.
|
||||||
|
// (The recipient "amount" above is intentionally a fixed-decimal STRING — that field is parsed
|
||||||
|
// with ParseFixedPoint, which a scientific-notation double would break.)
|
||||||
|
worker_->post([this, from, to, amount, fee, memo, recipients, callback]() -> rpc::RPCWorker::MainCb {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
std::string result_str;
|
std::string result_str;
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Send tab / Submit transaction");
|
rpc::RPCClient::TraceScope trace("Send tab / Submit transaction");
|
||||||
auto result = rpc_->call("z_sendmany", {from, recipients});
|
auto result = rpc_->call("z_sendmany", {from, recipients, 1, fee});
|
||||||
result_str = result.get<std::string>();
|
result_str = result.get<std::string>();
|
||||||
ok = true;
|
ok = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -1986,15 +2346,21 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
transactions_dirty_ = true;
|
transactions_dirty_ = true;
|
||||||
last_tx_block_height_ = -1;
|
last_tx_block_height_ = -1;
|
||||||
network_refresh_.markWalletMutationRefresh();
|
network_refresh_.markWalletMutationRefresh();
|
||||||
// Track the opid so we can poll for completion
|
// z_sendmany only returned an opid: the transaction is built/signed/
|
||||||
|
// broadcast asynchronously by the daemon. Defer the user-facing
|
||||||
|
// success/failure to the opid poller (app.cpp) so we don't report
|
||||||
|
// "sent successfully" for an operation that may still fail.
|
||||||
if (!result_str.empty()) {
|
if (!result_str.empty()) {
|
||||||
pending_opids_.push_back(result_str);
|
pending_opids_.push_back(result_str);
|
||||||
upsertPendingSendTransaction(result_str, from, to, amount, memo);
|
upsertPendingSendTransaction(result_str, from, to, amount, memo);
|
||||||
|
if (callback) pending_send_callbacks_[result_str] = callback;
|
||||||
|
} else if (callback) {
|
||||||
|
callback(true, result_str); // no opid to track — report as-is
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
send_progress_active_ = false;
|
send_progress_active_ = false;
|
||||||
|
if (callback) callback(false, result_str);
|
||||||
}
|
}
|
||||||
if (callback) callback(ok, result_str);
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ void App::checkIdleMining() {
|
|||||||
if (idle_mining_active_) {
|
if (idle_mining_active_) {
|
||||||
idle_mining_active_ = false;
|
idle_mining_active_ = false;
|
||||||
idle_scaled_to_idle_ = false;
|
idle_scaled_to_idle_ = false;
|
||||||
if (settings_ && settings_->getPoolMode()) {
|
if (settings_ && (settings_->getPoolMode() || !supportsSoloMining())) {
|
||||||
if (xmrig_manager_ && xmrig_manager_->isRunning())
|
if (xmrig_manager_ && xmrig_manager_->isRunning())
|
||||||
stopPoolMining();
|
stopPoolMining();
|
||||||
} else {
|
} else {
|
||||||
@@ -634,7 +634,8 @@ void App::checkIdleMining() {
|
|||||||
|
|
||||||
int idleSec = util::Platform::getSystemIdleSeconds();
|
int idleSec = util::Platform::getSystemIdleSeconds();
|
||||||
int delay = settings_->getMineIdleDelay();
|
int delay = settings_->getMineIdleDelay();
|
||||||
bool isPool = settings_->getPoolMode();
|
bool isPool = settings_->getPoolMode() || !supportsSoloMining();
|
||||||
|
if (isPool && !supportsPoolMining()) return;
|
||||||
bool threadScaling = settings_->getIdleThreadScaling();
|
bool threadScaling = settings_->getIdleThreadScaling();
|
||||||
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ WizardUiState s_wizardUi;
|
|||||||
|
|
||||||
void App::restartWizard()
|
void App::restartWizard()
|
||||||
{
|
{
|
||||||
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
|
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
||||||
|
|
||||||
// Reset crash counter for fresh wizard attempt
|
// Reset crash counter for fresh wizard attempt
|
||||||
@@ -804,6 +809,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
@@ -812,6 +818,7 @@ void App::renderFirstRunWizard() {
|
|||||||
bootstrap_->start(dataDir);
|
bootstrap_->start(dataDir);
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
@@ -985,6 +992,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
@@ -993,6 +1001,7 @@ void App::renderFirstRunWizard() {
|
|||||||
bootstrap_->start(dataDir);
|
bootstrap_->start(dataDir);
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
@@ -1002,6 +1011,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||||
stopDaemonForBootstrap();
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
@@ -1010,6 +1020,7 @@ void App::renderFirstRunWizard() {
|
|||||||
bootstrap_->start(dataDir, mirrorUrl);
|
bootstrap_->start(dataDir, mirrorUrl);
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,586 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef DRAGONX_ENABLE_CHAT
|
||||||
|
#define DRAGONX_ENABLE_CHAT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
enum class HushChatHeaderType {
|
||||||
|
Message,
|
||||||
|
ContactRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatHeader {
|
||||||
|
int header_number = 0;
|
||||||
|
int version = 0;
|
||||||
|
std::string reply_zaddr;
|
||||||
|
std::string conversation_id;
|
||||||
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string secretstream_header_hex;
|
||||||
|
std::string public_key_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatHeaderParseResult {
|
||||||
|
bool ok = false;
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoOutput {
|
||||||
|
std::size_t position = 0;
|
||||||
|
std::string memo;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoPair {
|
||||||
|
HushChatHeader header;
|
||||||
|
std::size_t header_position = 0;
|
||||||
|
std::size_t payload_position = 0;
|
||||||
|
std::string payload_memo;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatMemoGroupingIssue {
|
||||||
|
InvalidHeader,
|
||||||
|
MissingPayload,
|
||||||
|
DuplicateHeader,
|
||||||
|
OversizedMemo
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoGroupingIssueInfo {
|
||||||
|
HushChatMemoGroupingIssue issue = HushChatMemoGroupingIssue::InvalidHeader;
|
||||||
|
std::size_t position = 0;
|
||||||
|
std::string detail;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoGroupingResult {
|
||||||
|
std::vector<HushChatMemoPair> pairs;
|
||||||
|
std::vector<HushChatMemoGroupingIssueInfo> issues;
|
||||||
|
std::size_t ignored_memo_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatTransactionInput {
|
||||||
|
std::string txid;
|
||||||
|
std::vector<HushChatMemoOutput> outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatTransactionMetadata {
|
||||||
|
std::string txid;
|
||||||
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string conversation_id;
|
||||||
|
std::string reply_zaddr;
|
||||||
|
std::size_t header_position = 0;
|
||||||
|
std::size_t payload_position = 0;
|
||||||
|
std::size_t payload_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatTransactionExtractionResult {
|
||||||
|
bool feature_enabled = false;
|
||||||
|
std::vector<HushChatTransactionMetadata> metadata;
|
||||||
|
std::vector<HushChatMemoGroupingIssueInfo> issues;
|
||||||
|
std::size_t ignored_memo_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatDecryptPreflightError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
NonMessageHeader,
|
||||||
|
InvalidHeaderNumber,
|
||||||
|
UnsupportedVersion,
|
||||||
|
MissingReplyAddress,
|
||||||
|
MissingConversationId,
|
||||||
|
InvalidSecretstreamHeader,
|
||||||
|
InvalidPublicKey,
|
||||||
|
EmptyCiphertext,
|
||||||
|
OversizedCiphertext,
|
||||||
|
OddLengthCiphertext,
|
||||||
|
InvalidCiphertextHex,
|
||||||
|
TruncatedCiphertext
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptPreflightInput {
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string ciphertext_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptPreflightResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::size_t ciphertext_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatHexDecodeError {
|
||||||
|
None,
|
||||||
|
Empty,
|
||||||
|
OddLength,
|
||||||
|
InvalidHex,
|
||||||
|
UnexpectedByteLength
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatHexDecodeResult {
|
||||||
|
bool ok = false;
|
||||||
|
HushChatHexDecodeError error = HushChatHexDecodeError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatDecryptDirection {
|
||||||
|
Incoming,
|
||||||
|
Outgoing
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatSessionKeySelection {
|
||||||
|
ClientRx,
|
||||||
|
ServerTx
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatDecryptInputError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
InvalidStoredChatKey,
|
||||||
|
DecryptPreflightFailed,
|
||||||
|
InvalidPeerPublicKey,
|
||||||
|
InvalidStreamHeader,
|
||||||
|
InvalidCiphertext
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptInputMaterial {
|
||||||
|
std::string stored_chat_key_hex;
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string ciphertext_hex;
|
||||||
|
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||||
|
std::string peer_public_key_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatPreparedDecryptInput {
|
||||||
|
std::vector<unsigned char> stored_chat_key_bytes;
|
||||||
|
std::vector<unsigned char> seed_bytes;
|
||||||
|
std::vector<unsigned char> peer_public_key_bytes;
|
||||||
|
std::vector<unsigned char> stream_header_bytes;
|
||||||
|
std::vector<unsigned char> ciphertext_bytes;
|
||||||
|
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||||
|
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||||
|
std::size_t plaintext_capacity = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptInputPreparationResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatDecryptInputError error = HushChatDecryptInputError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||||
|
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
|
||||||
|
HushChatPreparedDecryptInput prepared;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptFixtureReadinessResult {
|
||||||
|
bool ready = false;
|
||||||
|
std::size_t stored_chat_key_size = 0;
|
||||||
|
std::size_t seed_size = 0;
|
||||||
|
std::size_t peer_public_key_size = 0;
|
||||||
|
std::size_t stream_header_size = 0;
|
||||||
|
std::size_t ciphertext_size = 0;
|
||||||
|
std::size_t plaintext_capacity = 0;
|
||||||
|
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
MissingFixtureId,
|
||||||
|
InvalidLocalPublicKey,
|
||||||
|
InvalidPeerPublicKey,
|
||||||
|
InvalidHeaderMemo,
|
||||||
|
InvalidMemoPair,
|
||||||
|
NonMemoHeader,
|
||||||
|
HeaderPublicKeyMismatch,
|
||||||
|
DecryptInputFailed,
|
||||||
|
NotFixtureReady,
|
||||||
|
ExpectedStoredChatKeyLengthMismatch,
|
||||||
|
ExpectedSeedLengthMismatch,
|
||||||
|
ExpectedLocalPublicKeyLengthMismatch,
|
||||||
|
ExpectedPeerPublicKeyLengthMismatch,
|
||||||
|
ExpectedStreamHeaderLengthMismatch,
|
||||||
|
ExpectedCiphertextLengthMismatch,
|
||||||
|
ExpectedPlaintextLengthMismatch,
|
||||||
|
ExpectedRoleMismatch,
|
||||||
|
InvalidPlaintextHash
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixture {
|
||||||
|
std::string fixture_id;
|
||||||
|
std::string stored_chat_key_hex;
|
||||||
|
std::string local_public_key_hex;
|
||||||
|
std::string peer_public_key_hex;
|
||||||
|
std::string header_memo;
|
||||||
|
std::string ciphertext_memo;
|
||||||
|
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||||
|
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||||
|
std::size_t expected_stored_chat_key_size = 32;
|
||||||
|
std::size_t expected_seed_size = 32;
|
||||||
|
std::size_t expected_local_public_key_size = 32;
|
||||||
|
std::size_t expected_peer_public_key_size = 32;
|
||||||
|
std::size_t expected_stream_header_size = 24;
|
||||||
|
std::size_t expected_ciphertext_size = 0;
|
||||||
|
std::size_t expected_plaintext_size = 0;
|
||||||
|
std::string expected_plaintext_hash_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureVerificationResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||||
|
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
|
||||||
|
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
|
||||||
|
HushChatHeader header;
|
||||||
|
HushChatDecryptInputPreparationResult preparation;
|
||||||
|
HushChatDecryptFixtureReadinessResult readiness;
|
||||||
|
std::size_t local_public_key_size = 0;
|
||||||
|
std::size_t peer_public_key_size = 0;
|
||||||
|
std::size_t plaintext_hash_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureKind {
|
||||||
|
IncomingMemo,
|
||||||
|
OutgoingMemo,
|
||||||
|
SeedPublicKeyProjection,
|
||||||
|
CorruptedAuthFailure,
|
||||||
|
ContactExclusion
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureFileStatus {
|
||||||
|
Pending,
|
||||||
|
Ready
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureFileError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
InvalidJson,
|
||||||
|
JsonNotObject,
|
||||||
|
InvalidSchema,
|
||||||
|
MissingKind,
|
||||||
|
UnknownKind,
|
||||||
|
MissingStatus,
|
||||||
|
UnknownStatus,
|
||||||
|
MissingFixtureId,
|
||||||
|
MissingPendingReason,
|
||||||
|
MissingFixtureObject,
|
||||||
|
InvalidFixtureField,
|
||||||
|
FixtureVerificationFailed,
|
||||||
|
ContactFixtureNotExcluded,
|
||||||
|
FileReadFailed
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureFile {
|
||||||
|
std::string schema;
|
||||||
|
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
|
||||||
|
std::string fixture_id;
|
||||||
|
std::string pending_reason;
|
||||||
|
HushChatCompatibilityFixture fixture;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureFileParseResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool verified = false;
|
||||||
|
bool excluded_from_decrypt = false;
|
||||||
|
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatCompatibilityFixtureFile file;
|
||||||
|
HushChatCompatibilityFixtureVerificationResult verification;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatSeedPublicKeyProjectionError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
MissingFixtureId,
|
||||||
|
InvalidStoredChatKey,
|
||||||
|
InvalidLocalPublicKey,
|
||||||
|
ExpectedStoredChatKeyLengthMismatch,
|
||||||
|
ExpectedSeedLengthMismatch,
|
||||||
|
ExpectedLocalPublicKeyLengthMismatch,
|
||||||
|
SodiumInitializationFailed,
|
||||||
|
KeypairProjectionFailed,
|
||||||
|
ProjectedPublicKeyMismatch
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatSeedPublicKeyProjectionResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||||
|
std::size_t stored_chat_key_size = 0;
|
||||||
|
std::size_t seed_size = 0;
|
||||||
|
std::size_t local_public_key_size = 0;
|
||||||
|
std::size_t projected_public_key_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCorruptedAuthFailureReadinessError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
FixturePending,
|
||||||
|
WrongFixtureKind,
|
||||||
|
FixtureNotVerified,
|
||||||
|
SeedProjectionNotVerified
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCorruptedAuthFailureReadinessResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool structurally_ready_for_future_auth_check = false;
|
||||||
|
bool requires_future_secretstream_auth_failure = false;
|
||||||
|
bool decrypted = false;
|
||||||
|
bool authenticated = false;
|
||||||
|
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureImportError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
MissingRequiredKind,
|
||||||
|
DuplicateKind,
|
||||||
|
FixtureLoadFailed,
|
||||||
|
FixtureKindMismatch,
|
||||||
|
FixturePending,
|
||||||
|
FixtureInvalid,
|
||||||
|
FixtureNotVerified,
|
||||||
|
SeedProjectionFailed,
|
||||||
|
AuthFailureScaffoldFailed,
|
||||||
|
ContactFixtureNotExcluded
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureImportCandidate {
|
||||||
|
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureImportItem {
|
||||||
|
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string path;
|
||||||
|
bool supplied = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool replacement_eligible = false;
|
||||||
|
bool seed_projection_verified = false;
|
||||||
|
bool future_auth_failure_required = false;
|
||||||
|
bool structurally_ready_for_future_auth_check = false;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatCompatibilityFixtureFileParseResult parsed;
|
||||||
|
HushChatSeedPublicKeyProjectionResult seed_projection;
|
||||||
|
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureImportChecklistResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool replacement_ready = false;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::size_t required_count = 0;
|
||||||
|
std::size_t supplied_count = 0;
|
||||||
|
std::size_t missing_count = 0;
|
||||||
|
std::size_t pending_count = 0;
|
||||||
|
std::size_t verified_count = 0;
|
||||||
|
std::size_t seed_projection_verified_count = 0;
|
||||||
|
std::size_t future_auth_failure_required_count = 0;
|
||||||
|
std::size_t auth_failure_structural_ready_count = 0;
|
||||||
|
std::size_t excluded_count = 0;
|
||||||
|
std::size_t rejected_count = 0;
|
||||||
|
std::vector<HushChatCompatibilityFixtureImportItem> items;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureReplacementReportItem {
|
||||||
|
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string path;
|
||||||
|
bool supplied = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool replacement_eligible = false;
|
||||||
|
bool refused = true;
|
||||||
|
bool seed_projection_verified = false;
|
||||||
|
bool future_auth_failure_required = false;
|
||||||
|
bool structurally_ready_for_future_auth_check = false;
|
||||||
|
bool cont_excluded = false;
|
||||||
|
bool decrypted = false;
|
||||||
|
bool authenticated = false;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureReplacementDryRunResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool dry_run_only = true;
|
||||||
|
bool redacted_report = true;
|
||||||
|
bool would_replace = false;
|
||||||
|
bool replacement_refused = true;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::size_t required_count = 0;
|
||||||
|
std::size_t supplied_count = 0;
|
||||||
|
std::size_t missing_count = 0;
|
||||||
|
std::size_t pending_count = 0;
|
||||||
|
std::size_t verified_count = 0;
|
||||||
|
std::size_t seed_projection_verified_count = 0;
|
||||||
|
std::size_t future_auth_failure_required_count = 0;
|
||||||
|
std::size_t auth_failure_structural_ready_count = 0;
|
||||||
|
std::size_t excluded_count = 0;
|
||||||
|
std::size_t rejected_count = 0;
|
||||||
|
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCaptureManifestError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
FileReadFailed,
|
||||||
|
InvalidJson,
|
||||||
|
JsonNotObject,
|
||||||
|
InvalidSchema,
|
||||||
|
MissingManifestId,
|
||||||
|
MissingStatus,
|
||||||
|
UnknownStatus,
|
||||||
|
MissingFixtureDirectory,
|
||||||
|
MissingDryRunCommand,
|
||||||
|
InvalidDryRunCommand,
|
||||||
|
MissingProvenance,
|
||||||
|
MissingSourceClient,
|
||||||
|
InvalidSourceClient,
|
||||||
|
MissingSourceClientVersion,
|
||||||
|
MissingCaptureDate,
|
||||||
|
MissingNetwork,
|
||||||
|
MissingCaptureMethod,
|
||||||
|
MissingHandling,
|
||||||
|
MissingHandlingFlag,
|
||||||
|
HandlingFlagNotTrue,
|
||||||
|
MissingCategories,
|
||||||
|
InvalidCategoryEntry,
|
||||||
|
UnknownCategory,
|
||||||
|
DuplicateCategory,
|
||||||
|
MissingRequiredCategory,
|
||||||
|
ProhibitedFieldPresent
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCaptureManifestStatus {
|
||||||
|
Staged
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCaptureManifestCategoryReport {
|
||||||
|
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string staged_filename;
|
||||||
|
bool declared = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCaptureManifestValidationResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool redacted_report = true;
|
||||||
|
bool validates_provenance_only = true;
|
||||||
|
bool no_sensitive_material_declared = false;
|
||||||
|
bool has_dry_run_command = false;
|
||||||
|
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
|
||||||
|
std::string manifest_path;
|
||||||
|
std::string fixture_directory;
|
||||||
|
std::size_t required_count = 0;
|
||||||
|
std::size_t declared_count = 0;
|
||||||
|
std::size_t missing_count = 0;
|
||||||
|
std::size_t duplicate_count = 0;
|
||||||
|
std::size_t prohibited_field_count = 0;
|
||||||
|
std::size_t handling_flag_count = 0;
|
||||||
|
std::vector<HushChatCaptureManifestCategoryReport> categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int kHushChatSupportedVersion = 0;
|
||||||
|
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
||||||
|
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
||||||
|
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
|
||||||
|
constexpr std::size_t kHushChatSecretstreamABytes = 17;
|
||||||
|
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
|
||||||
|
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
|
||||||
|
constexpr std::size_t kHushChatSeedByteLength = 32;
|
||||||
|
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
|
||||||
|
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
|
||||||
|
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
|
||||||
|
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
|
||||||
|
|
||||||
|
constexpr bool hushChatFeatureEnabledAtBuild()
|
||||||
|
{
|
||||||
|
return DRAGONX_ENABLE_CHAT != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo);
|
||||||
|
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs);
|
||||||
|
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||||
|
const HushChatTransactionInput& transaction,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
|
||||||
|
const HushChatDecryptPreflightInput& input,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
|
||||||
|
std::size_t expectedByteLength);
|
||||||
|
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
|
||||||
|
const HushChatDecryptInputMaterial& material,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
|
||||||
|
const HushChatPreparedDecryptInput& prepared);
|
||||||
|
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
|
||||||
|
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
|
||||||
|
const HushChatCompatibilityFixture& fixture,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
|
||||||
|
const std::string& jsonText,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
|
||||||
|
const std::string& path,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
|
||||||
|
const HushChatCompatibilityFixture& fixture,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
|
||||||
|
const HushChatCompatibilityFixtureFileParseResult& parsed,
|
||||||
|
const HushChatSeedPublicKeyProjectionResult& seedProjection,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
|
||||||
|
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
|
||||||
|
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
|
||||||
|
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
|
||||||
|
const std::string& jsonText,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
|
||||||
|
const std::string& path,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
const char* hushChatHeaderTypeName(HushChatHeaderType type);
|
||||||
|
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
|
||||||
|
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
|
||||||
|
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
|
||||||
|
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
|
||||||
|
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
|
||||||
|
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
|
||||||
|
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
|
||||||
|
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
|
||||||
|
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
|
||||||
|
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
|
||||||
|
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
|
||||||
|
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
|
||||||
|
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
|
||||||
|
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
+131
-14
@@ -11,8 +11,10 @@
|
|||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
@@ -30,12 +32,39 @@ namespace config {
|
|||||||
Settings::Settings() = default;
|
Settings::Settings() = default;
|
||||||
Settings::~Settings() = default;
|
Settings::~Settings() = default;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
|
||||||
|
const json& value)
|
||||||
|
{
|
||||||
|
if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
|
||||||
|
const std::string mode = value.get<std::string>();
|
||||||
|
if (mode == "random" || mode == "Random") {
|
||||||
|
return Settings::LiteServerSelectionPreferenceMode::Random;
|
||||||
|
}
|
||||||
|
return Settings::LiteServerSelectionPreferenceMode::Sticky;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* liteServerSelectionPreferenceModeName(
|
||||||
|
Settings::LiteServerSelectionPreferenceMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case Settings::LiteServerSelectionPreferenceMode::Sticky:
|
||||||
|
return "sticky";
|
||||||
|
case Settings::LiteServerSelectionPreferenceMode::Random:
|
||||||
|
return "random";
|
||||||
|
}
|
||||||
|
return "sticky";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
std::string Settings::getDefaultPath()
|
std::string Settings::getDefaultPath()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
char path[MAX_PATH];
|
char path[MAX_PATH];
|
||||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
||||||
std::string dir = std::string(path) + "\\ObsidianDragon";
|
std::string dir = std::string(path) + "\\" DRAGONX_APP_NAME;
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "\\settings.json";
|
return dir + "\\settings.json";
|
||||||
}
|
}
|
||||||
@@ -46,7 +75,7 @@ std::string Settings::getDefaultPath()
|
|||||||
struct passwd* pw = getpwuid(getuid());
|
struct passwd* pw = getpwuid(getuid());
|
||||||
home = pw->pw_dir;
|
home = pw->pw_dir;
|
||||||
}
|
}
|
||||||
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
|
std::string dir = std::string(home) + "/Library/Application Support/" DRAGONX_APP_NAME;
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "/settings.json";
|
return dir + "/settings.json";
|
||||||
#else
|
#else
|
||||||
@@ -55,7 +84,7 @@ std::string Settings::getDefaultPath()
|
|||||||
struct passwd* pw = getpwuid(getuid());
|
struct passwd* pw = getpwuid(getuid());
|
||||||
home = pw->pw_dir;
|
home = pw->pw_dir;
|
||||||
}
|
}
|
||||||
std::string dir = std::string(home) + "/.config/ObsidianDragon";
|
std::string dir = std::string(home) + "/.config/" DRAGONX_APP_NAME;
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "/settings.json";
|
return dir + "/settings.json";
|
||||||
#endif
|
#endif
|
||||||
@@ -148,6 +177,66 @@ bool Settings::load(const std::string& path)
|
|||||||
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].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("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("max_connections")) max_connections_ = j["max_connections"].get<int>();
|
||||||
|
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
||||||
|
const auto& lite = j["lite_wallet"];
|
||||||
|
if (lite.contains("server_selection_mode")) {
|
||||||
|
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
|
||||||
|
lite["server_selection_mode"]);
|
||||||
|
}
|
||||||
|
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
|
||||||
|
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
|
||||||
|
lite_chain_name_ = lite["chain_name"].get<std::string>();
|
||||||
|
}
|
||||||
|
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
|
||||||
|
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
|
||||||
|
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
|
||||||
|
// value to "main" and flag a re-save so the corrected setting persists.
|
||||||
|
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
|
||||||
|
lite_chain_name_ != "regtest") {
|
||||||
|
lite_chain_name_ = "main";
|
||||||
|
needs_upgrade_save_ = true;
|
||||||
|
}
|
||||||
|
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
|
||||||
|
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
|
||||||
|
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
|
||||||
|
const auto seed = lite["random_selection_seed"].get<long long>();
|
||||||
|
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
|
||||||
|
}
|
||||||
|
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
|
||||||
|
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
|
||||||
|
}
|
||||||
|
if (lite.contains("servers") && lite["servers"].is_array()) {
|
||||||
|
lite_servers_.clear();
|
||||||
|
for (const auto& server : lite["servers"]) {
|
||||||
|
if (!server.is_object()) continue;
|
||||||
|
LiteServerPreference preference;
|
||||||
|
if (server.contains("url") && server["url"].is_string()) {
|
||||||
|
preference.url = server["url"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (server.contains("label") && server["label"].is_string()) {
|
||||||
|
preference.label = server["label"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (server.contains("enabled") && server["enabled"].is_boolean()) {
|
||||||
|
preference.enabled = server["enabled"].get<bool>();
|
||||||
|
}
|
||||||
|
lite_servers_.push_back(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
|
||||||
|
const auto v = lite["rollout_override"].get<std::string>();
|
||||||
|
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
|
||||||
|
}
|
||||||
|
if (lite.contains("install_id") && lite["install_id"].is_string()) {
|
||||||
|
lite_install_id_ = lite["install_id"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
|
||||||
|
lite_hidden_servers_.clear();
|
||||||
|
for (const auto& u : lite["hidden_servers"])
|
||||||
|
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
||||||
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||||
debug_categories_.clear();
|
debug_categories_.clear();
|
||||||
@@ -169,6 +258,7 @@ bool Settings::load(const std::string& path)
|
|||||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
||||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||||
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
|
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
|
||||||
|
if (j.contains("xmrig_version")) xmrig_version_ = j["xmrig_version"].get<std::string>();
|
||||||
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
||||||
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
|
if (j.contains("idle_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_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
||||||
@@ -203,6 +293,17 @@ bool Settings::load(const std::string& path)
|
|||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
||||||
|
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
|
||||||
|
// next save() doesn't silently overwrite it with defaults — the user's data stays
|
||||||
|
// recoverable. Proceed with in-memory defaults.
|
||||||
|
file.close();
|
||||||
|
std::error_code ec;
|
||||||
|
const std::string quarantine =
|
||||||
|
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
|
||||||
|
fs::rename(path, quarantine, ec);
|
||||||
|
if (!ec) {
|
||||||
|
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,6 +366,27 @@ bool Settings::save(const std::string& path)
|
|||||||
j["keep_daemon_running"] = keep_daemon_running_;
|
j["keep_daemon_running"] = keep_daemon_running_;
|
||||||
j["stop_external_daemon"] = stop_external_daemon_;
|
j["stop_external_daemon"] = stop_external_daemon_;
|
||||||
j["max_connections"] = max_connections_;
|
j["max_connections"] = max_connections_;
|
||||||
|
{
|
||||||
|
json lite = json::object();
|
||||||
|
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
|
||||||
|
lite["sticky_server_url"] = lite_sticky_server_url_;
|
||||||
|
lite["chain_name"] = lite_chain_name_;
|
||||||
|
lite["random_selection_seed"] = lite_random_selection_seed_;
|
||||||
|
lite["persist_selected_server"] = lite_persist_selected_server_;
|
||||||
|
lite["servers"] = json::array();
|
||||||
|
for (const auto& server : lite_servers_) {
|
||||||
|
json entry = json::object();
|
||||||
|
entry["url"] = server.url;
|
||||||
|
entry["label"] = server.label;
|
||||||
|
entry["enabled"] = server.enabled;
|
||||||
|
lite["servers"].push_back(entry);
|
||||||
|
}
|
||||||
|
lite["rollout_override"] = lite_rollout_override_;
|
||||||
|
lite["install_id"] = lite_install_id_;
|
||||||
|
lite["hidden_servers"] = json::array();
|
||||||
|
for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
|
||||||
|
j["lite_wallet"] = lite;
|
||||||
|
}
|
||||||
j["verbose_logging"] = verbose_logging_;
|
j["verbose_logging"] = verbose_logging_;
|
||||||
j["debug_categories"] = json::array();
|
j["debug_categories"] = json::array();
|
||||||
for (const auto& cat : debug_categories_)
|
for (const auto& cat : debug_categories_)
|
||||||
@@ -282,6 +404,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["pool_hugepages"] = pool_hugepages_;
|
j["pool_hugepages"] = pool_hugepages_;
|
||||||
j["pool_mode"] = pool_mode_;
|
j["pool_mode"] = pool_mode_;
|
||||||
j["mine_when_idle"] = mine_when_idle_;
|
j["mine_when_idle"] = mine_when_idle_;
|
||||||
|
j["xmrig_version"] = xmrig_version_;
|
||||||
j["mine_idle_delay"]= mine_idle_delay_;
|
j["mine_idle_delay"]= mine_idle_delay_;
|
||||||
j["idle_thread_scaling"] = idle_thread_scaling_;
|
j["idle_thread_scaling"] = idle_thread_scaling_;
|
||||||
j["idle_threads_active"] = idle_threads_active_;
|
j["idle_threads_active"] = idle_threads_active_;
|
||||||
@@ -301,17 +424,11 @@ bool Settings::save(const std::string& path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure directory exists
|
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
|
||||||
fs::path p(path);
|
// A crash mid-write can no longer truncate settings.json (which would silently
|
||||||
fs::create_directories(p.parent_path());
|
// reset every preference on the next launch). Owner-only (0600) — it carries the
|
||||||
|
// lite-server list and address metadata.
|
||||||
std::ofstream file(path);
|
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
|
||||||
if (!file.is_open()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
file << j.dump(4);
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
|
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -54,6 +55,17 @@ public:
|
|||||||
*/
|
*/
|
||||||
static std::string getDefaultPath();
|
static std::string getDefaultPath();
|
||||||
|
|
||||||
|
enum class LiteServerSelectionPreferenceMode {
|
||||||
|
Sticky,
|
||||||
|
Random
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LiteServerPreference {
|
||||||
|
std::string url;
|
||||||
|
std::string label;
|
||||||
|
bool enabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
std::string getTheme() const { return theme_; }
|
std::string getTheme() const { return theme_; }
|
||||||
void setTheme(const std::string& theme) { theme_ = theme; }
|
void setTheme(const std::string& theme) { theme_ = theme; }
|
||||||
@@ -218,6 +230,35 @@ public:
|
|||||||
int getMaxConnections() const { return max_connections_; }
|
int getMaxConnections() const { return max_connections_; }
|
||||||
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
||||||
|
|
||||||
|
// Lite wallet server selection
|
||||||
|
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
|
||||||
|
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
|
||||||
|
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
|
||||||
|
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
|
||||||
|
std::string getLiteChainName() const { return lite_chain_name_; }
|
||||||
|
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
|
||||||
|
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
|
||||||
|
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
|
||||||
|
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
|
||||||
|
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
|
||||||
|
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
|
||||||
|
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
|
||||||
|
|
||||||
|
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
|
||||||
|
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
|
||||||
|
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
|
||||||
|
void hideLiteServer(const std::string& url) { lite_hidden_servers_.insert(url); }
|
||||||
|
void unhideLiteServer(const std::string& url) { lite_hidden_servers_.erase(url); }
|
||||||
|
|
||||||
|
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
|
||||||
|
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
|
||||||
|
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
|
||||||
|
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
|
||||||
|
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
|
||||||
|
// Never transmitted; carries no PII. Generated on first use if empty.
|
||||||
|
std::string getLiteInstallId() const { return lite_install_id_; }
|
||||||
|
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
|
||||||
|
|
||||||
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
||||||
bool getVerboseLogging() const { return verbose_logging_; }
|
bool getVerboseLogging() const { return verbose_logging_; }
|
||||||
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
||||||
@@ -265,6 +306,10 @@ public:
|
|||||||
bool getPoolMode() const { return pool_mode_; }
|
bool getPoolMode() const { return pool_mode_; }
|
||||||
void setPoolMode(bool v) { pool_mode_ = v; }
|
void setPoolMode(bool v) { pool_mode_ = v; }
|
||||||
|
|
||||||
|
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
|
||||||
|
std::string getXmrigVersion() const { return xmrig_version_; }
|
||||||
|
void setXmrigVersion(const std::string& v) { xmrig_version_ = v; }
|
||||||
|
|
||||||
// Mine when idle (auto-start mining when system is idle)
|
// Mine when idle (auto-start mining when system is idle)
|
||||||
bool getMineWhenIdle() const { return mine_when_idle_; }
|
bool getMineWhenIdle() const { return mine_when_idle_; }
|
||||||
void setMineWhenIdle(bool v) { mine_when_idle_ = v; }
|
void setMineWhenIdle(bool v) { mine_when_idle_ = v; }
|
||||||
@@ -358,6 +403,26 @@ private:
|
|||||||
bool keep_daemon_running_ = false;
|
bool keep_daemon_running_ = false;
|
||||||
bool stop_external_daemon_ = false;
|
bool stop_external_daemon_ = false;
|
||||||
int max_connections_ = 0; // 0 = daemon default
|
int max_connections_ = 0; // 0 = daemon default
|
||||||
|
|
||||||
|
// Lite wallet server preferences. These are user/server settings only;
|
||||||
|
// wallet secrets, wallet files, and lifecycle state are never stored here.
|
||||||
|
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
|
||||||
|
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
|
||||||
|
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
|
||||||
|
std::size_t lite_random_selection_seed_ = 0;
|
||||||
|
bool lite_persist_selected_server_ = true;
|
||||||
|
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
|
||||||
|
std::string lite_install_id_; // random local-only id; rollout-bucket source
|
||||||
|
std::vector<LiteServerPreference> lite_servers_ = {
|
||||||
|
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||||
|
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||||
|
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
|
||||||
|
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
|
||||||
|
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
|
||||||
|
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
|
||||||
|
};
|
||||||
|
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
|
||||||
|
|
||||||
bool verbose_logging_ = false;
|
bool verbose_logging_ = false;
|
||||||
std::set<std::string> debug_categories_;
|
std::set<std::string> debug_categories_;
|
||||||
bool theme_effects_enabled_ = true;
|
bool theme_effects_enabled_ = true;
|
||||||
@@ -374,6 +439,7 @@ private:
|
|||||||
bool pool_tls_ = false;
|
bool pool_tls_ = false;
|
||||||
bool pool_hugepages_ = true;
|
bool pool_hugepages_ = true;
|
||||||
bool pool_mode_ = false; // false=solo, true=pool
|
bool pool_mode_ = false; // false=solo, true=pool
|
||||||
|
std::string xmrig_version_; // installed DRG-XMRig release tag (update detection)
|
||||||
bool mine_when_idle_ = false; // auto-start mining when system idle
|
bool mine_when_idle_ = false; // auto-start mining when system idle
|
||||||
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
||||||
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
|
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
|
||||||
|
|||||||
+1
-29
@@ -1,31 +1,3 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
#include "dragonx_generated_version.h"
|
||||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
|
||||||
|
|
||||||
#define DRAGONX_VERSION "1.2.0-rc1"
|
|
||||||
#define DRAGONX_VERSION_MAJOR 1
|
|
||||||
#define DRAGONX_VERSION_MINOR 2
|
|
||||||
#define DRAGONX_VERSION_PATCH 0
|
|
||||||
|
|
||||||
#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"
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
// !! DO NOT EDIT generated version output — it is generated from version.h.in by CMake.
|
||||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) for the full-node app,
|
||||||
|
// !! or DRAGONX_LITE_VERSION for ObsidianDragonLite. DRAGONX_APP_VERSION is the active variant.
|
||||||
|
|
||||||
#define DRAGONX_VERSION "@PROJECT_VERSION@@DRAGONX_VERSION_SUFFIX@"
|
#define DRAGONX_VERSION "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@"
|
||||||
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
#define DRAGONX_VERSION_MAJOR @DRAGONX_APP_VERSION_MAJOR@
|
||||||
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
#define DRAGONX_VERSION_MINOR @DRAGONX_APP_VERSION_MINOR@
|
||||||
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
#define DRAGONX_VERSION_PATCH @DRAGONX_APP_VERSION_PATCH@
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|
||||||
// Default RPC settings
|
// Default RPC settings
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
@@ -113,17 +114,13 @@ bool AddressBook::save()
|
|||||||
j["entries"].push_back(e);
|
j["entries"].push_back(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
// Atomic + durable: temp file + fsync + rename, so a crash mid-write can't
|
||||||
fs::path p(file_path_);
|
// truncate addressbook.json (which is fully rewritten on every entry change).
|
||||||
fs::create_directories(p.parent_path());
|
// Owner-only (0600) — it holds the user's saved contacts.
|
||||||
|
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
|
||||||
std::ofstream file(file_path_);
|
DEBUG_LOGF("Could not write address book: %s\n", file_path_.c_str());
|
||||||
if (!file.is_open()) {
|
|
||||||
DEBUG_LOGF("Could not open address book for writing: %s\n", file_path_.c_str());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file << j.dump(2);
|
|
||||||
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
|
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@@ -194,6 +198,14 @@ bool TransactionHistoryCache::ensureOpen()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Owner-only (0600): although the payload is encrypted, don't leave the cache (or its
|
||||||
|
// WAL/SHM sidecars) world-readable. Best-effort; sidecars may not exist until first write.
|
||||||
|
::chmod(database_path_.c_str(), 0600);
|
||||||
|
::chmod((database_path_ + "-wal").c_str(), 0600);
|
||||||
|
::chmod((database_path_ + "-shm").c_str(), 0600);
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +344,9 @@ bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
|||||||
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
||||||
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
||||||
|
|
||||||
return sqlite3_step(statement.handle) == SQLITE_DONE;
|
if (sqlite3_step(statement.handle) != SQLITE_DONE) return false;
|
||||||
|
pruneOtherWallets(walletHash); // bound DB growth — drop stale-hash snapshots/salts
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
||||||
@@ -499,6 +513,20 @@ void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
|
|||||||
sqlite3_step(statement.handle);
|
sqlite3_step(statement.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::pruneOtherWallets(const std::string& keepWalletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen() || keepWalletHash.empty()) return;
|
||||||
|
// Table names are hardcoded literals (no injection surface). Prune both the snapshot
|
||||||
|
// blobs and the now-orphaned salt rows so a stale salt can't outlive its ciphertext.
|
||||||
|
for (const char* table : {"transaction_history_snapshots", "transaction_history_keys"}) {
|
||||||
|
const std::string sql = std::string("DELETE FROM ") + table + " WHERE wallet_hash <> ?";
|
||||||
|
Statement statement(db_, sql.c_str());
|
||||||
|
if (!statement.handle) continue;
|
||||||
|
if (!bindText(statement.handle, 1, keepWalletHash)) continue;
|
||||||
|
sqlite3_step(statement.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TransactionHistoryCache::close()
|
void TransactionHistoryCache::close()
|
||||||
{
|
{
|
||||||
if (!db_) return;
|
if (!db_) return;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ private:
|
|||||||
std::vector<unsigned char>& nonce,
|
std::vector<unsigned char>& nonce,
|
||||||
std::vector<unsigned char>& cipherText);
|
std::vector<unsigned char>& cipherText);
|
||||||
void clearWalletByHash(const std::string& walletHash);
|
void clearWalletByHash(const std::string& walletHash);
|
||||||
|
// Delete snapshot + salt rows for every wallet hash except the live one, bounding the
|
||||||
|
// DB so generating a new address (which changes the hash) doesn't orphan history forever.
|
||||||
|
void pruneOtherWallets(const std::string& keepWalletHash);
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
sqlite3* db_ = nullptr;
|
sqlite3* db_ = nullptr;
|
||||||
|
|||||||
@@ -189,6 +189,12 @@ struct WalletState {
|
|||||||
// Connection
|
// Connection
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
|
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
|
||||||
|
// True when the daemon is up/launching but not yet answering RPC (e.g. the connect probe
|
||||||
|
// times out because the node is loading the block index). Distinct from warming_up, which
|
||||||
|
// needs a JSON-RPC -28 reply; here getinfo never returns, so we infer the state from the
|
||||||
|
// daemon's launch state + its own console output. Drives the same loading overlay so the
|
||||||
|
// user sees WHAT the node is doing instead of a bare "Connection failed".
|
||||||
|
bool daemon_initializing = false;
|
||||||
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
|
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
|
||||||
std::string warmup_description; // subtitle explaining the stage
|
std::string warmup_description; // subtitle explaining the stage
|
||||||
int daemon_version = 0;
|
int daemon_version = 0;
|
||||||
@@ -262,6 +268,7 @@ struct WalletState {
|
|||||||
void clear() {
|
void clear() {
|
||||||
connected = false;
|
connected = false;
|
||||||
warming_up = false;
|
warming_up = false;
|
||||||
|
daemon_initializing = false;
|
||||||
warmup_status.clear();
|
warmup_status.clear();
|
||||||
warmup_description.clear();
|
warmup_description.clear();
|
||||||
daemon_version = 0;
|
daemon_version = 0;
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
// Embedded Resources Header
|
|
||||||
// This provides access to resources embedded in the binary
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace embedded {
|
|
||||||
|
|
||||||
// Forward declarations for embedded data (generated at build time)
|
|
||||||
struct EmbeddedResource {
|
|
||||||
const unsigned char* data;
|
|
||||||
size_t size;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resource registry
|
|
||||||
class Resources {
|
|
||||||
public:
|
|
||||||
static Resources& instance() {
|
|
||||||
static Resources inst;
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get embedded resource by name
|
|
||||||
// Returns nullptr if not found
|
|
||||||
const EmbeddedResource* get(const std::string& name) const {
|
|
||||||
auto it = resources_.find(name);
|
|
||||||
if (it != resources_.end()) {
|
|
||||||
return &it->second;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resource exists
|
|
||||||
bool has(const std::string& name) const {
|
|
||||||
return resources_.find(name) != resources_.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register a resource (called during static init)
|
|
||||||
void registerResource(const std::string& name, const unsigned char* data, size_t size) {
|
|
||||||
resources_[name] = {data, size};
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
Resources() = default;
|
|
||||||
std::unordered_map<std::string, EmbeddedResource> resources_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper macro for registering resources
|
|
||||||
#define REGISTER_EMBEDDED_RESOURCE(name, data, size) \
|
|
||||||
static struct _EmbeddedResourceRegister_##name { \
|
|
||||||
_EmbeddedResourceRegister_##name() { \
|
|
||||||
dragonx::embedded::Resources::instance().registerResource(#name, data, size); \
|
|
||||||
} \
|
|
||||||
} _embedded_resource_register_##name
|
|
||||||
|
|
||||||
} // namespace embedded
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -273,10 +273,32 @@ static bool extractResource(const EmbeddedResource* res, const std::string& dest
|
|||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
std::ofstream file(destPath, std::ios::binary);
|
std::ofstream file(destPath, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
// The destination may be locked because the previous daemon is still using the binary:
|
||||||
|
// Windows locks a running .exe against truncation, Linux returns ETXTBSY. Both platforms
|
||||||
|
// DO allow renaming/moving such a file — the running process keeps the moved copy — so move
|
||||||
|
// the stale binary aside and write a fresh one at the original path.
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(destPath)) {
|
||||||
|
std::string sidelined = destPath + ".old";
|
||||||
|
std::filesystem::remove(sidelined, ec); // clear any leftover from a prior swap
|
||||||
|
ec.clear();
|
||||||
|
std::filesystem::rename(destPath, sidelined, ec);
|
||||||
|
if (!ec) {
|
||||||
|
file.clear();
|
||||||
|
file.open(destPath, std::ios::binary);
|
||||||
|
if (file)
|
||||||
|
DEBUG_LOGF("[INFO] Replaced in-use %s (old copy moved to .old)\n", destPath.c_str());
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("[WARN] Could not move stale %s aside: %s\n",
|
||||||
|
destPath.c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!file) {
|
if (!file) {
|
||||||
DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", destPath.c_str());
|
DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", destPath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file.write(reinterpret_cast<const char*>(res->data), res->size);
|
file.write(reinterpret_cast<const char*>(res->data), res->size);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -414,6 +436,17 @@ bool extractEmbeddedResources()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement.
|
||||||
|
// Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly;
|
||||||
|
// if it's still running, the remove fails harmlessly and we retry on the next startup.
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
for (const char* name : { RESOURCE_DRAGONXD, RESOURCE_XMRIG }) {
|
||||||
|
std::filesystem::remove(daemonDir + pathSep + name + std::string(".old"), ec);
|
||||||
|
ec.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -332,6 +332,17 @@ bool Connection::createDefaultConfig(const std::string& path)
|
|||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
|
// The file holds the freshly-generated rpcuser/rpcpassword in plaintext. ofstream creates it
|
||||||
|
// with the process umask (typically world-readable 0644), so restrict it to owner read/write
|
||||||
|
// before another local user can read the credentials.
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
std::error_code ec;
|
||||||
|
fs::permissions(path, fs::perms::owner_read | fs::perms::owner_write,
|
||||||
|
fs::perm_options::replace, ec);
|
||||||
|
if (ec) DEBUG_LOGF("Could not restrict config permissions on %s: %s\n", path.c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-18
@@ -6,10 +6,12 @@
|
|||||||
// All calls are blocking; run on RPCWorker threads, never on main thread.
|
// All calls are blocking; run on RPCWorker threads, never on main thread.
|
||||||
|
|
||||||
#include "rpc_client.h"
|
#include "rpc_client.h"
|
||||||
|
#include "connection.h"
|
||||||
#include "../config/version.h"
|
#include "../config/version.h"
|
||||||
#include "../util/base64.h"
|
#include "../util/base64.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
#include <sodium.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -87,6 +89,14 @@ static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::stri
|
|||||||
return totalSize;
|
return totalSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// curl progress callback: a non-zero return aborts the in-flight transfer. This lets a
|
||||||
|
// requestAbort() from another thread (disconnect/shutdown) unblock curl_easy_perform so the
|
||||||
|
// UI thread's worker join() returns promptly instead of waiting out the request timeout.
|
||||||
|
static int xferInfoCallback(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
|
||||||
|
const auto* self = static_cast<const RPCClient*>(clientp);
|
||||||
|
return (self != nullptr && self->abortRequested()) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Private implementation using libcurl
|
// Private implementation using libcurl
|
||||||
class RPCClient::Impl {
|
class RPCClient::Impl {
|
||||||
public:
|
public:
|
||||||
@@ -136,9 +146,11 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
port_ = port;
|
port_ = port;
|
||||||
last_connect_info_ = json();
|
last_connect_info_ = json();
|
||||||
|
|
||||||
// Create Basic auth header with proper base64 encoding
|
// Create Basic auth header with proper base64 encoding, then wipe the plaintext
|
||||||
|
// "user:password" temporary (std::string does not zero its buffer on destruction).
|
||||||
std::string credentials = user + ":" + password;
|
std::string credentials = user + ":" + password;
|
||||||
auth_ = util::base64_encode(credentials);
|
auth_ = util::base64_encode(credentials);
|
||||||
|
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
||||||
|
|
||||||
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
||||||
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
||||||
@@ -169,12 +181,25 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
|
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||||
|
// Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
|
||||||
|
clearAbort(); // a fresh connection must not start in the aborted state
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 1L); // localhost — fails fast if not listening
|
// Localhost fails fast if nothing is listening; a remote/TLS daemon needs a larger
|
||||||
|
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
|
||||||
|
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
|
||||||
|
|
||||||
// Test connection with getinfo
|
// Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
|
||||||
|
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
|
||||||
|
// hang means a wedged/loading occupant — no point blocking the full 30s before we retry and
|
||||||
|
// update the UI. (call(timeoutSec) restores the persistent 30s afterwards, so normal RPC calls
|
||||||
|
// that legitimately take longer are unaffected.) Remote/TLS daemons keep the full budget.
|
||||||
|
const long probeTimeout = Connection::isLocalHost(host) ? 8L : 30L;
|
||||||
try {
|
try {
|
||||||
json result = call("getinfo");
|
json result = call("getinfo", json::array(), probeTimeout);
|
||||||
if (result.contains("version")) {
|
if (result.contains("version")) {
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
warming_up_ = false;
|
warming_up_ = false;
|
||||||
@@ -191,7 +216,12 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
// it just hasn't finished initializing yet. Mark as connected+warmup
|
// it just hasn't finished initializing yet. Mark as connected+warmup
|
||||||
// so the wallet can show the UI instead of a blocking overlay.
|
// so the wallet can show the UI instead of a blocking overlay.
|
||||||
std::string msg = e.what();
|
std::string msg = e.what();
|
||||||
bool isWarmup = (msg.find("Loading") != std::string::npos ||
|
// Warmup is JSON-RPC error code -28 (RPC_IN_WARMUP) — the robust signal. Fall back
|
||||||
|
// to message substrings for any path that didn't carry the numeric code.
|
||||||
|
int code = 0;
|
||||||
|
if (const auto* re = dynamic_cast<const RpcError*>(&e)) code = re->code;
|
||||||
|
bool isWarmup = (code == -28) ||
|
||||||
|
(msg.find("Loading") != std::string::npos ||
|
||||||
msg.find("Verifying") != std::string::npos ||
|
msg.find("Verifying") != std::string::npos ||
|
||||||
msg.find("Activating") != std::string::npos ||
|
msg.find("Activating") != std::string::npos ||
|
||||||
msg.find("Rewinding") != std::string::npos ||
|
msg.find("Rewinding") != std::string::npos ||
|
||||||
@@ -221,6 +251,18 @@ json RPCClient::getLastConnectInfo() const
|
|||||||
return last_connect_info_;
|
return last_connect_info_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RPCClient::requestAbort()
|
||||||
|
{
|
||||||
|
// Deliberately NOT taking curl_mutex_ — the whole point is to interrupt a call() that is
|
||||||
|
// currently holding it inside curl_easy_perform. The atomic is read by xferInfoCallback.
|
||||||
|
abort_.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RPCClient::clearAbort()
|
||||||
|
{
|
||||||
|
abort_.store(false, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
void RPCClient::disconnect()
|
void RPCClient::disconnect()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
@@ -281,23 +323,36 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
||||||
// Parse the body first to extract the real error message.
|
// Parse the body first to extract the real error message.
|
||||||
if (http_code != 200) {
|
if (http_code != 200) {
|
||||||
|
int errCode = 0;
|
||||||
try {
|
try {
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(response_data);
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
if (response.contains("error") && response["error"].is_object()) {
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
throw std::runtime_error(err_msg);
|
errCode = response["error"]["code"].get<int>();
|
||||||
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
||||||
|
throw RpcError(errCode, response["error"]["message"].get<std::string>());
|
||||||
|
// message missing/non-string — keep the detail instead of a bare HTTP code
|
||||||
|
throw RpcError(errCode, "RPC error: " + response["error"].dump());
|
||||||
}
|
}
|
||||||
} catch (const json::exception&) {
|
} catch (const json::exception&) {
|
||||||
// Body wasn't valid JSON — fall through to generic HTTP error
|
// Body wasn't valid JSON — fall through to generic HTTP error
|
||||||
}
|
}
|
||||||
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
|
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code));
|
||||||
}
|
}
|
||||||
|
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(response_data);
|
||||||
|
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
if (response.contains("error") && !response["error"].is_null()) {
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
int errCode = 0;
|
||||||
throw std::runtime_error("RPC error: " + err_msg);
|
std::string err_msg;
|
||||||
|
if (response["error"].is_object()) {
|
||||||
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
|
errCode = response["error"]["code"].get<int>();
|
||||||
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
||||||
|
err_msg = response["error"]["message"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (err_msg.empty()) err_msg = response["error"].dump();
|
||||||
|
throw RpcError(errCode, "RPC error: " + err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response["result"];
|
return response["result"];
|
||||||
@@ -340,20 +395,32 @@ json RPCClient::call(const std::string& method, const json& params, long timeout
|
|||||||
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
|
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
|
|
||||||
if (http_code != 200) {
|
if (http_code != 200) {
|
||||||
|
int errCode = 0;
|
||||||
try {
|
try {
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(response_data);
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
if (response.contains("error") && response["error"].is_object()) {
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
throw std::runtime_error(err_msg);
|
errCode = response["error"]["code"].get<int>();
|
||||||
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
||||||
|
throw RpcError(errCode, response["error"]["message"].get<std::string>());
|
||||||
|
throw RpcError(errCode, "RPC error: " + response["error"].dump());
|
||||||
}
|
}
|
||||||
} catch (const json::exception&) {}
|
} catch (const json::exception&) {}
|
||||||
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
|
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code));
|
||||||
}
|
}
|
||||||
|
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(response_data);
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
if (response.contains("error") && !response["error"].is_null()) {
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
int errCode = 0;
|
||||||
throw std::runtime_error("RPC error: " + err_msg);
|
std::string err_msg;
|
||||||
|
if (response["error"].is_object()) {
|
||||||
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
|
errCode = response["error"]["code"].get<int>();
|
||||||
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
||||||
|
err_msg = response["error"]["message"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (err_msg.empty()) err_msg = response["error"].dump();
|
||||||
|
throw RpcError(errCode, "RPC error: " + err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response["result"];
|
return response["result"];
|
||||||
@@ -403,7 +470,14 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
|||||||
// Parse with ordered_json to preserve the daemon's original key order
|
// Parse with ordered_json to preserve the daemon's original key order
|
||||||
nlohmann::ordered_json oj = nlohmann::ordered_json::parse(response_data);
|
nlohmann::ordered_json oj = nlohmann::ordered_json::parse(response_data);
|
||||||
if (oj.contains("error") && !oj["error"].is_null()) {
|
if (oj.contains("error") && !oj["error"].is_null()) {
|
||||||
std::string err_msg = oj["error"]["message"].get<std::string>();
|
// A daemon error object normally has a string "message", but don't assume it — a malformed
|
||||||
|
// error (missing/non-string message) must yield a clean RPC error, not a json type-exception.
|
||||||
|
const auto& err = oj["error"];
|
||||||
|
std::string err_msg;
|
||||||
|
if (err.is_object() && err.contains("message") && err["message"].is_string())
|
||||||
|
err_msg = err["message"].get<std::string>();
|
||||||
|
else
|
||||||
|
err_msg = err.dump();
|
||||||
throw std::runtime_error("RPC error: " + err_msg);
|
throw std::runtime_error("RPC error: " + err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
#include <atomic>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -18,6 +20,21 @@ using json = nlohmann::json;
|
|||||||
using Callback = std::function<void(const json&)>;
|
using Callback = std::function<void(const json&)>;
|
||||||
using ErrorCallback = std::function<void(const std::string&)>;
|
using ErrorCallback = std::function<void(const std::string&)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A JSON-RPC error carrying the daemon's numeric error code.
|
||||||
|
*
|
||||||
|
* what() preserves the exact human-readable message (so existing string matching
|
||||||
|
* still works); `code` exposes the JSON-RPC error code — notably -28 (RPC_IN_WARMUP)
|
||||||
|
* for a daemon still starting up. Derives from std::runtime_error, so every existing
|
||||||
|
* `catch (const std::exception&)` continues to handle it unchanged.
|
||||||
|
*/
|
||||||
|
class RpcError : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
RpcError(int errorCode, const std::string& message)
|
||||||
|
: std::runtime_error(message), code(errorCode) {}
|
||||||
|
int code = 0;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief JSON-RPC client for dragonxd
|
* @brief JSON-RPC client for dragonxd
|
||||||
*
|
*
|
||||||
@@ -71,6 +88,18 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool isConnected() const { return connected_; }
|
bool isConnected() const { return connected_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ask an in-flight call() to abort as soon as possible (thread-safe).
|
||||||
|
*
|
||||||
|
* Set from another thread (e.g. before stop()-ing the worker on disconnect/shutdown):
|
||||||
|
* a curl progress callback aborts the transfer, so a blocked curl_easy_perform returns
|
||||||
|
* promptly instead of freezing the UI thread's join() until the request timeout. Cleared
|
||||||
|
* on the next connect(); abortRequested() is read by the progress callback.
|
||||||
|
*/
|
||||||
|
void requestAbort();
|
||||||
|
void clearAbort();
|
||||||
|
bool abortRequested() const noexcept { return abort_.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief True if the last connect() succeeded but daemon returned a warmup error.
|
* @brief True if the last connect() succeeded but daemon returned a warmup error.
|
||||||
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
|
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
|
||||||
@@ -220,6 +249,7 @@ private:
|
|||||||
std::string port_;
|
std::string port_;
|
||||||
std::string auth_; // Base64 encoded "user:password"
|
std::string auth_; // Base64 encoded "user:password"
|
||||||
bool connected_ = false;
|
bool connected_ = false;
|
||||||
|
std::atomic<bool> abort_{false}; // set cross-thread to abort an in-flight transfer
|
||||||
bool warming_up_ = false;
|
bool warming_up_ = false;
|
||||||
std::string warmup_status_;
|
std::string warmup_status_;
|
||||||
std::string last_connect_error_;
|
std::string last_connect_error_;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@@ -136,6 +137,60 @@ void appendMissingPreviousTransactions(std::vector<TransactionInfo>& transaction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using HushChatMemoOutputMap = std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>;
|
||||||
|
|
||||||
|
std::size_t readMemoPosition(const json& source, std::size_t fallback)
|
||||||
|
{
|
||||||
|
for (const char* key : {"position", "outputIndex", "outindex"}) {
|
||||||
|
auto value = readOptional<int>(source, key);
|
||||||
|
if (value && *value >= 0) return static_cast<std::size_t>(*value);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordHushChatMemoOutput(HushChatMemoOutputMap* outputs,
|
||||||
|
const std::string& txid,
|
||||||
|
const std::string& memo,
|
||||||
|
std::size_t position)
|
||||||
|
{
|
||||||
|
if (!outputs || txid.empty() || memo.empty()) return;
|
||||||
|
(*outputs)[txid].push_back(chat::HushChatMemoOutput{position, memo});
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
||||||
|
const std::string& txid,
|
||||||
|
const std::vector<chat::HushChatMemoOutput>& outputs)
|
||||||
|
{
|
||||||
|
auto extracted = chat::extractHushChatTransactionMetadata(chat::HushChatTransactionInput{txid, outputs});
|
||||||
|
for (auto& metadata : extracted.metadata) {
|
||||||
|
destination.push_back(std::move(metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
||||||
|
const std::string& txid,
|
||||||
|
const NetworkRefreshService::TransactionViewCacheEntry& entry)
|
||||||
|
{
|
||||||
|
if (!chat::hushChatFeatureEnabledAtBuild()) return;
|
||||||
|
|
||||||
|
std::vector<chat::HushChatMemoOutput> outputs;
|
||||||
|
outputs.reserve(entry.outgoing_outputs.size());
|
||||||
|
for (const auto& output : entry.outgoing_outputs) {
|
||||||
|
if (!output.memo.empty()) outputs.push_back(chat::HushChatMemoOutput{output.position, output.memo});
|
||||||
|
}
|
||||||
|
appendExtractedHushChatMetadata(destination, txid, outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
||||||
|
const HushChatMemoOutputMap& outputsByTxid)
|
||||||
|
{
|
||||||
|
if (!chat::hushChatFeatureEnabledAtBuild()) return;
|
||||||
|
|
||||||
|
for (const auto& [txid, outputs] : outputsByTxid) {
|
||||||
|
appendExtractedHushChatMetadata(destination, txid, outputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info)
|
NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info)
|
||||||
@@ -229,19 +284,21 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc)
|
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance)
|
||||||
{
|
{
|
||||||
json totalBalance;
|
json totalBalance;
|
||||||
json blockInfo;
|
json blockInfo;
|
||||||
bool balanceOk = false;
|
bool balanceOk = false;
|
||||||
bool blockOk = false;
|
bool blockOk = false;
|
||||||
|
|
||||||
|
if (includeBalance) {
|
||||||
try {
|
try {
|
||||||
totalBalance = rpc.call("z_gettotalbalance", json::array());
|
totalBalance = rpc.call("z_gettotalbalance", json::array());
|
||||||
balanceOk = true;
|
balanceOk = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
blockInfo = rpc.call("getblockchaininfo", json::array());
|
blockInfo = rpc.call("getblockchaininfo", json::array());
|
||||||
@@ -614,17 +671,23 @@ void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector<Trans
|
|||||||
std::set<std::string>& knownTxids,
|
std::set<std::string>& knownTxids,
|
||||||
const std::string& address,
|
const std::string& address,
|
||||||
const json& received,
|
const json& received,
|
||||||
const std::set<std::string>& miningAddresses)
|
const std::set<std::string>& miningAddresses,
|
||||||
|
HushChatMemoOutputMap* chatMemoOutputs)
|
||||||
{
|
{
|
||||||
if (received.is_null() || !received.is_array()) return;
|
if (received.is_null() || !received.is_array()) return;
|
||||||
|
|
||||||
|
std::size_t fallbackPosition = 0;
|
||||||
for (const auto& note : received) {
|
for (const auto& note : received) {
|
||||||
|
const std::size_t notePosition = readMemoPosition(note, fallbackPosition++);
|
||||||
auto txid = readOptional<std::string>(note, "txid");
|
auto txid = readOptional<std::string>(note, "txid");
|
||||||
if (!txid || txid->empty()) continue;
|
if (!txid || txid->empty()) continue;
|
||||||
|
|
||||||
auto change = readOptional<bool>(note, "change");
|
auto change = readOptional<bool>(note, "change");
|
||||||
if (change && *change) continue;
|
if (change && *change) continue;
|
||||||
|
|
||||||
|
auto memoValue = readOptional<std::string>(note, "memoStr");
|
||||||
|
if (memoValue) recordHushChatMemoOutput(chatMemoOutputs, *txid, *memoValue, notePosition);
|
||||||
|
|
||||||
bool dominated = false;
|
bool dominated = false;
|
||||||
for (const auto& existing : transactions) {
|
for (const auto& existing : transactions) {
|
||||||
if (existing.txid == *txid && existing.type == "receive") {
|
if (existing.txid == *txid && existing.type == "receive") {
|
||||||
@@ -641,7 +704,7 @@ void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector<Trans
|
|||||||
if (auto value = readOptional<double>(note, "amount")) info.amount = *value;
|
if (auto value = readOptional<double>(note, "amount")) info.amount = *value;
|
||||||
if (auto value = readOptional<int>(note, "confirmations")) info.confirmations = *value;
|
if (auto value = readOptional<int>(note, "confirmations")) info.confirmations = *value;
|
||||||
if (auto value = readOptional<std::int64_t>(note, "time")) info.timestamp = *value;
|
if (auto value = readOptional<std::int64_t>(note, "time")) info.timestamp = *value;
|
||||||
if (auto value = readOptional<std::string>(note, "memoStr")) info.memo = *value;
|
if (memoValue) info.memo = *memoValue;
|
||||||
knownTxids.insert(*txid);
|
knownTxids.insert(*txid);
|
||||||
transactions.push_back(std::move(info));
|
transactions.push_back(std::move(info));
|
||||||
}
|
}
|
||||||
@@ -663,7 +726,9 @@ NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewTransaction.contains("outputs") && viewTransaction["outputs"].is_array()) {
|
if (viewTransaction.contains("outputs") && viewTransaction["outputs"].is_array()) {
|
||||||
|
std::size_t fallbackPosition = 0;
|
||||||
for (const auto& output : viewTransaction["outputs"]) {
|
for (const auto& output : viewTransaction["outputs"]) {
|
||||||
|
const std::size_t outputPosition = readMemoPosition(output, fallbackPosition++);
|
||||||
bool outgoing = false;
|
bool outgoing = false;
|
||||||
if (auto value = readOptional<bool>(output, "outgoing")) outgoing = *value;
|
if (auto value = readOptional<bool>(output, "outgoing")) outgoing = *value;
|
||||||
if (!outgoing) continue;
|
if (!outgoing) continue;
|
||||||
@@ -672,6 +737,7 @@ NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseVie
|
|||||||
if (auto value = readOptional<std::string>(output, "address")) out.address = *value;
|
if (auto value = readOptional<std::string>(output, "address")) out.address = *value;
|
||||||
if (auto value = readOptional<double>(output, "value")) out.value = *value;
|
if (auto value = readOptional<double>(output, "value")) out.value = *value;
|
||||||
if (auto value = readOptional<std::string>(output, "memoStr")) out.memo = *value;
|
if (auto value = readOptional<std::string>(output, "memoStr")) out.memo = *value;
|
||||||
|
out.position = outputPosition;
|
||||||
entry.outgoing_outputs.push_back(std::move(out));
|
entry.outgoing_outputs.push_back(std::move(out));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -738,6 +804,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
||||||
|
|
||||||
std::set<std::string> knownTxids;
|
std::set<std::string> knownTxids;
|
||||||
|
HushChatMemoOutputMap hushChatReceivedOutputs;
|
||||||
|
HushChatMemoOutputMap* hushChatReceivedOutputsPtr = chat::hushChatFeatureEnabledAtBuild()
|
||||||
|
? &hushChatReceivedOutputs
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
bool transactionRpcError = false;
|
bool transactionRpcError = false;
|
||||||
try {
|
try {
|
||||||
@@ -754,10 +824,14 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An address counts as "scanned at tip" if its last scan is within shieldedScanTipTolerance
|
||||||
|
// blocks of the current tip. Tolerance 0 means strict (exact tip); a small tolerance lets a
|
||||||
|
// multi-cycle pass over many addresses complete even as the tip advances during it.
|
||||||
|
const int scanStaleThreshold = currentBlockHeight - std::max(0, snapshot.shieldedScanTipTolerance);
|
||||||
auto scannedAtTip = [&](const std::string& address) {
|
auto scannedAtTip = [&](const std::string& address) {
|
||||||
if (currentBlockHeight < 0) return false;
|
if (currentBlockHeight < 0) return false;
|
||||||
auto it = result.shieldedScanHeights.find(address);
|
auto it = result.shieldedScanHeights.find(address);
|
||||||
return it != result.shieldedScanHeights.end() && it->second >= currentBlockHeight;
|
return it != result.shieldedScanHeights.end() && it->second >= scanStaleThreshold;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::size_t shieldedStart = snapshot.shieldedScanStartIndex;
|
std::size_t shieldedStart = snapshot.shieldedScanStartIndex;
|
||||||
@@ -779,7 +853,12 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
nextSearchIndex = index + 1;
|
nextSearchIndex = index + 1;
|
||||||
try {
|
try {
|
||||||
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
|
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
|
||||||
appendShieldedReceivedTransactions(result.transactions, knownTxids, address, received, snapshot.miningAddresses);
|
appendShieldedReceivedTransactions(result.transactions,
|
||||||
|
knownTxids,
|
||||||
|
address,
|
||||||
|
received,
|
||||||
|
snapshot.miningAddresses,
|
||||||
|
hushChatReceivedOutputsPtr);
|
||||||
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
|
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
transactionRpcError = true;
|
transactionRpcError = true;
|
||||||
@@ -817,6 +896,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
if (cached != snapshot.viewTxCache.end()) {
|
if (cached != snapshot.viewTxCache.end()) {
|
||||||
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
||||||
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
||||||
|
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, cached->second);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -841,6 +921,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
|
|
||||||
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
||||||
appendViewTransactionOutputs(result.transactions, txid, entry);
|
appendViewTransactionOutputs(result.transactions, txid, entry);
|
||||||
|
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, entry);
|
||||||
|
|
||||||
json rawTransaction;
|
json rawTransaction;
|
||||||
bool hasRawTransaction = false;
|
bool hasRawTransaction = false;
|
||||||
@@ -885,6 +966,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
|
||||||
|
|
||||||
if (!snapshot.previousTransactions.empty()) {
|
if (!snapshot.previousTransactions.empty()) {
|
||||||
if (transactionRpcError) {
|
if (transactionRpcError) {
|
||||||
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, true, snapshot.pendingOpids);
|
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, true, snapshot.pendingOpids);
|
||||||
@@ -909,6 +992,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
|||||||
result.transactions = snapshot.previousTransactions;
|
result.transactions = snapshot.previousTransactions;
|
||||||
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
|
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
|
||||||
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
||||||
|
HushChatMemoOutputMap hushChatReceivedOutputs;
|
||||||
|
HushChatMemoOutputMap* hushChatReceivedOutputsPtr = chat::hushChatFeatureEnabledAtBuild()
|
||||||
|
? &hushChatReceivedOutputs
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::set<std::string> recentTxids;
|
std::set<std::string> recentTxids;
|
||||||
@@ -944,7 +1031,12 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
|||||||
std::vector<TransactionInfo> scannedTransactions;
|
std::vector<TransactionInfo> scannedTransactions;
|
||||||
std::set<std::string> scannedTxids;
|
std::set<std::string> scannedTxids;
|
||||||
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
|
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
|
||||||
appendShieldedReceivedTransactions(scannedTransactions, scannedTxids, address, received, snapshot.miningAddresses);
|
appendShieldedReceivedTransactions(scannedTransactions,
|
||||||
|
scannedTxids,
|
||||||
|
address,
|
||||||
|
received,
|
||||||
|
snapshot.miningAddresses,
|
||||||
|
hushChatReceivedOutputsPtr);
|
||||||
for (auto& scanned : scannedTransactions) {
|
for (auto& scanned : scannedTransactions) {
|
||||||
bool replaced = false;
|
bool replaced = false;
|
||||||
for (auto& existing : result.transactions) {
|
for (auto& existing : result.transactions) {
|
||||||
@@ -967,6 +1059,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
|||||||
(shieldedStart + shieldedLimit >= snapshot.shieldedAddresses.size()) ? 0 : shieldedStart + shieldedLimit;
|
(shieldedStart + shieldedLimit >= snapshot.shieldedAddresses.size()) ? 0 : shieldedStart + shieldedLimit;
|
||||||
result.shieldedScanComplete = true;
|
result.shieldedScanComplete = true;
|
||||||
|
|
||||||
|
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
|
||||||
|
|
||||||
sortTransactionsNewestFirst(result.transactions);
|
sortTransactionsNewestFirst(result.transactions);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -978,11 +1072,17 @@ NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOpe
|
|||||||
OperationStatusPollResult parsed;
|
OperationStatusPollResult parsed;
|
||||||
if (!result.is_array()) return parsed;
|
if (!result.is_array()) return parsed;
|
||||||
|
|
||||||
|
// We poll z_getoperationstatus with no filter (this daemon rejects the per-opid filtered form),
|
||||||
|
// so the result lists ALL operations. Only act on the ones we're tracking — otherwise an
|
||||||
|
// unrelated failed/old operation would fire a spurious error toast or pollute send state.
|
||||||
|
const std::set<std::string> requested(requestedOpids.begin(), requestedOpids.end());
|
||||||
|
|
||||||
std::set<std::string> reported;
|
std::set<std::string> reported;
|
||||||
for (const auto& op : result) {
|
for (const auto& op : result) {
|
||||||
if (!op.is_object()) continue;
|
if (!op.is_object()) continue;
|
||||||
std::string opid = op.value("id", std::string());
|
std::string opid = op.value("id", std::string());
|
||||||
if (opid.empty()) continue;
|
if (opid.empty()) continue;
|
||||||
|
if (requested.find(opid) == requested.end()) continue; // not one of ours — ignore
|
||||||
reported.insert(opid);
|
reported.insert(opid);
|
||||||
|
|
||||||
std::string status = op.value("status", std::string());
|
std::string status = op.value("status", std::string());
|
||||||
@@ -1007,6 +1107,7 @@ NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOpe
|
|||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
}
|
}
|
||||||
parsed.failureMessages.push_back(msg);
|
parsed.failureMessages.push_back(msg);
|
||||||
|
parsed.failureByOpid[opid] = msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1221,6 +1322,13 @@ void NetworkRefreshService::resetJobs()
|
|||||||
priceInProgress_.store(false, std::memory_order_release);
|
priceInProgress_.store(false, std::memory_order_release);
|
||||||
encryptionInProgress_.store(false, std::memory_order_release);
|
encryptionInProgress_.store(false, std::memory_order_release);
|
||||||
connectionInitInProgress_.store(false, std::memory_order_release);
|
connectionInitInProgress_.store(false, std::memory_order_release);
|
||||||
|
|
||||||
|
// Bump every job's generation so any ticket minted before this reset (a refresh still
|
||||||
|
// in flight on the worker when a disconnect cleared state_) is treated as stale by
|
||||||
|
// completeDispatch — preventing last-connection data from landing on the new session.
|
||||||
|
for (auto& generation : generations_) {
|
||||||
|
generation.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
|
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "chat/chat_protocol.h"
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
#include "refresh_scheduler.h"
|
#include "refresh_scheduler.h"
|
||||||
#include "rpc/rpc_worker.h"
|
#include "rpc/rpc_worker.h"
|
||||||
@@ -159,6 +160,7 @@ public:
|
|||||||
std::string address;
|
std::string address;
|
||||||
double value = 0.0;
|
double value = 0.0;
|
||||||
std::string memo;
|
std::string memo;
|
||||||
|
std::size_t position = 0;
|
||||||
};
|
};
|
||||||
std::vector<Output> outgoing_outputs;
|
std::vector<Output> outgoing_outputs;
|
||||||
};
|
};
|
||||||
@@ -176,10 +178,18 @@ public:
|
|||||||
std::unordered_map<std::string, int> shieldedScanHeights;
|
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||||
std::size_t shieldedScanStartIndex = 0;
|
std::size_t shieldedScanStartIndex = 0;
|
||||||
std::size_t maxShieldedReceiveScans = 0;
|
std::size_t maxShieldedReceiveScans = 0;
|
||||||
|
// How many blocks the tip may advance past an address's last scan before it counts as stale
|
||||||
|
// and needs re-scanning. 0 = strict (must be scanned at the exact current tip). A small
|
||||||
|
// tolerance lets a multi-cycle pass over many shielded addresses COMPLETE even though new
|
||||||
|
// blocks arrive mid-pass — otherwise the "scanned at tip" bar moves every block and the scan
|
||||||
|
// (and the transactions_dirty_ flag it drives) never finishes. It also naturally throttles
|
||||||
|
// full rescans to roughly once per `tolerance` blocks.
|
||||||
|
int shieldedScanTipTolerance = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TransactionRefreshResult {
|
struct TransactionRefreshResult {
|
||||||
std::vector<TransactionInfo> transactions;
|
std::vector<TransactionInfo> transactions;
|
||||||
|
std::vector<chat::HushChatTransactionMetadata> hushChatMetadata;
|
||||||
int blockHeight = -1;
|
int blockHeight = -1;
|
||||||
TransactionViewCache newViewTxEntries;
|
TransactionViewCache newViewTxEntries;
|
||||||
std::size_t nextShieldedScanStartIndex = 0;
|
std::size_t nextShieldedScanStartIndex = 0;
|
||||||
@@ -195,6 +205,7 @@ public:
|
|||||||
std::vector<std::string> successTxids;
|
std::vector<std::string> successTxids;
|
||||||
std::unordered_map<std::string, std::string> successTxidsByOpid;
|
std::unordered_map<std::string, std::string> successTxidsByOpid;
|
||||||
std::vector<std::string> failureMessages;
|
std::vector<std::string> failureMessages;
|
||||||
|
std::unordered_map<std::string, std::string> failureByOpid; // opid -> error message
|
||||||
bool anySuccess = false;
|
bool anySuccess = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,7 +230,10 @@ public:
|
|||||||
bool balanceOk,
|
bool balanceOk,
|
||||||
const nlohmann::json& blockInfo,
|
const nlohmann::json& blockInfo,
|
||||||
bool blockOk);
|
bool blockOk);
|
||||||
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc);
|
// includeBalance=false skips z_gettotalbalance (which takes the wallet lock + cs_main) and only
|
||||||
|
// fetches getblockchaininfo — used while syncing, where the balance is incomplete anyway and the
|
||||||
|
// wallet should minimise lock contention with block connection.
|
||||||
|
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance = true);
|
||||||
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
|
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
|
||||||
bool miningOk,
|
bool miningOk,
|
||||||
const nlohmann::json& localHashrate,
|
const nlohmann::json& localHashrate,
|
||||||
@@ -260,7 +274,8 @@ public:
|
|||||||
std::set<std::string>& knownTxids,
|
std::set<std::string>& knownTxids,
|
||||||
const std::string& address,
|
const std::string& address,
|
||||||
const nlohmann::json& received,
|
const nlohmann::json& received,
|
||||||
const std::set<std::string>& miningAddresses = {});
|
const std::set<std::string>& miningAddresses = {},
|
||||||
|
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
|
||||||
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
||||||
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||||
const std::string& txid,
|
const std::string& txid,
|
||||||
|
|||||||
@@ -8,16 +8,21 @@ namespace services {
|
|||||||
RefreshScheduler::Intervals RefreshScheduler::intervalsForPage(ui::NavPage page)
|
RefreshScheduler::Intervals RefreshScheduler::intervalsForPage(ui::NavPage page)
|
||||||
{
|
{
|
||||||
using NP = ui::NavPage;
|
using NP = ui::NavPage;
|
||||||
|
// Intervals are {core, transactions, addresses, peers} in seconds (0 = disabled).
|
||||||
|
// The peers value keeps the status-bar peer count current on EVERY tab — previously it was 0
|
||||||
|
// off the Peers tab, so the count never updated until you opened Peers. A slow 20s cadence is
|
||||||
|
// plenty for a status-bar number; the Peers tab itself stays fast (5s) for its live list. During
|
||||||
|
// sync this is overridden by kSyncProfile (peers 0) so it can't contend with block download.
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case NP::Overview: return {2.0f, 10.0f, 15.0f, 0.0f};
|
case NP::Overview: return {2.0f, 10.0f, 15.0f, 20.0f};
|
||||||
case NP::Send: return {3.0f, 10.0f, 5.0f, 0.0f};
|
case NP::Send: return {3.0f, 10.0f, 5.0f, 20.0f};
|
||||||
case NP::Receive: return {5.0f, 15.0f, 5.0f, 0.0f};
|
case NP::Receive: return {5.0f, 15.0f, 5.0f, 20.0f};
|
||||||
case NP::History: return {5.0f, 3.0f, 15.0f, 0.0f};
|
case NP::History: return {5.0f, 3.0f, 15.0f, 20.0f};
|
||||||
case NP::Mining: return {5.0f, 15.0f, 15.0f, 0.0f};
|
case NP::Mining: return {5.0f, 15.0f, 15.0f, 20.0f};
|
||||||
case NP::Peers: return {5.0f, 15.0f, 15.0f, 5.0f};
|
case NP::Peers: return {5.0f, 15.0f, 15.0f, 5.0f};
|
||||||
case NP::Market: return {5.0f, 15.0f, 15.0f, 0.0f};
|
case NP::Market: return {5.0f, 15.0f, 15.0f, 20.0f};
|
||||||
case NP::Console: return {10.0f, 30.0f, 30.0f, 0.0f};
|
case NP::Console: return {10.0f, 30.0f, 30.0f, 30.0f};
|
||||||
default: return {5.0f, 15.0f, 15.0f, 0.0f};
|
default: return {5.0f, 15.0f, 15.0f, 20.0f};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ public:
|
|||||||
static constexpr float kTxMaxAge = 15.0f;
|
static constexpr float kTxMaxAge = 15.0f;
|
||||||
static constexpr float kOpidPoll = 2.0f;
|
static constexpr float kOpidPoll = 2.0f;
|
||||||
|
|
||||||
|
// Low-impact polling profile applied while the daemon is SYNCING, regardless of the active tab.
|
||||||
|
// Only a slow progress poll runs (core, 10s); transactions/addresses/peers are disabled (0).
|
||||||
|
// Frequent getpeerinfo, per-block transaction scans, and balance polls all contend for the
|
||||||
|
// daemon's cs_main lock and measurably slow block connection during sync — this is exactly why
|
||||||
|
// the lightweight Console tab syncs faster than the Peers tab. Reverts to the per-tab profile
|
||||||
|
// once sync completes. (Tx/address/balance data is incomplete mid-sync anyway.)
|
||||||
|
static constexpr Intervals kSyncProfile{10.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
|
||||||
static Intervals intervalsForPage(ui::NavPage page);
|
static Intervals intervalsForPage(ui::NavPage page);
|
||||||
|
|
||||||
void applyPage(ui::NavPage page);
|
void applyPage(ui::NavPage page);
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
//
|
|
||||||
// Offscreen render-target scroll fade — the ImGui equivalent of CSS mask-image.
|
|
||||||
// Renders scrollable content to an offscreen surface, then composites it back
|
|
||||||
// as a textured mesh strip with vertex alpha for edge fading.
|
|
||||||
// This produces a true per-pixel fade that works with any background
|
|
||||||
// (including acrylic/backdrop transparency).
|
|
||||||
//
|
|
||||||
// Supports both OpenGL (DRAGONX_HAS_GLAD) and DX11 (DRAGONX_USE_DX11).
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Platform detection
|
|
||||||
// ============================================================================
|
|
||||||
#if defined(DRAGONX_USE_DX11)
|
|
||||||
#include <d3d11.h>
|
|
||||||
#define SCROLL_FADE_HAS_OFFSCREEN 1
|
|
||||||
#define SCROLL_FADE_DX11 1
|
|
||||||
#elif defined(DRAGONX_HAS_GLAD)
|
|
||||||
#include <glad/gl.h>
|
|
||||||
#include "../../util/logger.h"
|
|
||||||
#ifndef GL_FRAMEBUFFER_BINDING
|
|
||||||
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
|
||||||
#endif
|
|
||||||
#ifndef GL_VIEWPORT
|
|
||||||
#define GL_VIEWPORT 0x0BA2
|
|
||||||
#endif
|
|
||||||
#ifndef GL_SCISSOR_TEST
|
|
||||||
#define GL_SCISSOR_TEST 0x0C11
|
|
||||||
#endif
|
|
||||||
#define SCROLL_FADE_HAS_OFFSCREEN 1
|
|
||||||
#define SCROLL_FADE_GL 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_HAS_OFFSCREEN
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace effects {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// ScrollFadeRT — manages an offscreen render target for scroll-fade rendering
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
class ScrollFadeRT {
|
|
||||||
public:
|
|
||||||
ScrollFadeRT() = default;
|
|
||||||
~ScrollFadeRT() { destroy(); }
|
|
||||||
|
|
||||||
// Non-copyable
|
|
||||||
ScrollFadeRT(const ScrollFadeRT&) = delete;
|
|
||||||
ScrollFadeRT& operator=(const ScrollFadeRT&) = delete;
|
|
||||||
|
|
||||||
/// Ensure RT matches the required dimensions. Returns true if ready.
|
|
||||||
bool ensure(int w, int h) {
|
|
||||||
if (w <= 0 || h <= 0) return false;
|
|
||||||
if (isValid() && w == width_ && h == height_) return true;
|
|
||||||
return init(w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
void destroy();
|
|
||||||
bool isValid() const;
|
|
||||||
|
|
||||||
/// Get the texture as an ImTextureID for compositing.
|
|
||||||
ImTextureID textureID() const;
|
|
||||||
|
|
||||||
int width() const { return width_; }
|
|
||||||
int height() const { return height_; }
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_DX11
|
|
||||||
ID3D11RenderTargetView* rtv() const { return rtv_; }
|
|
||||||
#endif
|
|
||||||
#ifdef SCROLL_FADE_GL
|
|
||||||
unsigned int fbo() const { return fbo_; }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool init(int w, int h);
|
|
||||||
|
|
||||||
int width_ = 0;
|
|
||||||
int height_ = 0;
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_DX11
|
|
||||||
ID3D11Texture2D* tex_ = nullptr;
|
|
||||||
ID3D11RenderTargetView* rtv_ = nullptr;
|
|
||||||
ID3D11ShaderResourceView* srv_ = nullptr;
|
|
||||||
#endif
|
|
||||||
#ifdef SCROLL_FADE_GL
|
|
||||||
unsigned int fbo_ = 0;
|
|
||||||
unsigned int colorTex_ = 0;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementations
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_DX11
|
|
||||||
|
|
||||||
// --- DX11 helpers to get device/context from ImGui backend ---
|
|
||||||
inline ID3D11Device* GetDX11Device() {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
if (!io.BackendRendererUserData) return nullptr;
|
|
||||||
return *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
|
|
||||||
}
|
|
||||||
inline ID3D11DeviceContext* GetDX11Context() {
|
|
||||||
ID3D11Device* dev = GetDX11Device();
|
|
||||||
if (!dev) return nullptr;
|
|
||||||
ID3D11DeviceContext* ctx = nullptr;
|
|
||||||
dev->GetImmediateContext(&ctx);
|
|
||||||
return ctx; // caller must Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ScrollFadeRT::init(int w, int h) {
|
|
||||||
destroy();
|
|
||||||
ID3D11Device* dev = GetDX11Device();
|
|
||||||
if (!dev) return false;
|
|
||||||
|
|
||||||
width_ = w;
|
|
||||||
height_ = h;
|
|
||||||
|
|
||||||
// Create texture
|
|
||||||
D3D11_TEXTURE2D_DESC td = {};
|
|
||||||
td.Width = (UINT)w;
|
|
||||||
td.Height = (UINT)h;
|
|
||||||
td.MipLevels = 1;
|
|
||||||
td.ArraySize = 1;
|
|
||||||
td.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
||||||
td.SampleDesc.Count = 1;
|
|
||||||
td.Usage = D3D11_USAGE_DEFAULT;
|
|
||||||
td.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
|
|
||||||
|
|
||||||
if (FAILED(dev->CreateTexture2D(&td, nullptr, &tex_))) {
|
|
||||||
DEBUG_LOGF("ScrollFadeRT: CreateTexture2D failed\n");
|
|
||||||
destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render target view
|
|
||||||
if (FAILED(dev->CreateRenderTargetView(tex_, nullptr, &rtv_))) {
|
|
||||||
DEBUG_LOGF("ScrollFadeRT: CreateRenderTargetView failed\n");
|
|
||||||
destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shader resource view (for sampling as texture)
|
|
||||||
if (FAILED(dev->CreateShaderResourceView(tex_, nullptr, &srv_))) {
|
|
||||||
DEBUG_LOGF("ScrollFadeRT: CreateShaderResourceView failed\n");
|
|
||||||
destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ScrollFadeRT::destroy() {
|
|
||||||
if (srv_) { srv_->Release(); srv_ = nullptr; }
|
|
||||||
if (rtv_) { rtv_->Release(); rtv_ = nullptr; }
|
|
||||||
if (tex_) { tex_->Release(); tex_ = nullptr; }
|
|
||||||
width_ = height_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ScrollFadeRT::isValid() const { return rtv_ != nullptr; }
|
|
||||||
|
|
||||||
inline ImTextureID ScrollFadeRT::textureID() const {
|
|
||||||
return (ImTextureID)srv_;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // SCROLL_FADE_DX11
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_GL
|
|
||||||
|
|
||||||
inline bool ScrollFadeRT::init(int w, int h) {
|
|
||||||
destroy();
|
|
||||||
width_ = w;
|
|
||||||
height_ = h;
|
|
||||||
|
|
||||||
glGenFramebuffers(1, &fbo_);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
|
|
||||||
|
|
||||||
glGenTextures(1, &colorTex_);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, colorTex_);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
|
|
||||||
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
||||||
GL_TEXTURE_2D, colorTex_, 0);
|
|
||||||
|
|
||||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
|
|
||||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
|
||||||
DEBUG_LOGF("ScrollFadeRT: FBO incomplete (0x%X)\n", status);
|
|
||||||
destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ScrollFadeRT::destroy() {
|
|
||||||
if (colorTex_) { glDeleteTextures(1, &colorTex_); colorTex_ = 0; }
|
|
||||||
if (fbo_) { glDeleteFramebuffers(1, &fbo_); fbo_ = 0; }
|
|
||||||
width_ = height_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ScrollFadeRT::isValid() const { return fbo_ != 0; }
|
|
||||||
|
|
||||||
inline ImTextureID ScrollFadeRT::textureID() const {
|
|
||||||
return (ImTextureID)(intptr_t)colorTex_;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // SCROLL_FADE_GL
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Callback state — singleton storage for bind/unbind data
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
struct ScrollFadeState {
|
|
||||||
#ifdef SCROLL_FADE_DX11
|
|
||||||
ID3D11RenderTargetView* offscreenRTV = nullptr;
|
|
||||||
ID3D11RenderTargetView* savedRTV = nullptr;
|
|
||||||
ID3D11DepthStencilView* savedDSV = nullptr;
|
|
||||||
D3D11_VIEWPORT savedVP = {};
|
|
||||||
#endif
|
|
||||||
#ifdef SCROLL_FADE_GL
|
|
||||||
unsigned int fbo = 0;
|
|
||||||
int savedFBO = 0;
|
|
||||||
int savedVP[4] = {};
|
|
||||||
bool savedScissorEnabled = true; // ImGui always has scissor enabled
|
|
||||||
#endif
|
|
||||||
int vpW = 0, vpH = 0; // framebuffer pixel dimensions for viewport
|
|
||||||
};
|
|
||||||
|
|
||||||
inline ScrollFadeState& GetScrollFadeState() {
|
|
||||||
static ScrollFadeState s;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Callbacks — inserted into the draw list via AddCallback
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_DX11
|
|
||||||
|
|
||||||
inline void BindRTCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
auto& st = GetScrollFadeState();
|
|
||||||
ID3D11DeviceContext* ctx = GetDX11Context();
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// Save current RT and viewport
|
|
||||||
UINT numVP = 1;
|
|
||||||
ctx->OMGetRenderTargets(1, &st.savedRTV, &st.savedDSV);
|
|
||||||
ctx->RSGetViewports(&numVP, &st.savedVP);
|
|
||||||
|
|
||||||
// Bind offscreen RT
|
|
||||||
ctx->OMSetRenderTargets(1, &st.offscreenRTV, nullptr);
|
|
||||||
|
|
||||||
// Set viewport to match RT size
|
|
||||||
D3D11_VIEWPORT vp = {};
|
|
||||||
vp.Width = (FLOAT)st.vpW;
|
|
||||||
vp.Height = (FLOAT)st.vpH;
|
|
||||||
vp.MaxDepth = 1.0f;
|
|
||||||
ctx->RSSetViewports(1, &vp);
|
|
||||||
|
|
||||||
// Clear to transparent
|
|
||||||
float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
|
||||||
ctx->ClearRenderTargetView(st.offscreenRTV, clearColor);
|
|
||||||
|
|
||||||
ctx->Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void UnbindRTCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
auto& st = GetScrollFadeState();
|
|
||||||
ID3D11DeviceContext* ctx = GetDX11Context();
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// Restore previous RT and viewport
|
|
||||||
ctx->OMSetRenderTargets(1, &st.savedRTV, st.savedDSV);
|
|
||||||
ctx->RSSetViewports(1, &st.savedVP);
|
|
||||||
|
|
||||||
// Release the refs from OMGetRenderTargets
|
|
||||||
if (st.savedRTV) { st.savedRTV->Release(); st.savedRTV = nullptr; }
|
|
||||||
if (st.savedDSV) { st.savedDSV->Release(); st.savedDSV = nullptr; }
|
|
||||||
|
|
||||||
ctx->Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // SCROLL_FADE_DX11
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_GL
|
|
||||||
|
|
||||||
inline void BindRTCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
auto& st = GetScrollFadeState();
|
|
||||||
|
|
||||||
// Save current FBO and viewport
|
|
||||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &st.savedFBO);
|
|
||||||
glGetIntegerv(GL_VIEWPORT, st.savedVP);
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, st.fbo);
|
|
||||||
glViewport(0, 0, st.vpW, st.vpH);
|
|
||||||
|
|
||||||
// Disable scissor test inside the FBO. ImGui's renderer computes
|
|
||||||
// scissor rects relative to the main framebuffer dimensions — those
|
|
||||||
// coordinates would be wrong for our offscreen surface. The child
|
|
||||||
// window's content is already bounded by ImGui's layout, and the
|
|
||||||
// composite step applies its own clip rect, so skipping scissor
|
|
||||||
// in the FBO is safe.
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
|
|
||||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void UnbindRTCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
auto& st = GetScrollFadeState();
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, (unsigned int)st.savedFBO);
|
|
||||||
glViewport(st.savedVP[0], st.savedVP[1], st.savedVP[2], st.savedVP[3]);
|
|
||||||
if (st.savedScissorEnabled)
|
|
||||||
glEnable(GL_SCISSOR_TEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // SCROLL_FADE_GL
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Composite helper — draw the RT texture as a mesh strip with alpha fade
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// Draw the offscreen texture onto `dl` as a vertical strip with alpha=0 at
|
|
||||||
/// the faded edges and alpha=1 in the middle. Produces a true CSS-like
|
|
||||||
/// mask-image: linear-gradient() result.
|
|
||||||
///
|
|
||||||
/// @param logicalW, logicalH Logical display dimensions (DisplaySize) for
|
|
||||||
/// UV calculation — NOT the RT pixel dimensions. ImGui screen coords
|
|
||||||
/// are in logical units, and the FBO projection maps them 1:1 to the
|
|
||||||
/// logical coordinate space, so UVs must divide by logical size.
|
|
||||||
inline void CompositeWithFade(ImDrawList* dl,
|
|
||||||
ImTextureID texID,
|
|
||||||
const ImVec2& screenMin,
|
|
||||||
const ImVec2& screenMax,
|
|
||||||
int logicalW, int logicalH,
|
|
||||||
float fadeTop, float fadeBot,
|
|
||||||
bool needTop, bool needBot)
|
|
||||||
{
|
|
||||||
float left = screenMin.x;
|
|
||||||
float right = screenMax.x;
|
|
||||||
float y0 = screenMin.y;
|
|
||||||
float y1 = screenMin.y + (needTop ? fadeTop : 0.0f);
|
|
||||||
float y2 = screenMax.y - (needBot ? fadeBot : 0.0f);
|
|
||||||
float y3 = screenMax.y;
|
|
||||||
|
|
||||||
// Clamp in case fade zones overlap
|
|
||||||
if (y1 > y2) { float mid = (y0 + y3) * 0.5f; y1 = y2 = mid; }
|
|
||||||
|
|
||||||
// UV coordinates — map screen position (logical) to render target texture.
|
|
||||||
// Screen coords are in logical (DisplaySize) space. The FBO projection
|
|
||||||
// maps these 1:1, so divide by logical dimensions to get [0,1] UVs.
|
|
||||||
float uL = screenMin.x / (float)logicalW;
|
|
||||||
float uR = screenMax.x / (float)logicalW;
|
|
||||||
|
|
||||||
#ifdef SCROLL_FADE_GL
|
|
||||||
// OpenGL: FBO Y is flipped (ImGui top=0 → GL bottom=0)
|
|
||||||
auto uvY = [&](float y) -> float { return 1.0f - y / (float)logicalH; };
|
|
||||||
#else
|
|
||||||
// DX11: no Y flip (both ImGui and DX11 have (0,0) at top-left)
|
|
||||||
auto uvY = [&](float y) -> float { return y / (float)logicalH; };
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ImU32 colOpaque = IM_COL32(255, 255, 255, 255);
|
|
||||||
ImU32 colClear = IM_COL32(255, 255, 255, 0);
|
|
||||||
ImU32 colTop = needTop ? colClear : colOpaque;
|
|
||||||
ImU32 colBot = needBot ? colClear : colOpaque;
|
|
||||||
|
|
||||||
dl->PushTextureID(texID);
|
|
||||||
dl->PrimReserve(18, 8);
|
|
||||||
|
|
||||||
ImDrawVert* vtx = dl->_VtxWritePtr;
|
|
||||||
ImDrawIdx* idx = dl->_IdxWritePtr;
|
|
||||||
ImDrawIdx base = (ImDrawIdx)dl->_VtxCurrentIdx;
|
|
||||||
|
|
||||||
vtx[0] = { ImVec2(left, y0), ImVec2(uL, uvY(y0)), colTop };
|
|
||||||
vtx[1] = { ImVec2(right, y0), ImVec2(uR, uvY(y0)), colTop };
|
|
||||||
vtx[2] = { ImVec2(left, y1), ImVec2(uL, uvY(y1)), colOpaque };
|
|
||||||
vtx[3] = { ImVec2(right, y1), ImVec2(uR, uvY(y1)), colOpaque };
|
|
||||||
vtx[4] = { ImVec2(left, y2), ImVec2(uL, uvY(y2)), colOpaque };
|
|
||||||
vtx[5] = { ImVec2(right, y2), ImVec2(uR, uvY(y2)), colOpaque };
|
|
||||||
vtx[6] = { ImVec2(left, y3), ImVec2(uL, uvY(y3)), colBot };
|
|
||||||
vtx[7] = { ImVec2(right, y3), ImVec2(uR, uvY(y3)), colBot };
|
|
||||||
|
|
||||||
idx[0] = base+0; idx[1] = base+1; idx[2] = base+3;
|
|
||||||
idx[3] = base+0; idx[4] = base+3; idx[5] = base+2;
|
|
||||||
idx[6] = base+2; idx[7] = base+3; idx[8] = base+5;
|
|
||||||
idx[9] = base+2; idx[10] = base+5; idx[11] = base+4;
|
|
||||||
idx[12] = base+4; idx[13] = base+5; idx[14] = base+7;
|
|
||||||
idx[15] = base+4; idx[16] = base+7; idx[17] = base+6;
|
|
||||||
|
|
||||||
dl->_VtxWritePtr += 8;
|
|
||||||
dl->_IdxWritePtr += 18;
|
|
||||||
dl->_VtxCurrentIdx += 8;
|
|
||||||
|
|
||||||
dl->PopTextureID();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace effects
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
|
|
||||||
#endif // SCROLL_FADE_HAS_OFFSCREEN
|
|
||||||
@@ -153,6 +153,45 @@ std::map<int, ExplorerBlockSummary> ExplorerBlockCache::loadRange(int minHeight,
|
|||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ExplorerBlockSummary> ExplorerBlockCache::searchBlocks(const std::string& query, int limit)
|
||||||
|
{
|
||||||
|
std::vector<ExplorerBlockSummary> results;
|
||||||
|
if (query.empty() || limit < 1 || !ensureOpen()) return results;
|
||||||
|
|
||||||
|
// Escape LIKE wildcards in the user input so '%' / '_' are matched literally.
|
||||||
|
std::string escaped;
|
||||||
|
escaped.reserve(query.size());
|
||||||
|
for (char c : query) {
|
||||||
|
if (c == '%' || c == '_' || c == '\\') escaped.push_back('\\');
|
||||||
|
escaped.push_back(c);
|
||||||
|
}
|
||||||
|
std::string pattern = "%" + escaped + "%";
|
||||||
|
|
||||||
|
Statement statement(db_,
|
||||||
|
"SELECT height, hash, tx_count, size, time, difficulty "
|
||||||
|
"FROM explorer_blocks "
|
||||||
|
"WHERE CAST(height AS TEXT) LIKE ?1 ESCAPE '\\' OR hash LIKE ?1 ESCAPE '\\' "
|
||||||
|
"ORDER BY height DESC LIMIT ?2");
|
||||||
|
if (!statement.handle) return results;
|
||||||
|
|
||||||
|
sqlite3_bind_text(statement.handle, 1, pattern.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_int(statement.handle, 2, limit);
|
||||||
|
|
||||||
|
while (sqlite3_step(statement.handle) == SQLITE_ROW) {
|
||||||
|
ExplorerBlockSummary block;
|
||||||
|
block.height = sqlite3_column_int(statement.handle, 0);
|
||||||
|
const unsigned char* hashText = sqlite3_column_text(statement.handle, 1);
|
||||||
|
if (hashText) block.hash = reinterpret_cast<const char*>(hashText);
|
||||||
|
block.tx_count = sqlite3_column_int(statement.handle, 2);
|
||||||
|
block.size = sqlite3_column_int(statement.handle, 3);
|
||||||
|
block.time = static_cast<std::int64_t>(sqlite3_column_int64(statement.handle, 4));
|
||||||
|
block.difficulty = sqlite3_column_double(statement.handle, 5);
|
||||||
|
if (block.height > 0 && !block.hash.empty()) results.push_back(std::move(block));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
|
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
|
||||||
{
|
{
|
||||||
if (block.height < 1 || block.hash.empty() || !ensureOpen()) return false;
|
if (block.height < 1 || block.hash.empty() || !ensureOpen()) return false;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
struct sqlite3;
|
struct sqlite3;
|
||||||
|
|
||||||
@@ -41,6 +42,9 @@ public:
|
|||||||
const std::string& databasePath() const { return database_path_; }
|
const std::string& databasePath() const { return database_path_; }
|
||||||
|
|
||||||
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
|
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
|
||||||
|
// Fuzzy search over cached blocks: matches when the query is a substring of the height (as text)
|
||||||
|
// or the block hash (case-insensitive). Returns newest-first, capped at `limit`.
|
||||||
|
std::vector<ExplorerBlockSummary> searchBlocks(const std::string& query, int limit);
|
||||||
bool storeBlock(const ExplorerBlockSummary& block);
|
bool storeBlock(const ExplorerBlockSummary& block);
|
||||||
int cachedBlockCount();
|
int cachedBlockCount();
|
||||||
void clearBlocks();
|
void clearBlocks();
|
||||||
|
|||||||
@@ -1,501 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "layout.h"
|
|
||||||
#include "colors.h"
|
|
||||||
#include "typography.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// App Layout Manager
|
|
||||||
// ============================================================================
|
|
||||||
// Manages the overall application layout following Material Design patterns.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// // In your main render loop:
|
|
||||||
// auto& layout = AppLayout::instance();
|
|
||||||
// layout.beginFrame();
|
|
||||||
//
|
|
||||||
// // Render app bar
|
|
||||||
// if (layout.beginAppBar("DragonX Wallet")) {
|
|
||||||
// // App bar content (menu items, etc.)
|
|
||||||
// layout.endAppBar();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Render navigation
|
|
||||||
// if (layout.beginNavigation()) {
|
|
||||||
// layout.navItem("Balance", ICON_WALLET, currentTab == 0);
|
|
||||||
// layout.navItem("Send", ICON_SEND, currentTab == 1);
|
|
||||||
// layout.endNavigation();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Render main content
|
|
||||||
// if (layout.beginContent()) {
|
|
||||||
// // Your content here
|
|
||||||
// layout.endContent();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// layout.endFrame();
|
|
||||||
|
|
||||||
class AppLayout {
|
|
||||||
public:
|
|
||||||
static AppLayout& instance() {
|
|
||||||
static AppLayout s_instance;
|
|
||||||
return s_instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Frame Management
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a new frame layout
|
|
||||||
*
|
|
||||||
* Call this at the start of each frame before any layout calls.
|
|
||||||
* Updates responsive breakpoints and calculates regions.
|
|
||||||
*/
|
|
||||||
void beginFrame();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End the frame layout
|
|
||||||
*/
|
|
||||||
void endFrame();
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Layout Regions
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin the app bar region
|
|
||||||
*
|
|
||||||
* @param title App title to display
|
|
||||||
* @param showBack Show back button (for sub-pages)
|
|
||||||
* @return true if app bar is visible
|
|
||||||
*/
|
|
||||||
bool beginAppBar(const char* title, bool showBack = false);
|
|
||||||
void endAppBar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin the navigation region (drawer/rail/bottom)
|
|
||||||
*
|
|
||||||
* @return true if navigation region is visible
|
|
||||||
*/
|
|
||||||
bool beginNavigation();
|
|
||||||
void endNavigation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render a navigation item
|
|
||||||
*
|
|
||||||
* @param label Item label
|
|
||||||
* @param icon Icon glyph (can be nullptr)
|
|
||||||
* @param selected Whether this item is currently selected
|
|
||||||
* @return true if clicked
|
|
||||||
*/
|
|
||||||
bool navItem(const char* label, const char* icon, bool selected);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a navigation section divider
|
|
||||||
*
|
|
||||||
* @param title Optional section title
|
|
||||||
*/
|
|
||||||
void navSection(const char* title = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin the main content region
|
|
||||||
*
|
|
||||||
* @return true if content region is visible
|
|
||||||
*/
|
|
||||||
bool beginContent();
|
|
||||||
void endContent();
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Card Helpers
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a Material Design card
|
|
||||||
*
|
|
||||||
* @param id Unique ID for the card
|
|
||||||
* @param layout Card layout configuration
|
|
||||||
* @return true if card is visible
|
|
||||||
*/
|
|
||||||
bool beginCard(const char* id, const CardLayout& layout = CardLayout());
|
|
||||||
void endCard();
|
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Layout Queries
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current breakpoint category
|
|
||||||
*/
|
|
||||||
breakpoint::Category getBreakpoint() const { return breakpoint_; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current navigation style
|
|
||||||
*/
|
|
||||||
breakpoint::NavStyle getNavStyle() const { return navStyle_; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get content region available width
|
|
||||||
*/
|
|
||||||
float getContentWidth() const { return contentWidth_; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get content region available height
|
|
||||||
*/
|
|
||||||
float getContentHeight() const { return contentHeight_; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if navigation drawer is expanded
|
|
||||||
*/
|
|
||||||
bool isNavExpanded() const { return navExpanded_; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Toggle navigation drawer expansion
|
|
||||||
*/
|
|
||||||
void toggleNav() { navExpanded_ = !navExpanded_; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set navigation drawer expansion state
|
|
||||||
*/
|
|
||||||
void setNavExpanded(bool expanded) { navExpanded_ = expanded; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
AppLayout();
|
|
||||||
~AppLayout() = default;
|
|
||||||
AppLayout(const AppLayout&) = delete;
|
|
||||||
AppLayout& operator=(const AppLayout&) = delete;
|
|
||||||
|
|
||||||
// Layout state
|
|
||||||
breakpoint::Category breakpoint_ = breakpoint::Category::Md;
|
|
||||||
breakpoint::NavStyle navStyle_ = breakpoint::NavStyle::NavDrawer;
|
|
||||||
float windowWidth_ = 0;
|
|
||||||
float windowHeight_ = 0;
|
|
||||||
float contentWidth_ = 0;
|
|
||||||
float contentHeight_ = 0;
|
|
||||||
bool navExpanded_ = true;
|
|
||||||
|
|
||||||
// Region tracking
|
|
||||||
bool inAppBar_ = false;
|
|
||||||
bool inNav_ = false;
|
|
||||||
bool inContent_ = false;
|
|
||||||
|
|
||||||
// Calculated regions
|
|
||||||
ImVec2 appBarPos_;
|
|
||||||
ImVec2 appBarSize_;
|
|
||||||
ImVec2 navPos_;
|
|
||||||
ImVec2 navSize_;
|
|
||||||
ImVec2 contentPos_;
|
|
||||||
ImVec2 contentSize_;
|
|
||||||
|
|
||||||
void calculateRegions();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Inline Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline AppLayout::AppLayout() {
|
|
||||||
// Initialize with reasonable defaults
|
|
||||||
navExpanded_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::beginFrame() {
|
|
||||||
// Get main viewport size
|
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
||||||
windowWidth_ = viewport->WorkSize.x;
|
|
||||||
windowHeight_ = viewport->WorkSize.y;
|
|
||||||
|
|
||||||
// Update responsive state
|
|
||||||
breakpoint_ = breakpoint::GetCategory(windowWidth_);
|
|
||||||
navStyle_ = breakpoint::GetNavStyle(breakpoint_);
|
|
||||||
|
|
||||||
// Auto-collapse nav on small screens
|
|
||||||
if (breakpoint_ == breakpoint::Category::Xs) {
|
|
||||||
navExpanded_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateRegions();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::endFrame() {
|
|
||||||
// Reset state
|
|
||||||
inAppBar_ = false;
|
|
||||||
inNav_ = false;
|
|
||||||
inContent_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::calculateRegions() {
|
|
||||||
// App bar at top
|
|
||||||
appBarPos_ = ImVec2(0, 0);
|
|
||||||
appBarSize_ = ImVec2(windowWidth_, size::AppBarHeight);
|
|
||||||
|
|
||||||
float belowAppBar = size::AppBarHeight;
|
|
||||||
float contentAreaHeight = windowHeight_ - belowAppBar;
|
|
||||||
|
|
||||||
// Navigation region
|
|
||||||
switch (navStyle_) {
|
|
||||||
case breakpoint::NavStyle::NavDrawer:
|
|
||||||
if (navExpanded_) {
|
|
||||||
navSize_ = ImVec2(size::NavDrawerWidth, contentAreaHeight);
|
|
||||||
} else {
|
|
||||||
navSize_ = ImVec2(size::NavRailWidth, contentAreaHeight);
|
|
||||||
}
|
|
||||||
navPos_ = ImVec2(0, belowAppBar);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case breakpoint::NavStyle::NavRail:
|
|
||||||
navSize_ = ImVec2(size::NavRailWidth, contentAreaHeight);
|
|
||||||
navPos_ = ImVec2(0, belowAppBar);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case breakpoint::NavStyle::BottomNav:
|
|
||||||
// Bottom nav handled separately
|
|
||||||
navSize_ = ImVec2(windowWidth_, size::NavItemHeight);
|
|
||||||
navPos_ = ImVec2(0, windowHeight_ - size::NavItemHeight);
|
|
||||||
contentAreaHeight -= size::NavItemHeight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content region
|
|
||||||
if (navStyle_ == breakpoint::NavStyle::BottomNav) {
|
|
||||||
contentPos_ = ImVec2(0, belowAppBar);
|
|
||||||
contentSize_ = ImVec2(windowWidth_, contentAreaHeight);
|
|
||||||
} else {
|
|
||||||
contentPos_ = ImVec2(navSize_.x, belowAppBar);
|
|
||||||
contentSize_ = ImVec2(windowWidth_ - navSize_.x, contentAreaHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWidth_ = contentSize_.x;
|
|
||||||
contentHeight_ = contentSize_.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppLayout::beginAppBar(const char* title, bool showBack) {
|
|
||||||
ImGui::SetNextWindowPos(appBarPos_);
|
|
||||||
ImGui::SetNextWindowSize(appBarSize_);
|
|
||||||
|
|
||||||
ImGuiWindowFlags flags =
|
|
||||||
ImGuiWindowFlags_NoTitleBar |
|
|
||||||
ImGuiWindowFlags_NoResize |
|
|
||||||
ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoScrollbar |
|
|
||||||
ImGuiWindowFlags_NoCollapse |
|
|
||||||
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
||||||
|
|
||||||
// Use elevated surface color for app bar
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, SurfaceVec4(4));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(size::AppBarPadding, 0));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
|
||||||
|
|
||||||
bool visible = ImGui::Begin("##AppBar", nullptr, flags);
|
|
||||||
|
|
||||||
if (visible) {
|
|
||||||
inAppBar_ = true;
|
|
||||||
|
|
||||||
// Center content vertically
|
|
||||||
float centerY = (size::AppBarHeight - Typography::instance().getFont(TypeStyle::H6)->FontSize) * 0.5f;
|
|
||||||
ImGui::SetCursorPosY(centerY);
|
|
||||||
|
|
||||||
// Menu/back button
|
|
||||||
if (showBack) {
|
|
||||||
if (ImGui::Button("<")) {
|
|
||||||
// Back action - handled by caller
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
} else if (navStyle_ != breakpoint::NavStyle::BottomNav) {
|
|
||||||
// Menu button to toggle nav
|
|
||||||
if (ImGui::Button("=")) {
|
|
||||||
toggleNav();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title
|
|
||||||
Typography::instance().text(TypeStyle::H6, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::endAppBar() {
|
|
||||||
ImGui::End();
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
inAppBar_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppLayout::beginNavigation() {
|
|
||||||
if (navStyle_ == breakpoint::NavStyle::BottomNav) {
|
|
||||||
ImGui::SetNextWindowPos(navPos_);
|
|
||||||
} else {
|
|
||||||
ImGui::SetNextWindowPos(navPos_);
|
|
||||||
}
|
|
||||||
ImGui::SetNextWindowSize(navSize_);
|
|
||||||
|
|
||||||
ImGuiWindowFlags flags =
|
|
||||||
ImGuiWindowFlags_NoTitleBar |
|
|
||||||
ImGuiWindowFlags_NoResize |
|
|
||||||
ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoCollapse |
|
|
||||||
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
||||||
|
|
||||||
// Nav drawer has higher elevation
|
|
||||||
int elevation = (navStyle_ == breakpoint::NavStyle::NavDrawer) ? 16 : 0;
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, SurfaceVec4(elevation));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, size::NavSectionPadding));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
|
||||||
|
|
||||||
bool visible = ImGui::Begin("##Navigation", nullptr, flags);
|
|
||||||
|
|
||||||
if (visible) {
|
|
||||||
inNav_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::endNavigation() {
|
|
||||||
ImGui::End();
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
inNav_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppLayout::navItem(const char* label, const char* icon, bool selected) {
|
|
||||||
bool compact = !navExpanded_ || navStyle_ == breakpoint::NavStyle::NavRail;
|
|
||||||
|
|
||||||
float itemWidth = navSize_.x;
|
|
||||||
float itemHeight = size::NavItemHeight;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
// Selection highlight
|
|
||||||
if (selected) {
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
pos,
|
|
||||||
ImVec2(pos.x + itemWidth, pos.y + itemHeight),
|
|
||||||
StateSelected()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Padding
|
|
||||||
ImGui::SetCursorPosX(size::NavItemPadding);
|
|
||||||
|
|
||||||
// Content
|
|
||||||
bool clicked = false;
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
|
|
||||||
if (compact) {
|
|
||||||
// Rail/collapsed: Icon only, centered
|
|
||||||
CenterHorizontally(size::IconSize);
|
|
||||||
clicked = ImGui::Selectable(icon ? icon : "?", selected, 0, ImVec2(size::IconSize, itemHeight));
|
|
||||||
} else {
|
|
||||||
// Drawer: Icon + label
|
|
||||||
if (icon) {
|
|
||||||
ImGui::Text("%s", icon);
|
|
||||||
ImGui::SameLine();
|
|
||||||
}
|
|
||||||
float selectableWidth = itemWidth - size::NavItemPadding * 2 - (icon ? size::IconSize + spacing::Sm : 0);
|
|
||||||
clicked = ImGui::Selectable(label, selected, 0, ImVec2(selectableWidth, itemHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndGroup();
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return clicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::navSection(const char* title) {
|
|
||||||
VSpace(1);
|
|
||||||
|
|
||||||
if (title && navExpanded_) {
|
|
||||||
ImGui::SetCursorPosX(size::NavItemPadding);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Divider
|
|
||||||
ImGui::Separator();
|
|
||||||
VSpace(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppLayout::beginContent() {
|
|
||||||
ImGui::SetNextWindowPos(contentPos_);
|
|
||||||
ImGui::SetNextWindowSize(contentSize_);
|
|
||||||
|
|
||||||
ImGuiWindowFlags flags =
|
|
||||||
ImGuiWindowFlags_NoTitleBar |
|
|
||||||
ImGuiWindowFlags_NoResize |
|
|
||||||
ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoScrollbar |
|
|
||||||
ImGuiWindowFlags_NoCollapse |
|
|
||||||
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::ColorConvertU32ToFloat4(Background()));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::Md, spacing::Md));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
|
||||||
|
|
||||||
bool visible = ImGui::Begin("##Content", nullptr, flags);
|
|
||||||
|
|
||||||
if (visible) {
|
|
||||||
inContent_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::endContent() {
|
|
||||||
ImGui::End();
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
inContent_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppLayout::beginCard(const char* id, const CardLayout& layout) {
|
|
||||||
float width = layout.width > 0 ? layout.width : ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
// Card background
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, SurfaceVec4(layout.elevation));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, layout.cornerRadius);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(layout.padding, layout.padding));
|
|
||||||
|
|
||||||
ImVec2 size(width, layout.minHeight > 0 ? layout.minHeight : 0);
|
|
||||||
bool visible = ImGui::BeginChild(id, size, ImGuiChildFlags_AutoResizeY);
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppLayout::endCard() {
|
|
||||||
ImGui::EndChild();
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
// Add spacing after card
|
|
||||||
VSpace(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Convenience Function
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the app layout instance
|
|
||||||
*/
|
|
||||||
inline AppLayout& Layout() {
|
|
||||||
return AppLayout::instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "buttons.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design App Bar Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/app-bars-top
|
|
||||||
//
|
|
||||||
// The top app bar displays information and actions relating to the current
|
|
||||||
// screen.
|
|
||||||
|
|
||||||
enum class AppBarType {
|
|
||||||
Regular, // Standard height (56/64dp)
|
|
||||||
Prominent, // Extended height for larger titles
|
|
||||||
Dense // Smaller height for desktop
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief App bar configuration
|
|
||||||
*/
|
|
||||||
struct AppBarSpec {
|
|
||||||
AppBarType type = AppBarType::Regular;
|
|
||||||
ImU32 backgroundColor = 0; // 0 = use elevated surface
|
|
||||||
bool elevated = true; // Show elevation
|
|
||||||
bool centerTitle = false; // Center title (default: left)
|
|
||||||
float elevation = 4.0f; // Elevation in dp
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a top app bar
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param spec App bar configuration
|
|
||||||
* @return true if app bar is visible
|
|
||||||
*/
|
|
||||||
bool BeginAppBar(const char* id, const AppBarSpec& spec = AppBarSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End app bar
|
|
||||||
*/
|
|
||||||
void EndAppBar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set app bar navigation icon (left side)
|
|
||||||
*
|
|
||||||
* @param icon Icon text (e.g., "☰" for menu)
|
|
||||||
* @param tooltip Optional tooltip
|
|
||||||
* @return true if clicked
|
|
||||||
*/
|
|
||||||
bool AppBarNavIcon(const char* icon, const char* tooltip = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set app bar title
|
|
||||||
*/
|
|
||||||
void AppBarTitle(const char* title);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add app bar action button (right side)
|
|
||||||
*
|
|
||||||
* @param icon Icon text
|
|
||||||
* @param tooltip Optional tooltip
|
|
||||||
* @return true if clicked
|
|
||||||
*/
|
|
||||||
bool AppBarAction(const char* icon, const char* tooltip = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin app bar action menu (for overflow)
|
|
||||||
*/
|
|
||||||
bool BeginAppBarMenu(const char* icon);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End app bar action menu
|
|
||||||
*/
|
|
||||||
void EndAppBarMenu();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add menu item to app bar menu
|
|
||||||
*/
|
|
||||||
bool AppBarMenuItem(const char* label, const char* icon = nullptr);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
struct AppBarState {
|
|
||||||
ImVec2 barMin;
|
|
||||||
ImVec2 barMax;
|
|
||||||
float height;
|
|
||||||
float navIconWidth;
|
|
||||||
float actionsStartX;
|
|
||||||
float titleX;
|
|
||||||
bool hasNavIcon;
|
|
||||||
bool centerTitle;
|
|
||||||
ImU32 backgroundColor;
|
|
||||||
};
|
|
||||||
|
|
||||||
static AppBarState g_appBarState;
|
|
||||||
|
|
||||||
inline bool BeginAppBar(const char* id, const AppBarSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
|
|
||||||
// Calculate height based on type
|
|
||||||
float barHeight;
|
|
||||||
switch (spec.type) {
|
|
||||||
case AppBarType::Prominent:
|
|
||||||
barHeight = 128.0f;
|
|
||||||
break;
|
|
||||||
case AppBarType::Dense:
|
|
||||||
barHeight = 48.0f;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
barHeight = size::AppBarHeight; // 56dp
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_appBarState.height = barHeight;
|
|
||||||
g_appBarState.hasNavIcon = false;
|
|
||||||
g_appBarState.centerTitle = spec.centerTitle;
|
|
||||||
g_appBarState.navIconWidth = 0;
|
|
||||||
g_appBarState.actionsStartX = io.DisplaySize.x; // Will be adjusted as actions added
|
|
||||||
|
|
||||||
// Bar position (always at top)
|
|
||||||
g_appBarState.barMin = ImVec2(0, 0);
|
|
||||||
g_appBarState.barMax = ImVec2(io.DisplaySize.x, barHeight);
|
|
||||||
|
|
||||||
// Background color
|
|
||||||
if (spec.backgroundColor != 0) {
|
|
||||||
g_appBarState.backgroundColor = spec.backgroundColor;
|
|
||||||
} else {
|
|
||||||
g_appBarState.backgroundColor = Surface(Elevation::Dp4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw app bar background
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
drawList->AddRectFilled(g_appBarState.barMin, g_appBarState.barMax, g_appBarState.backgroundColor);
|
|
||||||
|
|
||||||
// Bottom divider/shadow
|
|
||||||
if (spec.elevated) {
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(g_appBarState.barMin.x, g_appBarState.barMax.y),
|
|
||||||
ImVec2(g_appBarState.barMax.x, g_appBarState.barMax.y),
|
|
||||||
schema::UI().resolveColor("var(--app-bar-shadow)", IM_COL32(0, 0, 0, 50))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up layout
|
|
||||||
g_appBarState.titleX = spacing::dp(2); // Default title position
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndAppBar() {
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppBarNavIcon(const char* icon, const char* tooltip) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
g_appBarState.hasNavIcon = true;
|
|
||||||
g_appBarState.navIconWidth = size::TouchTarget;
|
|
||||||
|
|
||||||
// Position nav icon on left
|
|
||||||
float iconX = spacing::dp(0.5f); // 4dp left margin
|
|
||||||
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
|
|
||||||
|
|
||||||
ImVec2 buttonPos(iconX, centerY - size::TouchTarget * 0.5f);
|
|
||||||
|
|
||||||
// Draw icon button
|
|
||||||
ImGui::SetCursorScreenPos(buttonPos);
|
|
||||||
bool clicked = IconButton(icon, tooltip);
|
|
||||||
|
|
||||||
// Update title position
|
|
||||||
g_appBarState.titleX = iconX + size::TouchTarget + spacing::dp(1);
|
|
||||||
|
|
||||||
return clicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppBarTitle(const char* title) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
|
|
||||||
|
|
||||||
// Calculate title position
|
|
||||||
float titleX;
|
|
||||||
if (g_appBarState.centerTitle) {
|
|
||||||
// Center title between nav icon and actions
|
|
||||||
float availableWidth = g_appBarState.actionsStartX - g_appBarState.titleX;
|
|
||||||
Typography::instance().pushFont(TypeStyle::H6);
|
|
||||||
float titleWidth = ImGui::CalcTextSize(title).x;
|
|
||||||
Typography::instance().popFont();
|
|
||||||
titleX = g_appBarState.titleX + (availableWidth - titleWidth) * 0.5f;
|
|
||||||
} else {
|
|
||||||
titleX = g_appBarState.titleX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render title
|
|
||||||
Typography::instance().pushFont(TypeStyle::H6);
|
|
||||||
float titleY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
drawList->AddText(ImVec2(titleX, titleY), OnSurface(), title);
|
|
||||||
|
|
||||||
Typography::instance().popFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppBarAction(const char* icon, const char* tooltip) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Actions are positioned from right edge
|
|
||||||
g_appBarState.actionsStartX -= size::TouchTarget;
|
|
||||||
|
|
||||||
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
|
|
||||||
ImVec2 buttonPos(g_appBarState.actionsStartX, centerY - size::TouchTarget * 0.5f);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(buttonPos);
|
|
||||||
return IconButton(icon, tooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool BeginAppBarMenu(const char* icon) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Position menu button
|
|
||||||
g_appBarState.actionsStartX -= size::TouchTarget;
|
|
||||||
|
|
||||||
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
|
|
||||||
ImVec2 buttonPos(g_appBarState.actionsStartX, centerY - size::TouchTarget * 0.5f);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(buttonPos);
|
|
||||||
|
|
||||||
bool menuOpen = false;
|
|
||||||
if (IconButton(icon, "More options")) {
|
|
||||||
ImGui::OpenPopup("##appbar_menu");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style the popup menu
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, spacing::dp(1)));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, size::MenuCornerRadius);
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::ColorConvertU32ToFloat4(Surface(Elevation::Dp8)));
|
|
||||||
|
|
||||||
if (ImGui::BeginPopup("##appbar_menu")) {
|
|
||||||
menuOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndAppBarMenu() {
|
|
||||||
ImGui::EndPopup();
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AppBarMenuItem(const char* label, const char* icon) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
|
|
||||||
const float itemHeight = size::ListItemHeight;
|
|
||||||
const float itemWidth = 200.0f; // Menu min width
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect itemBB(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
|
|
||||||
|
|
||||||
ImGuiID id = window->GetID(label);
|
|
||||||
ImGui::ItemSize(itemBB);
|
|
||||||
if (!ImGui::ItemAdd(itemBB, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(itemBB, id, &hovered, &held);
|
|
||||||
|
|
||||||
// Draw
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
if (hovered) {
|
|
||||||
drawList->AddRectFilled(itemBB.Min, itemBB.Max, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 10)));
|
|
||||||
}
|
|
||||||
|
|
||||||
float contentX = pos.x + spacing::dp(2);
|
|
||||||
float centerY = pos.y + itemHeight * 0.5f;
|
|
||||||
|
|
||||||
// Icon
|
|
||||||
if (icon) {
|
|
||||||
drawList->AddText(ImVec2(contentX, centerY - 12.0f), OnSurfaceMedium(), icon);
|
|
||||||
contentX += 24.0f + spacing::dp(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label
|
|
||||||
Typography::instance().pushFont(TypeStyle::Body1);
|
|
||||||
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
||||||
drawList->AddText(ImVec2(contentX, labelY), OnSurface(), label);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
|
|
||||||
if (pressed) {
|
|
||||||
ImGui::CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
return pressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../draw_helpers.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Card Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/cards
|
|
||||||
//
|
|
||||||
// Cards contain content and actions about a single subject.
|
|
||||||
// They can be elevated (with shadow) or outlined (with border).
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card configuration
|
|
||||||
*/
|
|
||||||
struct CardSpec {
|
|
||||||
int elevation = 1; // Elevation in dp (0-24)
|
|
||||||
bool outlined = false; // Use outline instead of elevation
|
|
||||||
float cornerRadius = 4.0f; // Corner radius in dp
|
|
||||||
bool clickable = false; // Make entire card clickable
|
|
||||||
float padding = 16.0f; // Content padding
|
|
||||||
float minHeight = 0.0f; // Minimum height (0 = auto)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a Material Design card
|
|
||||||
*
|
|
||||||
* @param id Unique identifier for the card
|
|
||||||
* @param spec Card configuration
|
|
||||||
* @return true if card is visible and content should be rendered
|
|
||||||
*/
|
|
||||||
bool BeginCard(const char* id, const CardSpec& spec = CardSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End the card
|
|
||||||
*/
|
|
||||||
void EndCard();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a clickable card that returns click state
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param spec Card configuration
|
|
||||||
* @param clicked Output: true if card was clicked
|
|
||||||
* @return true if card is visible
|
|
||||||
*/
|
|
||||||
bool BeginClickableCard(const char* id, const CardSpec& spec, bool* clicked);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card header with title and optional subtitle
|
|
||||||
*
|
|
||||||
* @param title Primary title text
|
|
||||||
* @param subtitle Optional secondary text
|
|
||||||
* @param avatar Optional avatar texture (rendered as circle)
|
|
||||||
*/
|
|
||||||
void CardHeader(const char* title, const char* subtitle = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card supporting text/content
|
|
||||||
*
|
|
||||||
* @param text Body text content
|
|
||||||
*/
|
|
||||||
void CardContent(const char* text);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin card action area (for buttons)
|
|
||||||
*
|
|
||||||
* Actions are typically placed at the bottom of the card.
|
|
||||||
*/
|
|
||||||
void CardActions();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End card action area
|
|
||||||
*/
|
|
||||||
void CardActionsEnd();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add divider within card
|
|
||||||
*/
|
|
||||||
void CardDivider();
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline bool BeginCard(const char* id, const CardSpec& spec) {
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
// Calculate surface color based on elevation
|
|
||||||
ImU32 bgColor = spec.outlined ? Surface() : GetElevatedSurface(GetCurrentColorTheme(), spec.elevation);
|
|
||||||
|
|
||||||
// When acrylic backdrop is active, scale card bg alpha by UI opacity
|
|
||||||
// so cards smoothly transition from opaque (1.0) to see-through.
|
|
||||||
bool opaqueCards = dragonx::ui::effects::isLowSpecMode();
|
|
||||||
if (IsBackdropActive() && !opaqueCards) {
|
|
||||||
ImVec4 c = ImGui::ColorConvertU32ToFloat4(bgColor);
|
|
||||||
float uiOp = dragonx::ui::effects::ImGuiAcrylic::GetUIOpacity();
|
|
||||||
c.w *= uiOp;
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, c);
|
|
||||||
} else {
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(bgColor));
|
|
||||||
}
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, spec.cornerRadius);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spec.padding, spec.padding));
|
|
||||||
|
|
||||||
// Border for outlined variant
|
|
||||||
if (spec.outlined) {
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(Outline()));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
|
||||||
} else {
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 size(0, spec.minHeight); // 0 width = use available width
|
|
||||||
ImGuiChildFlags flags = ImGuiChildFlags_AutoResizeY;
|
|
||||||
if (spec.outlined) {
|
|
||||||
flags |= ImGuiChildFlags_Borders;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool visible = ImGui::BeginChild(id, size, flags);
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndCard() {
|
|
||||||
ImGui::EndChild();
|
|
||||||
|
|
||||||
ImGui::PopStyleVar(3); // ChildRounding, WindowPadding, ChildBorderSize
|
|
||||||
ImGui::PopStyleColor(1); // ChildBg
|
|
||||||
|
|
||||||
// Check if we used outline style (need to pop extra color)
|
|
||||||
// Note: We always push the border size var, handle outline color in BeginCard
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
// Add spacing after card
|
|
||||||
VSpace(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool BeginClickableCard(const char* id, const CardSpec& spec, bool* clicked) {
|
|
||||||
*clicked = false;
|
|
||||||
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
ImVec2 startPos = ImGui::GetCursorScreenPos();
|
|
||||||
|
|
||||||
// Render card background
|
|
||||||
ImU32 bgColor = spec.outlined ? Surface() : GetElevatedSurface(GetCurrentColorTheme(), spec.elevation);
|
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(bgColor));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, spec.cornerRadius);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spec.padding, spec.padding));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, spec.outlined ? 1.0f : 0.0f);
|
|
||||||
|
|
||||||
if (spec.outlined) {
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(Outline()));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 size(0, spec.minHeight);
|
|
||||||
ImGuiChildFlags flags = ImGuiChildFlags_AutoResizeY;
|
|
||||||
if (spec.outlined) {
|
|
||||||
flags |= ImGuiChildFlags_Borders;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool visible = ImGui::BeginChild(id, size, flags);
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CardHeader(const char* title, const char* subtitle) {
|
|
||||||
Typography::instance().text(TypeStyle::H6, title);
|
|
||||||
|
|
||||||
if (subtitle) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), subtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
VSpace(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CardContent(const char* text) {
|
|
||||||
Typography::instance().textWrapped(TypeStyle::Body2, text);
|
|
||||||
VSpace(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CardActions() {
|
|
||||||
ImGui::Separator();
|
|
||||||
VSpace(1);
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CardActionsEnd() {
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CardDivider() {
|
|
||||||
ImGui::Separator();
|
|
||||||
VSpace(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Chips Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/chips
|
|
||||||
//
|
|
||||||
// Chips are compact elements that represent an input, attribute, or action.
|
|
||||||
|
|
||||||
enum class ChipType {
|
|
||||||
Input, // User input (deletable)
|
|
||||||
Choice, // Single selection from set
|
|
||||||
Filter, // Filter/checkbox style
|
|
||||||
Action // Triggers action
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Chip configuration
|
|
||||||
*/
|
|
||||||
struct ChipSpec {
|
|
||||||
ChipType type = ChipType::Action;
|
|
||||||
const char* label = nullptr;
|
|
||||||
const char* icon = nullptr; // Leading icon
|
|
||||||
const char* avatar = nullptr; // Avatar text (overrides icon)
|
|
||||||
ImU32 avatarColor = 0; // Avatar background color
|
|
||||||
bool selected = false; // For choice/filter chips
|
|
||||||
bool disabled = false;
|
|
||||||
bool outlined = false; // Outlined vs filled style
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render a chip
|
|
||||||
*
|
|
||||||
* @param spec Chip configuration
|
|
||||||
* @return For filter/choice: true if clicked (toggle selection)
|
|
||||||
* For input: true if delete clicked
|
|
||||||
* For action: true if clicked
|
|
||||||
*/
|
|
||||||
bool Chip(const ChipSpec& spec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple action chip
|
|
||||||
*/
|
|
||||||
bool Chip(const char* label);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Filter chip (toggleable)
|
|
||||||
*/
|
|
||||||
bool FilterChip(const char* label, bool* selected);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Choice chip (radio-style)
|
|
||||||
*/
|
|
||||||
bool ChoiceChip(const char* label, bool selected);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Input chip with delete
|
|
||||||
*/
|
|
||||||
bool InputChip(const char* label, const char* avatar = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a chip group for layout
|
|
||||||
*/
|
|
||||||
void BeginChipGroup();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End a chip group
|
|
||||||
*/
|
|
||||||
void EndChipGroup();
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline bool Chip(const ChipSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(spec.label);
|
|
||||||
|
|
||||||
// Chip dimensions
|
|
||||||
const float chipHeight = 32.0f;
|
|
||||||
const float cornerRadius = chipHeight * 0.5f;
|
|
||||||
const float horizontalPadding = 12.0f;
|
|
||||||
const float iconSize = 18.0f;
|
|
||||||
const float avatarSize = 24.0f;
|
|
||||||
const float deleteIconSize = 18.0f;
|
|
||||||
|
|
||||||
// Calculate content width
|
|
||||||
float contentWidth = horizontalPadding * 2;
|
|
||||||
|
|
||||||
bool hasLeading = spec.icon || spec.avatar;
|
|
||||||
bool hasDelete = (spec.type == ChipType::Input);
|
|
||||||
bool hasCheckmark = (spec.type == ChipType::Filter && spec.selected);
|
|
||||||
|
|
||||||
if (spec.avatar) {
|
|
||||||
contentWidth += avatarSize + 8.0f;
|
|
||||||
} else if (spec.icon || hasCheckmark) {
|
|
||||||
contentWidth += iconSize + 8.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWidth += ImGui::CalcTextSize(spec.label).x;
|
|
||||||
|
|
||||||
if (hasDelete) {
|
|
||||||
contentWidth += deleteIconSize + 8.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect chipBB(pos, ImVec2(pos.x + contentWidth, pos.y + chipHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID id = window->GetID("##chip");
|
|
||||||
ImGui::ItemSize(chipBB);
|
|
||||||
if (!ImGui::ItemAdd(chipBB, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool clicked = ImGui::ButtonBehavior(chipBB, id, &hovered, &held) && !spec.disabled;
|
|
||||||
|
|
||||||
// Delete button hit test (for input chips)
|
|
||||||
bool deleteClicked = false;
|
|
||||||
if (hasDelete) {
|
|
||||||
float deleteX = chipBB.Max.x - horizontalPadding - deleteIconSize;
|
|
||||||
ImRect deleteBB(
|
|
||||||
ImVec2(deleteX, pos.y + (chipHeight - deleteIconSize) * 0.5f),
|
|
||||||
ImVec2(deleteX + deleteIconSize, pos.y + (chipHeight + deleteIconSize) * 0.5f)
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGuiID deleteId = window->GetID("##delete");
|
|
||||||
bool deleteHovered, deleteHeld;
|
|
||||||
deleteClicked = ImGui::ButtonBehavior(deleteBB, deleteId, &deleteHovered, &deleteHeld);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Background
|
|
||||||
ImU32 bgColor;
|
|
||||||
ImU32 borderColor = 0;
|
|
||||||
|
|
||||||
if (spec.disabled) {
|
|
||||||
bgColor = schema::UI().resolveColor("var(--surface-hover)", IM_COL32(255, 255, 255, 30));
|
|
||||||
} else if (spec.selected) {
|
|
||||||
bgColor = WithAlpha(Primary(), 51); // Primary at 20%
|
|
||||||
} else if (spec.outlined) {
|
|
||||||
bgColor = 0; // Transparent
|
|
||||||
borderColor = OnSurfaceMedium();
|
|
||||||
} else {
|
|
||||||
bgColor = schema::UI().resolveColor("var(--surface-hover)", IM_COL32(255, 255, 255, 30));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hover/press overlay
|
|
||||||
if (!spec.disabled) {
|
|
||||||
if (held) {
|
|
||||||
bgColor = IM_COL32_ADD(bgColor, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
|
|
||||||
} else if (hovered) {
|
|
||||||
bgColor = IM_COL32_ADD(bgColor, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw background
|
|
||||||
if (bgColor) {
|
|
||||||
drawList->AddRectFilled(chipBB.Min, chipBB.Max, bgColor, cornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw border for outlined
|
|
||||||
if (borderColor) {
|
|
||||||
drawList->AddRect(chipBB.Min, chipBB.Max, borderColor, cornerRadius, 0, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content
|
|
||||||
float currentX = pos.x + horizontalPadding;
|
|
||||||
float centerY = pos.y + chipHeight * 0.5f;
|
|
||||||
|
|
||||||
ImU32 contentColor = spec.disabled ? OnSurfaceDisabled() : OnSurface();
|
|
||||||
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() :
|
|
||||||
spec.selected ? Primary() : OnSurfaceMedium();
|
|
||||||
|
|
||||||
// Avatar or icon
|
|
||||||
if (spec.avatar) {
|
|
||||||
// Avatar circle
|
|
||||||
ImVec2 avatarCenter(currentX + avatarSize * 0.5f - 4.0f, centerY);
|
|
||||||
ImU32 avatarBg = spec.avatarColor ? spec.avatarColor : Primary();
|
|
||||||
drawList->AddCircleFilled(avatarCenter, avatarSize * 0.5f, avatarBg);
|
|
||||||
|
|
||||||
// Avatar text
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(spec.avatar);
|
|
||||||
ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f);
|
|
||||||
drawList->AddText(textPos, OnPrimary(), spec.avatar);
|
|
||||||
|
|
||||||
currentX += avatarSize + 4.0f;
|
|
||||||
} else if (hasCheckmark) {
|
|
||||||
// Checkmark for selected filter chips
|
|
||||||
ImFont* iconFont = Typography::instance().iconSmall();
|
|
||||||
ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK);
|
|
||||||
drawList->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(currentX, centerY - chkSz.y * 0.5f), Primary(), ICON_MD_CHECK);
|
|
||||||
currentX += iconSize + 4.0f;
|
|
||||||
} else if (spec.icon) {
|
|
||||||
drawList->AddText(ImVec2(currentX, centerY - iconSize * 0.5f), iconColor, spec.icon);
|
|
||||||
currentX += iconSize + 4.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label
|
|
||||||
Typography::instance().pushFont(TypeStyle::Body2);
|
|
||||||
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
||||||
drawList->AddText(ImVec2(currentX, labelY), contentColor, spec.label);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
|
|
||||||
// Delete icon (for input chips)
|
|
||||||
if (hasDelete) {
|
|
||||||
float deleteX = chipBB.Max.x - horizontalPadding - deleteIconSize;
|
|
||||||
ImFont* iconFont = Typography::instance().iconSmall();
|
|
||||||
ImVec2 delSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CLOSE);
|
|
||||||
drawList->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(deleteX, centerY - delSz.y * 0.5f),
|
|
||||||
OnSurfaceMedium(), ICON_MD_CLOSE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
// Return value depends on chip type
|
|
||||||
if (spec.type == ChipType::Input) {
|
|
||||||
return deleteClicked;
|
|
||||||
}
|
|
||||||
return clicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Chip(const char* label) {
|
|
||||||
ChipSpec spec;
|
|
||||||
spec.label = label;
|
|
||||||
spec.type = ChipType::Action;
|
|
||||||
return Chip(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool FilterChip(const char* label, bool* selected) {
|
|
||||||
ChipSpec spec;
|
|
||||||
spec.label = label;
|
|
||||||
spec.type = ChipType::Filter;
|
|
||||||
spec.selected = *selected;
|
|
||||||
|
|
||||||
bool clicked = Chip(spec);
|
|
||||||
if (clicked) {
|
|
||||||
*selected = !*selected;
|
|
||||||
}
|
|
||||||
return clicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ChoiceChip(const char* label, bool selected) {
|
|
||||||
ChipSpec spec;
|
|
||||||
spec.label = label;
|
|
||||||
spec.type = ChipType::Choice;
|
|
||||||
spec.selected = selected;
|
|
||||||
return Chip(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool InputChip(const char* label, const char* avatar) {
|
|
||||||
ChipSpec spec;
|
|
||||||
spec.label = label;
|
|
||||||
spec.type = ChipType::Input;
|
|
||||||
spec.avatar = avatar;
|
|
||||||
return Chip(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void BeginChipGroup() {
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(1), spacing::dp(1))); // 8dp spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndChipGroup() {
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Components - Unified Header
|
|
||||||
// ============================================================================
|
|
||||||
// Include this single header to get all Material Design components.
|
|
||||||
//
|
|
||||||
// Based on Material Design 2 (m2.material.io)
|
|
||||||
//
|
|
||||||
// All components are in the namespace: dragonx::ui::material
|
|
||||||
|
|
||||||
// Core dependencies
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
|
|
||||||
// Components
|
|
||||||
#include "buttons.h" // Button, IconButton, FAB
|
|
||||||
#include "cards.h" // Card, CardHeader, CardContent, CardActions
|
|
||||||
#include "text_fields.h" // TextField
|
|
||||||
#include "lists.h" // ListItem, ListDivider, ListSubheader
|
|
||||||
#include "dialogs.h" // Dialog, ConfirmDialog, AlertDialog
|
|
||||||
#include "inputs.h" // Switch, Checkbox, RadioButton
|
|
||||||
#include "progress.h" // LinearProgress, CircularProgress
|
|
||||||
#include "snackbar.h" // Snackbar, ShowSnackbar
|
|
||||||
#include "slider.h" // Slider, SliderDiscrete, SliderRange
|
|
||||||
#include "tabs.h" // TabBar, Tab
|
|
||||||
#include "chips.h" // Chip, FilterChip, ChoiceChip, InputChip
|
|
||||||
#include "nav_drawer.h" // NavDrawer, NavItem
|
|
||||||
#include "app_bar.h" // AppBar, AppBarTitle, AppBarAction
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Quick Reference
|
|
||||||
// ============================================================================
|
|
||||||
//
|
|
||||||
// BUTTONS:
|
|
||||||
// Button(label, spec) - Generic button with style config
|
|
||||||
// TextButton(label) - Text-only button
|
|
||||||
// OutlinedButton(label) - Button with outline
|
|
||||||
// ContainedButton(label) - Filled button (primary)
|
|
||||||
// IconButton(icon, tooltip) - Circular icon button
|
|
||||||
// FAB(icon) - Floating action button
|
|
||||||
//
|
|
||||||
// CARDS:
|
|
||||||
// BeginCard(spec)/EndCard() - Card container
|
|
||||||
// CardHeader(title, subtitle) - Card header section
|
|
||||||
// CardContent(text) - Card body text
|
|
||||||
// CardActions()/EndCardActions()- Card button area
|
|
||||||
//
|
|
||||||
// TEXT FIELDS:
|
|
||||||
// TextField(label, buf, size) - Text input field
|
|
||||||
// TextField(id, buf, size, spec)- Configurable text field
|
|
||||||
//
|
|
||||||
// LISTS:
|
|
||||||
// BeginList(id)/EndList() - List container
|
|
||||||
// ListItem(text) - Simple list item
|
|
||||||
// ListItem(primary, secondary) - Two-line item
|
|
||||||
// ListItem(spec) - Full config item
|
|
||||||
// ListDivider(inset) - Divider line
|
|
||||||
// ListSubheader(text) - Section header
|
|
||||||
//
|
|
||||||
// DIALOGS:
|
|
||||||
// BeginDialog(id, &open, spec) - Modal dialog
|
|
||||||
// EndDialog()
|
|
||||||
// ConfirmDialog(...) - Confirm/cancel dialog
|
|
||||||
// AlertDialog(...) - Single-action alert
|
|
||||||
//
|
|
||||||
// SELECTION CONTROLS:
|
|
||||||
// Switch(label, &value) - Toggle switch
|
|
||||||
// Checkbox(label, &value) - Checkbox
|
|
||||||
// RadioButton(label, active) - Radio button
|
|
||||||
// RadioButton(label, &sel, val) - Radio with int selection
|
|
||||||
//
|
|
||||||
// PROGRESS:
|
|
||||||
// LinearProgress(fraction) - Determinate progress bar
|
|
||||||
// LinearProgressIndeterminate() - Indeterminate progress bar
|
|
||||||
// CircularProgress(fraction) - Circular progress
|
|
||||||
// CircularProgressIndeterminate()- Spinner
|
|
||||||
//
|
|
||||||
// SNACKBAR:
|
|
||||||
// ShowSnackbar(msg, action) - Show notification
|
|
||||||
// DismissSnackbar() - Dismiss current snackbar
|
|
||||||
// RenderSnackbar() - Call each frame to render
|
|
||||||
//
|
|
||||||
// SLIDER:
|
|
||||||
// Slider(label, &val, min, max) - Continuous slider
|
|
||||||
// SliderInt(label, &val, ...) - Integer slider
|
|
||||||
// SliderDiscrete(...) - Stepped slider
|
|
||||||
// SliderRange(...) - Two-thumb range slider
|
|
||||||
//
|
|
||||||
// TABS:
|
|
||||||
// BeginTabBar(id, &idx) - Tab bar container
|
|
||||||
// Tab(label) - Tab item
|
|
||||||
// EndTabBar()
|
|
||||||
// TabBar(id, labels, count, &idx) - Simple tab bar
|
|
||||||
//
|
|
||||||
// CHIPS:
|
|
||||||
// Chip(label) - Action chip
|
|
||||||
// FilterChip(label, &selected) - Toggleable filter chip
|
|
||||||
// ChoiceChip(label, selected) - Choice chip
|
|
||||||
// InputChip(label, avatar) - Deletable input chip
|
|
||||||
// BeginChipGroup()/EndChipGroup()- Chip layout helper
|
|
||||||
//
|
|
||||||
// NAVIGATION DRAWER:
|
|
||||||
// BeginNavDrawer(id, &open, spec) - Navigation drawer
|
|
||||||
// EndNavDrawer()
|
|
||||||
// NavItem(icon, label, selected) - Navigation item
|
|
||||||
// NavDivider() - Drawer divider
|
|
||||||
// NavSubheader(text) - Section header
|
|
||||||
//
|
|
||||||
// APP BAR:
|
|
||||||
// BeginAppBar(id, spec) - Top app bar
|
|
||||||
// EndAppBar()
|
|
||||||
// AppBarNavIcon(icon) - Navigation icon (left)
|
|
||||||
// AppBarTitle(title) - App bar title
|
|
||||||
// AppBarAction(icon) - Action button (right)
|
|
||||||
// BeginAppBarMenu(icon) - Overflow menu
|
|
||||||
// AppBarMenuItem(label) - Menu item
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "buttons.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Dialog Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/dialogs
|
|
||||||
//
|
|
||||||
// Dialogs inform users about a task and can contain critical information,
|
|
||||||
// require decisions, or involve multiple tasks.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Dialog configuration
|
|
||||||
*/
|
|
||||||
struct DialogSpec {
|
|
||||||
const char* title = nullptr; // Dialog title
|
|
||||||
float width = 560.0f; // Dialog width (280-560dp typical)
|
|
||||||
float maxHeight = 0; // Max height (0 = auto)
|
|
||||||
bool scrollableContent = false; // Enable content scrolling
|
|
||||||
bool fullWidth = false; // Actions span full width
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a modal dialog
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param open Pointer to open state (will be set false when closed)
|
|
||||||
* @param spec Dialog configuration
|
|
||||||
* @return true if dialog is open
|
|
||||||
*/
|
|
||||||
bool BeginDialog(const char* id, bool* open, const DialogSpec& spec = DialogSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End a dialog
|
|
||||||
*/
|
|
||||||
void EndDialog();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple dialog with just text content
|
|
||||||
*/
|
|
||||||
bool BeginDialog(const char* id, bool* open, const char* title);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Dialog content area (scrollable if configured)
|
|
||||||
*/
|
|
||||||
void BeginDialogContent();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End dialog content area
|
|
||||||
*/
|
|
||||||
void EndDialogContent();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Dialog actions area (buttons)
|
|
||||||
*/
|
|
||||||
void BeginDialogActions();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End dialog actions area
|
|
||||||
*/
|
|
||||||
void EndDialogActions();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Standard confirm dialog
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param open Pointer to open state
|
|
||||||
* @param title Dialog title
|
|
||||||
* @param message Dialog message
|
|
||||||
* @param confirmText Confirm button text
|
|
||||||
* @param cancelText Cancel button text (nullptr for no cancel)
|
|
||||||
* @return 0 = still open, 1 = confirmed, -1 = cancelled
|
|
||||||
*/
|
|
||||||
int ConfirmDialog(const char* id, bool* open, const char* title, const char* message,
|
|
||||||
const char* confirmText = "Confirm", const char* cancelText = "Cancel");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Alert dialog (single action)
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param open Pointer to open state
|
|
||||||
* @param title Dialog title
|
|
||||||
* @param message Dialog message
|
|
||||||
* @param buttonText Button text
|
|
||||||
* @return true when dismissed
|
|
||||||
*/
|
|
||||||
bool AlertDialog(const char* id, bool* open, const char* title, const char* message,
|
|
||||||
const char* buttonText = "OK");
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Internal state for dialog rendering
|
|
||||||
struct DialogState {
|
|
||||||
ImVec2 contentMin;
|
|
||||||
ImVec2 contentMax;
|
|
||||||
float contentScrollY;
|
|
||||||
bool scrollable;
|
|
||||||
float width;
|
|
||||||
};
|
|
||||||
|
|
||||||
static DialogState g_currentDialog;
|
|
||||||
|
|
||||||
inline bool BeginDialog(const char* id, bool* open, const DialogSpec& spec) {
|
|
||||||
if (!*open)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Center dialog on screen
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
|
||||||
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
|
||||||
|
|
||||||
// Set dialog size
|
|
||||||
float dialogWidth = spec.width;
|
|
||||||
ImGui::SetNextWindowSizeConstraints(
|
|
||||||
ImVec2(280.0f, 0), // Min size
|
|
||||||
ImVec2(dialogWidth, spec.maxHeight > 0 ? spec.maxHeight : io.DisplaySize.y * 0.9f)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Style dialog
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, size::DialogCornerRadius);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::ColorConvertU32ToFloat4(Surface(Elevation::Dp24)));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::ColorConvertU32ToFloat4(Surface(Elevation::Dp24)));
|
|
||||||
|
|
||||||
// Modal background (scrim)
|
|
||||||
ImDrawList* bgDrawList = ImGui::GetBackgroundDrawList();
|
|
||||||
bgDrawList->AddRectFilled(
|
|
||||||
ImVec2(0, 0), io.DisplaySize,
|
|
||||||
schema::UI().resolveColor("var(--scrim)", IM_COL32(0, 0, 0, (int)(0.32f * 255)))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Open popup
|
|
||||||
ImGui::OpenPopup(id);
|
|
||||||
bool isOpen = ImGui::BeginPopupModal(id, open,
|
|
||||||
ImGuiWindowFlags_AlwaysAutoResize |
|
|
||||||
ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoTitleBar);
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
g_currentDialog.scrollable = spec.scrollableContent;
|
|
||||||
g_currentDialog.width = dialogWidth;
|
|
||||||
|
|
||||||
// Title
|
|
||||||
if (spec.title) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3))); // 24dp top padding
|
|
||||||
ImGui::SetCursorPosX(spacing::dp(3)); // 24dp left padding
|
|
||||||
Typography::instance().text(TypeStyle::H6, spec.title);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2))); // 16dp below title
|
|
||||||
} else {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2))); // 16dp top padding without title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
|
|
||||||
return isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndDialog() {
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool BeginDialog(const char* id, bool* open, const char* title) {
|
|
||||||
DialogSpec spec;
|
|
||||||
spec.title = title;
|
|
||||||
return BeginDialog(id, open, spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void BeginDialogContent() {
|
|
||||||
ImGui::SetCursorPosX(spacing::dp(3)); // 24dp left padding
|
|
||||||
|
|
||||||
// Start content region
|
|
||||||
float maxWidth = g_currentDialog.width - spacing::dp(6); // 24dp padding each side
|
|
||||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + maxWidth);
|
|
||||||
|
|
||||||
if (g_currentDialog.scrollable) {
|
|
||||||
ImGui::BeginChild("##dialogContent", ImVec2(maxWidth, 200), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndDialogContent() {
|
|
||||||
if (g_currentDialog.scrollable) {
|
|
||||||
ImGui::EndChild();
|
|
||||||
}
|
|
||||||
ImGui::PopTextWrapPos();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3))); // 24dp after content
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void BeginDialogActions() {
|
|
||||||
// Actions area - right-aligned buttons with 8dp spacing
|
|
||||||
float contentWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImGui::SetCursorPosX(spacing::dp(1)); // 8dp left padding for actions
|
|
||||||
|
|
||||||
// Push style for action buttons
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(1), 0)); // 8dp between buttons
|
|
||||||
|
|
||||||
// Right-align: use a dummy to push buttons right
|
|
||||||
// Buttons will be added inline with SameLine
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndDialogActions() {
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp bottom padding
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int ConfirmDialog(const char* id, bool* open, const char* title, const char* message,
|
|
||||||
const char* confirmText, const char* cancelText) {
|
|
||||||
int result = 0;
|
|
||||||
|
|
||||||
if (BeginDialog(id, open, title)) {
|
|
||||||
BeginDialogContent();
|
|
||||||
Typography::instance().textWrapped(TypeStyle::Body1, message);
|
|
||||||
EndDialogContent();
|
|
||||||
|
|
||||||
BeginDialogActions();
|
|
||||||
|
|
||||||
// Calculate button positions for right alignment
|
|
||||||
float cancelWidth = cancelText ? ImGui::CalcTextSize(cancelText).x + spacing::dp(2) : 0;
|
|
||||||
float confirmWidth = ImGui::CalcTextSize(confirmText).x + spacing::dp(2);
|
|
||||||
float totalButtonWidth = cancelWidth + confirmWidth + (cancelText ? spacing::dp(1) : 0);
|
|
||||||
float startX = ImGui::GetContentRegionAvail().x - totalButtonWidth - spacing::dp(2);
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + startX);
|
|
||||||
|
|
||||||
if (cancelText) {
|
|
||||||
if (TextButton(cancelText)) {
|
|
||||||
*open = false;
|
|
||||||
result = -1;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContainedButton(confirmText)) {
|
|
||||||
*open = false;
|
|
||||||
result = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialogActions();
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AlertDialog(const char* id, bool* open, const char* title, const char* message,
|
|
||||||
const char* buttonText) {
|
|
||||||
bool dismissed = false;
|
|
||||||
|
|
||||||
if (BeginDialog(id, open, title)) {
|
|
||||||
BeginDialogContent();
|
|
||||||
Typography::instance().textWrapped(TypeStyle::Body1, message);
|
|
||||||
EndDialogContent();
|
|
||||||
|
|
||||||
BeginDialogActions();
|
|
||||||
|
|
||||||
// Right-align single button
|
|
||||||
float buttonWidth = ImGui::CalcTextSize(buttonText).x + spacing::dp(2);
|
|
||||||
float startX = ImGui::GetContentRegionAvail().x - buttonWidth - spacing::dp(2);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + startX);
|
|
||||||
|
|
||||||
if (ContainedButton(buttonText)) {
|
|
||||||
*open = false;
|
|
||||||
dismissed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialogActions();
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
return dismissed;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Input Controls
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/selection-controls
|
|
||||||
//
|
|
||||||
// Selection controls allow users to complete tasks that involve making choices:
|
|
||||||
// - Switch: Toggle single option on/off
|
|
||||||
// - Checkbox: Select multiple options
|
|
||||||
// - Radio: Select one option from a set
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Switch
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Material Design switch (toggle)
|
|
||||||
*
|
|
||||||
* @param label Text label
|
|
||||||
* @param value Pointer to boolean value
|
|
||||||
* @param disabled If true, switch is non-interactive
|
|
||||||
* @return true if value changed
|
|
||||||
*/
|
|
||||||
bool Switch(const char* label, bool* value, bool disabled = false);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Checkbox
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checkbox state
|
|
||||||
*/
|
|
||||||
enum class CheckboxState {
|
|
||||||
Unchecked,
|
|
||||||
Checked,
|
|
||||||
Indeterminate // For parent with mixed children
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Material Design checkbox
|
|
||||||
*
|
|
||||||
* @param label Text label
|
|
||||||
* @param value Pointer to boolean value
|
|
||||||
* @param disabled If true, checkbox is non-interactive
|
|
||||||
* @return true if value changed
|
|
||||||
*/
|
|
||||||
bool Checkbox(const char* label, bool* value, bool disabled = false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Tri-state checkbox
|
|
||||||
*/
|
|
||||||
bool Checkbox(const char* label, CheckboxState* state, bool disabled = false);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Radio Button
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Material Design radio button
|
|
||||||
*
|
|
||||||
* @param label Text label
|
|
||||||
* @param active true if this option is selected
|
|
||||||
* @param disabled If true, radio is non-interactive
|
|
||||||
* @return true if clicked (caller should update selection)
|
|
||||||
*/
|
|
||||||
bool RadioButton(const char* label, bool active, bool disabled = false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Radio button with int selection
|
|
||||||
*
|
|
||||||
* @param label Text label
|
|
||||||
* @param selection Pointer to current selection
|
|
||||||
* @param value Value this radio represents
|
|
||||||
* @return true if clicked
|
|
||||||
*/
|
|
||||||
bool RadioButton(const char* label, int* selection, int value, bool disabled = false);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline bool Switch(const char* label, bool* value, bool disabled) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
// Switch dimensions (Material spec: 36x14 track, 20dp thumb)
|
|
||||||
const float trackWidth = 36.0f;
|
|
||||||
const float trackHeight = 14.0f;
|
|
||||||
const float thumbRadius = 10.0f; // 20dp diameter
|
|
||||||
const float thumbTravel = trackWidth - thumbRadius * 2;
|
|
||||||
|
|
||||||
// Calculate layout
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
float labelWidth = ImGui::CalcTextSize(label).x;
|
|
||||||
float totalWidth = trackWidth + spacing::dp(2) + labelWidth;
|
|
||||||
float totalHeight = ImMax(trackHeight + 6.0f, size::TouchTarget); // Min 48dp touch target
|
|
||||||
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID id = window->GetID("##switch");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (pressed) {
|
|
||||||
*value = !*value;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation (simple snap for now)
|
|
||||||
float thumbX = *value ? (thumbTravel) : 0;
|
|
||||||
|
|
||||||
// Draw track
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
float trackY = pos.y + totalHeight * 0.5f;
|
|
||||||
ImVec2 trackMin(pos.x, trackY - trackHeight * 0.5f);
|
|
||||||
ImVec2 trackMax(pos.x + trackWidth, trackY + trackHeight * 0.5f);
|
|
||||||
|
|
||||||
ImU32 trackColor;
|
|
||||||
if (disabled) {
|
|
||||||
trackColor = schema::UI().resolveColor("var(--switch-track-off)", IM_COL32(255, 255, 255, 30));
|
|
||||||
} else if (*value) {
|
|
||||||
trackColor = PrimaryVariant(); // Primary at 50% opacity
|
|
||||||
} else {
|
|
||||||
trackColor = schema::UI().resolveColor("var(--switch-track-on)", IM_COL32(255, 255, 255, 97));
|
|
||||||
}
|
|
||||||
|
|
||||||
drawList->AddRectFilled(trackMin, trackMax, trackColor, trackHeight * 0.5f);
|
|
||||||
|
|
||||||
// Draw thumb
|
|
||||||
ImVec2 thumbCenter(pos.x + thumbRadius + thumbX, trackY);
|
|
||||||
|
|
||||||
ImU32 thumbColor;
|
|
||||||
if (disabled) {
|
|
||||||
thumbColor = schema::UI().resolveColor("var(--switch-thumb-off)", IM_COL32(189, 189, 189, 255));
|
|
||||||
} else if (*value) {
|
|
||||||
thumbColor = Primary();
|
|
||||||
} else {
|
|
||||||
thumbColor = schema::UI().resolveColor("var(--switch-thumb-on)", IM_COL32(250, 250, 250, 255));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumb shadow
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbCenter.x + 1, thumbCenter.y + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
|
|
||||||
drawList->AddCircleFilled(thumbCenter, thumbRadius, thumbColor);
|
|
||||||
|
|
||||||
// Hover ripple effect
|
|
||||||
if (hovered && !disabled) {
|
|
||||||
ImU32 ripple = *value ? WithAlpha(Primary(), 25) : schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25));
|
|
||||||
drawList->AddCircleFilled(thumbCenter, thumbRadius + 12.0f, ripple);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw label
|
|
||||||
ImVec2 labelPos(pos.x + trackWidth + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
|
|
||||||
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
|
|
||||||
drawList->AddText(labelPos, labelColor, label);
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Checkbox(const char* label, bool* value, bool disabled) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
// Checkbox dimensions (18dp box, 48dp touch target)
|
|
||||||
const float boxSize = 18.0f;
|
|
||||||
|
|
||||||
// Calculate layout
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
float labelWidth = ImGui::CalcTextSize(label).x;
|
|
||||||
float totalWidth = boxSize + spacing::dp(2) + labelWidth;
|
|
||||||
float totalHeight = size::TouchTarget;
|
|
||||||
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID id = window->GetID("##checkbox");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (pressed) {
|
|
||||||
*value = !*value;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw checkbox
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
float centerY = pos.y + totalHeight * 0.5f;
|
|
||||||
ImVec2 boxMin(pos.x, centerY - boxSize * 0.5f);
|
|
||||||
ImVec2 boxMax(pos.x + boxSize, centerY + boxSize * 0.5f);
|
|
||||||
|
|
||||||
ImU32 boxColor, checkColor;
|
|
||||||
if (disabled) {
|
|
||||||
boxColor = OnSurfaceDisabled();
|
|
||||||
checkColor = schema::UI().resolveColor("var(--checkbox-check)", IM_COL32(0, 0, 0, 255));
|
|
||||||
} else if (*value) {
|
|
||||||
boxColor = Primary();
|
|
||||||
checkColor = OnPrimary();
|
|
||||||
} else {
|
|
||||||
boxColor = OnSurfaceMedium();
|
|
||||||
checkColor = OnPrimary();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (*value) {
|
|
||||||
// Filled checkbox with checkmark
|
|
||||||
drawList->AddRectFilled(boxMin, boxMax, boxColor, 2.0f);
|
|
||||||
|
|
||||||
// Draw checkmark
|
|
||||||
ImVec2 checkStart(boxMin.x + 4, centerY);
|
|
||||||
ImVec2 checkMid(boxMin.x + 7, centerY + 3);
|
|
||||||
ImVec2 checkEnd(boxMin.x + 14, centerY - 4);
|
|
||||||
|
|
||||||
drawList->AddLine(checkStart, checkMid, checkColor, 2.0f);
|
|
||||||
drawList->AddLine(checkMid, checkEnd, checkColor, 2.0f);
|
|
||||||
} else {
|
|
||||||
// Empty checkbox border
|
|
||||||
drawList->AddRect(boxMin, boxMax, boxColor, 2.0f, 0, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hover ripple
|
|
||||||
if (hovered && !disabled) {
|
|
||||||
ImVec2 boxCenter((boxMin.x + boxMax.x) * 0.5f, centerY);
|
|
||||||
drawList->AddCircleFilled(boxCenter, boxSize,
|
|
||||||
*value ? WithAlpha(Primary(), 25) : schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw label
|
|
||||||
ImVec2 labelPos(pos.x + boxSize + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
|
|
||||||
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
|
|
||||||
drawList->AddText(labelPos, labelColor, label);
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Checkbox(const char* label, CheckboxState* state, bool disabled) {
|
|
||||||
bool checked = (*state == CheckboxState::Checked);
|
|
||||||
bool indeterminate = (*state == CheckboxState::Indeterminate);
|
|
||||||
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
const float boxSize = 18.0f;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
float labelWidth = ImGui::CalcTextSize(label).x;
|
|
||||||
float totalWidth = boxSize + spacing::dp(2) + labelWidth;
|
|
||||||
float totalHeight = size::TouchTarget;
|
|
||||||
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
ImGuiID id = window->GetID("##checkbox");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (pressed) {
|
|
||||||
// Cycle: Unchecked -> Checked -> Unchecked (indeterminate only set programmatically)
|
|
||||||
*state = (*state == CheckboxState::Checked) ? CheckboxState::Unchecked : CheckboxState::Checked;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
float centerY = pos.y + totalHeight * 0.5f;
|
|
||||||
ImVec2 boxMin(pos.x, centerY - boxSize * 0.5f);
|
|
||||||
ImVec2 boxMax(pos.x + boxSize, centerY + boxSize * 0.5f);
|
|
||||||
|
|
||||||
ImU32 boxColor = disabled ? OnSurfaceDisabled() : (checked || indeterminate) ? Primary() : OnSurfaceMedium();
|
|
||||||
|
|
||||||
if (checked || indeterminate) {
|
|
||||||
drawList->AddRectFilled(boxMin, boxMax, boxColor, 2.0f);
|
|
||||||
|
|
||||||
if (indeterminate) {
|
|
||||||
// Horizontal line for indeterminate
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(boxMin.x + 4, centerY),
|
|
||||||
ImVec2(boxMax.x - 4, centerY),
|
|
||||||
OnPrimary(), 2.0f
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Checkmark
|
|
||||||
ImVec2 checkStart(boxMin.x + 4, centerY);
|
|
||||||
ImVec2 checkMid(boxMin.x + 7, centerY + 3);
|
|
||||||
ImVec2 checkEnd(boxMin.x + 14, centerY - 4);
|
|
||||||
drawList->AddLine(checkStart, checkMid, OnPrimary(), 2.0f);
|
|
||||||
drawList->AddLine(checkMid, checkEnd, OnPrimary(), 2.0f);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drawList->AddRect(boxMin, boxMax, boxColor, 2.0f, 0, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hovered && !disabled) {
|
|
||||||
ImVec2 boxCenter((boxMin.x + boxMax.x) * 0.5f, centerY);
|
|
||||||
drawList->AddCircleFilled(boxCenter, boxSize, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 labelPos(pos.x + boxSize + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
|
|
||||||
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
|
|
||||||
drawList->AddText(labelPos, labelColor, label);
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool RadioButton(const char* label, bool active, bool disabled) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
// Radio button dimensions (20dp outer, 10dp inner when selected)
|
|
||||||
const float outerRadius = 10.0f;
|
|
||||||
const float innerRadius = 5.0f;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
float labelWidth = ImGui::CalcTextSize(label).x;
|
|
||||||
float totalWidth = outerRadius * 2 + spacing::dp(2) + labelWidth;
|
|
||||||
float totalHeight = size::TouchTarget;
|
|
||||||
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
ImGuiID id = window->GetID("##radio");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
float centerY = pos.y + totalHeight * 0.5f;
|
|
||||||
ImVec2 center(pos.x + outerRadius, centerY);
|
|
||||||
|
|
||||||
ImU32 ringColor = disabled ? OnSurfaceDisabled() : active ? Primary() : OnSurfaceMedium();
|
|
||||||
|
|
||||||
// Outer ring
|
|
||||||
drawList->AddCircle(center, outerRadius, ringColor, 0, 2.0f);
|
|
||||||
|
|
||||||
// Inner dot when active
|
|
||||||
if (active) {
|
|
||||||
drawList->AddCircleFilled(center, innerRadius, ringColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hover ripple
|
|
||||||
if (hovered && !disabled) {
|
|
||||||
drawList->AddCircleFilled(center, outerRadius + 12.0f,
|
|
||||||
active ? WithAlpha(Primary(), 25) : schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label
|
|
||||||
ImVec2 labelPos(pos.x + outerRadius * 2 + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
|
|
||||||
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
|
|
||||||
drawList->AddText(labelPos, labelColor, label);
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return pressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool RadioButton(const char* label, int* selection, int value, bool disabled) {
|
|
||||||
bool active = (*selection == value);
|
|
||||||
if (RadioButton(label, active, disabled)) {
|
|
||||||
*selection = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design List Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/lists
|
|
||||||
//
|
|
||||||
// Lists present content in a way that makes it easy to identify a specific
|
|
||||||
// item in a collection and act on it.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief List item configuration
|
|
||||||
*/
|
|
||||||
struct ListItemSpec {
|
|
||||||
const char* leadingIcon = nullptr; // Optional leading icon (text representation)
|
|
||||||
const char* leadingAvatar = nullptr; // Optional avatar text (for initials)
|
|
||||||
ImU32 leadingAvatarColor = 0; // Avatar background color (0 = primary)
|
|
||||||
bool leadingCheckbox = false; // Show checkbox instead of icon
|
|
||||||
bool checkboxChecked = false; // Checkbox state
|
|
||||||
const char* primaryText = nullptr; // Main text (required)
|
|
||||||
const char* secondaryText = nullptr; // Secondary text (optional)
|
|
||||||
const char* trailingIcon = nullptr; // Optional trailing icon
|
|
||||||
const char* trailingText = nullptr; // Optional trailing metadata text
|
|
||||||
bool selected = false; // Selected state (highlight)
|
|
||||||
bool disabled = false; // Disabled state
|
|
||||||
bool dividerBelow = false; // Draw divider below item
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a list container
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param withPadding Add top/bottom padding
|
|
||||||
*/
|
|
||||||
void BeginList(const char* id, bool withPadding = true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End a list container
|
|
||||||
*/
|
|
||||||
void EndList();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render a list item
|
|
||||||
*
|
|
||||||
* @param spec Item configuration
|
|
||||||
* @return true if clicked
|
|
||||||
*/
|
|
||||||
bool ListItem(const ListItemSpec& spec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple single-line list item
|
|
||||||
*/
|
|
||||||
bool ListItem(const char* text);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Two-line list item with primary and secondary text
|
|
||||||
*/
|
|
||||||
bool ListItem(const char* primary, const char* secondary);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief List divider (full width or inset)
|
|
||||||
*
|
|
||||||
* @param inset If true, indented to align with text
|
|
||||||
*/
|
|
||||||
void ListDivider(bool inset = false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief List subheader text
|
|
||||||
*/
|
|
||||||
void ListSubheader(const char* text);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void BeginList(const char* id, bool withPadding) {
|
|
||||||
ImGui::PushID(id);
|
|
||||||
if (withPadding) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndList() {
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ListItem(const ListItemSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Calculate item height based on content
|
|
||||||
bool hasSecondary = (spec.secondaryText != nullptr);
|
|
||||||
bool hasLeadingElement = (spec.leadingIcon || spec.leadingAvatar || spec.leadingCheckbox);
|
|
||||||
|
|
||||||
float itemHeight;
|
|
||||||
if (hasSecondary) {
|
|
||||||
itemHeight = size::ListItemTwoLineHeight; // 72dp for two-line
|
|
||||||
} else if (hasLeadingElement) {
|
|
||||||
itemHeight = size::ListItemHeight; // 56dp with leading element
|
|
||||||
} else {
|
|
||||||
itemHeight = 48.0f; // 48dp simple one-line
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item dimensions
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
float itemWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID itemId = window->GetID(spec.primaryText);
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, itemId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(bb, itemId, &hovered, &held) && !spec.disabled;
|
|
||||||
|
|
||||||
// Draw background
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
ImU32 bgColor = 0;
|
|
||||||
|
|
||||||
if (spec.selected) {
|
|
||||||
bgColor = PrimaryContainer();
|
|
||||||
} else if (held && !spec.disabled) {
|
|
||||||
bgColor = schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25));
|
|
||||||
} else if (hovered && !spec.disabled) {
|
|
||||||
bgColor = schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bgColor) {
|
|
||||||
drawList->AddRectFilled(bb.Min, bb.Max, bgColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout positions
|
|
||||||
float leftPadding = spacing::dp(2); // 16dp left padding
|
|
||||||
float currentX = bb.Min.x + leftPadding;
|
|
||||||
float centerY = bb.Min.y + itemHeight * 0.5f;
|
|
||||||
|
|
||||||
// Draw leading element
|
|
||||||
if (spec.leadingAvatar) {
|
|
||||||
// Avatar circle with text
|
|
||||||
float avatarRadius = 20.0f; // 40dp diameter
|
|
||||||
ImVec2 avatarCenter(currentX + avatarRadius, centerY);
|
|
||||||
|
|
||||||
ImU32 avatarBg = spec.leadingAvatarColor ? spec.leadingAvatarColor : Primary();
|
|
||||||
drawList->AddCircleFilled(avatarCenter, avatarRadius, avatarBg);
|
|
||||||
|
|
||||||
// Avatar text (centered)
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(spec.leadingAvatar);
|
|
||||||
ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f);
|
|
||||||
drawList->AddText(textPos, OnPrimary(), spec.leadingAvatar);
|
|
||||||
|
|
||||||
currentX += 40.0f + spacing::dp(2); // 40dp avatar + 16dp gap
|
|
||||||
} else if (spec.leadingIcon) {
|
|
||||||
// Icon
|
|
||||||
ImVec2 iconSize = ImGui::CalcTextSize(spec.leadingIcon);
|
|
||||||
float iconY = centerY - iconSize.y * 0.5f;
|
|
||||||
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium();
|
|
||||||
drawList->AddText(ImVec2(currentX, iconY), iconColor, spec.leadingIcon);
|
|
||||||
currentX += 24.0f + spacing::dp(2); // 24dp icon + 16dp gap
|
|
||||||
} else if (spec.leadingCheckbox) {
|
|
||||||
// Checkbox (simplified visual)
|
|
||||||
float checkboxSize = 18.0f;
|
|
||||||
ImVec2 checkMin(currentX, centerY - checkboxSize * 0.5f);
|
|
||||||
ImVec2 checkMax(currentX + checkboxSize, centerY + checkboxSize * 0.5f);
|
|
||||||
|
|
||||||
if (spec.checkboxChecked) {
|
|
||||||
drawList->AddRectFilled(checkMin, checkMax, Primary(), 2.0f);
|
|
||||||
// Checkmark
|
|
||||||
ImFont* iconFont = Typography::instance().iconSmall();
|
|
||||||
ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK);
|
|
||||||
drawList->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(checkMin.x + (checkboxSize - chkSz.x) * 0.5f, checkMin.y + (checkboxSize - chkSz.y) * 0.5f),
|
|
||||||
OnPrimary(), ICON_MD_CHECK);
|
|
||||||
} else {
|
|
||||||
drawList->AddRect(checkMin, checkMax, OnSurfaceMedium(), 2.0f, 0, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentX += checkboxSize + spacing::dp(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate text area
|
|
||||||
float rightPadding = spacing::dp(2); // 16dp right padding
|
|
||||||
float trailingSpace = 0;
|
|
||||||
if (spec.trailingIcon) trailingSpace += 24.0f + spacing::dp(1);
|
|
||||||
if (spec.trailingText) trailingSpace += ImGui::CalcTextSize(spec.trailingText).x + spacing::dp(1);
|
|
||||||
|
|
||||||
float textMaxX = bb.Max.x - rightPadding - trailingSpace;
|
|
||||||
|
|
||||||
// Draw text
|
|
||||||
ImU32 primaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurface();
|
|
||||||
ImU32 secondaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium();
|
|
||||||
|
|
||||||
if (hasSecondary) {
|
|
||||||
// Two-line layout
|
|
||||||
float primaryY = bb.Min.y + 16.0f;
|
|
||||||
float secondaryY = primaryY + 20.0f;
|
|
||||||
|
|
||||||
Typography::instance().pushFont(TypeStyle::Body1);
|
|
||||||
drawList->AddText(ImVec2(currentX, primaryY), primaryColor, spec.primaryText);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
|
|
||||||
Typography::instance().pushFont(TypeStyle::Body2);
|
|
||||||
drawList->AddText(ImVec2(currentX, secondaryY), secondaryColor, spec.secondaryText);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
} else {
|
|
||||||
// Single-line layout
|
|
||||||
Typography::instance().pushFont(TypeStyle::Body1);
|
|
||||||
float textY = centerY - Typography::instance().getFont(TypeStyle::Body1)->FontSize * 0.5f;
|
|
||||||
drawList->AddText(ImVec2(currentX, textY), primaryColor, spec.primaryText);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw trailing elements
|
|
||||||
float trailingX = bb.Max.x - rightPadding;
|
|
||||||
|
|
||||||
if (spec.trailingText) {
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(spec.trailingText);
|
|
||||||
trailingX -= textSize.x;
|
|
||||||
float textY = centerY - textSize.y * 0.5f;
|
|
||||||
drawList->AddText(ImVec2(trailingX, textY), secondaryColor, spec.trailingText);
|
|
||||||
trailingX -= spacing::dp(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec.trailingIcon) {
|
|
||||||
ImVec2 iconSize = ImGui::CalcTextSize(spec.trailingIcon);
|
|
||||||
trailingX -= 24.0f;
|
|
||||||
float iconY = centerY - iconSize.y * 0.5f;
|
|
||||||
drawList->AddText(ImVec2(trailingX, iconY), OnSurfaceMedium(), spec.trailingIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw divider
|
|
||||||
if (spec.dividerBelow) {
|
|
||||||
float dividerY = bb.Max.y - 0.5f;
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(bb.Min.x + leftPadding, dividerY),
|
|
||||||
ImVec2(bb.Max.x, dividerY),
|
|
||||||
OnSurfaceDisabled()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ListItem(const char* text) {
|
|
||||||
ListItemSpec spec;
|
|
||||||
spec.primaryText = text;
|
|
||||||
return ListItem(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ListItem(const char* primary, const char* secondary) {
|
|
||||||
ListItemSpec spec;
|
|
||||||
spec.primaryText = primary;
|
|
||||||
spec.secondaryText = secondary;
|
|
||||||
return ListItem(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ListDivider(bool inset) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float width = ImGui::GetContentRegionAvail().x;
|
|
||||||
float leftOffset = inset ? 72.0f : 0; // Align with text after avatar/icon
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(pos.x + leftOffset, pos.y),
|
|
||||||
ImVec2(pos.x + width, pos.y),
|
|
||||||
OnSurfaceDisabled()
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(width, 1.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ListSubheader(const char* text) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + spacing::dp(2)); // 16dp left padding
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, Primary(), text);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp bottom padding
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Navigation Drawer Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/navigation-drawer
|
|
||||||
//
|
|
||||||
// Navigation drawers provide access to destinations in your app.
|
|
||||||
|
|
||||||
enum class NavDrawerType {
|
|
||||||
Standard, // Permanent, always visible
|
|
||||||
Modal, // Overlay with scrim, can be dismissed
|
|
||||||
Dismissible // Can be shown/hidden, no scrim
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Navigation drawer configuration
|
|
||||||
*/
|
|
||||||
struct NavDrawerSpec {
|
|
||||||
NavDrawerType type = NavDrawerType::Standard;
|
|
||||||
float width = 256.0f; // 256dp standard width
|
|
||||||
bool showDividerBottom = true; // Divider at bottom
|
|
||||||
const char* headerTitle = nullptr; // Optional header title
|
|
||||||
const char* headerSubtitle = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Navigation item configuration
|
|
||||||
*/
|
|
||||||
struct NavItemSpec {
|
|
||||||
const char* icon = nullptr; // Leading icon
|
|
||||||
const char* label = nullptr; // Item label (required)
|
|
||||||
bool selected = false; // Selected state
|
|
||||||
bool disabled = false;
|
|
||||||
int badgeCount = 0; // Badge (0 = no badge)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a navigation drawer
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param open Pointer to open state (for modal/dismissible)
|
|
||||||
* @param spec Drawer configuration
|
|
||||||
* @return true if drawer is visible
|
|
||||||
*/
|
|
||||||
bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec = NavDrawerSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin standard (always visible) navigation drawer
|
|
||||||
*/
|
|
||||||
bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec = NavDrawerSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End navigation drawer
|
|
||||||
*/
|
|
||||||
void EndNavDrawer();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render a navigation item
|
|
||||||
*
|
|
||||||
* @param spec Item configuration
|
|
||||||
* @return true if clicked
|
|
||||||
*/
|
|
||||||
bool NavItem(const NavItemSpec& spec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple navigation item
|
|
||||||
*/
|
|
||||||
bool NavItem(const char* icon, const char* label, bool selected = false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Navigation divider
|
|
||||||
*/
|
|
||||||
void NavDivider();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Navigation subheader
|
|
||||||
*/
|
|
||||||
void NavSubheader(const char* text);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
struct NavDrawerState {
|
|
||||||
float width;
|
|
||||||
ImVec2 contentMin;
|
|
||||||
ImVec2 contentMax;
|
|
||||||
bool isModal;
|
|
||||||
};
|
|
||||||
|
|
||||||
static NavDrawerState g_navDrawerState;
|
|
||||||
|
|
||||||
inline bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec) {
|
|
||||||
// For modal drawers, check open state
|
|
||||||
if (spec.type == NavDrawerType::Modal && !*open) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
g_navDrawerState.width = spec.width;
|
|
||||||
g_navDrawerState.isModal = (spec.type == NavDrawerType::Modal);
|
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// For modal, draw scrim and handle dismiss
|
|
||||||
if (spec.type == NavDrawerType::Modal) {
|
|
||||||
ImDrawList* bgDrawList = ImGui::GetBackgroundDrawList();
|
|
||||||
bgDrawList->AddRectFilled(
|
|
||||||
ImVec2(0, 0), io.DisplaySize,
|
|
||||||
schema::UI().resolveColor("var(--scrim)", IM_COL32(0, 0, 0, (int)(0.32f * 255)))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Click outside to dismiss
|
|
||||||
if (ImGui::IsMouseClicked(0)) {
|
|
||||||
ImVec2 mousePos = io.MousePos;
|
|
||||||
if (mousePos.x > spec.width) {
|
|
||||||
*open = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drawer position and size
|
|
||||||
ImVec2 drawerPos(0, 0);
|
|
||||||
ImVec2 drawerSize(spec.width, io.DisplaySize.y);
|
|
||||||
|
|
||||||
// If not modal, account for app bar
|
|
||||||
if (spec.type != NavDrawerType::Modal) {
|
|
||||||
drawerPos.y = size::AppBarHeight;
|
|
||||||
drawerSize.y = io.DisplaySize.y - size::AppBarHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImRect drawerBB(drawerPos, ImVec2(drawerPos.x + drawerSize.x, drawerPos.y + drawerSize.y));
|
|
||||||
|
|
||||||
// Draw drawer background
|
|
||||||
ImU32 bgColor = Surface(Elevation::Dp16);
|
|
||||||
drawList->AddRectFilled(drawerBB.Min, drawerBB.Max, bgColor);
|
|
||||||
|
|
||||||
// Store content region
|
|
||||||
g_navDrawerState.contentMin = ImVec2(drawerBB.Min.x, drawerBB.Min.y);
|
|
||||||
g_navDrawerState.contentMax = drawerBB.Max;
|
|
||||||
|
|
||||||
// Header
|
|
||||||
float currentY = drawerBB.Min.y;
|
|
||||||
|
|
||||||
if (spec.headerTitle || spec.headerSubtitle) {
|
|
||||||
// Header area (optional)
|
|
||||||
float headerHeight = 64.0f;
|
|
||||||
|
|
||||||
ImVec2 headerMin(drawerBB.Min.x, currentY);
|
|
||||||
ImVec2 headerMax(drawerBB.Max.x, currentY + headerHeight);
|
|
||||||
|
|
||||||
// Header background (slightly elevated)
|
|
||||||
drawList->AddRectFilled(headerMin, headerMax, Surface(Elevation::Dp16));
|
|
||||||
|
|
||||||
// Header title
|
|
||||||
if (spec.headerTitle) {
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 20.0f));
|
|
||||||
Typography::instance().text(TypeStyle::H6, spec.headerTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec.headerSubtitle) {
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 42.0f));
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), spec.headerSubtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentY += headerHeight;
|
|
||||||
|
|
||||||
// Divider under header
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(drawerBB.Min.x, currentY),
|
|
||||||
ImVec2(drawerBB.Max.x, currentY),
|
|
||||||
OnSurfaceDisabled()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cursor for nav items
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x, currentY + spacing::dp(1)));
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec) {
|
|
||||||
static bool alwaysOpen = true;
|
|
||||||
NavDrawerSpec standardSpec = spec;
|
|
||||||
standardSpec.type = NavDrawerType::Standard;
|
|
||||||
return BeginNavDrawer(id, &alwaysOpen, standardSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndNavDrawer() {
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
// Divider at bottom if configured
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Right edge divider
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMin.y),
|
|
||||||
ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMax.y),
|
|
||||||
OnSurfaceDisabled()
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool NavItem(const NavItemSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(spec.label);
|
|
||||||
|
|
||||||
// Item dimensions
|
|
||||||
const float itemHeight = 48.0f;
|
|
||||||
const float iconSize = 24.0f;
|
|
||||||
const float horizontalPadding = spacing::dp(2); // 16dp
|
|
||||||
const float iconLabelGap = spacing::dp(4); // 32dp from left edge to label
|
|
||||||
|
|
||||||
float itemWidth = g_navDrawerState.width - spacing::dp(1); // 8dp margin right
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
pos.x += spacing::dp(1); // 8dp margin left
|
|
||||||
|
|
||||||
ImRect itemBB(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID id = window->GetID("##navitem");
|
|
||||||
ImGui::ItemSize(ImRect(window->DC.CursorPos, ImVec2(window->DC.CursorPos.x + g_navDrawerState.width, window->DC.CursorPos.y + itemHeight)));
|
|
||||||
if (!ImGui::ItemAdd(itemBB, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(itemBB, id, &hovered, &held) && !spec.disabled;
|
|
||||||
|
|
||||||
// Draw background
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
ImU32 bgColor = 0;
|
|
||||||
if (spec.selected) {
|
|
||||||
bgColor = WithAlpha(Primary(), 30); // Primary at ~12%
|
|
||||||
} else if (held && !spec.disabled) {
|
|
||||||
bgColor = schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25));
|
|
||||||
} else if (hovered && !spec.disabled) {
|
|
||||||
bgColor = schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bgColor) {
|
|
||||||
drawList->AddRectFilled(itemBB.Min, itemBB.Max, bgColor, size::ButtonCornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected indicator (left edge)
|
|
||||||
if (spec.selected) {
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(itemBB.Min.x, itemBB.Min.y + 8.0f),
|
|
||||||
ImVec2(itemBB.Min.x + 4.0f, itemBB.Max.y - 8.0f),
|
|
||||||
Primary(), 2.0f
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content
|
|
||||||
float contentX = pos.x + horizontalPadding;
|
|
||||||
float centerY = pos.y + itemHeight * 0.5f;
|
|
||||||
|
|
||||||
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() :
|
|
||||||
spec.selected ? Primary() : OnSurfaceMedium();
|
|
||||||
ImU32 labelColor = spec.disabled ? OnSurfaceDisabled() :
|
|
||||||
spec.selected ? Primary() : OnSurface();
|
|
||||||
|
|
||||||
// Icon
|
|
||||||
if (spec.icon) {
|
|
||||||
drawList->AddText(
|
|
||||||
ImVec2(contentX, centerY - iconSize * 0.5f),
|
|
||||||
iconColor, spec.icon
|
|
||||||
);
|
|
||||||
contentX += iconSize + spacing::dp(2); // 16dp gap after icon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label
|
|
||||||
Typography::instance().pushFont(TypeStyle::Body1);
|
|
||||||
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
||||||
drawList->AddText(ImVec2(contentX, labelY), labelColor, spec.label);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
|
|
||||||
// Badge
|
|
||||||
if (spec.badgeCount > 0) {
|
|
||||||
char badgeText[8];
|
|
||||||
if (spec.badgeCount > 999) {
|
|
||||||
snprintf(badgeText, sizeof(badgeText), "999+");
|
|
||||||
} else {
|
|
||||||
snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 badgeSize = ImGui::CalcTextSize(badgeText);
|
|
||||||
float badgeWidth = ImMax(24.0f, badgeSize.x + 12.0f);
|
|
||||||
float badgeHeight = 20.0f;
|
|
||||||
float badgeX = itemBB.Max.x - horizontalPadding - badgeWidth;
|
|
||||||
float badgeY = centerY - badgeHeight * 0.5f;
|
|
||||||
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(badgeX, badgeY),
|
|
||||||
ImVec2(badgeX + badgeWidth, badgeY + badgeHeight),
|
|
||||||
Primary(), badgeHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
Typography::instance().pushFont(TypeStyle::Caption);
|
|
||||||
ImVec2 textPos(badgeX + (badgeWidth - badgeSize.x) * 0.5f, badgeY + (badgeHeight - badgeSize.y) * 0.5f);
|
|
||||||
drawList->AddText(textPos, OnPrimary(), badgeText);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return pressed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool NavItem(const char* icon, const char* label, bool selected) {
|
|
||||||
NavItemSpec spec;
|
|
||||||
spec.icon = icon;
|
|
||||||
spec.label = label;
|
|
||||||
spec.selected = selected;
|
|
||||||
return NavItem(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void NavDivider() {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing above
|
|
||||||
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(pos.x + spacing::dp(2), pos.y + spacing::dp(1)),
|
|
||||||
ImVec2(pos.x + g_navDrawerState.width - spacing::dp(2), pos.y + spacing::dp(1)),
|
|
||||||
OnSurfaceDisabled()
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing below
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void NavSubheader(const char* text) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp above
|
|
||||||
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(pos.x + spacing::dp(2), pos.y));
|
|
||||||
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), text);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp below
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Progress Indicators
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/progress-indicators
|
|
||||||
//
|
|
||||||
// Progress indicators express an unspecified wait time or display the length
|
|
||||||
// of a process.
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Linear Progress
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Determinate linear progress bar
|
|
||||||
*
|
|
||||||
* @param fraction Progress value 0.0 to 1.0
|
|
||||||
* @param width Width of bar (0 = full available width)
|
|
||||||
*/
|
|
||||||
void LinearProgress(float fraction, float width = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Indeterminate linear progress bar (animated)
|
|
||||||
*
|
|
||||||
* @param width Width of bar (0 = full available width)
|
|
||||||
*/
|
|
||||||
void LinearProgressIndeterminate(float width = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Buffer linear progress bar
|
|
||||||
*
|
|
||||||
* @param fraction Primary progress 0.0 to 1.0
|
|
||||||
* @param buffer Buffer progress 0.0 to 1.0
|
|
||||||
* @param width Width of bar (0 = full available width)
|
|
||||||
*/
|
|
||||||
void LinearProgressBuffer(float fraction, float buffer, float width = 0);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Circular Progress
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Determinate circular progress indicator
|
|
||||||
*
|
|
||||||
* @param fraction Progress value 0.0 to 1.0
|
|
||||||
* @param radius Radius of circle (default 20dp)
|
|
||||||
*/
|
|
||||||
void CircularProgress(float fraction, float radius = 20.0f);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Indeterminate circular progress (spinner)
|
|
||||||
*
|
|
||||||
* @param radius Radius of circle (default 20dp)
|
|
||||||
*/
|
|
||||||
void CircularProgressIndeterminate(float radius = 20.0f);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void LinearProgress(float fraction, float width) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const float barHeight = 4.0f; // Material spec: 4dp height
|
|
||||||
float barWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + barWidth, pos.y + barHeight));
|
|
||||||
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, 0))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Track (background)
|
|
||||||
ImU32 trackColor = WithAlpha(Primary(), 64); // Primary at 25%
|
|
||||||
drawList->AddRectFilled(bb.Min, bb.Max, trackColor, 0);
|
|
||||||
|
|
||||||
// Progress indicator
|
|
||||||
float progressWidth = barWidth * ImClamp(fraction, 0.0f, 1.0f);
|
|
||||||
if (progressWidth > 0) {
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
bb.Min,
|
|
||||||
ImVec2(bb.Min.x + progressWidth, bb.Max.y),
|
|
||||||
Primary(), 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void LinearProgressIndeterminate(float width) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const float barHeight = 4.0f;
|
|
||||||
float barWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + barWidth, pos.y + barHeight));
|
|
||||||
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, 0))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Track
|
|
||||||
ImU32 trackColor = WithAlpha(Primary(), 64);
|
|
||||||
drawList->AddRectFilled(bb.Min, bb.Max, trackColor, 0);
|
|
||||||
|
|
||||||
// Animated indicator - sliding back and forth
|
|
||||||
float time = (float)ImGui::GetTime();
|
|
||||||
float cycleTime = fmodf(time, 2.0f); // 2 second cycle
|
|
||||||
|
|
||||||
// Two bars: primary and secondary with different phases
|
|
||||||
float indicatorWidth = barWidth * 0.3f; // 30% of track
|
|
||||||
|
|
||||||
// Primary indicator
|
|
||||||
float primaryPhase = fmodf(time * 1.2f, 2.0f);
|
|
||||||
float primaryPos;
|
|
||||||
if (primaryPhase < 1.0f) {
|
|
||||||
// Accelerating from left
|
|
||||||
primaryPos = primaryPhase * primaryPhase * (barWidth + indicatorWidth) - indicatorWidth;
|
|
||||||
} else {
|
|
||||||
// Continue off right (reset happens at 2.0)
|
|
||||||
primaryPos = (2.0f - primaryPhase) * (2.0f - primaryPhase) * -(barWidth + indicatorWidth) + barWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
float primaryStart = ImMax(bb.Min.x, bb.Min.x + primaryPos);
|
|
||||||
float primaryEnd = ImMin(bb.Max.x, bb.Min.x + primaryPos + indicatorWidth);
|
|
||||||
|
|
||||||
if (primaryEnd > primaryStart) {
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(primaryStart, bb.Min.y),
|
|
||||||
ImVec2(primaryEnd, bb.Max.y),
|
|
||||||
Primary(), 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void LinearProgressBuffer(float fraction, float buffer, float width) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const float barHeight = 4.0f;
|
|
||||||
float barWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + barWidth, pos.y + barHeight));
|
|
||||||
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, 0))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Track
|
|
||||||
ImU32 trackColor = WithAlpha(Primary(), 38); // Primary at 15%
|
|
||||||
drawList->AddRectFilled(bb.Min, bb.Max, trackColor, 0);
|
|
||||||
|
|
||||||
// Buffer (lighter than progress)
|
|
||||||
float bufferWidth = barWidth * ImClamp(buffer, 0.0f, 1.0f);
|
|
||||||
if (bufferWidth > 0) {
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
bb.Min,
|
|
||||||
ImVec2(bb.Min.x + bufferWidth, bb.Max.y),
|
|
||||||
WithAlpha(Primary(), 102), 0 // Primary at 40%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
float progressWidth = barWidth * ImClamp(fraction, 0.0f, 1.0f);
|
|
||||||
if (progressWidth > 0) {
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
bb.Min,
|
|
||||||
ImVec2(bb.Min.x + progressWidth, bb.Max.y),
|
|
||||||
Primary(), 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CircularProgress(float fraction, float radius) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const float thickness = 4.0f; // Stroke width
|
|
||||||
float diameter = radius * 2;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImVec2 center(pos.x + radius, pos.y + radius);
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + diameter, pos.y + diameter));
|
|
||||||
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, 0))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Track circle
|
|
||||||
ImU32 trackColor = WithAlpha(Primary(), 64);
|
|
||||||
drawList->AddCircle(center, radius - thickness * 0.5f, trackColor, 0, thickness);
|
|
||||||
|
|
||||||
// Progress arc
|
|
||||||
float clampedFraction = ImClamp(fraction, 0.0f, 1.0f);
|
|
||||||
if (clampedFraction > 0) {
|
|
||||||
float startAngle = -IM_PI * 0.5f; // Start at top (12 o'clock)
|
|
||||||
float endAngle = startAngle + IM_PI * 2.0f * clampedFraction;
|
|
||||||
|
|
||||||
// Draw arc as line segments
|
|
||||||
const int segments = (int)(32 * clampedFraction) + 1;
|
|
||||||
float angleStep = (endAngle - startAngle) / segments;
|
|
||||||
|
|
||||||
for (int i = 0; i < segments; i++) {
|
|
||||||
float a1 = startAngle + angleStep * i;
|
|
||||||
float a2 = startAngle + angleStep * (i + 1);
|
|
||||||
|
|
||||||
ImVec2 p1(center.x + cosf(a1) * (radius - thickness * 0.5f),
|
|
||||||
center.y + sinf(a1) * (radius - thickness * 0.5f));
|
|
||||||
ImVec2 p2(center.x + cosf(a2) * (radius - thickness * 0.5f),
|
|
||||||
center.y + sinf(a2) * (radius - thickness * 0.5f));
|
|
||||||
|
|
||||||
drawList->AddLine(p1, p2, Primary(), thickness);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void CircularProgressIndeterminate(float radius) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const float thickness = 4.0f;
|
|
||||||
float diameter = radius * 2;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImVec2 center(pos.x + radius, pos.y + radius);
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + diameter, pos.y + diameter));
|
|
||||||
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, 0))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
float time = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
// Rotation animation
|
|
||||||
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f); // ~1.4s rotation
|
|
||||||
|
|
||||||
// Arc length animation (grows and shrinks)
|
|
||||||
float cycleTime = fmodf(time, 1.333f); // ~1.333s cycle
|
|
||||||
float arcLength;
|
|
||||||
if (cycleTime < 0.666f) {
|
|
||||||
// Growing phase
|
|
||||||
arcLength = (cycleTime / 0.666f) * 0.75f + 0.1f; // 10% to 85%
|
|
||||||
} else {
|
|
||||||
// Shrinking phase
|
|
||||||
arcLength = ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float startAngle = rotation - IM_PI * 0.5f;
|
|
||||||
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
|
|
||||||
|
|
||||||
// Draw arc
|
|
||||||
const int segments = (int)(32 * arcLength) + 1;
|
|
||||||
float angleStep = (endAngle - startAngle) / segments;
|
|
||||||
|
|
||||||
for (int i = 0; i < segments; i++) {
|
|
||||||
float a1 = startAngle + angleStep * i;
|
|
||||||
float a2 = startAngle + angleStep * (i + 1);
|
|
||||||
|
|
||||||
ImVec2 p1(center.x + cosf(a1) * (radius - thickness * 0.5f),
|
|
||||||
center.y + sinf(a1) * (radius - thickness * 0.5f));
|
|
||||||
ImVec2 p2(center.x + cosf(a2) * (radius - thickness * 0.5f),
|
|
||||||
center.y + sinf(a2) * (radius - thickness * 0.5f));
|
|
||||||
|
|
||||||
drawList->AddLine(p1, p2, Primary(), thickness);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Slider Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/sliders
|
|
||||||
//
|
|
||||||
// Sliders allow users to make selections from a range of values.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Continuous slider
|
|
||||||
*
|
|
||||||
* @param label Label for the slider (hidden, used for ID)
|
|
||||||
* @param value Pointer to current value
|
|
||||||
* @param minValue Minimum value
|
|
||||||
* @param maxValue Maximum value
|
|
||||||
* @param format Printf format for value display (nullptr = no display)
|
|
||||||
* @param width Slider width (0 = full available)
|
|
||||||
* @return true if value changed
|
|
||||||
*/
|
|
||||||
bool Slider(const char* label, float* value, float minValue, float maxValue,
|
|
||||||
const char* format = nullptr, float width = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Integer slider
|
|
||||||
*/
|
|
||||||
bool SliderInt(const char* label, int* value, int minValue, int maxValue,
|
|
||||||
const char* format = nullptr, float width = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Discrete slider with steps
|
|
||||||
*
|
|
||||||
* @param label Label for the slider
|
|
||||||
* @param value Pointer to current value
|
|
||||||
* @param minValue Minimum value
|
|
||||||
* @param maxValue Maximum value
|
|
||||||
* @param step Step size
|
|
||||||
* @param showTicks Show tick marks
|
|
||||||
* @return true if value changed
|
|
||||||
*/
|
|
||||||
bool SliderDiscrete(const char* label, float* value, float minValue, float maxValue,
|
|
||||||
float step, bool showTicks = true, float width = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Range slider (two thumbs)
|
|
||||||
*
|
|
||||||
* @param label Label for the slider
|
|
||||||
* @param minVal Pointer to range minimum
|
|
||||||
* @param maxVal Pointer to range maximum
|
|
||||||
* @param rangeMin Allowed minimum
|
|
||||||
* @param rangeMax Allowed maximum
|
|
||||||
* @return true if either value changed
|
|
||||||
*/
|
|
||||||
bool SliderRange(const char* label, float* minVal, float* maxVal,
|
|
||||||
float rangeMin, float rangeMax, float width = 0);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline bool Slider(const char* label, float* value, float minValue, float maxValue,
|
|
||||||
const char* format, float width) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
// Slider dimensions
|
|
||||||
const float trackHeight = 4.0f;
|
|
||||||
const float thumbRadius = 10.0f; // 20dp diameter
|
|
||||||
float sliderWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
|
|
||||||
float totalHeight = size::TouchTarget; // 48dp touch target
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + sliderWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
// Item interaction
|
|
||||||
ImGuiID id = window->GetID("##slider");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
|
|
||||||
|
|
||||||
// Calculate thumb position
|
|
||||||
float trackLeft = pos.x + thumbRadius;
|
|
||||||
float trackRight = pos.x + sliderWidth - thumbRadius;
|
|
||||||
float trackWidth = trackRight - trackLeft;
|
|
||||||
float centerY = pos.y + totalHeight * 0.5f;
|
|
||||||
|
|
||||||
float fraction = (*value - minValue) / (maxValue - minValue);
|
|
||||||
fraction = ImClamp(fraction, 0.0f, 1.0f);
|
|
||||||
float thumbX = trackLeft + trackWidth * fraction;
|
|
||||||
|
|
||||||
// Handle dragging
|
|
||||||
bool changed = false;
|
|
||||||
if (held) {
|
|
||||||
float mouseX = ImGui::GetIO().MousePos.x;
|
|
||||||
float newFraction = (mouseX - trackLeft) / trackWidth;
|
|
||||||
newFraction = ImClamp(newFraction, 0.0f, 1.0f);
|
|
||||||
float newValue = minValue + newFraction * (maxValue - minValue);
|
|
||||||
|
|
||||||
if (newValue != *value) {
|
|
||||||
*value = newValue;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
thumbX = trackLeft + trackWidth * newFraction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Track (inactive part)
|
|
||||||
ImU32 trackInactiveColor = WithAlpha(Primary(), 64); // Primary at 25%
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
|
|
||||||
ImVec2(trackRight, centerY + trackHeight * 0.5f),
|
|
||||||
trackInactiveColor, trackHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Track (active part)
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
|
|
||||||
ImVec2(thumbX, centerY + trackHeight * 0.5f),
|
|
||||||
Primary(), trackHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Thumb shadow
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
|
|
||||||
|
|
||||||
// Thumb
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius, Primary());
|
|
||||||
|
|
||||||
// Hover/pressed ripple
|
|
||||||
if (hovered || held) {
|
|
||||||
ImU32 rippleColor = WithAlpha(Primary(), held ? 51 : 25);
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius + 12.0f, rippleColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value label (when held)
|
|
||||||
if (held && format) {
|
|
||||||
char valueText[64];
|
|
||||||
snprintf(valueText, sizeof(valueText), format, *value);
|
|
||||||
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(valueText);
|
|
||||||
float labelY = centerY - thumbRadius - 32.0f;
|
|
||||||
float labelX = thumbX - textSize.x * 0.5f;
|
|
||||||
|
|
||||||
// Label background (rounded rectangle)
|
|
||||||
float labelPadX = 8.0f;
|
|
||||||
float labelPadY = 4.0f;
|
|
||||||
ImVec2 labelMin(labelX - labelPadX, labelY - labelPadY);
|
|
||||||
ImVec2 labelMax(labelX + textSize.x + labelPadX, labelY + textSize.y + labelPadY);
|
|
||||||
|
|
||||||
drawList->AddRectFilled(labelMin, labelMax, Primary(), 4.0f);
|
|
||||||
drawList->AddText(ImVec2(labelX, labelY), OnPrimary(), valueText);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool SliderInt(const char* label, int* value, int minValue, int maxValue,
|
|
||||||
const char* format, float width) {
|
|
||||||
float floatVal = (float)*value;
|
|
||||||
bool changed = Slider(label, &floatVal, (float)minValue, (float)maxValue, format, width);
|
|
||||||
if (changed) {
|
|
||||||
*value = (int)roundf(floatVal);
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool SliderDiscrete(const char* label, float* value, float minValue, float maxValue,
|
|
||||||
float step, bool showTicks, float width) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
const float trackHeight = 4.0f;
|
|
||||||
const float thumbRadius = 10.0f;
|
|
||||||
const float tickRadius = 2.0f;
|
|
||||||
float sliderWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
|
|
||||||
float totalHeight = size::TouchTarget;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + sliderWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
ImGuiID id = window->GetID("##slider");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hovered, held;
|
|
||||||
ImGui::ButtonBehavior(bb, id, &hovered, &held);
|
|
||||||
|
|
||||||
float trackLeft = pos.x + thumbRadius;
|
|
||||||
float trackRight = pos.x + sliderWidth - thumbRadius;
|
|
||||||
float trackWidth = trackRight - trackLeft;
|
|
||||||
float centerY = pos.y + totalHeight * 0.5f;
|
|
||||||
|
|
||||||
// Snap to step
|
|
||||||
float snappedValue = roundf((*value - minValue) / step) * step + minValue;
|
|
||||||
snappedValue = ImClamp(snappedValue, minValue, maxValue);
|
|
||||||
|
|
||||||
float fraction = (snappedValue - minValue) / (maxValue - minValue);
|
|
||||||
float thumbX = trackLeft + trackWidth * fraction;
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (held) {
|
|
||||||
float mouseX = ImGui::GetIO().MousePos.x;
|
|
||||||
float newFraction = (mouseX - trackLeft) / trackWidth;
|
|
||||||
newFraction = ImClamp(newFraction, 0.0f, 1.0f);
|
|
||||||
float rawValue = minValue + newFraction * (maxValue - minValue);
|
|
||||||
float newValue = roundf((rawValue - minValue) / step) * step + minValue;
|
|
||||||
newValue = ImClamp(newValue, minValue, maxValue);
|
|
||||||
|
|
||||||
if (newValue != *value) {
|
|
||||||
*value = newValue;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
fraction = (newValue - minValue) / (maxValue - minValue);
|
|
||||||
thumbX = trackLeft + trackWidth * fraction;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Track
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
|
|
||||||
ImVec2(trackRight, centerY + trackHeight * 0.5f),
|
|
||||||
WithAlpha(Primary(), 64), trackHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
|
|
||||||
ImVec2(thumbX, centerY + trackHeight * 0.5f),
|
|
||||||
Primary(), trackHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tick marks
|
|
||||||
if (showTicks) {
|
|
||||||
int numSteps = (int)((maxValue - minValue) / step);
|
|
||||||
for (int i = 0; i <= numSteps; i++) {
|
|
||||||
float tickFraction = (float)i / numSteps;
|
|
||||||
float tickX = trackLeft + trackWidth * tickFraction;
|
|
||||||
|
|
||||||
ImU32 tickColor = (tickX <= thumbX) ? OnPrimary() : WithAlpha(Primary(), 128);
|
|
||||||
drawList->AddCircleFilled(ImVec2(tickX, centerY), tickRadius, tickColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumb
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius, Primary());
|
|
||||||
|
|
||||||
if (hovered || held) {
|
|
||||||
ImU32 rippleColor = WithAlpha(Primary(), held ? 51 : 25);
|
|
||||||
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius + 12.0f, rippleColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool SliderRange(const char* label, float* minVal, float* maxVal,
|
|
||||||
float rangeMin, float rangeMax, float width) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(label);
|
|
||||||
|
|
||||||
const float trackHeight = 4.0f;
|
|
||||||
const float thumbRadius = 10.0f;
|
|
||||||
float sliderWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
|
|
||||||
float totalHeight = size::TouchTarget;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + sliderWidth, pos.y + totalHeight));
|
|
||||||
|
|
||||||
ImGuiID id = window->GetID("##slider");
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
if (!ImGui::ItemAdd(bb, id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
float trackLeft = pos.x + thumbRadius;
|
|
||||||
float trackRight = pos.x + sliderWidth - thumbRadius;
|
|
||||||
float trackWidth = trackRight - trackLeft;
|
|
||||||
float centerY = pos.y + totalHeight * 0.5f;
|
|
||||||
|
|
||||||
float minFraction = (*minVal - rangeMin) / (rangeMax - rangeMin);
|
|
||||||
float maxFraction = (*maxVal - rangeMin) / (rangeMax - rangeMin);
|
|
||||||
float minThumbX = trackLeft + trackWidth * minFraction;
|
|
||||||
float maxThumbX = trackLeft + trackWidth * maxFraction;
|
|
||||||
|
|
||||||
// Hit test both thumbs
|
|
||||||
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
||||||
float distToMin = fabsf(mousePos.x - minThumbX);
|
|
||||||
float distToMax = fabsf(mousePos.x - maxThumbX);
|
|
||||||
bool nearMin = distToMin < distToMax;
|
|
||||||
|
|
||||||
ImGuiID minId = window->GetID("##min");
|
|
||||||
ImGuiID maxId = window->GetID("##max");
|
|
||||||
|
|
||||||
bool minHovered, minHeld;
|
|
||||||
bool maxHovered, maxHeld;
|
|
||||||
ImRect minHitBox(ImVec2(minThumbX - thumbRadius - 8, centerY - thumbRadius - 8),
|
|
||||||
ImVec2(minThumbX + thumbRadius + 8, centerY + thumbRadius + 8));
|
|
||||||
ImRect maxHitBox(ImVec2(maxThumbX - thumbRadius - 8, centerY - thumbRadius - 8),
|
|
||||||
ImVec2(maxThumbX + thumbRadius + 8, centerY + thumbRadius + 8));
|
|
||||||
|
|
||||||
ImGui::ButtonBehavior(nearMin ? minHitBox : maxHitBox, nearMin ? minId : maxId,
|
|
||||||
nearMin ? &minHovered : &maxHovered,
|
|
||||||
nearMin ? &minHeld : &maxHeld);
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
|
|
||||||
if (minHeld) {
|
|
||||||
float newFraction = (mousePos.x - trackLeft) / trackWidth;
|
|
||||||
newFraction = ImClamp(newFraction, 0.0f, maxFraction - 0.01f);
|
|
||||||
float newValue = rangeMin + newFraction * (rangeMax - rangeMin);
|
|
||||||
if (newValue != *minVal) {
|
|
||||||
*minVal = newValue;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
minThumbX = trackLeft + trackWidth * newFraction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxHeld) {
|
|
||||||
float newFraction = (mousePos.x - trackLeft) / trackWidth;
|
|
||||||
newFraction = ImClamp(newFraction, minFraction + 0.01f, 1.0f);
|
|
||||||
float newValue = rangeMin + newFraction * (rangeMax - rangeMin);
|
|
||||||
if (newValue != *maxVal) {
|
|
||||||
*maxVal = newValue;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
maxThumbX = trackLeft + trackWidth * newFraction;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Inactive track
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
|
|
||||||
ImVec2(trackRight, centerY + trackHeight * 0.5f),
|
|
||||||
WithAlpha(Primary(), 64), trackHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Active track (between thumbs)
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(minThumbX, centerY - trackHeight * 0.5f),
|
|
||||||
ImVec2(maxThumbX, centerY + trackHeight * 0.5f),
|
|
||||||
Primary(), trackHeight * 0.5f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Min thumb
|
|
||||||
drawList->AddCircleFilled(ImVec2(minThumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
|
|
||||||
drawList->AddCircleFilled(ImVec2(minThumbX, centerY), thumbRadius, Primary());
|
|
||||||
|
|
||||||
if (minHovered || minHeld) {
|
|
||||||
ImU32 rippleColor = WithAlpha(Primary(), minHeld ? 51 : 25);
|
|
||||||
drawList->AddCircleFilled(ImVec2(minThumbX, centerY), thumbRadius + 12.0f, rippleColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max thumb
|
|
||||||
drawList->AddCircleFilled(ImVec2(maxThumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
|
|
||||||
drawList->AddCircleFilled(ImVec2(maxThumbX, centerY), thumbRadius, Primary());
|
|
||||||
|
|
||||||
if (maxHovered || maxHeld) {
|
|
||||||
ImU32 rippleColor = WithAlpha(Primary(), maxHeld ? 51 : 25);
|
|
||||||
drawList->AddCircleFilled(ImVec2(maxThumbX, centerY), thumbRadius + 12.0f, rippleColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "../draw_helpers.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Snackbar Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/snackbars
|
|
||||||
//
|
|
||||||
// Snackbars provide brief messages about app processes at the bottom of the
|
|
||||||
// screen. They can include a single action.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Snackbar configuration
|
|
||||||
*/
|
|
||||||
struct SnackbarSpec {
|
|
||||||
const char* message = nullptr; // Message text
|
|
||||||
const char* actionText = nullptr; // Optional action button text
|
|
||||||
float duration = 4.0f; // Duration in seconds (0 = indefinite)
|
|
||||||
bool multiLine = false; // Allow multi-line message
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Snackbar manager for showing notifications
|
|
||||||
*/
|
|
||||||
class Snackbar {
|
|
||||||
public:
|
|
||||||
static Snackbar& instance();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a snackbar message
|
|
||||||
*
|
|
||||||
* @param message Message text
|
|
||||||
* @param actionText Optional action text
|
|
||||||
* @param duration Display duration (0 = until dismissed)
|
|
||||||
*/
|
|
||||||
void show(const char* message, const char* actionText = nullptr, float duration = 4.0f);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a snackbar with full configuration
|
|
||||||
*/
|
|
||||||
void show(const SnackbarSpec& spec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Dismiss current snackbar
|
|
||||||
*/
|
|
||||||
void dismiss();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render snackbar (call each frame)
|
|
||||||
*
|
|
||||||
* @return true if action was clicked
|
|
||||||
*/
|
|
||||||
bool render();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if snackbar is visible
|
|
||||||
*/
|
|
||||||
bool isVisible() const { return m_visible; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
Snackbar() = default;
|
|
||||||
|
|
||||||
bool m_visible = false;
|
|
||||||
SnackbarSpec m_currentSpec;
|
|
||||||
float m_showTime = 0;
|
|
||||||
float m_animProgress = 0; // 0 = hidden, 1 = fully shown
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Convenience Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a snackbar message
|
|
||||||
*/
|
|
||||||
inline void ShowSnackbar(const char* message, const char* action = nullptr, float duration = 4.0f) {
|
|
||||||
Snackbar::instance().show(message, action, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Dismiss current snackbar
|
|
||||||
*/
|
|
||||||
inline void DismissSnackbar() {
|
|
||||||
Snackbar::instance().dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render snackbar system (call once per frame in main render loop)
|
|
||||||
*
|
|
||||||
* @return true if action was clicked
|
|
||||||
*/
|
|
||||||
inline bool RenderSnackbar() {
|
|
||||||
return Snackbar::instance().render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline Snackbar& Snackbar::instance() {
|
|
||||||
static Snackbar s_instance;
|
|
||||||
return s_instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Snackbar::show(const char* message, const char* actionText, float duration) {
|
|
||||||
SnackbarSpec spec;
|
|
||||||
spec.message = message;
|
|
||||||
spec.actionText = actionText;
|
|
||||||
spec.duration = duration;
|
|
||||||
show(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Snackbar::show(const SnackbarSpec& spec) {
|
|
||||||
m_currentSpec = spec;
|
|
||||||
m_visible = true;
|
|
||||||
m_showTime = (float)ImGui::GetTime();
|
|
||||||
m_animProgress = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Snackbar::dismiss() {
|
|
||||||
m_visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Snackbar::render() {
|
|
||||||
if (!m_visible && m_animProgress <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool actionClicked = false;
|
|
||||||
float currentTime = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
// Check auto-dismiss
|
|
||||||
if (m_visible && m_currentSpec.duration > 0) {
|
|
||||||
if (currentTime - m_showTime > m_currentSpec.duration) {
|
|
||||||
m_visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate in/out
|
|
||||||
float animTarget = m_visible ? 1.0f : 0.0f;
|
|
||||||
float animSpeed = 8.0f; // Animation speed
|
|
||||||
if (m_animProgress < animTarget) {
|
|
||||||
m_animProgress = ImMin(m_animProgress + ImGui::GetIO().DeltaTime * animSpeed, animTarget);
|
|
||||||
} else if (m_animProgress > animTarget) {
|
|
||||||
m_animProgress = ImMax(m_animProgress - ImGui::GetIO().DeltaTime * animSpeed, animTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_animProgress <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Snackbar dimensions
|
|
||||||
const float snackbarHeight = m_currentSpec.multiLine ? 68.0f : 48.0f;
|
|
||||||
const float snackbarMinWidth = 344.0f;
|
|
||||||
const float snackbarMaxWidth = 672.0f;
|
|
||||||
const float margin = spacing::dp(3); // 24dp from edges
|
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
|
|
||||||
// Calculate width based on content
|
|
||||||
float messageWidth = ImGui::CalcTextSize(m_currentSpec.message).x;
|
|
||||||
float actionWidth = m_currentSpec.actionText ?
|
|
||||||
ImGui::CalcTextSize(m_currentSpec.actionText).x + spacing::dp(2) : 0;
|
|
||||||
float contentWidth = messageWidth + actionWidth + spacing::dp(4); // 32dp padding
|
|
||||||
float snackbarWidth = ImClamp(contentWidth, snackbarMinWidth, snackbarMaxWidth);
|
|
||||||
|
|
||||||
// Position at bottom center
|
|
||||||
float bottomY = io.DisplaySize.y - margin - snackbarHeight;
|
|
||||||
float slideOffset = (1.0f - m_animProgress) * (snackbarHeight + margin);
|
|
||||||
|
|
||||||
ImVec2 snackbarPos(
|
|
||||||
(io.DisplaySize.x - snackbarWidth) * 0.5f,
|
|
||||||
bottomY + slideOffset
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw snackbar
|
|
||||||
ImDrawList* drawList = ImGui::GetForegroundDrawList();
|
|
||||||
|
|
||||||
// Background (elevation dp6 equivalent)
|
|
||||||
ImU32 snackBg = schema::UI().resolveColor("var(--snackbar-bg)", IM_COL32(50, 50, 50, 255));
|
|
||||||
ImU32 bgColor = ScaleAlpha(snackBg, m_animProgress);
|
|
||||||
ImVec2 snackbarMin = snackbarPos;
|
|
||||||
ImVec2 snackbarMax(snackbarPos.x + snackbarWidth, snackbarPos.y + snackbarHeight);
|
|
||||||
|
|
||||||
drawList->AddRectFilled(snackbarMin, snackbarMax, bgColor, 4.0f);
|
|
||||||
|
|
||||||
// Message text
|
|
||||||
float textY = snackbarPos.y + (snackbarHeight - ImGui::GetFontSize()) * 0.5f;
|
|
||||||
float textX = snackbarPos.x + spacing::dp(2); // 16dp left padding
|
|
||||||
|
|
||||||
ImU32 snackText = schema::UI().resolveColor("var(--snackbar-text)", IM_COL32(255, 255, 255, 222));
|
|
||||||
ImU32 textColor = ScaleAlpha(snackText, m_animProgress);
|
|
||||||
drawList->AddText(ImVec2(textX, textY), textColor, m_currentSpec.message);
|
|
||||||
|
|
||||||
// Action button
|
|
||||||
if (m_currentSpec.actionText) {
|
|
||||||
float actionX = snackbarMax.x - spacing::dp(2) - actionWidth;
|
|
||||||
|
|
||||||
// Hit test for action
|
|
||||||
ImVec2 actionMin(actionX, snackbarPos.y);
|
|
||||||
ImVec2 actionMax(snackbarMax.x, snackbarMax.y);
|
|
||||||
|
|
||||||
ImVec2 mousePos = io.MousePos;
|
|
||||||
bool hovered = (mousePos.x >= actionMin.x && mousePos.x < actionMax.x &&
|
|
||||||
mousePos.y >= actionMin.y && mousePos.y < actionMax.y);
|
|
||||||
|
|
||||||
// Action text color
|
|
||||||
ImU32 actionColor;
|
|
||||||
if (hovered) {
|
|
||||||
actionColor = ScaleAlpha(schema::UI().resolveColor("var(--snackbar-action-hover)", IM_COL32(255, 213, 79, 255)), m_animProgress);
|
|
||||||
} else {
|
|
||||||
actionColor = ScaleAlpha(schema::UI().resolveColor("var(--snackbar-action)", IM_COL32(255, 193, 7, 255)), m_animProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawList->AddText(ImVec2(actionX, textY), actionColor, m_currentSpec.actionText);
|
|
||||||
|
|
||||||
// Check click
|
|
||||||
if (hovered && io.MouseClicked[0]) {
|
|
||||||
actionClicked = true;
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return actionClicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "../../schema/ui_schema.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Tabs Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/tabs
|
|
||||||
//
|
|
||||||
// Tabs organize content across different screens, data sets, and other
|
|
||||||
// interactions.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Tab bar configuration
|
|
||||||
*/
|
|
||||||
struct TabBarSpec {
|
|
||||||
bool scrollable = false; // Enable horizontal scrolling
|
|
||||||
bool fullWidth = true; // Tabs fill available width
|
|
||||||
bool showIndicator = true; // Show selection indicator
|
|
||||||
bool centered = false; // Center tabs (when not full width)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Individual tab configuration
|
|
||||||
*/
|
|
||||||
struct TabSpec {
|
|
||||||
const char* label = nullptr;
|
|
||||||
const char* icon = nullptr; // Optional icon (text representation)
|
|
||||||
bool disabled = false;
|
|
||||||
int badgeCount = 0; // Badge count (0 = no badge)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin a tab bar
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param selectedIndex Pointer to selected tab index
|
|
||||||
* @param spec Tab bar configuration
|
|
||||||
* @return true if tab bar is visible
|
|
||||||
*/
|
|
||||||
bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec = TabBarSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End a tab bar
|
|
||||||
*/
|
|
||||||
void EndTabBar();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a tab to current tab bar
|
|
||||||
*
|
|
||||||
* @param spec Tab configuration
|
|
||||||
* @return true if this tab is selected
|
|
||||||
*/
|
|
||||||
bool Tab(const TabSpec& spec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple tab with just label
|
|
||||||
*/
|
|
||||||
bool Tab(const char* label);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple tab bar - returns selected index
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param labels Array of tab labels
|
|
||||||
* @param count Number of tabs
|
|
||||||
* @param selectedIndex Current selected index (will be updated)
|
|
||||||
* @return true if selection changed
|
|
||||||
*/
|
|
||||||
bool TabBar(const char* id, const char** labels, int count, int* selectedIndex);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Internal state for tab rendering
|
|
||||||
struct TabBarState {
|
|
||||||
int* selectedIndex;
|
|
||||||
int currentTabIndex;
|
|
||||||
TabBarSpec spec;
|
|
||||||
float tabBarWidth;
|
|
||||||
float tabWidth;
|
|
||||||
float indicatorX;
|
|
||||||
float indicatorWidth;
|
|
||||||
ImVec2 barPos;
|
|
||||||
};
|
|
||||||
|
|
||||||
static TabBarState g_tabBarState;
|
|
||||||
|
|
||||||
inline bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
g_tabBarState.selectedIndex = selectedIndex;
|
|
||||||
g_tabBarState.currentTabIndex = 0;
|
|
||||||
g_tabBarState.spec = spec;
|
|
||||||
g_tabBarState.tabBarWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
g_tabBarState.tabWidth = 0; // Will be calculated if fullWidth
|
|
||||||
g_tabBarState.barPos = window->DC.CursorPos;
|
|
||||||
g_tabBarState.indicatorX = 0;
|
|
||||||
g_tabBarState.indicatorWidth = 0;
|
|
||||||
|
|
||||||
// Reserve space for tab bar
|
|
||||||
float barHeight = size::TabBarHeight;
|
|
||||||
ImRect bb(g_tabBarState.barPos,
|
|
||||||
ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth,
|
|
||||||
g_tabBarState.barPos.y + barHeight));
|
|
||||||
|
|
||||||
ImGui::ItemSize(bb);
|
|
||||||
|
|
||||||
// Draw tab bar background
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
drawList->AddRectFilled(bb.Min, bb.Max, Surface(Elevation::Dp4));
|
|
||||||
|
|
||||||
// Begin horizontal layout for tabs
|
|
||||||
ImGui::SetCursorScreenPos(g_tabBarState.barPos);
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void EndTabBar() {
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
// Draw indicator line
|
|
||||||
if (g_tabBarState.spec.showIndicator && g_tabBarState.indicatorWidth > 0) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
float indicatorY = g_tabBarState.barPos.y + size::TabBarHeight - 2.0f;
|
|
||||||
drawList->AddRectFilled(
|
|
||||||
ImVec2(g_tabBarState.indicatorX, indicatorY),
|
|
||||||
ImVec2(g_tabBarState.indicatorX + g_tabBarState.indicatorWidth, indicatorY + 2.0f),
|
|
||||||
Primary()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add bottom divider
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
float dividerY = g_tabBarState.barPos.y + size::TabBarHeight;
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(g_tabBarState.barPos.x, dividerY),
|
|
||||||
ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth, dividerY),
|
|
||||||
OnSurfaceDisabled()
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Tab(const TabSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
int tabIndex = g_tabBarState.currentTabIndex++;
|
|
||||||
bool isSelected = (*g_tabBarState.selectedIndex == tabIndex);
|
|
||||||
|
|
||||||
// Calculate tab dimensions
|
|
||||||
float minTabWidth = spec.icon ? 72.0f : 90.0f; // Material min widths
|
|
||||||
float maxTabWidth = 360.0f;
|
|
||||||
float labelWidth = ImGui::CalcTextSize(spec.label).x;
|
|
||||||
float iconWidth = spec.icon ? 24.0f + spacing::dp(1) : 0;
|
|
||||||
float contentWidth = labelWidth + iconWidth + spacing::dp(4); // 32dp padding
|
|
||||||
|
|
||||||
float tabWidth;
|
|
||||||
if (g_tabBarState.spec.fullWidth) {
|
|
||||||
// Divide evenly (assuming we don't know total count here - simplified)
|
|
||||||
tabWidth = ImMax(minTabWidth, contentWidth);
|
|
||||||
} else {
|
|
||||||
tabWidth = ImClamp(contentWidth, minTabWidth, maxTabWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
float tabHeight = size::TabBarHeight;
|
|
||||||
|
|
||||||
ImVec2 tabPos = window->DC.CursorPos;
|
|
||||||
ImRect tabBB(tabPos, ImVec2(tabPos.x + tabWidth, tabPos.y + tabHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID id = window->GetID(spec.label);
|
|
||||||
bool hovered, held;
|
|
||||||
bool pressed = ImGui::ButtonBehavior(tabBB, id, &hovered, &held) && !spec.disabled;
|
|
||||||
|
|
||||||
if (pressed && !isSelected) {
|
|
||||||
*g_tabBarState.selectedIndex = tabIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update indicator position for selected tab
|
|
||||||
if (isSelected) {
|
|
||||||
g_tabBarState.indicatorX = tabPos.x;
|
|
||||||
g_tabBarState.indicatorWidth = tabWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
// Hover/press state overlay
|
|
||||||
if (!spec.disabled) {
|
|
||||||
if (held) {
|
|
||||||
drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
|
|
||||||
} else if (hovered) {
|
|
||||||
drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content color
|
|
||||||
ImU32 contentColor;
|
|
||||||
if (spec.disabled) {
|
|
||||||
contentColor = OnSurfaceDisabled();
|
|
||||||
} else if (isSelected) {
|
|
||||||
contentColor = Primary();
|
|
||||||
} else {
|
|
||||||
contentColor = OnSurfaceMedium();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw content (icon and/or label)
|
|
||||||
float contentX = tabPos.x + (tabWidth - labelWidth - iconWidth) * 0.5f;
|
|
||||||
float centerY = tabPos.y + tabHeight * 0.5f;
|
|
||||||
|
|
||||||
if (spec.icon) {
|
|
||||||
ImFont* iconFont = Type().iconMed();
|
|
||||||
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, spec.icon);
|
|
||||||
ImVec2 iconPos(contentX, centerY - iconSize.y * 0.5f);
|
|
||||||
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, contentColor, spec.icon);
|
|
||||||
contentX += iconSize.x + spacing::Xs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label (uppercase)
|
|
||||||
Typography::instance().pushFont(TypeStyle::Button);
|
|
||||||
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
||||||
|
|
||||||
// Convert to uppercase
|
|
||||||
char upperLabel[128];
|
|
||||||
size_t i = 0;
|
|
||||||
for (const char* p = spec.label; *p && i < sizeof(upperLabel) - 1; p++, i++) {
|
|
||||||
upperLabel[i] = (*p >= 'a' && *p <= 'z') ? (*p - 32) : *p;
|
|
||||||
}
|
|
||||||
upperLabel[i] = '\0';
|
|
||||||
|
|
||||||
drawList->AddText(ImVec2(contentX, labelY), contentColor, upperLabel);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
|
|
||||||
// Badge
|
|
||||||
if (spec.badgeCount > 0) {
|
|
||||||
float badgeX = tabPos.x + tabWidth - 16.0f;
|
|
||||||
float badgeY = tabPos.y + 8.0f;
|
|
||||||
float badgeRadius = 8.0f;
|
|
||||||
|
|
||||||
drawList->AddCircleFilled(ImVec2(badgeX, badgeY), badgeRadius, Error());
|
|
||||||
|
|
||||||
char badgeText[8];
|
|
||||||
if (spec.badgeCount > 99) {
|
|
||||||
snprintf(badgeText, sizeof(badgeText), "99+");
|
|
||||||
} else {
|
|
||||||
snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 badgeTextSize = ImGui::CalcTextSize(badgeText);
|
|
||||||
ImVec2 badgeTextPos(badgeX - badgeTextSize.x * 0.5f, badgeY - badgeTextSize.y * 0.5f);
|
|
||||||
|
|
||||||
Typography::instance().pushFont(TypeStyle::Caption);
|
|
||||||
drawList->AddText(badgeTextPos, OnError(), badgeText);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance cursor
|
|
||||||
ImGui::SameLine(0, 0);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(tabPos.x + tabWidth, tabPos.y));
|
|
||||||
|
|
||||||
return isSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Tab(const char* label) {
|
|
||||||
TabSpec spec;
|
|
||||||
spec.label = label;
|
|
||||||
return Tab(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool TabBar(const char* id, const char** labels, int count, int* selectedIndex) {
|
|
||||||
int oldIndex = *selectedIndex;
|
|
||||||
|
|
||||||
TabBarSpec spec;
|
|
||||||
spec.fullWidth = true;
|
|
||||||
|
|
||||||
if (BeginTabBar(id, selectedIndex, spec)) {
|
|
||||||
// Calculate tab width for full-width mode
|
|
||||||
float tabWidth = ImGui::GetContentRegionAvail().x / count;
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
TabSpec tabSpec;
|
|
||||||
tabSpec.label = labels[i];
|
|
||||||
Tab(tabSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (*selectedIndex != oldIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../colors.h"
|
|
||||||
#include "../typography.h"
|
|
||||||
#include "../layout.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Text Field Component
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/components/text-fields
|
|
||||||
//
|
|
||||||
// Two variants:
|
|
||||||
// - Filled: Background fill with bottom line indicator
|
|
||||||
// - Outlined: Border around entire field
|
|
||||||
|
|
||||||
enum class TextFieldStyle {
|
|
||||||
Filled, // Background fill
|
|
||||||
Outlined // Border only
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Text field configuration
|
|
||||||
*/
|
|
||||||
struct TextFieldSpec {
|
|
||||||
TextFieldStyle style = TextFieldStyle::Outlined;
|
|
||||||
const char* label = nullptr; // Floating label text
|
|
||||||
const char* hint = nullptr; // Placeholder when empty
|
|
||||||
const char* helperText = nullptr; // Helper text below field
|
|
||||||
const char* errorText = nullptr; // Error message (shows in error state)
|
|
||||||
const char* prefix = nullptr; // Prefix text (e.g., "$")
|
|
||||||
const char* suffix = nullptr; // Suffix text (e.g., "DRGX")
|
|
||||||
bool password = false; // Mask input
|
|
||||||
bool readOnly = false; // Read-only field
|
|
||||||
bool multiline = false; // Multi-line text area
|
|
||||||
int multilineRows = 3; // Number of rows for multiline
|
|
||||||
float width = 0; // Width (0 = full available)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render a Material Design text field
|
|
||||||
*
|
|
||||||
* @param id Unique identifier
|
|
||||||
* @param buf Text buffer
|
|
||||||
* @param bufSize Buffer size
|
|
||||||
* @param spec Field configuration
|
|
||||||
* @return true if value changed
|
|
||||||
*/
|
|
||||||
bool TextField(const char* id, char* buf, size_t bufSize, const TextFieldSpec& spec = TextFieldSpec());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render a simple text field with label
|
|
||||||
*/
|
|
||||||
inline bool TextField(const char* label, char* buf, size_t bufSize) {
|
|
||||||
TextFieldSpec spec;
|
|
||||||
spec.label = label;
|
|
||||||
return TextField(label, buf, bufSize, spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline bool TextField(const char* id, char* buf, size_t bufSize, const TextFieldSpec& spec) {
|
|
||||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
||||||
if (window->SkipItems)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushID(id);
|
|
||||||
|
|
||||||
bool hasError = (spec.errorText != nullptr);
|
|
||||||
bool hasValue = (buf[0] != '\0');
|
|
||||||
|
|
||||||
// Calculate dimensions
|
|
||||||
float fieldWidth = spec.width > 0 ? spec.width : ImGui::GetContentRegionAvail().x;
|
|
||||||
float fieldHeight = spec.multiline ?
|
|
||||||
(size::TextFieldHeight + (spec.multilineRows - 1) * Typography::instance().getFont(TypeStyle::Body1)->FontSize * 1.5f) :
|
|
||||||
size::TextFieldHeight;
|
|
||||||
|
|
||||||
ImVec2 pos = window->DC.CursorPos;
|
|
||||||
ImRect bb(pos, ImVec2(pos.x + fieldWidth, pos.y + fieldHeight));
|
|
||||||
|
|
||||||
// Interaction
|
|
||||||
ImGuiID inputId = window->GetID("##input");
|
|
||||||
bool focused = (ImGui::GetFocusID() == inputId);
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
ImU32 bgColor, borderColor, labelColor;
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
borderColor = Error();
|
|
||||||
labelColor = Error();
|
|
||||||
} else if (focused) {
|
|
||||||
borderColor = Primary();
|
|
||||||
labelColor = Primary();
|
|
||||||
} else {
|
|
||||||
borderColor = OnSurfaceMedium();
|
|
||||||
labelColor = OnSurfaceMedium();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec.style == TextFieldStyle::Filled) {
|
|
||||||
bgColor = GetElevatedSurface(GetCurrentColorTheme(), 1);
|
|
||||||
} else {
|
|
||||||
bgColor = 0; // Transparent for outlined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw background/border
|
|
||||||
ImDrawList* drawList = window->DrawList;
|
|
||||||
|
|
||||||
if (spec.style == TextFieldStyle::Filled) {
|
|
||||||
// Filled style: background with bottom line
|
|
||||||
drawList->AddRectFilled(bb.Min, bb.Max, bgColor,
|
|
||||||
size::TextFieldCornerRadius, ImDrawFlags_RoundCornersTop);
|
|
||||||
|
|
||||||
// Bottom indicator line
|
|
||||||
float lineThickness = focused ? 2.0f : 1.0f;
|
|
||||||
drawList->AddLine(
|
|
||||||
ImVec2(bb.Min.x, bb.Max.y - lineThickness),
|
|
||||||
ImVec2(bb.Max.x, bb.Max.y - lineThickness),
|
|
||||||
borderColor, lineThickness
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Outlined style: border around entire field
|
|
||||||
float lineThickness = focused ? 2.0f : 1.0f;
|
|
||||||
drawList->AddRect(bb.Min, bb.Max, borderColor,
|
|
||||||
size::TextFieldCornerRadius, 0, lineThickness);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label (floating or inline)
|
|
||||||
bool labelFloating = focused || hasValue;
|
|
||||||
if (spec.label) {
|
|
||||||
ImVec2 labelPos;
|
|
||||||
TypeStyle labelStyle;
|
|
||||||
|
|
||||||
if (labelFloating) {
|
|
||||||
// Floating label (smaller, at top)
|
|
||||||
labelPos.x = bb.Min.x + size::TextFieldPadding;
|
|
||||||
labelPos.y = bb.Min.y + 4.0f;
|
|
||||||
labelStyle = TypeStyle::Caption;
|
|
||||||
} else {
|
|
||||||
// Inline label (body size, centered)
|
|
||||||
labelPos.x = bb.Min.x + size::TextFieldPadding;
|
|
||||||
labelPos.y = bb.Min.y + (fieldHeight - Typography::instance().getFont(TypeStyle::Body1)->FontSize) * 0.5f;
|
|
||||||
labelStyle = TypeStyle::Body1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For outlined style, need to clear background behind floating label
|
|
||||||
if (spec.style == TextFieldStyle::Outlined && labelFloating) {
|
|
||||||
ImVec2 labelSize = ImGui::CalcTextSize(spec.label);
|
|
||||||
ImVec2 clearMin(labelPos.x - 4.0f, bb.Min.y - 1.0f);
|
|
||||||
ImVec2 clearMax(labelPos.x + labelSize.x + 4.0f, bb.Min.y + Typography::instance().getFont(TypeStyle::Caption)->FontSize);
|
|
||||||
drawList->AddRectFilled(clearMin, clearMax, Background());
|
|
||||||
}
|
|
||||||
|
|
||||||
Typography::instance().pushFont(labelStyle);
|
|
||||||
drawList->AddText(labelPos, labelColor, spec.label);
|
|
||||||
Typography::instance().popFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input field
|
|
||||||
float inputY = spec.label && labelFloating ? bb.Min.y + 20.0f : bb.Min.y + 12.0f;
|
|
||||||
float inputHeight = bb.Max.y - inputY - 8.0f;
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x + size::TextFieldPadding, inputY));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0, 0, 0, 0));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
|
|
||||||
|
|
||||||
ImGuiInputTextFlags flags = 0;
|
|
||||||
if (spec.password) flags |= ImGuiInputTextFlags_Password;
|
|
||||||
if (spec.readOnly) flags |= ImGuiInputTextFlags_ReadOnly;
|
|
||||||
|
|
||||||
float inputWidth = fieldWidth - size::TextFieldPadding * 2;
|
|
||||||
if (spec.prefix) {
|
|
||||||
ImGui::TextUnformatted(spec.prefix);
|
|
||||||
ImGui::SameLine();
|
|
||||||
inputWidth -= ImGui::CalcTextSize(spec.prefix).x + 4.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PushItemWidth(inputWidth);
|
|
||||||
bool changed;
|
|
||||||
if (spec.multiline) {
|
|
||||||
changed = ImGui::InputTextMultiline("##input", buf, bufSize,
|
|
||||||
ImVec2(inputWidth, inputHeight), flags);
|
|
||||||
} else {
|
|
||||||
changed = ImGui::InputText("##input", buf, bufSize, flags);
|
|
||||||
}
|
|
||||||
ImGui::PopItemWidth();
|
|
||||||
|
|
||||||
if (spec.suffix) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", spec.suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
|
|
||||||
// Helper/Error text below field
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x, bb.Max.y + 4.0f));
|
|
||||||
if (spec.errorText) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, Error(), spec.errorText);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x, bb.Max.y + 4.0f + Typography::instance().getFont(TypeStyle::Caption)->FontSize + 4.0f));
|
|
||||||
} else if (spec.helperText) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), spec.helperText);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x, bb.Max.y + 4.0f + Typography::instance().getFont(TypeStyle::Caption)->FontSize + 4.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance cursor
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(pos.x, bb.Max.y + (spec.errorText || spec.helperText ? 24.0f : 8.0f)));
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -916,6 +916,12 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
|
|||||||
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
|
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
|
||||||
// Similar to the shutdown screen pattern but for interactive dialogs.
|
// Similar to the shutdown screen pattern but for interactive dialogs.
|
||||||
|
|
||||||
|
// Per-dialog content height (keyed by the child's id) measured at the end of each frame, so the next
|
||||||
|
// frame can size the glass card to its content instead of a fixed viewport band. g_overlayCurrentKey
|
||||||
|
// carries the active dialog's key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
|
||||||
|
inline std::unordered_map<std::string, float> g_overlayCardHeights;
|
||||||
|
inline std::string g_overlayCurrentKey;
|
||||||
|
|
||||||
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
|
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
|
||||||
float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr)
|
float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr)
|
||||||
{
|
{
|
||||||
@@ -925,6 +931,14 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
ImVec2 vp_pos = vp->Pos;
|
ImVec2 vp_pos = vp->Pos;
|
||||||
ImVec2 vp_size = vp->Size;
|
ImVec2 vp_size = vp->Size;
|
||||||
|
|
||||||
|
// Dialog widths are authored as raw pixels, but the fonts/spacing inside scale with
|
||||||
|
// Layout::dpiScale() (which includes the user's font-size setting). Scale the card by the same
|
||||||
|
// factor so the content doesn't outgrow a fixed card and overflow/misalign at non-default
|
||||||
|
// scales. No-op at the default scale (dpiScale() == 1). Clamped to the viewport so a large scale
|
||||||
|
// can't push the card off-screen.
|
||||||
|
cardWidth *= Layout::dpiScale();
|
||||||
|
cardWidth = std::min(cardWidth, vp_size.x - 32.0f);
|
||||||
|
|
||||||
// Fullscreen scrim overlay
|
// Fullscreen scrim overlay
|
||||||
ImGui::SetNextWindowPos(vp_pos);
|
ImGui::SetNextWindowPos(vp_pos);
|
||||||
ImGui::SetNextWindowSize(vp_size);
|
ImGui::SetNextWindowSize(vp_size);
|
||||||
@@ -970,9 +984,21 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
||||||
float cardY = vp_pos.y + vp_size.y * 0.15f;
|
float cardY = vp_pos.y + vp_size.y * 0.15f;
|
||||||
|
|
||||||
|
// Size the card height to its content. The content child below is AutoResizeY, so a glass card
|
||||||
|
// drawn to a fixed viewport ratio left a tall band of empty glass under short dialogs. Reuse the
|
||||||
|
// height the child reported LAST frame (content is stable frame-to-frame, so no visible lag) and
|
||||||
|
// fall back to the ratio on the first frame. Still capped at the ratio so a very tall dialog can't
|
||||||
|
// run off-screen (its content spills/scrolls as before).
|
||||||
|
g_overlayCurrentKey = childId;
|
||||||
|
float ratioMaxY = vp_pos.y + vp_size.y * cardBottomViewportRatio;
|
||||||
|
auto prevHeightIt = g_overlayCardHeights.find(childId);
|
||||||
|
float cardBottomY = (prevHeightIt != g_overlayCardHeights.end() && prevHeightIt->second > 0.0f)
|
||||||
|
? std::min(cardY + prevHeightIt->second, ratioMaxY)
|
||||||
|
: ratioMaxY;
|
||||||
|
|
||||||
// Draw glass card background
|
// Draw glass card background
|
||||||
ImVec2 cardMin(cardX, cardY);
|
ImVec2 cardMin(cardX, cardY);
|
||||||
ImVec2 cardMax(cardX + cardWidth, vp_pos.y + vp_size.y * cardBottomViewportRatio);
|
ImVec2 cardMax(cardX + cardWidth, cardBottomY);
|
||||||
|
|
||||||
// Card background with glass effect
|
// Card background with glass effect
|
||||||
GlassPanelSpec cardGlass;
|
GlassPanelSpec cardGlass;
|
||||||
@@ -982,7 +1008,10 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
cardGlass.borderWidth = 1.0f;
|
cardGlass.borderWidth = 1.0f;
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||||
|
|
||||||
if (p_open && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
// Click outside the card dismisses the dialog — but NOT on the frame it first appears, otherwise
|
||||||
|
// the very click that opened it (a button fired the same frame) is read as an outside-click and
|
||||||
|
// the dialog flashes open then instantly closes.
|
||||||
|
if (p_open && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||||
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
|
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
|
||||||
*p_open = false;
|
*p_open = false;
|
||||||
}
|
}
|
||||||
@@ -1009,6 +1038,11 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
inline void EndOverlayDialog()
|
inline void EndOverlayDialog()
|
||||||
{
|
{
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
// Remember the rendered card height (the child is the last item) so the next frame's
|
||||||
|
// BeginOverlayDialog can size the glass to the content — kills the empty band under short dialogs.
|
||||||
|
if (!g_overlayCurrentKey.empty()) {
|
||||||
|
g_overlayCardHeights[g_overlayCurrentKey] = ImGui::GetItemRectSize().y;
|
||||||
|
}
|
||||||
ImGui::PopStyleColor(); // ChildBg
|
ImGui::PopStyleColor(); // ChildBg
|
||||||
ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child)
|
ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child)
|
||||||
|
|
||||||
|
|||||||
@@ -1,345 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "colors.h"
|
|
||||||
#include "../effects/low_spec.h"
|
|
||||||
#include "../schema/ui_schema.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Elevation and Shadow System
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/design/environment/elevation.html
|
|
||||||
//
|
|
||||||
// Material Design uses two light sources to create shadows:
|
|
||||||
// - Key light: Creates sharper, directional shadows
|
|
||||||
// - Ambient light: Creates softer, omnidirectional shadows
|
|
||||||
//
|
|
||||||
// In dark themes, elevation is primarily shown through surface color overlays
|
|
||||||
// rather than shadows. However, shadows can still enhance depth perception.
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Shadow Specifications
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Individual shadow layer specification
|
|
||||||
*
|
|
||||||
* Material shadows are composed of multiple layers with different
|
|
||||||
* blur radii and offsets to simulate real-world lighting.
|
|
||||||
*/
|
|
||||||
struct ShadowLayer {
|
|
||||||
float offsetX; // Horizontal offset (typically 0)
|
|
||||||
float offsetY; // Vertical offset (key light from above)
|
|
||||||
float blurRadius; // Blur spread
|
|
||||||
float spreadRadius; // Size adjustment
|
|
||||||
float opacity; // Alpha 0.0-1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Complete shadow specification for an elevation level
|
|
||||||
*/
|
|
||||||
struct ShadowSpec {
|
|
||||||
ShadowLayer umbra; // Darkest part, sharp edge
|
|
||||||
ShadowLayer penumbra; // Mid-tone, softer
|
|
||||||
ShadowLayer ambient; // Lightest, most diffuse
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get shadow specification for elevation level
|
|
||||||
*
|
|
||||||
* @param elevationDp Elevation in dp (0, 1, 2, 3, 4, 6, 8, 12, 16, 24)
|
|
||||||
* @return ShadowSpec for the elevation
|
|
||||||
*/
|
|
||||||
ShadowSpec GetShadowSpec(int elevationDp);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Shadow Rendering
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Draw Material Design shadow for a rectangle
|
|
||||||
*
|
|
||||||
* Uses multi-layer soft shadow rendering to approximate Material shadows.
|
|
||||||
*
|
|
||||||
* @param drawList ImGui draw list
|
|
||||||
* @param rect Rectangle bounds
|
|
||||||
* @param elevationDp Elevation in dp
|
|
||||||
* @param cornerRadius Corner radius for rounded rectangles
|
|
||||||
*/
|
|
||||||
void DrawShadow(ImDrawList* drawList, const ImRect& rect, int elevationDp, float cornerRadius = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Draw shadow with position/size parameters
|
|
||||||
*/
|
|
||||||
void DrawShadow(ImDrawList* drawList, const ImVec2& pos, const ImVec2& size,
|
|
||||||
int elevationDp, float cornerRadius = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Draw soft shadow (single layer, for custom effects)
|
|
||||||
*
|
|
||||||
* @param drawList ImGui draw list
|
|
||||||
* @param rect Rectangle bounds
|
|
||||||
* @param color Shadow color with alpha
|
|
||||||
* @param blurRadius Blur amount
|
|
||||||
* @param offset Shadow offset
|
|
||||||
* @param cornerRadius Corner radius
|
|
||||||
*/
|
|
||||||
void DrawSoftShadow(ImDrawList* drawList, const ImRect& rect, ImU32 color,
|
|
||||||
float blurRadius, const ImVec2& offset = ImVec2(0, 0),
|
|
||||||
float cornerRadius = 0);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Elevation Transition Helper
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Animated elevation value
|
|
||||||
*
|
|
||||||
* Use this to smoothly transition between elevation levels (e.g., card hover)
|
|
||||||
*/
|
|
||||||
class ElevationAnimator {
|
|
||||||
public:
|
|
||||||
ElevationAnimator(int initialElevation = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set target elevation (will animate towards it)
|
|
||||||
*/
|
|
||||||
void setTarget(int targetElevation);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update animation (call each frame)
|
|
||||||
* @param deltaTime Frame delta time
|
|
||||||
*/
|
|
||||||
void update(float deltaTime);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current animated elevation value
|
|
||||||
*/
|
|
||||||
float getCurrent() const { return m_current; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current elevation as integer (for shadow lookup)
|
|
||||||
*/
|
|
||||||
int getCurrentInt() const { return static_cast<int>(m_current + 0.5f); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if currently animating
|
|
||||||
*/
|
|
||||||
bool isAnimating() const { return m_current != m_target; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
float m_current;
|
|
||||||
float m_target;
|
|
||||||
float m_animationSpeed = 16.0f; // dp per second
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline ShadowSpec GetShadowSpec(int elevationDp) {
|
|
||||||
// Material Design shadow values adapted from the spec
|
|
||||||
// These approximate the CSS box-shadow values from material.io
|
|
||||||
|
|
||||||
switch (elevationDp) {
|
|
||||||
case 0:
|
|
||||||
return {
|
|
||||||
{0, 0, 0, 0, 0}, // No shadow
|
|
||||||
{0, 0, 0, 0, 0},
|
|
||||||
{0, 0, 0, 0, 0}
|
|
||||||
};
|
|
||||||
case 1:
|
|
||||||
return {
|
|
||||||
{0, 2, 1, -1, 0.2f}, // Umbra
|
|
||||||
{0, 1, 1, 0, 0.14f}, // Penumbra
|
|
||||||
{0, 1, 3, 0, 0.12f} // Ambient
|
|
||||||
};
|
|
||||||
case 2:
|
|
||||||
return {
|
|
||||||
{0, 3, 1, -2, 0.2f},
|
|
||||||
{0, 2, 2, 0, 0.14f},
|
|
||||||
{0, 1, 5, 0, 0.12f}
|
|
||||||
};
|
|
||||||
case 3:
|
|
||||||
return {
|
|
||||||
{0, 3, 3, -2, 0.2f},
|
|
||||||
{0, 3, 4, 0, 0.14f},
|
|
||||||
{0, 1, 8, 0, 0.12f}
|
|
||||||
};
|
|
||||||
case 4:
|
|
||||||
return {
|
|
||||||
{0, 2, 4, -1, 0.2f},
|
|
||||||
{0, 4, 5, 0, 0.14f},
|
|
||||||
{0, 1, 10, 0, 0.12f}
|
|
||||||
};
|
|
||||||
case 6:
|
|
||||||
return {
|
|
||||||
{0, 3, 5, -1, 0.2f},
|
|
||||||
{0, 6, 10, 0, 0.14f},
|
|
||||||
{0, 1, 18, 0, 0.12f}
|
|
||||||
};
|
|
||||||
case 8:
|
|
||||||
return {
|
|
||||||
{0, 5, 5, -3, 0.2f},
|
|
||||||
{0, 8, 10, 1, 0.14f},
|
|
||||||
{0, 3, 14, 2, 0.12f}
|
|
||||||
};
|
|
||||||
case 12:
|
|
||||||
return {
|
|
||||||
{0, 7, 8, -4, 0.2f},
|
|
||||||
{0, 12, 17, 2, 0.14f},
|
|
||||||
{0, 5, 22, 4, 0.12f}
|
|
||||||
};
|
|
||||||
case 16:
|
|
||||||
return {
|
|
||||||
{0, 8, 10, -5, 0.2f},
|
|
||||||
{0, 16, 24, 2, 0.14f},
|
|
||||||
{0, 6, 30, 5, 0.12f}
|
|
||||||
};
|
|
||||||
case 24:
|
|
||||||
return {
|
|
||||||
{0, 11, 15, -7, 0.2f},
|
|
||||||
{0, 24, 38, 3, 0.14f},
|
|
||||||
{0, 9, 46, 8, 0.12f}
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
// Interpolate for non-standard elevations
|
|
||||||
if (elevationDp < 0) return GetShadowSpec(0);
|
|
||||||
if (elevationDp > 24) return GetShadowSpec(24);
|
|
||||||
|
|
||||||
// Find nearest standard elevation
|
|
||||||
int lower = 0, upper = 1;
|
|
||||||
int standards[] = {0, 1, 2, 3, 4, 6, 8, 12, 16, 24};
|
|
||||||
for (int i = 0; i < 9; i++) {
|
|
||||||
if (standards[i] <= elevationDp && standards[i + 1] >= elevationDp) {
|
|
||||||
lower = standards[i];
|
|
||||||
upper = standards[i + 1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use nearest
|
|
||||||
return GetShadowSpec((elevationDp - lower < upper - elevationDp) ? lower : upper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void DrawSoftShadow(ImDrawList* drawList, const ImRect& rect, ImU32 color,
|
|
||||||
float blurRadius, const ImVec2& offset, float cornerRadius) {
|
|
||||||
if (blurRadius <= 0 || (color & IM_COL32_A_MASK) == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// For ImGui, we'll simulate soft shadows using multiple semi-transparent layers
|
|
||||||
// This is a performance-friendly approximation
|
|
||||||
|
|
||||||
// In low-spec mode use only 1 layer instead of up to 8
|
|
||||||
const int numLayers = dragonx::ui::effects::isLowSpecMode()
|
|
||||||
? 1
|
|
||||||
: ImClamp((int)(blurRadius / 2), 2, 8);
|
|
||||||
const float layerStep = blurRadius / numLayers;
|
|
||||||
|
|
||||||
// Extract base alpha
|
|
||||||
float baseAlpha = ((color >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
ImU32 baseColor = color & ~IM_COL32_A_MASK;
|
|
||||||
|
|
||||||
for (int i = numLayers - 1; i >= 0; i--) {
|
|
||||||
float expansion = layerStep * (i + 1);
|
|
||||||
float alpha = baseAlpha * (1.0f - (float)i / numLayers) / numLayers;
|
|
||||||
|
|
||||||
ImU32 layerColor = baseColor | (((ImU32)(alpha * 255)) << IM_COL32_A_SHIFT);
|
|
||||||
|
|
||||||
ImRect expandedRect(
|
|
||||||
rect.Min.x - expansion + offset.x,
|
|
||||||
rect.Min.y - expansion + offset.y,
|
|
||||||
rect.Max.x + expansion + offset.x,
|
|
||||||
rect.Max.y + expansion + offset.y
|
|
||||||
);
|
|
||||||
|
|
||||||
drawList->AddRectFilled(expandedRect.Min, expandedRect.Max, layerColor,
|
|
||||||
cornerRadius + expansion * 0.5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void DrawShadow(ImDrawList* drawList, const ImRect& rect, int elevationDp, float cornerRadius) {
|
|
||||||
if (elevationDp <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ShadowSpec spec = GetShadowSpec(elevationDp);
|
|
||||||
|
|
||||||
// Shadow multiplier: light themes need stronger shadows for card depth,
|
|
||||||
// dark themes rely more on surface color overlay for elevation.
|
|
||||||
// Configurable via ui.toml [style] shadow-multiplier / shadow-multiplier-light.
|
|
||||||
const float shadowMultiplier = schema::UI().isDarkTheme()
|
|
||||||
? schema::UI().drawElement("style", "shadow-multiplier").sizeOr(0.6f)
|
|
||||||
: schema::UI().drawElement("style", "shadow-multiplier-light").sizeOr(1.0f);
|
|
||||||
|
|
||||||
// Draw ambient shadow (largest, most diffuse)
|
|
||||||
if (spec.ambient.opacity > 0) {
|
|
||||||
ImU32 ambientColor = IM_COL32(0, 0, 0, (int)(spec.ambient.opacity * shadowMultiplier * 255));
|
|
||||||
ImRect ambientRect = rect;
|
|
||||||
ambientRect.Expand(spec.ambient.spreadRadius);
|
|
||||||
DrawSoftShadow(drawList, ambientRect, ambientColor, spec.ambient.blurRadius,
|
|
||||||
ImVec2(spec.ambient.offsetX, spec.ambient.offsetY), cornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw penumbra (medium)
|
|
||||||
if (spec.penumbra.opacity > 0) {
|
|
||||||
ImU32 penumbraColor = IM_COL32(0, 0, 0, (int)(spec.penumbra.opacity * shadowMultiplier * 255));
|
|
||||||
ImRect penumbraRect = rect;
|
|
||||||
penumbraRect.Expand(spec.penumbra.spreadRadius);
|
|
||||||
DrawSoftShadow(drawList, penumbraRect, penumbraColor, spec.penumbra.blurRadius,
|
|
||||||
ImVec2(spec.penumbra.offsetX, spec.penumbra.offsetY), cornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw umbra (sharpest, darkest)
|
|
||||||
if (spec.umbra.opacity > 0) {
|
|
||||||
ImU32 umbraColor = IM_COL32(0, 0, 0, (int)(spec.umbra.opacity * shadowMultiplier * 255));
|
|
||||||
ImRect umbraRect = rect;
|
|
||||||
umbraRect.Expand(spec.umbra.spreadRadius);
|
|
||||||
DrawSoftShadow(drawList, umbraRect, umbraColor, spec.umbra.blurRadius,
|
|
||||||
ImVec2(spec.umbra.offsetX, spec.umbra.offsetY), cornerRadius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void DrawShadow(ImDrawList* drawList, const ImVec2& pos, const ImVec2& size,
|
|
||||||
int elevationDp, float cornerRadius) {
|
|
||||||
ImRect rect(pos, ImVec2(pos.x + size.x, pos.y + size.y));
|
|
||||||
DrawShadow(drawList, rect, elevationDp, cornerRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline ElevationAnimator::ElevationAnimator(int initialElevation)
|
|
||||||
: m_current(static_cast<float>(initialElevation))
|
|
||||||
, m_target(static_cast<float>(initialElevation))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ElevationAnimator::setTarget(int targetElevation) {
|
|
||||||
m_target = static_cast<float>(targetElevation);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ElevationAnimator::update(float deltaTime) {
|
|
||||||
if (m_current == m_target)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float diff = m_target - m_current;
|
|
||||||
float change = m_animationSpeed * deltaTime;
|
|
||||||
|
|
||||||
if (std::abs(diff) <= change) {
|
|
||||||
m_current = m_target;
|
|
||||||
} else {
|
|
||||||
m_current += (diff > 0 ? 1 : -1) * change;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
//
|
|
||||||
// GPU alpha mask — the ImGui equivalent of CSS mask-image: linear-gradient().
|
|
||||||
// Uses AddCallback to switch the GPU blend mode so that gradient quads
|
|
||||||
// multiply the framebuffer's alpha (and RGB) by the source alpha, producing
|
|
||||||
// a smooth per-pixel fade without vertex-spacing artefacts.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "imgui.h"
|
|
||||||
|
|
||||||
#ifdef DRAGONX_USE_DX11
|
|
||||||
#include <d3d11.h>
|
|
||||||
#else
|
|
||||||
#ifdef DRAGONX_HAS_GLAD
|
|
||||||
#include <glad/gl.h>
|
|
||||||
#else
|
|
||||||
#include <SDL3/SDL_opengl.h>
|
|
||||||
#endif
|
|
||||||
#include <SDL3/SDL.h> // for SDL_GL_GetProcAddress
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Blend-mode callbacks — called by ImGui's backend during draw list rendering
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#ifdef DRAGONX_USE_DX11
|
|
||||||
|
|
||||||
// Cached DX11 blend state for the mask pass
|
|
||||||
inline ID3D11BlendState* GetMaskBlendState() {
|
|
||||||
static ID3D11BlendState* s_maskBlend = nullptr;
|
|
||||||
if (!s_maskBlend) {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
if (!io.BackendRendererUserData) return nullptr;
|
|
||||||
ID3D11Device* dev = *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
|
|
||||||
if (!dev) return nullptr;
|
|
||||||
|
|
||||||
D3D11_BLEND_DESC desc = {};
|
|
||||||
desc.RenderTarget[0].BlendEnable = TRUE;
|
|
||||||
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ZERO;
|
|
||||||
desc.RenderTarget[0].DestBlend = D3D11_BLEND_SRC_ALPHA;
|
|
||||||
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
|
|
||||||
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ZERO;
|
|
||||||
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_SRC_ALPHA;
|
|
||||||
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
|
|
||||||
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
|
||||||
dev->CreateBlendState(&desc, &s_maskBlend);
|
|
||||||
}
|
|
||||||
return s_maskBlend;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to mask blend: dst *= srcAlpha (both RGB and A)
|
|
||||||
inline void MaskBlendCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
if (!io.BackendRendererUserData) return;
|
|
||||||
// The ImGui DX11 backend stores the device as the first pointer
|
|
||||||
ID3D11Device* dev = *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
|
|
||||||
if (!dev) return;
|
|
||||||
ID3D11DeviceContext* ctx = nullptr;
|
|
||||||
dev->GetImmediateContext(&ctx);
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
ID3D11BlendState* bs = GetMaskBlendState();
|
|
||||||
if (bs) {
|
|
||||||
float blendFactor[4] = {0, 0, 0, 0};
|
|
||||||
ctx->OMSetBlendState(bs, blendFactor, 0xFFFFFFFF);
|
|
||||||
}
|
|
||||||
ctx->Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore normal ImGui blend: src*srcA + dst*(1-srcA)
|
|
||||||
inline void RestoreBlendCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
if (!io.BackendRendererUserData) return;
|
|
||||||
ID3D11Device* dev = *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
|
|
||||||
if (!dev) return;
|
|
||||||
ID3D11DeviceContext* ctx = nullptr;
|
|
||||||
dev->GetImmediateContext(&ctx);
|
|
||||||
if (!ctx) return;
|
|
||||||
// Setting nullptr restores the default blend state that ImGui's DX11
|
|
||||||
// backend configures at the start of each frame.
|
|
||||||
ctx->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
|
|
||||||
ctx->Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
#else // OpenGL
|
|
||||||
|
|
||||||
// glBlendFuncSeparate may not be in the GLAD profile — load it once via SDL.
|
|
||||||
typedef void (*PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum);
|
|
||||||
inline PFN_glBlendFuncSeparate GetBlendFuncSeparate() {
|
|
||||||
static PFN_glBlendFuncSeparate fn = nullptr;
|
|
||||||
static bool resolved = false;
|
|
||||||
if (!resolved) {
|
|
||||||
resolved = true;
|
|
||||||
fn = (PFN_glBlendFuncSeparate)(void*)SDL_GL_GetProcAddress("glBlendFuncSeparate");
|
|
||||||
}
|
|
||||||
return fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MaskBlendCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
// dst.rgb = dst.rgb * srcAlpha (erase content where mask alpha < 1)
|
|
||||||
// dst.a = dst.a * srcAlpha (match alpha channel)
|
|
||||||
auto fn = GetBlendFuncSeparate();
|
|
||||||
if (fn)
|
|
||||||
fn(GL_ZERO, GL_SRC_ALPHA, GL_ZERO, GL_SRC_ALPHA);
|
|
||||||
else
|
|
||||||
glBlendFunc(GL_ZERO, GL_SRC_ALPHA);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void RestoreBlendCallback(const ImDrawList*, const ImDrawCmd*) {
|
|
||||||
// Restore ImGui's exact blend state:
|
|
||||||
// RGB: src*srcA + dst*(1-srcA)
|
|
||||||
// Alpha: src*1 + dst*(1-srcA)
|
|
||||||
auto fn = GetBlendFuncSeparate();
|
|
||||||
if (fn)
|
|
||||||
fn(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
|
||||||
else
|
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// DrawScrollFadeMask — draw gradient quads that mask the top/bottom edges
|
|
||||||
// of a scrollable child region, producing a smooth per-pixel fade.
|
|
||||||
//
|
|
||||||
// Call this on the child's draw list BEFORE EndChild().
|
|
||||||
// The gradient quads use the mask blend mode to multiply the existing
|
|
||||||
// framebuffer content by their alpha, so alpha=1 means "keep" and
|
|
||||||
// alpha=0 means "erase to transparent/black".
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// dl — the child window's draw list (ImGui::GetWindowDrawList())
|
|
||||||
// clipMin/Max — the child window's visible area (screen coords)
|
|
||||||
// fadeH — the height of the fade zone in pixels
|
|
||||||
// scrollY — current scroll offset (ImGui::GetScrollY())
|
|
||||||
// scrollMaxY — maximum scroll offset (ImGui::GetScrollMaxY())
|
|
||||||
// ============================================================================
|
|
||||||
inline void DrawScrollFadeMask(ImDrawList* dl,
|
|
||||||
const ImVec2& clipMin, const ImVec2& clipMax,
|
|
||||||
float fadeH,
|
|
||||||
float scrollY, float scrollMaxY)
|
|
||||||
{
|
|
||||||
if (fadeH <= 0.0f) return;
|
|
||||||
|
|
||||||
bool needTop = scrollY > 1.0f;
|
|
||||||
bool needBottom = scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f;
|
|
||||||
if (!needTop && !needBottom) return;
|
|
||||||
|
|
||||||
float left = clipMin.x;
|
|
||||||
float right = clipMax.x;
|
|
||||||
|
|
||||||
// Switch to mask blend mode
|
|
||||||
dl->AddCallback(MaskBlendCallback, nullptr);
|
|
||||||
|
|
||||||
if (needTop) {
|
|
||||||
// Top gradient: alpha=0 at top edge (erase) → alpha=1 at top+fadeH (keep)
|
|
||||||
ImVec2 tMin(left, clipMin.y);
|
|
||||||
ImVec2 tMax(right, clipMin.y + fadeH);
|
|
||||||
ImU32 transparent = IM_COL32(0, 0, 0, 0);
|
|
||||||
ImU32 opaque = IM_COL32(0, 0, 0, 255);
|
|
||||||
dl->AddRectFilledMultiColor(tMin, tMax,
|
|
||||||
transparent, transparent, // top-left, top-right
|
|
||||||
opaque, opaque); // bottom-left, bottom-right
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needBottom) {
|
|
||||||
// Bottom gradient: alpha=1 at bottom-fadeH (keep) → alpha=0 at bottom (erase)
|
|
||||||
ImVec2 bMin(left, clipMax.y - fadeH);
|
|
||||||
ImVec2 bMax(right, clipMax.y);
|
|
||||||
ImU32 opaque = IM_COL32(0, 0, 0, 255);
|
|
||||||
ImU32 transparent = IM_COL32(0, 0, 0, 0);
|
|
||||||
dl->AddRectFilledMultiColor(bMin, bMax,
|
|
||||||
opaque, opaque, // top-left, top-right
|
|
||||||
transparent, transparent); // bottom-left, bottom-right
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore normal blend mode
|
|
||||||
dl->AddCallback(RestoreBlendCallback, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design 2 - Complete UI System
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/design/foundation-overview
|
|
||||||
//
|
|
||||||
// This header provides the complete Material Design 2 implementation for
|
|
||||||
// the DragonX Wallet ImGui interface.
|
|
||||||
//
|
|
||||||
// Namespace: dragonx::ui::material
|
|
||||||
|
|
||||||
// Foundation
|
|
||||||
#include "color_theme.h" // ColorTheme struct, theme presets
|
|
||||||
#include "colors.h" // Color accessor functions
|
|
||||||
#include "typography.h" // Typography system, type scale
|
|
||||||
#include "layout.h" // Spacing grid, breakpoints, sizes
|
|
||||||
|
|
||||||
// Effects
|
|
||||||
#include "elevation.h" // Shadow rendering, elevation animation
|
|
||||||
#include "ripple.h" // Touch ripple effect
|
|
||||||
#include "draw_helpers.h" // DrawTextShadow, DrawGlassPanel
|
|
||||||
|
|
||||||
// Motion
|
|
||||||
#include "motion.h" // Easing curves, AnimatedValue, StaggerAnimation
|
|
||||||
#include "transitions.h" // View transitions, FadeTransition, ExpandableSection
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
#include "app_layout.h" // Application layout manager
|
|
||||||
|
|
||||||
// Components
|
|
||||||
#include "components/components.h" // All Material components
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Quick Start Guide
|
|
||||||
// ============================================================================
|
|
||||||
//
|
|
||||||
// 1. INITIALIZATION
|
|
||||||
// In your app startup, initialize the material system:
|
|
||||||
//
|
|
||||||
// ```cpp
|
|
||||||
// using namespace dragonx::ui::material;
|
|
||||||
//
|
|
||||||
// // Initialize color theme (creates global theme)
|
|
||||||
// SetDragonXTheme(); // or SetHushTheme() for HUSH variant
|
|
||||||
//
|
|
||||||
// // Initialize typography (load fonts)
|
|
||||||
// Typography::instance().initialize(io);
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// 2. FRAME SETUP
|
|
||||||
// At the start of each frame:
|
|
||||||
//
|
|
||||||
// ```cpp
|
|
||||||
// // Update ripple animations
|
|
||||||
// UpdateRipples();
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// 3. USING COLORS
|
|
||||||
// Access theme colors with helper functions:
|
|
||||||
//
|
|
||||||
// ```cpp
|
|
||||||
// ImU32 bg = Background(); // App background
|
|
||||||
// ImU32 primary = Primary(); // Brand color
|
|
||||||
// ImU32 cardBg = Surface(Elevation::Dp4); // Elevated surface
|
|
||||||
// ImU32 text = OnSurface(); // Text on surfaces
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// 4. USING TYPOGRAPHY
|
|
||||||
// Render text with the type scale:
|
|
||||||
//
|
|
||||||
// ```cpp
|
|
||||||
// Typography::instance().text(TypeStyle::H6, "Section Title");
|
|
||||||
// Typography::instance().text(TypeStyle::Body1, "Body text here...");
|
|
||||||
// Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "Hint");
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// 5. USING COMPONENTS
|
|
||||||
// Components follow Material Design patterns:
|
|
||||||
//
|
|
||||||
// ```cpp
|
|
||||||
// // Buttons
|
|
||||||
// if (ContainedButton("Send")) { ... }
|
|
||||||
// if (OutlinedButton("Cancel")) { ... }
|
|
||||||
// if (TextButton("Learn More")) { ... }
|
|
||||||
//
|
|
||||||
// // Cards
|
|
||||||
// BeginCard(myCardSpec);
|
|
||||||
// CardHeader("Card Title", "Subtitle");
|
|
||||||
// CardContent("Card body content...");
|
|
||||||
// CardActions();
|
|
||||||
// TextButton("Action 1");
|
|
||||||
// TextButton("Action 2");
|
|
||||||
// CardActionsEnd();
|
|
||||||
// EndCard();
|
|
||||||
//
|
|
||||||
// // Lists
|
|
||||||
// BeginList("myList");
|
|
||||||
// if (ListItem("Item 1")) { ... }
|
|
||||||
// if (ListItem("Item 2", "Secondary text")) { ... }
|
|
||||||
// ListDivider();
|
|
||||||
// if (ListItem("Item 3")) { ... }
|
|
||||||
// EndList();
|
|
||||||
//
|
|
||||||
// // Dialogs
|
|
||||||
// static bool showDialog = false;
|
|
||||||
// if (ContainedButton("Open Dialog")) showDialog = true;
|
|
||||||
// int result = ConfirmDialog("confirm", &showDialog, "Confirm",
|
|
||||||
// "Are you sure?", "Yes", "No");
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// 6. LAYOUT
|
|
||||||
// Use the spacing system for consistent layouts:
|
|
||||||
//
|
|
||||||
// ```cpp
|
|
||||||
// ImGui::Dummy(ImVec2(0, spacing::dp(2))); // 16dp vertical space
|
|
||||||
// ImGui::SetCursorPosX(spacing::dp(3)); // 24dp indent
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// ============================================================================
|
|
||||||
// Module Reference
|
|
||||||
// ============================================================================
|
|
||||||
//
|
|
||||||
// COLORS (colors.h)
|
|
||||||
// Primary(), PrimaryVariant(), PrimaryContainer()
|
|
||||||
// Secondary(), SecondaryVariant()
|
|
||||||
// Background(), Surface(elevation), SurfaceVariant()
|
|
||||||
// OnPrimary(), OnSecondary(), OnBackground(), OnSurface()
|
|
||||||
// OnSurfaceMedium(), OnSurfaceDisabled()
|
|
||||||
// Error(), OnError()
|
|
||||||
// StateHover(), StateFocus(), StatePressed(), StateSelected()
|
|
||||||
//
|
|
||||||
// TYPOGRAPHY (typography.h)
|
|
||||||
// TypeStyle: H1-H6, Subtitle1-2, Body1-2, Button, Caption, Overline
|
|
||||||
// Typography::text(style, text)
|
|
||||||
// Typography::textColored(style, color, text)
|
|
||||||
// Typography::textWrapped(style, text)
|
|
||||||
// Typography::pushFont(style) / popFont()
|
|
||||||
//
|
|
||||||
// LAYOUT (layout.h)
|
|
||||||
// spacing::dp(n) - n * 8dp
|
|
||||||
// spacing::Unit - 8dp
|
|
||||||
// size::TouchTarget - 48dp
|
|
||||||
// size::ButtonHeight - 36dp
|
|
||||||
// breakpoint::current() - Get current breakpoint
|
|
||||||
//
|
|
||||||
// ELEVATION (elevation.h)
|
|
||||||
// DrawShadow(drawList, rect, elevationDp, cornerRadius)
|
|
||||||
// ElevationAnimator - Smooth elevation transitions
|
|
||||||
//
|
|
||||||
// RIPPLE (ripple.h)
|
|
||||||
// DrawRippleEffect(drawList, rect, id, cornerRadius, hovered, held)
|
|
||||||
// UpdateRipples() - Call each frame
|
|
||||||
//
|
|
||||||
// COMPONENTS (components/components.h)
|
|
||||||
// See components.h for full component reference
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <cmath>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Motion System
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/design/motion/speed.html
|
|
||||||
// and https://m2.material.io/design/motion/customization.html
|
|
||||||
//
|
|
||||||
// Material motion uses specific easing curves and durations to create
|
|
||||||
// natural, responsive animations that feel connected to user input.
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Standard Durations (in seconds)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
namespace duration {
|
|
||||||
// Simple transitions (toggle, fade)
|
|
||||||
constexpr float Instant = 0.0f;
|
|
||||||
constexpr float VeryFast = 0.05f; // 50ms
|
|
||||||
constexpr float Fast = 0.1f; // 100ms - simple toggles
|
|
||||||
constexpr float Short = 0.15f; // 150ms
|
|
||||||
|
|
||||||
// Standard transitions
|
|
||||||
constexpr float Medium = 0.2f; // 200ms - collapse, simple move
|
|
||||||
constexpr float Standard = 0.25f; // 250ms - expand, standard
|
|
||||||
constexpr float Long = 0.3f; // 300ms - large transforms
|
|
||||||
|
|
||||||
// Complex transitions
|
|
||||||
constexpr float Complex = 0.375f; // 375ms
|
|
||||||
constexpr float VeryLong = 0.5f; // 500ms - elaborate sequences
|
|
||||||
|
|
||||||
// Screen transitions
|
|
||||||
constexpr float EnterScreen = 0.225f; // Entering screen
|
|
||||||
constexpr float ExitScreen = 0.195f; // Leaving screen
|
|
||||||
constexpr float ScreenChange = 0.3f; // Full screen transition
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Easing Curves
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Cubic bezier curve evaluation
|
|
||||||
*
|
|
||||||
* Evaluates a cubic bezier curve defined by control points (0,0), (x1,y1), (x2,y2), (1,1)
|
|
||||||
*
|
|
||||||
* @param t Progress 0.0-1.0
|
|
||||||
* @param x1 First control point X
|
|
||||||
* @param y1 First control point Y
|
|
||||||
* @param x2 Second control point X
|
|
||||||
* @param y2 Second control point Y
|
|
||||||
* @return Eased value
|
|
||||||
*/
|
|
||||||
float CubicBezier(float t, float x1, float y1, float x2, float y2);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Standard easing - for objects moving between on-screen positions
|
|
||||||
*
|
|
||||||
* CSS: cubic-bezier(0.4, 0.0, 0.2, 1.0)
|
|
||||||
* Starts quickly, slows down to rest
|
|
||||||
*/
|
|
||||||
float EaseStandard(float t);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Deceleration easing - for objects entering the screen
|
|
||||||
*
|
|
||||||
* CSS: cubic-bezier(0.0, 0.0, 0.2, 1.0)
|
|
||||||
* Starts at full velocity, decelerates to rest
|
|
||||||
*/
|
|
||||||
float EaseDecelerate(float t);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Acceleration easing - for objects leaving the screen
|
|
||||||
*
|
|
||||||
* CSS: cubic-bezier(0.4, 0.0, 1.0, 1.0)
|
|
||||||
* Accelerates from rest, exits at full speed
|
|
||||||
*/
|
|
||||||
float EaseAccelerate(float t);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sharp easing - for objects that may return to screen
|
|
||||||
*
|
|
||||||
* CSS: cubic-bezier(0.4, 0.0, 0.6, 1.0)
|
|
||||||
* Quicker than standard, maintains connection
|
|
||||||
*/
|
|
||||||
float EaseSharp(float t);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Linear interpolation (no easing)
|
|
||||||
*/
|
|
||||||
float EaseLinear(float t);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Overshoot easing - goes past target then settles
|
|
||||||
*
|
|
||||||
* Good for bouncy, playful animations
|
|
||||||
*/
|
|
||||||
float EaseOvershoot(float t, float overshoot = 1.70158f);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Elastic easing - springy oscillation
|
|
||||||
*/
|
|
||||||
float EaseElastic(float t);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Easing Function Type
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
using EasingFunction = float(*)(float);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Animated Value
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Animated value with automatic interpolation
|
|
||||||
*
|
|
||||||
* Template class for smooth value transitions.
|
|
||||||
*/
|
|
||||||
template<typename T>
|
|
||||||
class AnimatedValue {
|
|
||||||
public:
|
|
||||||
AnimatedValue(const T& initialValue = T())
|
|
||||||
: m_current(initialValue)
|
|
||||||
, m_target(initialValue)
|
|
||||||
, m_start(initialValue)
|
|
||||||
, m_duration(duration::Standard)
|
|
||||||
, m_elapsed(0)
|
|
||||||
, m_easingFunc(EaseStandard)
|
|
||||||
, m_animating(false)
|
|
||||||
{}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set target value with animation
|
|
||||||
*/
|
|
||||||
void animateTo(const T& target, float dur = duration::Standard,
|
|
||||||
EasingFunction easing = EaseStandard) {
|
|
||||||
if (target == m_target && m_animating)
|
|
||||||
return; // Already animating to this target
|
|
||||||
|
|
||||||
m_start = m_current;
|
|
||||||
m_target = target;
|
|
||||||
m_duration = dur;
|
|
||||||
m_elapsed = 0;
|
|
||||||
m_easingFunc = easing;
|
|
||||||
m_animating = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set value immediately (no animation)
|
|
||||||
*/
|
|
||||||
void set(const T& value) {
|
|
||||||
m_current = value;
|
|
||||||
m_target = value;
|
|
||||||
m_start = value;
|
|
||||||
m_animating = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update animation (call each frame)
|
|
||||||
* @param deltaTime Frame delta time in seconds
|
|
||||||
*/
|
|
||||||
void update(float deltaTime) {
|
|
||||||
if (!m_animating)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_elapsed += deltaTime;
|
|
||||||
|
|
||||||
if (m_elapsed >= m_duration) {
|
|
||||||
m_current = m_target;
|
|
||||||
m_animating = false;
|
|
||||||
} else {
|
|
||||||
float t = m_elapsed / m_duration;
|
|
||||||
float eased = m_easingFunc(t);
|
|
||||||
m_current = lerp(m_start, m_target, eased);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current value
|
|
||||||
*/
|
|
||||||
const T& get() const { return m_current; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get target value
|
|
||||||
*/
|
|
||||||
const T& getTarget() const { return m_target; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if currently animating
|
|
||||||
*/
|
|
||||||
bool isAnimating() const { return m_animating; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get animation progress (0-1)
|
|
||||||
*/
|
|
||||||
float getProgress() const {
|
|
||||||
if (!m_animating) return 1.0f;
|
|
||||||
return m_elapsed / m_duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Implicit conversion to value type
|
|
||||||
*/
|
|
||||||
operator const T&() const { return m_current; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
T m_current;
|
|
||||||
T m_target;
|
|
||||||
T m_start;
|
|
||||||
float m_duration;
|
|
||||||
float m_elapsed;
|
|
||||||
EasingFunction m_easingFunc;
|
|
||||||
bool m_animating;
|
|
||||||
|
|
||||||
// Lerp specializations
|
|
||||||
static T lerp(const T& a, const T& b, float t) {
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Specialization for ImVec2
|
|
||||||
template<>
|
|
||||||
inline ImVec2 AnimatedValue<ImVec2>::lerp(const ImVec2& a, const ImVec2& b, float t) {
|
|
||||||
return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specialization for ImVec4/color
|
|
||||||
template<>
|
|
||||||
inline ImVec4 AnimatedValue<ImVec4>::lerp(const ImVec4& a, const ImVec4& b, float t) {
|
|
||||||
return ImVec4(
|
|
||||||
a.x + (b.x - a.x) * t,
|
|
||||||
a.y + (b.y - a.y) * t,
|
|
||||||
a.z + (b.z - a.z) * t,
|
|
||||||
a.w + (b.w - a.w) * t
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Animation Sequencer
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Staggered animation for lists
|
|
||||||
*
|
|
||||||
* Creates staggered entrance animations for list items.
|
|
||||||
*/
|
|
||||||
class StaggerAnimation {
|
|
||||||
public:
|
|
||||||
StaggerAnimation(int itemCount, float staggerDelay = 0.05f,
|
|
||||||
float itemDuration = duration::EnterScreen)
|
|
||||||
: m_itemCount(itemCount)
|
|
||||||
, m_staggerDelay(staggerDelay)
|
|
||||||
, m_itemDuration(itemDuration)
|
|
||||||
, m_elapsed(0)
|
|
||||||
, m_running(false)
|
|
||||||
{}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Start the stagger animation
|
|
||||||
*/
|
|
||||||
void start() {
|
|
||||||
m_elapsed = 0;
|
|
||||||
m_running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update animation
|
|
||||||
*/
|
|
||||||
void update(float deltaTime) {
|
|
||||||
if (!m_running) return;
|
|
||||||
m_elapsed += deltaTime;
|
|
||||||
|
|
||||||
// Check if all items have finished
|
|
||||||
float totalDuration = m_staggerDelay * (m_itemCount - 1) + m_itemDuration;
|
|
||||||
if (m_elapsed >= totalDuration) {
|
|
||||||
m_running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get animation progress for a specific item
|
|
||||||
*
|
|
||||||
* @param itemIndex Item index (0-based)
|
|
||||||
* @return Progress 0.0-1.0 (clamped)
|
|
||||||
*/
|
|
||||||
float getItemProgress(int itemIndex) const {
|
|
||||||
if (!m_running && m_elapsed > 0)
|
|
||||||
return 1.0f; // Animation complete
|
|
||||||
if (itemIndex < 0 || itemIndex >= m_itemCount)
|
|
||||||
return 0.0f;
|
|
||||||
|
|
||||||
float itemStart = m_staggerDelay * itemIndex;
|
|
||||||
float itemElapsed = m_elapsed - itemStart;
|
|
||||||
|
|
||||||
if (itemElapsed <= 0) return 0.0f;
|
|
||||||
if (itemElapsed >= m_itemDuration) return 1.0f;
|
|
||||||
|
|
||||||
return EaseDecelerate(itemElapsed / m_itemDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get eased alpha for item (for fade-in)
|
|
||||||
*/
|
|
||||||
float getItemAlpha(int itemIndex) const {
|
|
||||||
return getItemProgress(itemIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get Y offset for item (for slide-in from bottom)
|
|
||||||
*/
|
|
||||||
float getItemYOffset(int itemIndex, float maxOffset = 20.0f) const {
|
|
||||||
float progress = getItemProgress(itemIndex);
|
|
||||||
return maxOffset * (1.0f - progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isRunning() const { return m_running; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
int m_itemCount;
|
|
||||||
float m_staggerDelay;
|
|
||||||
float m_itemDuration;
|
|
||||||
float m_elapsed;
|
|
||||||
bool m_running;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Container Transform
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Container transform animation state
|
|
||||||
*
|
|
||||||
* For hero-style transitions where a card expands into a full dialog/page.
|
|
||||||
*/
|
|
||||||
struct ContainerTransform {
|
|
||||||
ImRect startRect; // Starting bounds (e.g., card)
|
|
||||||
ImRect endRect; // Ending bounds (e.g., dialog)
|
|
||||||
float progress; // 0 = start, 1 = end
|
|
||||||
bool expanding; // Direction
|
|
||||||
|
|
||||||
ContainerTransform()
|
|
||||||
: progress(0)
|
|
||||||
, expanding(true)
|
|
||||||
{}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get interpolated bounds at current progress
|
|
||||||
*/
|
|
||||||
ImRect getCurrentRect() const {
|
|
||||||
float t = expanding ? progress : (1.0f - progress);
|
|
||||||
float eased = EaseStandard(t);
|
|
||||||
|
|
||||||
return ImRect(
|
|
||||||
ImLerp(startRect.Min, endRect.Min, eased),
|
|
||||||
ImLerp(startRect.Max, endRect.Max, eased)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get corner radius (shrinks as container expands)
|
|
||||||
*/
|
|
||||||
float getCornerRadius(float startRadius, float endRadius) const {
|
|
||||||
float t = expanding ? progress : (1.0f - progress);
|
|
||||||
float eased = EaseStandard(t);
|
|
||||||
return startRadius + (endRadius - startRadius) * eased;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline float CubicBezier(float t, float x1, float y1, float x2, float y2) {
|
|
||||||
// Attempt to find t value for given x (Newton-Raphson approximation)
|
|
||||||
// This is needed because CSS bezier curves are defined in terms of x
|
|
||||||
|
|
||||||
// For simplicity, we'll use a direct parametric approach
|
|
||||||
// which is accurate enough for UI animations
|
|
||||||
|
|
||||||
float cx = 3.0f * x1;
|
|
||||||
float bx = 3.0f * (x2 - x1) - cx;
|
|
||||||
float ax = 1.0f - cx - bx;
|
|
||||||
|
|
||||||
float cy = 3.0f * y1;
|
|
||||||
float by = 3.0f * (y2 - y1) - cy;
|
|
||||||
float ay = 1.0f - cy - by;
|
|
||||||
|
|
||||||
// Sample y at parameter t
|
|
||||||
// Note: This assumes t directly maps to time, which is an approximation
|
|
||||||
// For more accuracy, we'd need to solve for the bezier parameter given x=t
|
|
||||||
|
|
||||||
float t2 = t * t;
|
|
||||||
float t3 = t2 * t;
|
|
||||||
|
|
||||||
return ay * t3 + by * t2 + cy * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseStandard(float t) {
|
|
||||||
// cubic-bezier(0.4, 0.0, 0.2, 1.0)
|
|
||||||
return CubicBezier(t, 0.4f, 0.0f, 0.2f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseDecelerate(float t) {
|
|
||||||
// cubic-bezier(0.0, 0.0, 0.2, 1.0)
|
|
||||||
return CubicBezier(t, 0.0f, 0.0f, 0.2f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseAccelerate(float t) {
|
|
||||||
// cubic-bezier(0.4, 0.0, 1.0, 1.0)
|
|
||||||
return CubicBezier(t, 0.4f, 0.0f, 1.0f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseSharp(float t) {
|
|
||||||
// cubic-bezier(0.4, 0.0, 0.6, 1.0)
|
|
||||||
return CubicBezier(t, 0.4f, 0.0f, 0.6f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseLinear(float t) {
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseOvershoot(float t, float overshoot) {
|
|
||||||
// Back ease out
|
|
||||||
t = t - 1.0f;
|
|
||||||
return t * t * ((overshoot + 1.0f) * t + overshoot) + 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float EaseElastic(float t) {
|
|
||||||
if (t == 0.0f || t == 1.0f) return t;
|
|
||||||
|
|
||||||
float p = 0.3f;
|
|
||||||
float s = p / 4.0f;
|
|
||||||
|
|
||||||
return std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * IM_PI) / p) + 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "colors.h"
|
|
||||||
#include "../effects/low_spec.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
#include <vector>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Ripple Effect
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/design/motion/customization.html
|
|
||||||
//
|
|
||||||
// Ripple effects provide visual feedback when users touch interactive elements.
|
|
||||||
// The ripple emanates from the touch point and expands outward.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Individual ripple instance
|
|
||||||
*/
|
|
||||||
struct Ripple {
|
|
||||||
ImVec2 center; // Ripple center point
|
|
||||||
float startTime; // When ripple started
|
|
||||||
float maxRadius; // Maximum expansion radius
|
|
||||||
ImU32 color; // Ripple color
|
|
||||||
bool releasing; // True when finger lifted
|
|
||||||
float releaseTime; // When release started
|
|
||||||
|
|
||||||
Ripple(const ImVec2& center, float maxRadius, ImU32 color, float currentTime)
|
|
||||||
: center(center)
|
|
||||||
, startTime(currentTime)
|
|
||||||
, maxRadius(maxRadius)
|
|
||||||
, color(color)
|
|
||||||
, releasing(false)
|
|
||||||
, releaseTime(0)
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Ripple effect manager
|
|
||||||
*
|
|
||||||
* Manages ripple animations for interactive elements.
|
|
||||||
*/
|
|
||||||
class RippleManager {
|
|
||||||
public:
|
|
||||||
static RippleManager& instance();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Start a new ripple
|
|
||||||
*
|
|
||||||
* @param id Widget ID
|
|
||||||
* @param center Ripple center (touch point)
|
|
||||||
* @param maxRadius Maximum ripple radius
|
|
||||||
* @param color Ripple color (typically primary with low alpha)
|
|
||||||
*/
|
|
||||||
void startRipple(ImGuiID id, const ImVec2& center, float maxRadius, ImU32 color);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Release current ripple (finger lifted)
|
|
||||||
*/
|
|
||||||
void releaseRipple(ImGuiID id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Draw ripple effect for a widget
|
|
||||||
*
|
|
||||||
* @param drawList Draw list to use
|
|
||||||
* @param id Widget ID
|
|
||||||
* @param clipRect Clip rectangle for the ripple
|
|
||||||
* @param cornerRadius Corner radius for clip shape
|
|
||||||
*/
|
|
||||||
void drawRipple(ImDrawList* drawList, ImGuiID id, const ImRect& clipRect,
|
|
||||||
float cornerRadius = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update all ripples (call once per frame)
|
|
||||||
*/
|
|
||||||
void update();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Clear all ripples
|
|
||||||
*/
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
private:
|
|
||||||
RippleManager() = default;
|
|
||||||
|
|
||||||
struct RippleEntry {
|
|
||||||
ImGuiID id;
|
|
||||||
Ripple ripple;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<RippleEntry> m_ripples;
|
|
||||||
|
|
||||||
static constexpr float kExpandDuration = 0.3f; // 300ms to full size
|
|
||||||
static constexpr float kFadeDuration = 0.2f; // 200ms fade out
|
|
||||||
static constexpr float kMaxOpacity = 0.12f; // 12% max opacity
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Convenience Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Draw ripple effect for a button/interactive element
|
|
||||||
*
|
|
||||||
* Call this after drawing the element background but before text/content.
|
|
||||||
*
|
|
||||||
* @param drawList Draw list
|
|
||||||
* @param rect Element bounds
|
|
||||||
* @param id Element ID
|
|
||||||
* @param cornerRadius Corner radius
|
|
||||||
* @param hovered Is element hovered
|
|
||||||
* @param held Is element being pressed
|
|
||||||
* @param color Optional ripple color (default: white for dark theme)
|
|
||||||
*/
|
|
||||||
void DrawRippleEffect(ImDrawList* drawList, const ImRect& rect, ImGuiID id,
|
|
||||||
float cornerRadius = 0, bool hovered = false, bool held = false,
|
|
||||||
ImU32 color = IM_COL32(255, 255, 255, 255));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update ripple system (call once per frame in main loop)
|
|
||||||
*/
|
|
||||||
void UpdateRipples();
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline RippleManager& RippleManager::instance() {
|
|
||||||
static RippleManager s_instance;
|
|
||||||
return s_instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void RippleManager::startRipple(ImGuiID id, const ImVec2& center,
|
|
||||||
float maxRadius, ImU32 color) {
|
|
||||||
float currentTime = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
// Check if ripple already exists for this ID
|
|
||||||
for (auto& entry : m_ripples) {
|
|
||||||
if (entry.id == id) {
|
|
||||||
// Reset existing ripple
|
|
||||||
entry.ripple = Ripple(center, maxRadius, color, currentTime);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new ripple
|
|
||||||
m_ripples.push_back({id, Ripple(center, maxRadius, color, currentTime)});
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void RippleManager::releaseRipple(ImGuiID id) {
|
|
||||||
float currentTime = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
for (auto& entry : m_ripples) {
|
|
||||||
if (entry.id == id && !entry.ripple.releasing) {
|
|
||||||
entry.ripple.releasing = true;
|
|
||||||
entry.ripple.releaseTime = currentTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void RippleManager::drawRipple(ImDrawList* drawList, ImGuiID id,
|
|
||||||
const ImRect& clipRect, float cornerRadius) {
|
|
||||||
float currentTime = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
for (const auto& entry : m_ripples) {
|
|
||||||
if (entry.id != id)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const Ripple& ripple = entry.ripple;
|
|
||||||
|
|
||||||
// Calculate expansion progress
|
|
||||||
float expandProgress = (currentTime - ripple.startTime) / kExpandDuration;
|
|
||||||
expandProgress = ImClamp(expandProgress, 0.0f, 1.0f);
|
|
||||||
|
|
||||||
// Deceleration easing for expansion
|
|
||||||
expandProgress = 1.0f - (1.0f - expandProgress) * (1.0f - expandProgress);
|
|
||||||
|
|
||||||
float currentRadius = ripple.maxRadius * expandProgress;
|
|
||||||
|
|
||||||
// Calculate opacity
|
|
||||||
float opacity = kMaxOpacity;
|
|
||||||
|
|
||||||
if (ripple.releasing) {
|
|
||||||
float fadeProgress = (currentTime - ripple.releaseTime) / kFadeDuration;
|
|
||||||
fadeProgress = ImClamp(fadeProgress, 0.0f, 1.0f);
|
|
||||||
opacity *= (1.0f - fadeProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opacity <= 0.001f)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Extract color components
|
|
||||||
float r = ((ripple.color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
float g = ((ripple.color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
float b = ((ripple.color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
ImU32 rippleColor = ImGui::ColorConvertFloat4ToU32(ImVec4(r, g, b, opacity));
|
|
||||||
|
|
||||||
// Push clip rect for rounded corners
|
|
||||||
if (cornerRadius > 0) {
|
|
||||||
// For rounded rectangles, we approximate with the clip rect
|
|
||||||
// A perfect solution would require custom clipping
|
|
||||||
drawList->PushClipRect(clipRect.Min, clipRect.Max, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw ripple circle (skip expanding animation in low-spec mode)
|
|
||||||
if (!dragonx::ui::effects::isLowSpecMode())
|
|
||||||
drawList->AddCircleFilled(ripple.center, currentRadius, rippleColor, 32);
|
|
||||||
|
|
||||||
if (cornerRadius > 0) {
|
|
||||||
drawList->PopClipRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
break; // Only one ripple per ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void RippleManager::update() {
|
|
||||||
float currentTime = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
// Remove completed ripples
|
|
||||||
m_ripples.erase(
|
|
||||||
std::remove_if(m_ripples.begin(), m_ripples.end(),
|
|
||||||
[currentTime](const RippleEntry& entry) {
|
|
||||||
if (!entry.ripple.releasing)
|
|
||||||
return false;
|
|
||||||
float fadeProgress = (currentTime - entry.ripple.releaseTime) / kFadeDuration;
|
|
||||||
return fadeProgress >= 1.0f;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
m_ripples.end()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void RippleManager::clear() {
|
|
||||||
m_ripples.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void DrawRippleEffect(ImDrawList* drawList, const ImRect& rect, ImGuiID id,
|
|
||||||
float cornerRadius, bool hovered, bool held, ImU32 color) {
|
|
||||||
RippleManager& manager = RippleManager::instance();
|
|
||||||
|
|
||||||
// Start ripple on press
|
|
||||||
if (held && ImGui::IsMouseClicked(0)) {
|
|
||||||
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
||||||
// Calculate max radius (distance from click to farthest corner)
|
|
||||||
float dx1 = mousePos.x - rect.Min.x;
|
|
||||||
float dy1 = mousePos.y - rect.Min.y;
|
|
||||||
float dx2 = rect.Max.x - mousePos.x;
|
|
||||||
float dy2 = rect.Max.y - mousePos.y;
|
|
||||||
float maxDx = ImMax(dx1, dx2);
|
|
||||||
float maxDy = ImMax(dy1, dy2);
|
|
||||||
float maxRadius = std::sqrt(maxDx * maxDx + maxDy * maxDy) * 1.2f;
|
|
||||||
|
|
||||||
manager.startRipple(id, mousePos, maxRadius, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release ripple when mouse released
|
|
||||||
if (!held && !ImGui::IsMouseDown(0)) {
|
|
||||||
manager.releaseRipple(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the ripple
|
|
||||||
manager.drawRipple(drawList, id, rect, cornerRadius);
|
|
||||||
|
|
||||||
// Also draw hover state overlay
|
|
||||||
if (hovered && !held) {
|
|
||||||
float r = ((color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
float g = ((color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
float b = ((color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
|
||||||
ImU32 hoverColor = ImGui::ColorConvertFloat4ToU32(ImVec4(r, g, b, 0.04f)); // 4% overlay
|
|
||||||
drawList->AddRectFilled(rect.Min, rect.Max, hoverColor, cornerRadius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void UpdateRipples() {
|
|
||||||
RippleManager::instance().update();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "motion.h"
|
|
||||||
#include "colors.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include "imgui_internal.h"
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace material {
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design Transitions
|
|
||||||
// ============================================================================
|
|
||||||
// Based on https://m2.material.io/design/motion/the-motion-system.html
|
|
||||||
//
|
|
||||||
// Transition patterns for navigating between views and states.
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Transition Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
enum class TransitionType {
|
|
||||||
None, // Instant switch
|
|
||||||
Fade, // Cross-fade
|
|
||||||
SlideLeft, // Slide from right to left (forward navigation)
|
|
||||||
SlideRight, // Slide from left to right (back navigation)
|
|
||||||
SlideUp, // Slide from bottom (modal entry)
|
|
||||||
SlideDown, // Slide to bottom (modal exit)
|
|
||||||
Scale, // Scale up/down
|
|
||||||
SharedAxis, // Material shared axis transition
|
|
||||||
ContainerTransform // Card to full-screen
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// View Transition Manager
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Manages transitions between views/screens
|
|
||||||
*/
|
|
||||||
class TransitionManager {
|
|
||||||
public:
|
|
||||||
static TransitionManager& instance();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Start a transition to a new view
|
|
||||||
*
|
|
||||||
* @param newViewId Identifier for the new view
|
|
||||||
* @param type Transition type
|
|
||||||
* @param duration Transition duration (default from motion.h)
|
|
||||||
*/
|
|
||||||
void transitionTo(const std::string& newViewId,
|
|
||||||
TransitionType type = TransitionType::Fade,
|
|
||||||
float duration = duration::ScreenChange);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Go back with reverse transition
|
|
||||||
*/
|
|
||||||
void goBack(TransitionType type = TransitionType::SlideRight,
|
|
||||||
float duration = duration::ExitScreen);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update transition state (call each frame)
|
|
||||||
*/
|
|
||||||
void update(float deltaTime);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current view ID
|
|
||||||
*/
|
|
||||||
const std::string& getCurrentView() const { return m_currentView; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get previous view ID (during transition)
|
|
||||||
*/
|
|
||||||
const std::string& getPreviousView() const { return m_previousView; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if transition is in progress
|
|
||||||
*/
|
|
||||||
bool isTransitioning() const { return m_transitioning; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get transition progress (0-1)
|
|
||||||
*/
|
|
||||||
float getProgress() const { return m_progress; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current transition type
|
|
||||||
*/
|
|
||||||
TransitionType getType() const { return m_type; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Apply transition effect to outgoing content
|
|
||||||
*
|
|
||||||
* Call before rendering the previous view content.
|
|
||||||
* Returns false if outgoing content should be skipped.
|
|
||||||
*/
|
|
||||||
bool beginOutgoingTransition();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End outgoing content transition
|
|
||||||
*/
|
|
||||||
void endOutgoingTransition();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Apply transition effect to incoming content
|
|
||||||
*
|
|
||||||
* Call before rendering the new view content.
|
|
||||||
* Returns false if incoming content should be skipped.
|
|
||||||
*/
|
|
||||||
bool beginIncomingTransition();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End incoming content transition
|
|
||||||
*/
|
|
||||||
void endIncomingTransition();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get alpha for fading effects
|
|
||||||
*/
|
|
||||||
float getOutgoingAlpha() const;
|
|
||||||
float getIncomingAlpha() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get offset for sliding effects
|
|
||||||
*/
|
|
||||||
ImVec2 getOutgoingOffset() const;
|
|
||||||
ImVec2 getIncomingOffset() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
TransitionManager() = default;
|
|
||||||
|
|
||||||
std::string m_currentView;
|
|
||||||
std::string m_previousView;
|
|
||||||
TransitionType m_type = TransitionType::None;
|
|
||||||
float m_duration = 0;
|
|
||||||
float m_elapsed = 0;
|
|
||||||
float m_progress = 0;
|
|
||||||
bool m_transitioning = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Fade Transition Helper
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Simple fade transition between two states
|
|
||||||
*/
|
|
||||||
class FadeTransition {
|
|
||||||
public:
|
|
||||||
FadeTransition() : m_alpha(1.0f) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Fade out
|
|
||||||
*/
|
|
||||||
void fadeOut(float duration = duration::Fast) {
|
|
||||||
m_alpha.animateTo(0.0f, duration, EaseAccelerate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Fade in
|
|
||||||
*/
|
|
||||||
void fadeIn(float duration = duration::Fast) {
|
|
||||||
m_alpha.animateTo(1.0f, duration, EaseDecelerate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update (call each frame)
|
|
||||||
*/
|
|
||||||
void update(float deltaTime) {
|
|
||||||
m_alpha.update(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get current alpha
|
|
||||||
*/
|
|
||||||
float getAlpha() const { return m_alpha.get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if visible (alpha > 0)
|
|
||||||
*/
|
|
||||||
bool isVisible() const { return m_alpha.get() > 0.001f; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if animating
|
|
||||||
*/
|
|
||||||
bool isAnimating() const { return m_alpha.isAnimating(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Push alpha to ImGui
|
|
||||||
*/
|
|
||||||
void pushAlpha() const {
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_alpha.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Pop alpha from ImGui
|
|
||||||
*/
|
|
||||||
void popAlpha() const {
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
AnimatedValue<float> m_alpha;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Expandable/Collapsible Section
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Animated expandable section
|
|
||||||
*/
|
|
||||||
class ExpandableSection {
|
|
||||||
public:
|
|
||||||
ExpandableSection(bool initiallyExpanded = false)
|
|
||||||
: m_expanded(initiallyExpanded)
|
|
||||||
, m_height(initiallyExpanded ? 1.0f : 0.0f)
|
|
||||||
{}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Toggle expansion state
|
|
||||||
*/
|
|
||||||
void toggle() {
|
|
||||||
setExpanded(!m_expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set expansion state
|
|
||||||
*/
|
|
||||||
void setExpanded(bool expanded) {
|
|
||||||
m_expanded = expanded;
|
|
||||||
m_height.animateTo(expanded ? 1.0f : 0.0f,
|
|
||||||
expanded ? duration::Standard : duration::Medium,
|
|
||||||
EaseStandard);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update animation
|
|
||||||
*/
|
|
||||||
void update(float deltaTime) {
|
|
||||||
m_height.update(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if expanded
|
|
||||||
*/
|
|
||||||
bool isExpanded() const { return m_expanded; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get height multiplier (0-1)
|
|
||||||
*/
|
|
||||||
float getHeightMultiplier() const { return m_height.get(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if animating
|
|
||||||
*/
|
|
||||||
bool isAnimating() const { return m_height.isAnimating(); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Begin expandable content (applies clipping)
|
|
||||||
*
|
|
||||||
* @param maxHeight Maximum content height when fully expanded
|
|
||||||
* @return true if content should be rendered
|
|
||||||
*/
|
|
||||||
bool beginContent(float maxHeight) {
|
|
||||||
float currentHeight = maxHeight * m_height.get();
|
|
||||||
|
|
||||||
if (currentHeight < 1.0f)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::BeginChild("##expandable", ImVec2(0, currentHeight), false,
|
|
||||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief End expandable content
|
|
||||||
*/
|
|
||||||
void endContent() {
|
|
||||||
ImGui::EndChild();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_expanded;
|
|
||||||
AnimatedValue<float> m_height;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline TransitionManager& TransitionManager::instance() {
|
|
||||||
static TransitionManager s_instance;
|
|
||||||
return s_instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransitionManager::transitionTo(const std::string& newViewId,
|
|
||||||
TransitionType type, float duration) {
|
|
||||||
if (m_transitioning)
|
|
||||||
return; // Don't interrupt ongoing transition
|
|
||||||
|
|
||||||
m_previousView = m_currentView;
|
|
||||||
m_currentView = newViewId;
|
|
||||||
m_type = type;
|
|
||||||
m_duration = duration;
|
|
||||||
m_elapsed = 0;
|
|
||||||
m_progress = 0;
|
|
||||||
m_transitioning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransitionManager::goBack(TransitionType type, float duration) {
|
|
||||||
if (m_transitioning || m_previousView.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::string temp = m_currentView;
|
|
||||||
m_currentView = m_previousView;
|
|
||||||
m_previousView = temp;
|
|
||||||
m_type = type;
|
|
||||||
m_duration = duration;
|
|
||||||
m_elapsed = 0;
|
|
||||||
m_progress = 0;
|
|
||||||
m_transitioning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransitionManager::update(float deltaTime) {
|
|
||||||
if (!m_transitioning)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_elapsed += deltaTime;
|
|
||||||
m_progress = m_elapsed / m_duration;
|
|
||||||
|
|
||||||
if (m_progress >= 1.0f) {
|
|
||||||
m_progress = 1.0f;
|
|
||||||
m_transitioning = false;
|
|
||||||
m_previousView.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool TransitionManager::beginOutgoingTransition() {
|
|
||||||
if (!m_transitioning || m_type == TransitionType::None)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
float alpha = getOutgoingAlpha();
|
|
||||||
if (alpha < 0.001f)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
|
|
||||||
|
|
||||||
ImVec2 offset = getOutgoingOffset();
|
|
||||||
if (offset.x != 0 || offset.y != 0) {
|
|
||||||
ImVec2 cursor = ImGui::GetCursorPos();
|
|
||||||
ImGui::SetCursorPos(ImVec2(cursor.x + offset.x, cursor.y + offset.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransitionManager::endOutgoingTransition() {
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool TransitionManager::beginIncomingTransition() {
|
|
||||||
if (!m_transitioning && m_progress >= 1.0f)
|
|
||||||
return true; // No transition, just render normally
|
|
||||||
|
|
||||||
if (m_type == TransitionType::None)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
float alpha = getIncomingAlpha();
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
|
|
||||||
|
|
||||||
ImVec2 offset = getIncomingOffset();
|
|
||||||
if (offset.x != 0 || offset.y != 0) {
|
|
||||||
ImVec2 cursor = ImGui::GetCursorPos();
|
|
||||||
ImGui::SetCursorPos(ImVec2(cursor.x + offset.x, cursor.y + offset.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransitionManager::endIncomingTransition() {
|
|
||||||
if (m_transitioning || m_type != TransitionType::None) {
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float TransitionManager::getOutgoingAlpha() const {
|
|
||||||
float eased = EaseAccelerate(m_progress);
|
|
||||||
|
|
||||||
switch (m_type) {
|
|
||||||
case TransitionType::Fade:
|
|
||||||
return 1.0f - eased;
|
|
||||||
case TransitionType::Scale:
|
|
||||||
return 1.0f - eased;
|
|
||||||
default:
|
|
||||||
// For slides, fade quickly in first half
|
|
||||||
return m_progress < 0.5f ? 1.0f - eased * 2.0f : 0.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float TransitionManager::getIncomingAlpha() const {
|
|
||||||
float eased = EaseDecelerate(m_progress);
|
|
||||||
|
|
||||||
switch (m_type) {
|
|
||||||
case TransitionType::Fade:
|
|
||||||
return eased;
|
|
||||||
case TransitionType::Scale:
|
|
||||||
return eased;
|
|
||||||
default:
|
|
||||||
// For slides, fade in during second half
|
|
||||||
return m_progress > 0.5f ? (eased - 0.5f) * 2.0f : 0.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline ImVec2 TransitionManager::getOutgoingOffset() const {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
float eased = EaseAccelerate(m_progress);
|
|
||||||
float slideDistance = 100.0f; // Pixels to slide
|
|
||||||
|
|
||||||
switch (m_type) {
|
|
||||||
case TransitionType::SlideLeft:
|
|
||||||
return ImVec2(-slideDistance * eased, 0);
|
|
||||||
case TransitionType::SlideRight:
|
|
||||||
return ImVec2(slideDistance * eased, 0);
|
|
||||||
case TransitionType::SlideUp:
|
|
||||||
return ImVec2(0, -slideDistance * eased);
|
|
||||||
case TransitionType::SlideDown:
|
|
||||||
return ImVec2(0, slideDistance * eased);
|
|
||||||
case TransitionType::SharedAxis:
|
|
||||||
return ImVec2(-slideDistance * 0.3f * eased, 0);
|
|
||||||
default:
|
|
||||||
return ImVec2(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline ImVec2 TransitionManager::getIncomingOffset() const {
|
|
||||||
float eased = EaseDecelerate(m_progress);
|
|
||||||
float slideDistance = 100.0f;
|
|
||||||
float remaining = 1.0f - eased;
|
|
||||||
|
|
||||||
switch (m_type) {
|
|
||||||
case TransitionType::SlideLeft:
|
|
||||||
return ImVec2(slideDistance * remaining, 0);
|
|
||||||
case TransitionType::SlideRight:
|
|
||||||
return ImVec2(-slideDistance * remaining, 0);
|
|
||||||
case TransitionType::SlideUp:
|
|
||||||
return ImVec2(0, slideDistance * remaining);
|
|
||||||
case TransitionType::SlideDown:
|
|
||||||
return ImVec2(0, -slideDistance * remaining);
|
|
||||||
case TransitionType::SharedAxis:
|
|
||||||
return ImVec2(slideDistance * 0.3f * remaining, 0);
|
|
||||||
default:
|
|
||||||
return ImVec2(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace material
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
+565
-12
@@ -6,6 +6,10 @@
|
|||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../config/settings.h"
|
#include "../../config/settings.h"
|
||||||
|
#include "../../wallet/lite_wallet_lifecycle_ui_adapter.h"
|
||||||
|
#include "../../wallet/lite_wallet_server_selection_adapter.h"
|
||||||
|
#include "../../wallet/lite_wallet_controller.h"
|
||||||
|
#include <sodium.h>
|
||||||
#include "../../util/logger.h"
|
#include "../../util/logger.h"
|
||||||
#include "../windows/balance_tab.h"
|
#include "../windows/balance_tab.h"
|
||||||
#include "../windows/console_tab.h"
|
#include "../windows/console_tab.h"
|
||||||
@@ -42,6 +46,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
@@ -99,6 +105,33 @@ struct SettingsPageState {
|
|||||||
LowSpecSnapshot low_spec_snapshot;
|
LowSpecSnapshot low_spec_snapshot;
|
||||||
bool keep_daemon_running = false;
|
bool keep_daemon_running = false;
|
||||||
bool stop_external_daemon = false;
|
bool stop_external_daemon = false;
|
||||||
|
bool lite_lifecycle_expanded = false;
|
||||||
|
int lite_lifecycle_operation = 0;
|
||||||
|
char lite_wallet_path[256] = "";
|
||||||
|
char lite_lifecycle_passphrase[128] = "";
|
||||||
|
char lite_restore_seed[512] = "";
|
||||||
|
int lite_restore_birthday = 0;
|
||||||
|
int lite_restore_account = 0;
|
||||||
|
bool lite_restore_overwrite = false;
|
||||||
|
std::string lite_lifecycle_status;
|
||||||
|
std::string lite_lifecycle_summary;
|
||||||
|
// True while an async create/open/restore is in flight (Settings drives the controller's
|
||||||
|
// async lifecycle path so a flaky server never freezes the UI; the result is polled each frame).
|
||||||
|
bool lite_lifecycle_pending = false;
|
||||||
|
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
|
||||||
|
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
|
||||||
|
// import. lite_import_key is the import input buffer (wiped right after submission).
|
||||||
|
std::string lite_export_secret;
|
||||||
|
std::string lite_export_label;
|
||||||
|
bool lite_export_is_seed = false; // revealed secret is a seed (show birthday + save-to-file)
|
||||||
|
unsigned long long lite_export_birthday = 0; // seed birthday (shown with the seed)
|
||||||
|
char lite_import_key[512] = "";
|
||||||
|
std::string lite_backup_status;
|
||||||
|
// Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is
|
||||||
|
// reused for Encrypt (unencrypted wallet) and Unlock (locked wallet); lite_dec_pass for Decrypt.
|
||||||
|
char lite_enc_pass[128] = "";
|
||||||
|
char lite_dec_pass[128] = "";
|
||||||
|
std::string lite_encryption_status;
|
||||||
bool mine_when_idle = false;
|
bool mine_when_idle = false;
|
||||||
int mine_idle_delay = 120;
|
int mine_idle_delay = 120;
|
||||||
bool idle_thread_scaling = false;
|
bool idle_thread_scaling = false;
|
||||||
@@ -112,11 +145,142 @@ struct SettingsPageState {
|
|||||||
bool tools_expanded = false;
|
bool tools_expanded = false;
|
||||||
bool confirm_clear_ztx = false;
|
bool confirm_clear_ztx = false;
|
||||||
bool confirm_delete_blockchain = false;
|
bool confirm_delete_blockchain = false;
|
||||||
|
bool confirm_rescan = false;
|
||||||
|
bool confirm_restart_daemon = false;
|
||||||
effects::ScrollFadeShader fade_shader;
|
effects::ScrollFadeShader fade_shader;
|
||||||
};
|
};
|
||||||
|
|
||||||
static SettingsPageState s_settingsState;
|
static SettingsPageState s_settingsState;
|
||||||
|
|
||||||
|
// Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input.
|
||||||
|
static int liteSeedWordCount(const char* s) {
|
||||||
|
int words = 0;
|
||||||
|
bool inWord = false;
|
||||||
|
for (; s && *s; ++s) {
|
||||||
|
const bool space = std::isspace(static_cast<unsigned char>(*s)) != 0;
|
||||||
|
if (space) inWord = false;
|
||||||
|
else if (!inWord) { inWord = true; ++words; }
|
||||||
|
}
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
|
||||||
|
switch (s_settingsState.lite_lifecycle_operation) {
|
||||||
|
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
|
||||||
|
case 2: return wallet::LiteWalletLifecycleOperation::RestoreFromSeed;
|
||||||
|
default: return wallet::LiteWalletLifecycleOperation::CreateNew;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
||||||
|
if (!app || !app->settings()) return;
|
||||||
|
|
||||||
|
wallet::LiteWalletLifecycleUiExecutionInput input;
|
||||||
|
input.capabilities = app->walletCapabilities();
|
||||||
|
input.settingsLoaded = true;
|
||||||
|
input.requirePersistedServerSelectionIntent = true;
|
||||||
|
input.ui.selectedServerDisplayReady = true;
|
||||||
|
input.ui.lifecycleUiOwnerReady = true;
|
||||||
|
input.ui.operationConfirmed = true;
|
||||||
|
input.ui.privateDataRedactionReady = true;
|
||||||
|
input.ui.syncPlannerFeedReady = true;
|
||||||
|
input.request.requestProvided = true;
|
||||||
|
input.request.operation = liteLifecycleOperationFromPageState();
|
||||||
|
|
||||||
|
switch (input.request.operation) {
|
||||||
|
case wallet::LiteWalletLifecycleOperation::CreateNew:
|
||||||
|
input.request.createRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
||||||
|
break;
|
||||||
|
case wallet::LiteWalletLifecycleOperation::OpenExisting:
|
||||||
|
input.request.openRequest.walletPath = s_settingsState.lite_wallet_path;
|
||||||
|
input.request.openRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
||||||
|
break;
|
||||||
|
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
|
||||||
|
input.request.restoreRequest.walletPath = s_settingsState.lite_wallet_path;
|
||||||
|
input.request.restoreRequest.seedPhrase = s_settingsState.lite_restore_seed;
|
||||||
|
input.request.restoreRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
|
||||||
|
input.request.restoreRequest.birthday = static_cast<unsigned long long>(std::max(0, s_settingsState.lite_restore_birthday));
|
||||||
|
input.request.restoreRequest.account = static_cast<unsigned long long>(std::max(0, s_settingsState.lite_restore_account));
|
||||||
|
input.request.restoreRequest.overwrite = s_settingsState.lite_restore_overwrite;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wipe ALL secret material when leaving this function, on every path (real execution,
|
||||||
|
// validation-only fallback, or early return): the UI char buffers AND the std::string
|
||||||
|
// copies inside `input.request.*Request`. The controller wipes its own by-value request
|
||||||
|
// copy, but these page-local copies are separate; leaving them would defeat the wipe.
|
||||||
|
struct LiteSecretScrubber {
|
||||||
|
wallet::LiteWalletLifecycleUiExecutionInput& in;
|
||||||
|
~LiteSecretScrubber() {
|
||||||
|
sodium_memzero(s_settingsState.lite_lifecycle_passphrase,
|
||||||
|
sizeof(s_settingsState.lite_lifecycle_passphrase));
|
||||||
|
sodium_memzero(s_settingsState.lite_restore_seed,
|
||||||
|
sizeof(s_settingsState.lite_restore_seed));
|
||||||
|
wallet::secureWipeLiteSecret(in.request.createRequest.passphrase);
|
||||||
|
wallet::secureWipeLiteSecret(in.request.openRequest.passphrase);
|
||||||
|
wallet::secureWipeLiteSecret(in.request.restoreRequest.seedPhrase);
|
||||||
|
wallet::secureWipeLiteSecret(in.request.restoreRequest.passphrase);
|
||||||
|
}
|
||||||
|
} liteSecretScrubber{input};
|
||||||
|
|
||||||
|
// Restore needs a complete 24-word seed; reject early (the scrubber above still wipes the
|
||||||
|
// entered secret on this return path).
|
||||||
|
if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) {
|
||||||
|
const int words = liteSeedWordCount(s_settingsState.lite_restore_seed);
|
||||||
|
if (words != 24) {
|
||||||
|
s_settingsState.lite_lifecycle_status =
|
||||||
|
"Enter all 24 seed words to restore (got " + std::to_string(words) + ")";
|
||||||
|
s_settingsState.lite_lifecycle_summary.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a linked lite backend is present, execute the operation for real through the App-owned
|
||||||
|
// controller — ASYNCHRONOUSLY, with server failover, so an unreachable/flaky server never
|
||||||
|
// freezes the UI. The request (with its secrets) is moved into the controller; the page polls
|
||||||
|
// the outcome each frame in renderLiteLifecyclePending() below. (Secret wiping of the page char
|
||||||
|
// buffers + the moved-from request copies is handled by liteSecretScrubber at function exit.)
|
||||||
|
if (auto* lite = app->liteWallet()) {
|
||||||
|
bool started = false;
|
||||||
|
switch (input.request.operation) {
|
||||||
|
case wallet::LiteWalletLifecycleOperation::CreateNew:
|
||||||
|
started = lite->beginCreateWalletAsync(std::move(input.request.createRequest));
|
||||||
|
break;
|
||||||
|
case wallet::LiteWalletLifecycleOperation::OpenExisting:
|
||||||
|
started = lite->beginOpenWalletAsync(std::move(input.request.openRequest));
|
||||||
|
break;
|
||||||
|
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
|
||||||
|
started = lite->beginRestoreWalletAsync(std::move(input.request.restoreRequest));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (started) {
|
||||||
|
s_settingsState.lite_lifecycle_pending = true;
|
||||||
|
s_settingsState.lite_lifecycle_status = "Working…";
|
||||||
|
s_settingsState.lite_lifecycle_summary.clear();
|
||||||
|
} else {
|
||||||
|
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
|
||||||
|
// usable server). The controller's status carries the reason.
|
||||||
|
s_settingsState.lite_lifecycle_status = lite->status().message.empty()
|
||||||
|
? "Could not start the operation"
|
||||||
|
: lite->status().message;
|
||||||
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = wallet::executeLiteWalletLifecycleUiRequest(*app->settings(), input);
|
||||||
|
s_settingsState.lite_lifecycle_summary = result.requestSummaryRedacted;
|
||||||
|
if (result.ok) {
|
||||||
|
s_settingsState.lite_lifecycle_status = "Ready";
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_lifecycle_status = result.error.empty()
|
||||||
|
? wallet::liteWalletLifecycleUiExecutionStatusName(result.status)
|
||||||
|
: result.error;
|
||||||
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
||||||
|
|
||||||
static void loadSettingsPageState(config::Settings* settings) {
|
static void loadSettingsPageState(config::Settings* settings) {
|
||||||
@@ -166,6 +330,7 @@ static void loadSettingsPageState(config::Settings* settings) {
|
|||||||
Layout::setUserFontScale(s_settingsState.font_scale); // sync with Layout on load
|
Layout::setUserFontScale(s_settingsState.font_scale); // sync with Layout on load
|
||||||
s_settingsState.keep_daemon_running = settings->getKeepDaemonRunning();
|
s_settingsState.keep_daemon_running = settings->getKeepDaemonRunning();
|
||||||
s_settingsState.stop_external_daemon = settings->getStopExternalDaemon();
|
s_settingsState.stop_external_daemon = settings->getStopExternalDaemon();
|
||||||
|
// Lite-server selection is managed entirely by the Network tab (not the Settings page).
|
||||||
s_settingsState.mine_when_idle = settings->getMineWhenIdle();
|
s_settingsState.mine_when_idle = settings->getMineWhenIdle();
|
||||||
s_settingsState.mine_idle_delay = settings->getMineIdleDelay();
|
s_settingsState.mine_idle_delay = settings->getMineIdleDelay();
|
||||||
s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling();
|
s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling();
|
||||||
@@ -220,6 +385,7 @@ static void saveSettingsPageState(config::Settings* settings) {
|
|||||||
settings->setFontScale(s_settingsState.font_scale);
|
settings->setFontScale(s_settingsState.font_scale);
|
||||||
settings->setKeepDaemonRunning(s_settingsState.keep_daemon_running);
|
settings->setKeepDaemonRunning(s_settingsState.keep_daemon_running);
|
||||||
settings->setStopExternalDaemon(s_settingsState.stop_external_daemon);
|
settings->setStopExternalDaemon(s_settingsState.stop_external_daemon);
|
||||||
|
// Lite-server selection is owned by the Network tab; the Settings page no longer writes it.
|
||||||
settings->setMineWhenIdle(s_settingsState.mine_when_idle);
|
settings->setMineWhenIdle(s_settingsState.mine_when_idle);
|
||||||
settings->setMineIdleDelay(s_settingsState.mine_idle_delay);
|
settings->setMineIdleDelay(s_settingsState.mine_idle_delay);
|
||||||
settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling);
|
settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling);
|
||||||
@@ -1073,6 +1239,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
|
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
|
||||||
{
|
{
|
||||||
|
const bool showDaemonOptions = app->supportsFullNodeLifecycleActions();
|
||||||
float cbSpacing = Layout::spacingMd();
|
float cbSpacing = Layout::spacingMd();
|
||||||
float fh = ImGui::GetFrameHeight();
|
float fh = ImGui::GetFrameHeight();
|
||||||
float inner = ImGui::GetStyle().ItemInnerSpacing.x;
|
float inner = ImGui::GetStyle().ItemInnerSpacing.x;
|
||||||
@@ -1080,9 +1247,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
float totalW = cbW(TR("save_z_transactions")) + cbSpacing
|
float totalW = cbW(TR("save_z_transactions")) + cbSpacing
|
||||||
+ cbW(TR("auto_shield")) + cbSpacing
|
+ cbW(TR("auto_shield")) + cbSpacing
|
||||||
+ cbW(TR("use_tor")) + cbSpacing
|
+ cbW(TR("use_tor")) + cbSpacing;
|
||||||
+ cbW(TR("keep_daemon")) + cbSpacing
|
if (showDaemonOptions) {
|
||||||
+ cbW(TR("stop_external"));
|
totalW += cbW(TR("keep_daemon")) + cbSpacing
|
||||||
|
+ cbW(TR("stop_external")) + cbSpacing;
|
||||||
|
}
|
||||||
|
totalW += cbW(TR("verbose_logging"));
|
||||||
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);
|
||||||
float sp = cbSpacing * scale;
|
float sp = cbSpacing * scale;
|
||||||
@@ -1095,6 +1265,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
|
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
|
||||||
|
if (showDaemonOptions) {
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
|
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
@@ -1107,6 +1278,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", TR("tt_stop_external"));
|
ImGui::SetTooltip("%s", TR("tt_stop_external"));
|
||||||
|
}
|
||||||
ImGui::SameLine(0, sp);
|
ImGui::SameLine(0, sp);
|
||||||
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
|
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
|
||||||
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
|
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
|
||||||
@@ -1215,6 +1387,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
TR("tt_backup"),
|
TR("tt_backup"),
|
||||||
TR("tt_export_csv")
|
TR("tt_export_csv")
|
||||||
};
|
};
|
||||||
|
const bool showFullNodeLifecycleActions = app->supportsFullNodeLifecycleActions();
|
||||||
const char* wizLabel = TR("setup_wizard");
|
const char* wizLabel = TR("setup_wizard");
|
||||||
const char* bsLabel = TR("download_bootstrap");
|
const char* bsLabel = TR("download_bootstrap");
|
||||||
float sp = Layout::spacingSm();
|
float sp = Layout::spacingSm();
|
||||||
@@ -1224,9 +1397,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
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 = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
|
||||||
float bsW = ImGui::CalcTextSize(bsLabel).x + btnPadX;
|
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
|
||||||
float totalW = naturalW + wizW + bsW + sp * 7;
|
float totalW = naturalW + sp * 5;
|
||||||
|
if (showFullNodeLifecycleActions) totalW += wizW + bsW + sp * 2;
|
||||||
|
|
||||||
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);
|
||||||
@@ -1253,6 +1427,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ExportTransactionsDialog::show();
|
ExportTransactionsDialog::show();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
|
||||||
|
|
||||||
|
if (showFullNodeLifecycleActions) {
|
||||||
// Right-align Setup Wizard + Download Bootstrap
|
// Right-align Setup Wizard + Download Bootstrap
|
||||||
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
|
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
|
||||||
float curX = ImGui::GetCursorScreenPos().x;
|
float curX = ImGui::GetCursorScreenPos().x;
|
||||||
@@ -1274,6 +1449,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
||||||
app->restartWizard();
|
app->restartWizard();
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
|
||||||
|
}
|
||||||
|
|
||||||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||||||
}
|
}
|
||||||
@@ -1336,6 +1512,322 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (!body2Info) body2Info = Type().body2();
|
if (!body2Info) body2Info = Type().body2();
|
||||||
ImGui::PushFont(body2Info);
|
ImGui::PushFont(body2Info);
|
||||||
|
|
||||||
|
if (app->isLiteBuild()) {
|
||||||
|
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
|
||||||
|
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
|
||||||
|
|
||||||
|
// Lite-server selection lives in the dedicated Network tab now.
|
||||||
|
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
||||||
|
"Lite servers are managed in the Network tab.");
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) {
|
||||||
|
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_settingsState.lite_lifecycle_expanded) {
|
||||||
|
const char* lifecycleLabels[] = {"Create", "Open", "Restore"};
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Action");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
if (ImGui::BeginCombo("##LiteLifecycleOperation",
|
||||||
|
lifecycleLabels[std::max(0, std::min(2, s_settingsState.lite_lifecycle_operation))])) {
|
||||||
|
for (int operationIndex = 0; operationIndex < 3; ++operationIndex) {
|
||||||
|
const bool selected = s_settingsState.lite_lifecycle_operation == operationIndex;
|
||||||
|
if (ImGui::Selectable(lifecycleLabels[operationIndex], selected)) {
|
||||||
|
s_settingsState.lite_lifecycle_operation = operationIndex;
|
||||||
|
s_settingsState.lite_lifecycle_status.clear();
|
||||||
|
s_settingsState.lite_lifecycle_summary.clear();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_settingsState.lite_lifecycle_operation == 1 ||
|
||||||
|
s_settingsState.lite_lifecycle_operation == 2) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Wallet");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
|
||||||
|
sizeof(s_settingsState.lite_wallet_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_settingsState.lite_lifecycle_operation == 2) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Seed");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
|
||||||
|
sizeof(s_settingsState.lite_restore_seed),
|
||||||
|
ImGuiInputTextFlags_Password);
|
||||||
|
// Live 24-word count so the user knows the seed is complete before restoring.
|
||||||
|
{
|
||||||
|
const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed);
|
||||||
|
char wbuf[32];
|
||||||
|
snprintf(wbuf, sizeof(wbuf), "%d / 24 words", wc);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Birthday");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||||
|
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
|
||||||
|
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
"Block height to start scanning from. Leave 0 if unknown (slower full scan).");
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Account");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
|
||||||
|
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
|
||||||
|
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::Checkbox("Overwrite##LiteRestoreOverwrite",
|
||||||
|
&s_settingsState.lite_restore_overwrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Passphrase");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteLifecyclePassphrase",
|
||||||
|
s_settingsState.lite_lifecycle_passphrase,
|
||||||
|
sizeof(s_settingsState.lite_lifecycle_passphrase),
|
||||||
|
ImGuiInputTextFlags_Password);
|
||||||
|
|
||||||
|
// Poll a completed async create/open/restore (driven by the App-owned
|
||||||
|
// controller; finalized on the main thread by App::update's
|
||||||
|
// pumpLifecycleResult()). While in flight the button is disabled.
|
||||||
|
if (s_settingsState.lite_lifecycle_pending) {
|
||||||
|
if (auto* lite = app->liteWallet()) {
|
||||||
|
if (!lite->lifecycleRequestInProgress()) {
|
||||||
|
s_settingsState.lite_lifecycle_pending = false;
|
||||||
|
const auto& result = lite->lastLifecycleResult();
|
||||||
|
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
|
||||||
|
if (result.walletReady) {
|
||||||
|
s_settingsState.lite_lifecycle_status = "Wallet ready";
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_lifecycle_status = result.error.empty()
|
||||||
|
? result.status.message
|
||||||
|
: result.error;
|
||||||
|
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_lifecycle_pending = false; // backend went away
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
|
||||||
|
if (liteLifecycleBusy) ImGui::BeginDisabled();
|
||||||
|
if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
evaluateLiteLifecycleRequestFromPageState(app);
|
||||||
|
}
|
||||||
|
if (liteLifecycleBusy) ImGui::EndDisabled();
|
||||||
|
|
||||||
|
if (!s_settingsState.lite_lifecycle_status.empty()) {
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_lifecycle_status.c_str());
|
||||||
|
}
|
||||||
|
if (!s_settingsState.lite_lifecycle_summary.empty()) {
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_lifecycle_summary.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Backup & keys (open wallet only) ----------------------------------
|
||||||
|
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().text(TypeStyle::Body2, "Backup & keys");
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
auto r = app->liteWallet()->exportSeed();
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
if (r.ok) {
|
||||||
|
s_settingsState.lite_export_secret = r.seedPhrase;
|
||||||
|
s_settingsState.lite_export_label =
|
||||||
|
"Seed phrase — the ONLY way to restore your wallet. "
|
||||||
|
"Write it down, store it offline, never share it.";
|
||||||
|
s_settingsState.lite_export_is_seed = true;
|
||||||
|
s_settingsState.lite_export_birthday = r.birthday;
|
||||||
|
s_settingsState.lite_backup_status.clear();
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
s_settingsState.lite_export_is_seed = false;
|
||||||
|
s_settingsState.lite_backup_status = r.error;
|
||||||
|
}
|
||||||
|
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
auto r = app->liteWallet()->exportPrivateKeys();
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
s_settingsState.lite_export_is_seed = false; // keys, not a seed
|
||||||
|
if (r.ok) {
|
||||||
|
s_settingsState.lite_export_secret = r.privateKeysJson;
|
||||||
|
s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds";
|
||||||
|
s_settingsState.lite_backup_status.clear();
|
||||||
|
} else {
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
s_settingsState.lite_backup_status = r.error;
|
||||||
|
}
|
||||||
|
wallet::secureWipeLiteSecret(r.privateKeysJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
|
||||||
|
if (!s_settingsState.lite_export_secret.empty()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_export_label.c_str());
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
// The seed's birthday is needed to restore quickly — show + back it up too.
|
||||||
|
if (s_settingsState.lite_export_is_seed) {
|
||||||
|
char bday[64];
|
||||||
|
snprintf(bday, sizeof(bday), "Birthday: %llu (back this up too)",
|
||||||
|
s_settingsState.lite_export_birthday);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday);
|
||||||
|
}
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
app->copySecretToClipboard(s_settingsState.lite_export_secret);
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
|
||||||
|
if (s_settingsState.lite_export_is_seed &&
|
||||||
|
TactileButton("Save to file##LiteExportSave", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
std::string path = util::Platform::getConfigDir() + "/lite-seed-backup.txt";
|
||||||
|
std::string content = s_settingsState.lite_export_secret + "\nBirthday: " +
|
||||||
|
std::to_string(s_settingsState.lite_export_birthday) + "\n";
|
||||||
|
const bool ok = util::Platform::writeFileAtomically(
|
||||||
|
path, content, /*restrictPermissions=*/true);
|
||||||
|
wallet::secureWipeLiteSecret(content);
|
||||||
|
s_settingsState.lite_backup_status = ok
|
||||||
|
? ("Saved (plaintext, owner-only) to " + path)
|
||||||
|
: ("Could not write " + path);
|
||||||
|
}
|
||||||
|
if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
s_settingsState.lite_export_is_seed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import a spending/viewing key (history appears after the next sync).
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Import key");
|
||||||
|
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
|
||||||
|
sizeof(s_settingsState.lite_import_key),
|
||||||
|
ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
|
||||||
|
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
||||||
|
s_settingsState.lite_backup_status =
|
||||||
|
r.ok ? "Key imported — run a sync to scan its history" : r.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s_settingsState.lite_backup_status.empty()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_backup_status.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ----
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().text(TypeStyle::Body2, "Security");
|
||||||
|
const auto& wstate = app->getWalletState();
|
||||||
|
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
|
||||||
|
if (!wstate.isEncrypted()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Passphrase");
|
||||||
|
ImGui::SameLine(encLabelX);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
|
||||||
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Encrypt wallet##LiteEncrypt", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
|
||||||
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
||||||
|
s_settingsState.lite_encryption_status = r.ok ? "Wallet encrypted" : r.error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wstate.isLocked()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Unlock");
|
||||||
|
ImGui::SameLine(encLabelX);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
|
||||||
|
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Unlock##LiteUnlock", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
|
||||||
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
||||||
|
s_settingsState.lite_encryption_status = ok ? "Wallet unlocked" : "Unlock failed";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
s_settingsState.lite_encryption_status =
|
||||||
|
app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Passphrase");
|
||||||
|
ImGui::SameLine(encLabelX);
|
||||||
|
ImGui::SetNextItemWidth(liteInputW);
|
||||||
|
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
|
||||||
|
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
if (TactileButton("Remove encryption##LiteDecrypt", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
|
||||||
|
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
||||||
|
s_settingsState.lite_encryption_status = r.ok ? "Encryption removed" : r.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!s_settingsState.lite_encryption_status.empty()) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
s_settingsState.lite_encryption_status.c_str());
|
||||||
|
}
|
||||||
|
} else if (!s_settingsState.lite_export_secret.empty()) {
|
||||||
|
// Wallet closed while a backup/secret was revealed — don't leave it in memory.
|
||||||
|
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
|
||||||
|
s_settingsState.lite_export_label.clear();
|
||||||
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
||||||
|
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
float rpcLblW = std::max(
|
float rpcLblW = std::max(
|
||||||
S.drawElement("components.settings-page", "rpc-label-min-width").size,
|
S.drawElement("components.settings-page", "rpc-label-min-width").size,
|
||||||
std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs));
|
std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs));
|
||||||
@@ -1469,8 +1961,8 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
// Node maintenance buttons
|
// Node maintenance buttons (full-node build only)
|
||||||
{
|
if (app->supportsFullNodeLifecycleActions()) {
|
||||||
ImFont* btnFont = S.resolveFont("button");
|
ImFont* btnFont = S.resolveFont("button");
|
||||||
float nodeBtnW;
|
float nodeBtnW;
|
||||||
{
|
{
|
||||||
@@ -1503,7 +1995,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_test_conn"));
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_test_conn"));
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) {
|
if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) {
|
||||||
app->rescanBlockchain();
|
s_settingsState.confirm_rescan = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
@@ -1518,6 +2010,8 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1944,7 +2438,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
// "Restart daemon" button — only active when categories changed
|
// "Restart daemon" button — only active when categories changed
|
||||||
if (s_settingsState.debug_cats_dirty) {
|
if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255));
|
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255));
|
||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
if (iconFont) {
|
if (iconFont) {
|
||||||
@@ -1959,8 +2453,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
s_settingsState.debug_cats_dirty = false;
|
s_settingsState.confirm_restart_daemon = true;
|
||||||
app->restartDaemon();
|
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon"));
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon"));
|
||||||
}
|
}
|
||||||
@@ -2079,6 +2572,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
|
||||||
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
|
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
if (app->supportsFullNodeLifecycleActions())
|
||||||
app->deleteBlockchainData();
|
app->deleteBlockchainData();
|
||||||
s_settingsState.confirm_delete_blockchain = false;
|
s_settingsState.confirm_delete_blockchain = false;
|
||||||
}
|
}
|
||||||
@@ -2087,6 +2581,65 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirm: rescan blockchain (restarts the daemon, re-scans the whole chain — long but safe)
|
||||||
|
if (s_settingsState.confirm_rescan) {
|
||||||
|
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
||||||
|
ImGui::PushFont(Type().iconLarge());
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
if (ImGui::Button(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
s_settingsState.confirm_rescan = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
app->rescanBlockchain();
|
||||||
|
s_settingsState.confirm_rescan = false;
|
||||||
|
}
|
||||||
|
EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm: restart daemon (briefly drops the connection to apply changed options)
|
||||||
|
if (s_settingsState.confirm_restart_daemon) {
|
||||||
|
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
|
||||||
|
ImGui::PushFont(Type().iconLarge());
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
if (ImGui::Button(TrId("cancel", "restartd_cancel").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
s_settingsState.confirm_restart_daemon = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TrId("settings_restart_daemon", "restartd_confirm").c_str(), ImVec2(btnW, 40))) {
|
||||||
|
s_settingsState.debug_cats_dirty = false;
|
||||||
|
app->restartDaemon();
|
||||||
|
s_settingsState.confirm_restart_daemon = false;
|
||||||
|
}
|
||||||
|
EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -1,352 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Confirmation Dialog
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design confirmation dialog for critical actions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Confirmation dialog type
|
|
||||||
*/
|
|
||||||
enum class ConfirmationType {
|
|
||||||
Info,
|
|
||||||
Warning,
|
|
||||||
Danger,
|
|
||||||
Transaction
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Transaction confirmation details
|
|
||||||
*/
|
|
||||||
struct TransactionConfirmation {
|
|
||||||
std::string toAddress;
|
|
||||||
double amount;
|
|
||||||
double fee;
|
|
||||||
std::string memo;
|
|
||||||
bool isShielded;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Confirmation dialog manager
|
|
||||||
*/
|
|
||||||
class ConfirmationDialog {
|
|
||||||
public:
|
|
||||||
static ConfirmationDialog& instance() {
|
|
||||||
static ConfirmationDialog s_instance;
|
|
||||||
return s_instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a simple confirmation dialog
|
|
||||||
*/
|
|
||||||
void show(const std::string& title,
|
|
||||||
const std::string& message,
|
|
||||||
ConfirmationType type,
|
|
||||||
std::function<void(bool)> callback) {
|
|
||||||
m_title = title;
|
|
||||||
m_message = message;
|
|
||||||
m_type = type;
|
|
||||||
m_callback = callback;
|
|
||||||
m_isOpen = true;
|
|
||||||
m_isTransaction = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show transaction confirmation dialog
|
|
||||||
*/
|
|
||||||
void showTransaction(const TransactionConfirmation& tx,
|
|
||||||
std::function<void(bool)> callback) {
|
|
||||||
m_title = "Confirm Transaction";
|
|
||||||
m_type = ConfirmationType::Transaction;
|
|
||||||
m_transaction = tx;
|
|
||||||
m_callback = callback;
|
|
||||||
m_isOpen = true;
|
|
||||||
m_isTransaction = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render the dialog (call every frame)
|
|
||||||
*/
|
|
||||||
void render();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if dialog is currently open
|
|
||||||
*/
|
|
||||||
bool isOpen() const { return m_isOpen; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
ConfirmationDialog() = default;
|
|
||||||
|
|
||||||
void renderSimpleDialog();
|
|
||||||
void renderTransactionDialog();
|
|
||||||
|
|
||||||
std::string m_title;
|
|
||||||
std::string m_message;
|
|
||||||
ConfirmationType m_type = ConfirmationType::Info;
|
|
||||||
TransactionConfirmation m_transaction;
|
|
||||||
std::function<void(bool)> m_callback;
|
|
||||||
bool m_isOpen = false;
|
|
||||||
bool m_isTransaction = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void ConfirmationDialog::render() {
|
|
||||||
if (!m_isOpen) return;
|
|
||||||
|
|
||||||
if (m_isTransaction) {
|
|
||||||
renderTransactionDialog();
|
|
||||||
} else {
|
|
||||||
renderSimpleDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ConfirmationDialog::renderSimpleDialog() {
|
|
||||||
DialogSpec dialogSpec;
|
|
||||||
dialogSpec.title = m_title.c_str();
|
|
||||||
dialogSpec.maxWidth = 400.0f;
|
|
||||||
|
|
||||||
DialogResult result = BeginDialog("confirm_dialog", dialogSpec);
|
|
||||||
|
|
||||||
if (result.isOpen) {
|
|
||||||
// Icon based on type
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
ImVec4 iconColor;
|
|
||||||
const char* icon;
|
|
||||||
|
|
||||||
switch (m_type) {
|
|
||||||
case ConfirmationType::Info:
|
|
||||||
iconColor = colors::Blue500;
|
|
||||||
icon = ICON_MD_INFO;
|
|
||||||
break;
|
|
||||||
case ConfirmationType::Warning:
|
|
||||||
iconColor = Secondary();
|
|
||||||
icon = ICON_MD_WARNING;
|
|
||||||
break;
|
|
||||||
case ConfirmationType::Danger:
|
|
||||||
iconColor = Error();
|
|
||||||
icon = ICON_MD_DANGEROUS;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
iconColor = Primary();
|
|
||||||
icon = ICON_MD_HELP;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Typography::instance().textColored(TypeStyle::H4, iconColor, icon);
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
ImGui::TextWrapped("%s", m_message.c_str());
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 200);
|
|
||||||
|
|
||||||
if (TextButton("CANCEL")) {
|
|
||||||
m_isOpen = false;
|
|
||||||
if (m_callback) m_callback(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Confirm button color based on type
|
|
||||||
if (m_type == ConfirmationType::Danger) {
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, Error());
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::Red400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContainedButton("CONFIRM")) {
|
|
||||||
m_isOpen = false;
|
|
||||||
if (m_callback) m_callback(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_type == ConfirmationType::Danger) {
|
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.dismissed) {
|
|
||||||
m_isOpen = false;
|
|
||||||
if (m_callback) m_callback(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ConfirmationDialog::renderTransactionDialog() {
|
|
||||||
DialogSpec dialogSpec;
|
|
||||||
dialogSpec.title = "Confirm Transaction";
|
|
||||||
dialogSpec.maxWidth = 500.0f;
|
|
||||||
|
|
||||||
DialogResult result = BeginDialog("tx_confirm_dialog", dialogSpec);
|
|
||||||
|
|
||||||
if (result.isOpen) {
|
|
||||||
// Transaction type badge
|
|
||||||
ChipSpec chipSpec;
|
|
||||||
chipSpec.variant = ChipVariant::Filled;
|
|
||||||
|
|
||||||
if (m_transaction.isShielded) {
|
|
||||||
chipSpec.color = Primary();
|
|
||||||
Chip(ICON_MD_SHIELD " Shielded Transaction", chipSpec);
|
|
||||||
} else {
|
|
||||||
chipSpec.color = Secondary();
|
|
||||||
Chip(ICON_MD_DESCRIPTION " Transparent Transaction", chipSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Amount (large, prominent)
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
|
|
||||||
char amountStr[64];
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "%.8f DRGX", m_transaction.amount);
|
|
||||||
Typography::instance().textColored(TypeStyle::H4, Primary(), amountStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Recipient
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TO ADDRESS");
|
|
||||||
ImGui::TextWrapped("%s", m_transaction.toAddress.c_str());
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Fee
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK FEE");
|
|
||||||
char feeStr[64];
|
|
||||||
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", m_transaction.fee);
|
|
||||||
Typography::instance().text(TypeStyle::Body1, feeStr);
|
|
||||||
|
|
||||||
// Memo if present
|
|
||||||
if (!m_transaction.memo.empty()) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (ENCRYPTED)");
|
|
||||||
ImGui::TextWrapped("%s", m_transaction.memo.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Total
|
|
||||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImGui::GetWindowDrawList()->AddLine(
|
|
||||||
divPos,
|
|
||||||
ImVec2(divPos.x + availWidth, divPos.y),
|
|
||||||
ImGui::GetColorU32(Divider())
|
|
||||||
);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL");
|
|
||||||
char totalStr[64];
|
|
||||||
snprintf(totalStr, sizeof(totalStr), "%.8f DRGX", m_transaction.amount + m_transaction.fee);
|
|
||||||
Typography::instance().text(TypeStyle::H5, totalStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Warning for shielded transactions
|
|
||||||
if (m_transaction.isShielded) {
|
|
||||||
CardSpec warningCard;
|
|
||||||
warningCard.elevation = 0;
|
|
||||||
warningCard.padding = spacing::dp(2);
|
|
||||||
warningCard.outlined = true;
|
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, colors::withAlpha(Secondary(), 0.5f));
|
|
||||||
if (BeginCard(warningCard)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, Secondary(),
|
|
||||||
ICON_MD_WARNING " Shielded transactions are private but take longer to process.");
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 220);
|
|
||||||
|
|
||||||
if (OutlinedButton("CANCEL")) {
|
|
||||||
m_isOpen = false;
|
|
||||||
if (m_callback) m_callback(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
if (ContainedButton("SEND NOW")) {
|
|
||||||
m_isOpen = false;
|
|
||||||
if (m_callback) m_callback(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.dismissed) {
|
|
||||||
m_isOpen = false;
|
|
||||||
if (m_callback) m_callback(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Quick Confirmation Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a simple confirmation dialog
|
|
||||||
*/
|
|
||||||
inline void ShowConfirmation(const std::string& title,
|
|
||||||
const std::string& message,
|
|
||||||
std::function<void(bool)> callback) {
|
|
||||||
ConfirmationDialog::instance().show(title, message, ConfirmationType::Info, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a warning confirmation dialog
|
|
||||||
*/
|
|
||||||
inline void ShowWarningConfirmation(const std::string& title,
|
|
||||||
const std::string& message,
|
|
||||||
std::function<void(bool)> callback) {
|
|
||||||
ConfirmationDialog::instance().show(title, message, ConfirmationType::Warning, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a danger confirmation dialog
|
|
||||||
*/
|
|
||||||
inline void ShowDangerConfirmation(const std::string& title,
|
|
||||||
const std::string& message,
|
|
||||||
std::function<void(bool)> callback) {
|
|
||||||
ConfirmationDialog::instance().show(title, message, ConfirmationType::Danger, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Show a transaction confirmation dialog
|
|
||||||
*/
|
|
||||||
inline void ShowTransactionConfirmation(const TransactionConfirmation& tx,
|
|
||||||
std::function<void(bool)> callback) {
|
|
||||||
ConfirmationDialog::instance().showTransaction(tx, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Balance/Home Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Main dashboard showing wallet balances and recent activity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Balance information for display
|
|
||||||
*/
|
|
||||||
struct BalanceInfo {
|
|
||||||
double total = 0.0;
|
|
||||||
double shielded = 0.0;
|
|
||||||
double transparent = 0.0;
|
|
||||||
double pending = 0.0;
|
|
||||||
double fiatValue = 0.0;
|
|
||||||
std::string fiatCurrency = "USD";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Recent transaction summary
|
|
||||||
*/
|
|
||||||
struct RecentTransaction {
|
|
||||||
std::string txid;
|
|
||||||
std::string type; // "sent", "received", "mined"
|
|
||||||
std::string address; // Truncated address
|
|
||||||
double amount;
|
|
||||||
std::string time; // Relative time "2 hours ago"
|
|
||||||
bool confirmed;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render the home/balance screen
|
|
||||||
*/
|
|
||||||
class HomeScreen {
|
|
||||||
public:
|
|
||||||
void setBalance(const BalanceInfo& balance) { m_balance = balance; }
|
|
||||||
void setRecentTransactions(const std::vector<RecentTransaction>& txns) { m_recentTxns = txns; }
|
|
||||||
void setOnSendClick(std::function<void()> callback) { m_onSendClick = callback; }
|
|
||||||
void setOnReceiveClick(std::function<void()> callback) { m_onReceiveClick = callback; }
|
|
||||||
void setOnTransactionClick(std::function<void(const std::string&)> callback) { m_onTxClick = callback; }
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderBalanceCard();
|
|
||||||
void renderQuickActions();
|
|
||||||
void renderShieldedCard();
|
|
||||||
void renderTransparentCard();
|
|
||||||
void renderRecentTransactions();
|
|
||||||
|
|
||||||
BalanceInfo m_balance;
|
|
||||||
std::vector<RecentTransaction> m_recentTxns;
|
|
||||||
std::function<void()> m_onSendClick;
|
|
||||||
std::function<void()> m_onReceiveClick;
|
|
||||||
std::function<void(const std::string&)> m_onTxClick;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void HomeScreen::render() {
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(2), spacing::dp(2)));
|
|
||||||
|
|
||||||
// Two-column layout
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
float cardWidth = (availWidth - spacing::dp(2)) * 0.5f;
|
|
||||||
|
|
||||||
// Row 1: Total Balance + Recent Transactions
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
// Total Balance Card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
renderBalanceCard();
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Recent Transactions Card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
renderRecentTransactions();
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Row 2: Shielded + Transparent
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
renderShieldedCard();
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
renderTransparentCard();
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Quick Actions
|
|
||||||
renderQuickActions();
|
|
||||||
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void HomeScreen::renderBalanceCard() {
|
|
||||||
float cardWidth = (ImGui::GetContentRegionAvail().x - spacing::dp(2)) * 0.5f;
|
|
||||||
|
|
||||||
CardSpec spec;
|
|
||||||
spec.elevation = 2;
|
|
||||||
spec.padding = spacing::dp(3);
|
|
||||||
|
|
||||||
ImGui::PushID("balance_card");
|
|
||||||
if (BeginCard(spec)) {
|
|
||||||
// Overline
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Balance amount
|
|
||||||
char balanceStr[64];
|
|
||||||
snprintf(balanceStr, sizeof(balanceStr), "%.8f", m_balance.total);
|
|
||||||
Typography::instance().text(TypeStyle::H4, balanceStr);
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::H6, OnSurfaceMedium(), "DRGX");
|
|
||||||
|
|
||||||
// Fiat value
|
|
||||||
if (m_balance.fiatValue > 0) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(0.5f)));
|
|
||||||
char fiatStr[64];
|
|
||||||
snprintf(fiatStr, sizeof(fiatStr), "≈ $%.2f %s", m_balance.fiatValue, m_balance.fiatCurrency.c_str());
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), fiatStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pending indicator
|
|
||||||
if (m_balance.pending > 0) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
char pendingStr[64];
|
|
||||||
snprintf(pendingStr, sizeof(pendingStr), ICON_MD_HOURGLASS_EMPTY " %.8f pending", m_balance.pending);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, Secondary(), pendingStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void HomeScreen::renderQuickActions() {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
float buttonWidth = 120.0f;
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
float startX = (availWidth - buttonWidth * 2 - spacing::dp(2)) * 0.5f;
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + startX);
|
|
||||||
|
|
||||||
if (ContainedButton(ICON_MD_CALL_MADE " SEND")) {
|
|
||||||
if (m_onSendClick) m_onSendClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
if (OutlinedButton(ICON_MD_CALL_RECEIVED " RECEIVE")) {
|
|
||||||
if (m_onReceiveClick) m_onReceiveClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void HomeScreen::renderShieldedCard() {
|
|
||||||
CardSpec spec;
|
|
||||||
spec.elevation = 1;
|
|
||||||
spec.padding = spacing::dp(2);
|
|
||||||
|
|
||||||
ImGui::PushID("shielded_card");
|
|
||||||
if (BeginCard(spec)) {
|
|
||||||
// Icon and label
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, Primary(), ICON_MD_SHIELD);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SHIELDED (PRIVATE)");
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
char amountStr[64];
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "%.8f", m_balance.shielded);
|
|
||||||
Typography::instance().text(TypeStyle::H5, amountStr);
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "DRGX");
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void HomeScreen::renderTransparentCard() {
|
|
||||||
CardSpec spec;
|
|
||||||
spec.elevation = 1;
|
|
||||||
spec.padding = spacing::dp(2);
|
|
||||||
|
|
||||||
ImGui::PushID("transparent_card");
|
|
||||||
if (BeginCard(spec)) {
|
|
||||||
// Icon and label
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, Secondary(), ICON_MD_CONTENT_COPY);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSPARENT");
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
char amountStr[64];
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "%.8f", m_balance.transparent);
|
|
||||||
Typography::instance().text(TypeStyle::H5, amountStr);
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "DRGX");
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void HomeScreen::renderRecentTransactions() {
|
|
||||||
CardSpec spec;
|
|
||||||
spec.elevation = 1;
|
|
||||||
spec.padding = spacing::dp(2);
|
|
||||||
|
|
||||||
ImGui::PushID("recent_txns");
|
|
||||||
if (BeginCard(spec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
if (m_recentTxns.empty()) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), "No recent transactions");
|
|
||||||
} else {
|
|
||||||
BeginList("recent_list", false);
|
|
||||||
|
|
||||||
int count = 0;
|
|
||||||
for (const auto& tx : m_recentTxns) {
|
|
||||||
if (count >= 5) break; // Show max 5
|
|
||||||
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
|
|
||||||
// Icon based on type
|
|
||||||
if (tx.type == "received") {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
|
|
||||||
} else if (tx.type == "sent") {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CALL_MADE;
|
|
||||||
} else {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CONSTRUCTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Amount as primary text
|
|
||||||
char amountStr[32];
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "%+.4f DRGX",
|
|
||||||
tx.type == "sent" ? -tx.amount : tx.amount);
|
|
||||||
itemSpec.primaryText = amountStr;
|
|
||||||
itemSpec.secondaryText = tx.time.c_str();
|
|
||||||
itemSpec.dividerBelow = (count < 4 && count < (int)m_recentTxns.size() - 1);
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
if (m_onTxClick) m_onTxClick(tx.txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,553 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "../schema/ui_schema.h"
|
|
||||||
#include "home_screen.h"
|
|
||||||
#include "send_screen.h"
|
|
||||||
#include "receive_screen.h"
|
|
||||||
#include "transactions_screen.h"
|
|
||||||
#include "mining_screen.h"
|
|
||||||
#include "peers_screen.h"
|
|
||||||
#include "settings_screen.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Main Application Layout
|
|
||||||
// ============================================================================
|
|
||||||
// Combines app bar, navigation drawer, content area, and status bar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Navigation destinations
|
|
||||||
*/
|
|
||||||
enum class NavDestination {
|
|
||||||
Home,
|
|
||||||
Send,
|
|
||||||
Receive,
|
|
||||||
Transactions,
|
|
||||||
Mining,
|
|
||||||
Peers,
|
|
||||||
Settings
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Connection status for status bar
|
|
||||||
*/
|
|
||||||
struct ConnectionStatus {
|
|
||||||
bool connected = false;
|
|
||||||
int blockHeight = 0;
|
|
||||||
int peers = 0;
|
|
||||||
double networkHashrate = 0.0;
|
|
||||||
bool syncing = false;
|
|
||||||
float syncProgress = 0.0f;
|
|
||||||
std::string statusMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Main application layout with Material Design structure
|
|
||||||
*/
|
|
||||||
class MainLayout {
|
|
||||||
public:
|
|
||||||
MainLayout() = default;
|
|
||||||
|
|
||||||
void setConnectionStatus(const ConnectionStatus& status) { m_status = status; }
|
|
||||||
|
|
||||||
void setOnNavigation(std::function<void(NavDestination)> callback) {
|
|
||||||
m_onNavigation = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnSearch(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onSearch = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnSettingsClick(std::function<void()> callback) {
|
|
||||||
m_onSettingsClick = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void navigateTo(NavDestination dest) {
|
|
||||||
m_currentDestination = dest;
|
|
||||||
m_drawerOpen = false; // Close drawer on navigation
|
|
||||||
}
|
|
||||||
|
|
||||||
NavDestination getCurrentDestination() const { return m_currentDestination; }
|
|
||||||
|
|
||||||
// Screen access
|
|
||||||
HomeScreen& homeScreen() { return m_homeScreen; }
|
|
||||||
SendScreen& sendScreen() { return m_sendScreen; }
|
|
||||||
ReceiveScreen& receiveScreen() { return m_receiveScreen; }
|
|
||||||
TransactionsScreen& transactionsScreen() { return m_transactionsScreen; }
|
|
||||||
MiningScreen& miningScreen() { return m_miningScreen; }
|
|
||||||
PeersScreen& peersScreen() { return m_peersScreen; }
|
|
||||||
SettingsScreen& settingsScreen() { return m_settingsScreen; }
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderAppBar();
|
|
||||||
void renderNavigationDrawer();
|
|
||||||
void renderContent();
|
|
||||||
void renderStatusBar();
|
|
||||||
void renderSearchOverlay();
|
|
||||||
|
|
||||||
NavDestination m_currentDestination = NavDestination::Home;
|
|
||||||
ConnectionStatus m_status;
|
|
||||||
|
|
||||||
std::function<void(NavDestination)> m_onNavigation;
|
|
||||||
std::function<void(const std::string&)> m_onSearch;
|
|
||||||
std::function<void()> m_onSettingsClick;
|
|
||||||
|
|
||||||
// Navigation drawer state
|
|
||||||
bool m_drawerOpen = false;
|
|
||||||
AnimatedValue<float> m_drawerAnimation{0.0f};
|
|
||||||
|
|
||||||
// Search state
|
|
||||||
bool m_searchOpen = false;
|
|
||||||
char m_searchBuffer[256] = {0};
|
|
||||||
|
|
||||||
// Screens
|
|
||||||
HomeScreen m_homeScreen;
|
|
||||||
SendScreen m_sendScreen;
|
|
||||||
ReceiveScreen m_receiveScreen;
|
|
||||||
TransactionsScreen m_transactionsScreen;
|
|
||||||
MiningScreen m_miningScreen;
|
|
||||||
PeersScreen m_peersScreen;
|
|
||||||
SettingsScreen m_settingsScreen;
|
|
||||||
|
|
||||||
// Layout values — read from ui.toml schema at runtime
|
|
||||||
static float APP_BAR_HEIGHT;
|
|
||||||
static float STATUS_BAR_HEIGHT;
|
|
||||||
static float DRAWER_WIDTH;
|
|
||||||
|
|
||||||
static void initLayoutConstants() {
|
|
||||||
APP_BAR_HEIGHT = schema::UI().drawElement("components.main-layout", "app-bar-height").size;
|
|
||||||
STATUS_BAR_HEIGHT = schema::UI().drawElement("components.main-layout", "status-bar-height").size;
|
|
||||||
DRAWER_WIDTH = schema::UI().drawElement("components.main-layout", "drawer-width").size;
|
|
||||||
if (APP_BAR_HEIGHT <= 0) APP_BAR_HEIGHT = 64.0f;
|
|
||||||
if (STATUS_BAR_HEIGHT <= 0) STATUS_BAR_HEIGHT = 32.0f;
|
|
||||||
if (DRAWER_WIDTH <= 0) DRAWER_WIDTH = 256.0f;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Static member definitions
|
|
||||||
inline float MainLayout::APP_BAR_HEIGHT = 64.0f;
|
|
||||||
inline float MainLayout::STATUS_BAR_HEIGHT = 32.0f;
|
|
||||||
inline float MainLayout::DRAWER_WIDTH = 256.0f;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void MainLayout::render() {
|
|
||||||
static bool layoutInitialized = false;
|
|
||||||
if (!layoutInitialized) {
|
|
||||||
initLayoutConstants();
|
|
||||||
layoutInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
float windowWidth = io.DisplaySize.x;
|
|
||||||
float windowHeight = io.DisplaySize.y;
|
|
||||||
|
|
||||||
// Full-screen window
|
|
||||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
||||||
ImGui::SetNextWindowSize(ImVec2(windowWidth, windowHeight));
|
|
||||||
|
|
||||||
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoTitleBar |
|
|
||||||
ImGuiWindowFlags_NoResize |
|
|
||||||
ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoCollapse |
|
|
||||||
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, Background());
|
|
||||||
|
|
||||||
if (ImGui::Begin("MainLayout", nullptr, windowFlags)) {
|
|
||||||
// App bar
|
|
||||||
renderAppBar();
|
|
||||||
|
|
||||||
// Content area with optional drawer
|
|
||||||
float contentY = APP_BAR_HEIGHT;
|
|
||||||
float contentHeight = windowHeight - APP_BAR_HEIGHT - STATUS_BAR_HEIGHT;
|
|
||||||
|
|
||||||
// Update drawer animation
|
|
||||||
float targetDrawer = m_drawerOpen ? 1.0f : 0.0f;
|
|
||||||
m_drawerAnimation.animateTo(targetDrawer, duration::Medium);
|
|
||||||
m_drawerAnimation.update(io.DeltaTime);
|
|
||||||
float drawerProgress = m_drawerAnimation.value();
|
|
||||||
|
|
||||||
// Navigation drawer (slides in from left)
|
|
||||||
if (drawerProgress > 0.01f) {
|
|
||||||
renderNavigationDrawer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content offset based on drawer
|
|
||||||
float contentX = drawerProgress * DRAWER_WIDTH;
|
|
||||||
|
|
||||||
// Content area
|
|
||||||
ImGui::SetCursorPos(ImVec2(contentX, contentY));
|
|
||||||
ImGui::BeginChild("Content", ImVec2(windowWidth - contentX, contentHeight), false,
|
|
||||||
ImGuiWindowFlags_NoScrollbar);
|
|
||||||
{
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::dp(3), spacing::dp(3)));
|
|
||||||
renderContent();
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
ImGui::EndChild();
|
|
||||||
|
|
||||||
// Status bar
|
|
||||||
ImGui::SetCursorPos(ImVec2(0, windowHeight - STATUS_BAR_HEIGHT));
|
|
||||||
renderStatusBar();
|
|
||||||
|
|
||||||
// Search overlay
|
|
||||||
if (m_searchOpen) {
|
|
||||||
renderSearchOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click outside drawer to close
|
|
||||||
if (m_drawerOpen && ImGui::IsMouseClicked(0)) {
|
|
||||||
ImVec2 mousePos = ImGui::GetMousePos();
|
|
||||||
if (mousePos.x > DRAWER_WIDTH) {
|
|
||||||
m_drawerOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::End();
|
|
||||||
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MainLayout::renderAppBar() {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
float windowWidth = io.DisplaySize.x;
|
|
||||||
|
|
||||||
// App bar background
|
|
||||||
ImVec2 appBarPos = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
appBarPos,
|
|
||||||
ImVec2(appBarPos.x + windowWidth, appBarPos.y + APP_BAR_HEIGHT),
|
|
||||||
ImGui::GetColorU32(Surface())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Elevation shadow
|
|
||||||
DrawShadow(ImVec2(appBarPos.x, appBarPos.y + APP_BAR_HEIGHT - 4),
|
|
||||||
ImVec2(windowWidth, 4), 4);
|
|
||||||
|
|
||||||
ImGui::SetCursorPos(ImVec2(spacing::dp(1), (APP_BAR_HEIGHT - schema::UI().drawElement("components.main-layout", "app-bar-button-size").size) / 2));
|
|
||||||
|
|
||||||
// Menu button
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(OnSurface(), 0.08f));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("components.main-layout", "app-bar-btn-rounding").size);
|
|
||||||
|
|
||||||
float appBarBtnSz = schema::UI().drawElement("components.main-layout", "app-bar-button-size").size;
|
|
||||||
if (ImGui::Button(ICON_MD_MENU, ImVec2(appBarBtnSz, appBarBtnSz))) {
|
|
||||||
m_drawerOpen = !m_drawerOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
|
|
||||||
// Title
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
ImGui::SetCursorPosY((APP_BAR_HEIGHT - schema::UI().drawElement("components.main-layout", "title-font-height").size) / 2);
|
|
||||||
Typography::instance().text(TypeStyle::H6, "DragonX Wallet");
|
|
||||||
|
|
||||||
// Right actions
|
|
||||||
float rightOffset = windowWidth - spacing::dp(1) - appBarBtnSz;
|
|
||||||
|
|
||||||
// Settings button
|
|
||||||
ImGui::SameLine(rightOffset);
|
|
||||||
ImGui::SetCursorPosY((APP_BAR_HEIGHT - appBarBtnSz) / 2);
|
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(OnSurface(), 0.08f));
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("components.main-layout", "app-bar-btn-rounding").size);
|
|
||||||
|
|
||||||
if (ImGui::Button(ICON_MD_SETTINGS, ImVec2(appBarBtnSz, appBarBtnSz))) {
|
|
||||||
navigateTo(NavDestination::Settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search button
|
|
||||||
ImGui::SameLine(rightOffset - 48);
|
|
||||||
ImGui::SetCursorPosY((APP_BAR_HEIGHT - appBarBtnSz) / 2);
|
|
||||||
|
|
||||||
if (ImGui::Button(ICON_MD_SEARCH, ImVec2(appBarBtnSz, appBarBtnSz))) {
|
|
||||||
m_searchOpen = !m_searchOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MainLayout::renderNavigationDrawer() {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
float windowHeight = io.DisplaySize.y;
|
|
||||||
float drawerProgress = m_drawerAnimation.value();
|
|
||||||
|
|
||||||
// Scrim (semi-transparent overlay)
|
|
||||||
if (drawerProgress > 0.01f) {
|
|
||||||
ImVec2 scrimStart = ImVec2(DRAWER_WIDTH, APP_BAR_HEIGHT);
|
|
||||||
ImVec2 scrimEnd = ImVec2(io.DisplaySize.x, windowHeight - STATUS_BAR_HEIGHT);
|
|
||||||
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
scrimStart, scrimEnd,
|
|
||||||
ImGui::GetColorU32(ImVec4(0, 0, 0, 0.5f * drawerProgress))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drawer background
|
|
||||||
float drawerX = (drawerProgress - 1.0f) * DRAWER_WIDTH;
|
|
||||||
ImVec2 drawerStart = ImVec2(0, APP_BAR_HEIGHT);
|
|
||||||
ImVec2 drawerEnd = ImVec2(DRAWER_WIDTH, windowHeight - STATUS_BAR_HEIGHT);
|
|
||||||
|
|
||||||
ImGui::SetCursorPos(ImVec2(drawerX, APP_BAR_HEIGHT));
|
|
||||||
ImGui::BeginChild("NavDrawer", ImVec2(DRAWER_WIDTH, windowHeight - APP_BAR_HEIGHT - STATUS_BAR_HEIGHT),
|
|
||||||
false, ImGuiWindowFlags_NoScrollbar);
|
|
||||||
{
|
|
||||||
// Drawer surface
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
pos,
|
|
||||||
ImVec2(pos.x + DRAWER_WIDTH, pos.y + windowHeight),
|
|
||||||
ImGui::GetColorU32(Surface())
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Navigation items
|
|
||||||
auto navItem = [this](const char* icon, const char* label, NavDestination dest) {
|
|
||||||
bool selected = (m_currentDestination == dest);
|
|
||||||
|
|
||||||
NavItemSpec spec;
|
|
||||||
spec.icon = icon;
|
|
||||||
spec.label = label;
|
|
||||||
spec.selected = selected;
|
|
||||||
|
|
||||||
if (NavItem(spec)) {
|
|
||||||
navigateTo(dest);
|
|
||||||
if (m_onNavigation) m_onNavigation(dest);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
navItem(ICON_MD_HOME, "Home", NavDestination::Home);
|
|
||||||
navItem(ICON_MD_CALL_MADE, "Send", NavDestination::Send);
|
|
||||||
navItem(ICON_MD_CALL_RECEIVED, "Receive", NavDestination::Receive);
|
|
||||||
navItem(ICON_MD_RECEIPT, "Transactions", NavDestination::Transactions);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Divider
|
|
||||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddLine(
|
|
||||||
ImVec2(divStart.x + spacing::dp(2), divStart.y),
|
|
||||||
ImVec2(divStart.x + DRAWER_WIDTH - spacing::dp(2), divStart.y),
|
|
||||||
ImGui::GetColorU32(Divider())
|
|
||||||
);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
navItem(ICON_MD_CONSTRUCTION, "Mining", NavDestination::Mining);
|
|
||||||
navItem(ICON_MD_HUB, "Network", NavDestination::Peers);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Another divider
|
|
||||||
divStart = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddLine(
|
|
||||||
ImVec2(divStart.x + spacing::dp(2), divStart.y),
|
|
||||||
ImVec2(divStart.x + DRAWER_WIDTH - spacing::dp(2), divStart.y),
|
|
||||||
ImGui::GetColorU32(Divider())
|
|
||||||
);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
navItem(ICON_MD_SETTINGS, "Settings", NavDestination::Settings);
|
|
||||||
}
|
|
||||||
ImGui::EndChild();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MainLayout::renderContent() {
|
|
||||||
// Padding
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
switch (m_currentDestination) {
|
|
||||||
case NavDestination::Home:
|
|
||||||
m_homeScreen.render();
|
|
||||||
break;
|
|
||||||
case NavDestination::Send:
|
|
||||||
m_sendScreen.render();
|
|
||||||
break;
|
|
||||||
case NavDestination::Receive:
|
|
||||||
m_receiveScreen.render();
|
|
||||||
break;
|
|
||||||
case NavDestination::Transactions:
|
|
||||||
m_transactionsScreen.render();
|
|
||||||
break;
|
|
||||||
case NavDestination::Mining:
|
|
||||||
m_miningScreen.render();
|
|
||||||
break;
|
|
||||||
case NavDestination::Peers:
|
|
||||||
m_peersScreen.render();
|
|
||||||
break;
|
|
||||||
case NavDestination::Settings:
|
|
||||||
m_settingsScreen.render();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MainLayout::renderStatusBar() {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
float windowWidth = io.DisplaySize.x;
|
|
||||||
|
|
||||||
// Status bar background
|
|
||||||
ImVec2 statusPos = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
statusPos,
|
|
||||||
ImVec2(statusPos.x + windowWidth, statusPos.y + STATUS_BAR_HEIGHT),
|
|
||||||
ImGui::GetColorU32(SurfaceVariant())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Top border
|
|
||||||
ImGui::GetWindowDrawList()->AddLine(
|
|
||||||
statusPos,
|
|
||||||
ImVec2(statusPos.x + windowWidth, statusPos.y),
|
|
||||||
ImGui::GetColorU32(Divider())
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::SetCursorPos(ImVec2(spacing::dp(2), ImGui::GetCursorPosY() + 6));
|
|
||||||
|
|
||||||
// Connection status
|
|
||||||
if (m_status.connected) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, colors::Green500, ICON_MD_FIBER_MANUAL_RECORD " Connected");
|
|
||||||
} else {
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, colors::Red500, ICON_MD_FIBER_MANUAL_RECORD " Disconnected");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(3));
|
|
||||||
|
|
||||||
// Block height
|
|
||||||
char heightStr[64];
|
|
||||||
snprintf(heightStr, sizeof(heightStr), "Block: %d", m_status.blockHeight);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), heightStr);
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(3));
|
|
||||||
|
|
||||||
// Peer count
|
|
||||||
char peerStr[32];
|
|
||||||
snprintf(peerStr, sizeof(peerStr), "Peers: %d", m_status.peers);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), peerStr);
|
|
||||||
|
|
||||||
// Sync progress (if syncing)
|
|
||||||
if (m_status.syncing) {
|
|
||||||
ImGui::SameLine(0, spacing::dp(3));
|
|
||||||
|
|
||||||
// Mini progress bar
|
|
||||||
ImVec2 progressPos = ImGui::GetCursorScreenPos();
|
|
||||||
float progressWidth = schema::UI().drawElement("components.main-layout", "sync-bar-width").size;
|
|
||||||
float progressHeight = schema::UI().drawElement("components.main-layout", "sync-bar-height").size;
|
|
||||||
|
|
||||||
progressPos.y += 6;
|
|
||||||
|
|
||||||
// Background
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
progressPos,
|
|
||||||
ImVec2(progressPos.x + progressWidth, progressPos.y + progressHeight),
|
|
||||||
ImGui::GetColorU32(Divider()),
|
|
||||||
2.0f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
progressPos,
|
|
||||||
ImVec2(progressPos.x + progressWidth * m_status.syncProgress, progressPos.y + progressHeight),
|
|
||||||
ImGui::GetColorU32(Primary()),
|
|
||||||
2.0f
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(progressWidth, 0));
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
char syncStr[32];
|
|
||||||
snprintf(syncStr, sizeof(syncStr), "%.1f%%", m_status.syncProgress * 100.0f);
|
|
||||||
ImU32 syncTextColor = IsDarkTheme() ? Primary() : PrimaryVariant();
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, syncTextColor, syncStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Network hashrate (right aligned)
|
|
||||||
if (m_status.networkHashrate > 0) {
|
|
||||||
char hashrateStr[64];
|
|
||||||
if (m_status.networkHashrate > 1e9) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "Network: %.2f GH/s",
|
|
||||||
m_status.networkHashrate / 1e9);
|
|
||||||
} else if (m_status.networkHashrate > 1e6) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "Network: %.2f MH/s",
|
|
||||||
m_status.networkHashrate / 1e6);
|
|
||||||
} else {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "Network: %.2f KH/s",
|
|
||||||
m_status.networkHashrate / 1e3);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(hashrateStr);
|
|
||||||
ImGui::SameLine(windowWidth - textSize.x - spacing::dp(2));
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), hashrateStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MainLayout::renderSearchOverlay() {
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
float windowWidth = io.DisplaySize.x;
|
|
||||||
|
|
||||||
// Search bar at top (below app bar)
|
|
||||||
ImGui::SetCursorPos(ImVec2(0, APP_BAR_HEIGHT));
|
|
||||||
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 8;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = windowWidth;
|
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::dp(2), spacing::dp(2)));
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
ImGui::SetNextItemWidth(windowWidth - spacing::dp(8));
|
|
||||||
|
|
||||||
TextFieldSpec textSpec;
|
|
||||||
textSpec.placeholder = "Search transactions, addresses...";
|
|
||||||
textSpec.variant = TextFieldVariant::Filled;
|
|
||||||
textSpec.leadingIcon = ICON_MD_SEARCH;
|
|
||||||
textSpec.width = windowWidth - spacing::dp(12);
|
|
||||||
|
|
||||||
TextFieldResult result = TextField("global_search", m_searchBuffer,
|
|
||||||
sizeof(m_searchBuffer), textSpec);
|
|
||||||
|
|
||||||
if (result.submitted && strlen(m_searchBuffer) > 0) {
|
|
||||||
if (m_onSearch) m_onSearch(m_searchBuffer);
|
|
||||||
m_searchOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close on escape
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
|
||||||
m_searchOpen = false;
|
|
||||||
memset(m_searchBuffer, 0, sizeof(m_searchBuffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Mining Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Mining controls and statistics display
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Mining statistics
|
|
||||||
*/
|
|
||||||
struct MiningStats {
|
|
||||||
bool isMining = false;
|
|
||||||
int threads = 0;
|
|
||||||
int maxThreads = 8;
|
|
||||||
double hashrate = 0.0; // Hashes per second
|
|
||||||
double networkHashrate = 0.0; // Network hashrate
|
|
||||||
int blocksFound = 0;
|
|
||||||
double totalMined = 0.0;
|
|
||||||
std::string miningAddress;
|
|
||||||
int currentHeight = 0;
|
|
||||||
double networkDifficulty = 0.0;
|
|
||||||
std::string estimatedTimeToBlock;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Recent mined block
|
|
||||||
*/
|
|
||||||
struct MinedBlock {
|
|
||||||
int height;
|
|
||||||
double reward;
|
|
||||||
std::string time;
|
|
||||||
std::string txid;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Mining screen with controls and stats
|
|
||||||
*/
|
|
||||||
class MiningScreen {
|
|
||||||
public:
|
|
||||||
void setStats(const MiningStats& stats) { m_stats = stats; }
|
|
||||||
void setMinedBlocks(const std::vector<MinedBlock>& blocks) { m_minedBlocks = blocks; }
|
|
||||||
|
|
||||||
void setOnStartMining(std::function<void(int threads)> callback) {
|
|
||||||
m_onStartMining = callback;
|
|
||||||
}
|
|
||||||
void setOnStopMining(std::function<void()> callback) {
|
|
||||||
m_onStopMining = callback;
|
|
||||||
}
|
|
||||||
void setOnSetAddress(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onSetAddress = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderMiningControls();
|
|
||||||
void renderStatsCards();
|
|
||||||
void renderMinedBlocksList();
|
|
||||||
|
|
||||||
MiningStats m_stats;
|
|
||||||
std::vector<MinedBlock> m_minedBlocks;
|
|
||||||
|
|
||||||
std::function<void(int)> m_onStartMining;
|
|
||||||
std::function<void()> m_onStopMining;
|
|
||||||
std::function<void(const std::string&)> m_onSetAddress;
|
|
||||||
|
|
||||||
int m_selectedThreads = 4;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void MiningScreen::render() {
|
|
||||||
// Title
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
Typography::instance().text(TypeStyle::H5, "Mining");
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Mining status indicator
|
|
||||||
if (m_stats.isMining) {
|
|
||||||
ChipSpec chipSpec;
|
|
||||||
chipSpec.variant = ChipVariant::Filled;
|
|
||||||
chipSpec.color = colors::Green500;
|
|
||||||
Chip(ICON_MD_HARDWARE " Mining Active", chipSpec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Mining controls card
|
|
||||||
renderMiningControls();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Stats cards row
|
|
||||||
renderStatsCards();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Mined blocks history
|
|
||||||
renderMinedBlocksList();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MiningScreen::renderMiningControls() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 2;
|
|
||||||
cardSpec.padding = spacing::dp(3);
|
|
||||||
|
|
||||||
ImGui::PushID("mining_controls");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MINING CONTROLS");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Thread selector
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
Typography::instance().text(TypeStyle::Body1, "Mining Threads:");
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
ImGui::SetNextItemWidth(150.0f);
|
|
||||||
ImGui::SliderInt("##threads", &m_selectedThreads, 1, m_stats.maxThreads);
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
char threadInfo[64];
|
|
||||||
snprintf(threadInfo, sizeof(threadInfo), "(%d available)", m_stats.maxThreads);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), threadInfo);
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Mining address display
|
|
||||||
if (!m_stats.miningAddress.empty()) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "Mining to:");
|
|
||||||
|
|
||||||
std::string displayAddr = m_stats.miningAddress;
|
|
||||||
if (displayAddr.length() > 50) {
|
|
||||||
displayAddr = displayAddr.substr(0, 25) + "..." +
|
|
||||||
displayAddr.substr(displayAddr.length() - 20);
|
|
||||||
}
|
|
||||||
Typography::instance().text(TypeStyle::Body2, displayAddr.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Start/Stop buttons
|
|
||||||
if (m_stats.isMining) {
|
|
||||||
// Stop button (red)
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(colors::Red700.x, colors::Red700.y, colors::Red700.z, 1.0f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(colors::Red500.x, colors::Red500.y, colors::Red500.z, 1.0f));
|
|
||||||
|
|
||||||
if (ImGui::Button("⏹ STOP MINING", ImVec2(150, 40))) {
|
|
||||||
if (m_onStopMining) m_onStopMining();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleColor(2);
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Live hashrate display
|
|
||||||
char hashrateStr[64];
|
|
||||||
if (m_stats.hashrate > 1000000) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f MH/s", m_stats.hashrate / 1000000.0);
|
|
||||||
} else if (m_stats.hashrate > 1000) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f KH/s", m_stats.hashrate / 1000.0);
|
|
||||||
} else {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.0f H/s", m_stats.hashrate);
|
|
||||||
}
|
|
||||||
|
|
||||||
Typography::instance().textColored(TypeStyle::H6, Primary(), hashrateStr);
|
|
||||||
} else {
|
|
||||||
// Start button (green)
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(colors::Green700.x, colors::Green700.y, colors::Green700.z, 1.0f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(colors::Green500.x, colors::Green500.y, colors::Green500.z, 1.0f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
|
|
||||||
|
|
||||||
if (ImGui::Button(ICON_MD_PLAY_ARROW " START MINING", ImVec2(150, 40))) {
|
|
||||||
if (m_onStartMining) m_onStartMining(m_selectedThreads);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleColor(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MiningScreen::renderStatsCards() {
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
float cardWidth = (availWidth - spacing::dp(2) * 2) / 3.0f;
|
|
||||||
|
|
||||||
// Hashrate card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("hashrate_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR HASHRATE");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char hashrateStr[64];
|
|
||||||
if (m_stats.hashrate > 1000000) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.hashrate / 1000000.0);
|
|
||||||
Typography::instance().text(TypeStyle::H4, hashrateStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "MH/s");
|
|
||||||
} else if (m_stats.hashrate > 1000) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.hashrate / 1000.0);
|
|
||||||
Typography::instance().text(TypeStyle::H4, hashrateStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "KH/s");
|
|
||||||
} else {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.0f", m_stats.hashrate);
|
|
||||||
Typography::instance().text(TypeStyle::H4, hashrateStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "H/s");
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Network hashrate card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("network_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK HASHRATE");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char hashrateStr[64];
|
|
||||||
if (m_stats.networkHashrate > 1000000000) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.networkHashrate / 1000000000.0);
|
|
||||||
Typography::instance().text(TypeStyle::H4, hashrateStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "GH/s");
|
|
||||||
} else if (m_stats.networkHashrate > 1000000) {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.networkHashrate / 1000000.0);
|
|
||||||
Typography::instance().text(TypeStyle::H4, hashrateStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "MH/s");
|
|
||||||
} else {
|
|
||||||
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.networkHashrate / 1000.0);
|
|
||||||
Typography::instance().text(TypeStyle::H4, hashrateStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "KH/s");
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Total mined card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("mined_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL MINED");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char minedStr[64];
|
|
||||||
snprintf(minedStr, sizeof(minedStr), "%.4f", m_stats.totalMined);
|
|
||||||
Typography::instance().text(TypeStyle::H4, minedStr);
|
|
||||||
ImGui::SameLine();
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "DRGX");
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(0.5f)));
|
|
||||||
|
|
||||||
char blocksStr[32];
|
|
||||||
snprintf(blocksStr, sizeof(blocksStr), "%d blocks found", m_stats.blocksFound);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), blocksStr);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void MiningScreen::renderMinedBlocksList() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
|
|
||||||
ImGui::PushID("mined_blocks");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCKS YOU MINED");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
if (m_minedBlocks.empty()) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
|
||||||
"No blocks mined yet. Keep mining!");
|
|
||||||
} else {
|
|
||||||
BeginList("mined_list", false);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < m_minedBlocks.size() && i < 10; i++) {
|
|
||||||
const auto& block = m_minedBlocks[i];
|
|
||||||
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = "🏆";
|
|
||||||
|
|
||||||
char primaryStr[64];
|
|
||||||
snprintf(primaryStr, sizeof(primaryStr), "Block #%d", block.height);
|
|
||||||
itemSpec.primaryText = primaryStr;
|
|
||||||
|
|
||||||
char secondaryStr[64];
|
|
||||||
snprintf(secondaryStr, sizeof(secondaryStr), "+%.4f DRGX • %s",
|
|
||||||
block.reward, block.time.c_str());
|
|
||||||
itemSpec.secondaryText = secondaryStr;
|
|
||||||
|
|
||||||
itemSpec.dividerBelow = (i < m_minedBlocks.size() - 1 && i < 9);
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Peers Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Display connected peers with Material list
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Peer connection info
|
|
||||||
*/
|
|
||||||
struct PeerInfo {
|
|
||||||
int id;
|
|
||||||
std::string address;
|
|
||||||
std::string subversion;
|
|
||||||
int startingHeight;
|
|
||||||
int currentHeight;
|
|
||||||
int latency; // ms
|
|
||||||
int64_t bytesSent;
|
|
||||||
int64_t bytesReceived;
|
|
||||||
std::string connTime; // Connection duration
|
|
||||||
bool isInbound;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Network statistics
|
|
||||||
*/
|
|
||||||
struct NetworkStats {
|
|
||||||
int totalConnections;
|
|
||||||
int inboundCount;
|
|
||||||
int outboundCount;
|
|
||||||
int64_t totalBytesSent;
|
|
||||||
int64_t totalBytesReceived;
|
|
||||||
int currentHeight;
|
|
||||||
std::string networkName;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Peers screen with network info
|
|
||||||
*/
|
|
||||||
class PeersScreen {
|
|
||||||
public:
|
|
||||||
void setPeers(const std::vector<PeerInfo>& peers) { m_peers = peers; }
|
|
||||||
void setNetworkStats(const NetworkStats& stats) { m_networkStats = stats; }
|
|
||||||
|
|
||||||
void setOnDisconnectPeer(std::function<void(int peerId)> callback) {
|
|
||||||
m_onDisconnectPeer = callback;
|
|
||||||
}
|
|
||||||
void setOnAddNode(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onAddNode = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderNetworkStats();
|
|
||||||
void renderPeersList();
|
|
||||||
void renderPeerDetails();
|
|
||||||
void renderAddNodeDialog();
|
|
||||||
|
|
||||||
std::vector<PeerInfo> m_peers;
|
|
||||||
NetworkStats m_networkStats;
|
|
||||||
|
|
||||||
std::function<void(int)> m_onDisconnectPeer;
|
|
||||||
std::function<void(const std::string&)> m_onAddNode;
|
|
||||||
|
|
||||||
int m_selectedPeerId = -1;
|
|
||||||
bool m_showDetails = false;
|
|
||||||
bool m_showAddNode = false;
|
|
||||||
char m_addNodeBuffer[256] = {0};
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void PeersScreen::render() {
|
|
||||||
// Title
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
Typography::instance().text(TypeStyle::H5, "Network Peers");
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 120);
|
|
||||||
|
|
||||||
if (OutlinedButton("+ ADD NODE")) {
|
|
||||||
m_showAddNode = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Network stats cards
|
|
||||||
renderNetworkStats();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Peers list
|
|
||||||
renderPeersList();
|
|
||||||
|
|
||||||
// Dialogs
|
|
||||||
if (m_showDetails) {
|
|
||||||
renderPeerDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_showAddNode) {
|
|
||||||
renderAddNodeDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void PeersScreen::renderNetworkStats() {
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
float cardWidth = (availWidth - spacing::dp(2) * 3) / 4.0f;
|
|
||||||
|
|
||||||
// Connections card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("conn_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTIONS");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char connStr[32];
|
|
||||||
snprintf(connStr, sizeof(connStr), "%d", m_networkStats.totalConnections);
|
|
||||||
Typography::instance().text(TypeStyle::H4, connStr);
|
|
||||||
|
|
||||||
char detailStr[64];
|
|
||||||
snprintf(detailStr, sizeof(detailStr), ICON_MD_ARROW_DOWNWARD "%d in " ICON_MD_ARROW_UPWARD "%d out",
|
|
||||||
m_networkStats.inboundCount, m_networkStats.outboundCount);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), detailStr);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Block height card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("height_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char heightStr[32];
|
|
||||||
snprintf(heightStr, sizeof(heightStr), "%d", m_networkStats.currentHeight);
|
|
||||||
Typography::instance().text(TypeStyle::H4, heightStr);
|
|
||||||
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
|
||||||
m_networkStats.networkName.c_str());
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Data sent card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("sent_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA SENT");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char dataStr[32];
|
|
||||||
double mb = m_networkStats.totalBytesSent / (1024.0 * 1024.0);
|
|
||||||
if (mb > 1024) {
|
|
||||||
snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0);
|
|
||||||
} else {
|
|
||||||
snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb);
|
|
||||||
}
|
|
||||||
Typography::instance().text(TypeStyle::H5, dataStr);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Data received card
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
cardSpec.width = cardWidth;
|
|
||||||
|
|
||||||
ImGui::PushID("recv_card");
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA RECEIVED");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
char dataStr[32];
|
|
||||||
double mb = m_networkStats.totalBytesReceived / (1024.0 * 1024.0);
|
|
||||||
if (mb > 1024) {
|
|
||||||
snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0);
|
|
||||||
} else {
|
|
||||||
snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb);
|
|
||||||
}
|
|
||||||
Typography::instance().text(TypeStyle::H5, dataStr);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void PeersScreen::renderPeersList() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
if (m_peers.empty()) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(4)));
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
const char* emptyText = "No peers connected";
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(emptyText);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(4)));
|
|
||||||
} else {
|
|
||||||
BeginList("peers_list", false);
|
|
||||||
|
|
||||||
for (const auto& peer : m_peers) {
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
|
|
||||||
// Direction icon
|
|
||||||
itemSpec.leadingIcon = peer.isInbound ? ICON_MD_ARROW_DOWNWARD : ICON_MD_ARROW_UPWARD;
|
|
||||||
|
|
||||||
// Address as primary
|
|
||||||
itemSpec.primaryText = peer.address.c_str();
|
|
||||||
|
|
||||||
// Version + latency as secondary
|
|
||||||
char secondaryStr[128];
|
|
||||||
snprintf(secondaryStr, sizeof(secondaryStr), "%s • %dms • Height: %d",
|
|
||||||
peer.subversion.c_str(), peer.latency, peer.currentHeight);
|
|
||||||
itemSpec.secondaryText = secondaryStr;
|
|
||||||
|
|
||||||
itemSpec.trailingIcon = ICON_MD_INFO;
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
char peerId[16];
|
|
||||||
snprintf(peerId, sizeof(peerId), "peer_%d", peer.id);
|
|
||||||
ImGui::PushID(peerId);
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
m_selectedPeerId = peer.id;
|
|
||||||
m_showDetails = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void PeersScreen::renderPeerDetails() {
|
|
||||||
// Find selected peer
|
|
||||||
const PeerInfo* selected = nullptr;
|
|
||||||
for (const auto& peer : m_peers) {
|
|
||||||
if (peer.id == m_selectedPeerId) {
|
|
||||||
selected = &peer;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selected) {
|
|
||||||
m_showDetails = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogSpec dialogSpec;
|
|
||||||
dialogSpec.title = "Peer Details";
|
|
||||||
dialogSpec.maxWidth = 450.0f;
|
|
||||||
|
|
||||||
DialogResult result = BeginDialog("peer_details", dialogSpec);
|
|
||||||
|
|
||||||
if (result.isOpen) {
|
|
||||||
const PeerInfo& peer = *selected;
|
|
||||||
|
|
||||||
// Direction badge
|
|
||||||
ChipSpec chipSpec;
|
|
||||||
chipSpec.variant = ChipVariant::Filled;
|
|
||||||
chipSpec.color = peer.isInbound ? colors::Blue500 : colors::Orange500;
|
|
||||||
Chip(peer.isInbound ? ICON_MD_ARROW_DOWNWARD " Inbound" : ICON_MD_ARROW_UPWARD " Outbound", chipSpec);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Address
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS");
|
|
||||||
Typography::instance().text(TypeStyle::Body1, peer.address.c_str());
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Client
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CLIENT");
|
|
||||||
Typography::instance().text(TypeStyle::Body2, peer.subversion.c_str());
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Height
|
|
||||||
char heightStr[64];
|
|
||||||
snprintf(heightStr, sizeof(heightStr), "%d (started at %d)",
|
|
||||||
peer.currentHeight, peer.startingHeight);
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT");
|
|
||||||
Typography::instance().text(TypeStyle::Body2, heightStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Latency
|
|
||||||
char latencyStr[32];
|
|
||||||
snprintf(latencyStr, sizeof(latencyStr), "%d ms", peer.latency);
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "LATENCY");
|
|
||||||
Typography::instance().text(TypeStyle::Body2, latencyStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Connected time
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTED");
|
|
||||||
Typography::instance().text(TypeStyle::Body2, peer.connTime.c_str());
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Data transferred
|
|
||||||
char dataStr[64];
|
|
||||||
snprintf(dataStr, sizeof(dataStr), ICON_MD_ARROW_UPWARD " %.2f MB " ICON_MD_ARROW_DOWNWARD " %.2f MB",
|
|
||||||
peer.bytesSent / (1024.0 * 1024.0),
|
|
||||||
peer.bytesReceived / (1024.0 * 1024.0));
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA TRANSFERRED");
|
|
||||||
Typography::instance().text(TypeStyle::Body2, dataStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
// Disconnect button (danger)
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, colors::withAlpha(colors::Red500, 0.1f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(colors::Red500, 0.2f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::Red500);
|
|
||||||
|
|
||||||
if (ImGui::Button("DISCONNECT", ImVec2(120, 36))) {
|
|
||||||
if (m_onDisconnectPeer) m_onDisconnectPeer(peer.id);
|
|
||||||
m_showDetails = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleColor(3);
|
|
||||||
|
|
||||||
ImGui::SameLine(availWidth - 80);
|
|
||||||
|
|
||||||
if (ContainedButton("CLOSE")) {
|
|
||||||
m_showDetails = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.dismissed) {
|
|
||||||
m_showDetails = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void PeersScreen::renderAddNodeDialog() {
|
|
||||||
DialogSpec dialogSpec;
|
|
||||||
dialogSpec.title = "Add Node";
|
|
||||||
dialogSpec.maxWidth = 400.0f;
|
|
||||||
|
|
||||||
DialogResult result = BeginDialog("add_node", dialogSpec);
|
|
||||||
|
|
||||||
if (result.isOpen) {
|
|
||||||
Typography::instance().text(TypeStyle::Body1,
|
|
||||||
"Enter the IP address or hostname of a node to connect to:");
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
TextFieldSpec textSpec;
|
|
||||||
textSpec.label = "Node Address";
|
|
||||||
textSpec.placeholder = "192.168.1.1:18030 or node.example.com";
|
|
||||||
textSpec.variant = TextFieldVariant::Outlined;
|
|
||||||
textSpec.width = -1;
|
|
||||||
|
|
||||||
TextField("add_node_addr", m_addNodeBuffer, sizeof(m_addNodeBuffer), textSpec);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 200);
|
|
||||||
|
|
||||||
if (TextButton("CANCEL")) {
|
|
||||||
m_showAddNode = false;
|
|
||||||
memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
bool canAdd = strlen(m_addNodeBuffer) > 0;
|
|
||||||
ImGui::BeginDisabled(!canAdd);
|
|
||||||
if (ContainedButton("ADD")) {
|
|
||||||
if (m_onAddNode) m_onAddNode(m_addNodeBuffer);
|
|
||||||
m_showAddNode = false;
|
|
||||||
memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer));
|
|
||||||
}
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.dismissed) {
|
|
||||||
m_showAddNode = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Receive Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Display receiving addresses with QR codes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Wallet address for receiving
|
|
||||||
*/
|
|
||||||
struct WalletAddress {
|
|
||||||
std::string address;
|
|
||||||
std::string label;
|
|
||||||
std::string type; // "shielded" or "transparent"
|
|
||||||
double balance;
|
|
||||||
bool isDefault;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Receive screen with address cards
|
|
||||||
*/
|
|
||||||
class ReceiveScreen {
|
|
||||||
public:
|
|
||||||
void setAddresses(const std::vector<WalletAddress>& addresses) {
|
|
||||||
m_addresses = addresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnCopyAddress(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onCopyAddress = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnNewAddress(std::function<void(bool shielded)> callback) {
|
|
||||||
m_onNewAddress = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnShowQR(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onShowQR = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnEditLabel(std::function<void(const std::string&, const std::string&)> callback) {
|
|
||||||
m_onEditLabel = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderAddressCard(const WalletAddress& addr, int index);
|
|
||||||
void renderNewAddressButton();
|
|
||||||
void renderQRCodePopup();
|
|
||||||
|
|
||||||
std::vector<WalletAddress> m_addresses;
|
|
||||||
std::function<void(const std::string&)> m_onCopyAddress;
|
|
||||||
std::function<void(bool shielded)> m_onNewAddress;
|
|
||||||
std::function<void(const std::string&)> m_onShowQR;
|
|
||||||
std::function<void(const std::string&, const std::string&)> m_onEditLabel;
|
|
||||||
|
|
||||||
std::string m_selectedQRAddress;
|
|
||||||
bool m_showQRPopup = false;
|
|
||||||
int m_selectedTab = 0; // 0 = shielded, 1 = transparent
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void ReceiveScreen::render() {
|
|
||||||
// Title
|
|
||||||
Typography::instance().text(TypeStyle::H5, "Receive DRGX");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Tab selection
|
|
||||||
TabSpec tabSpec;
|
|
||||||
tabSpec.variant = TabVariant::Fixed;
|
|
||||||
|
|
||||||
if (BeginTabs("receive_tabs", tabSpec)) {
|
|
||||||
if (Tab(ICON_MD_SHIELD " Shielded", m_selectedTab == 0)) {
|
|
||||||
m_selectedTab = 0;
|
|
||||||
}
|
|
||||||
if (Tab(ICON_MD_DESCRIPTION " Transparent", m_selectedTab == 1)) {
|
|
||||||
m_selectedTab = 1;
|
|
||||||
}
|
|
||||||
EndTabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Filter addresses by type
|
|
||||||
std::string filterType = m_selectedTab == 0 ? "shielded" : "transparent";
|
|
||||||
|
|
||||||
// Display filtered addresses
|
|
||||||
int visibleCount = 0;
|
|
||||||
for (size_t i = 0; i < m_addresses.size(); i++) {
|
|
||||||
if (m_addresses[i].type == filterType) {
|
|
||||||
renderAddressCard(m_addresses[i], static_cast<int>(i));
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
visibleCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty state
|
|
||||||
if (visibleCount == 0) {
|
|
||||||
CardSpec emptySpec;
|
|
||||||
emptySpec.elevation = 1;
|
|
||||||
emptySpec.padding = spacing::dp(4);
|
|
||||||
|
|
||||||
if (BeginCard(emptySpec)) {
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
// Center text
|
|
||||||
const char* emptyText = m_selectedTab == 0 ?
|
|
||||||
"No shielded addresses yet" : "No transparent addresses yet";
|
|
||||||
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(emptyText);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// New address button
|
|
||||||
renderNewAddressButton();
|
|
||||||
|
|
||||||
// QR popup
|
|
||||||
if (m_showQRPopup) {
|
|
||||||
renderQRCodePopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ReceiveScreen::renderAddressCard(const WalletAddress& addr, int index) {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = addr.isDefault ? 3 : 1;
|
|
||||||
cardSpec.padding = spacing::dp(2);
|
|
||||||
|
|
||||||
char cardId[32];
|
|
||||||
snprintf(cardId, sizeof(cardId), "addr_card_%d", index);
|
|
||||||
ImGui::PushID(cardId);
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
// Header row: Label + Default badge
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
if (!addr.label.empty()) {
|
|
||||||
Typography::instance().text(TypeStyle::Subtitle1, addr.label.c_str());
|
|
||||||
} else {
|
|
||||||
Typography::instance().textColored(TypeStyle::Subtitle1, OnSurfaceMedium(), "Unnamed Address");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addr.isDefault) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
ChipSpec chipSpec;
|
|
||||||
chipSpec.variant = ChipVariant::Filled;
|
|
||||||
chipSpec.color = Primary();
|
|
||||||
|
|
||||||
Chip("DEFAULT", chipSpec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Address (truncated with copy)
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
// Show truncated address
|
|
||||||
std::string displayAddr = addr.address;
|
|
||||||
if (displayAddr.length() > 40) {
|
|
||||||
displayAddr = displayAddr.substr(0, 20) + "..." +
|
|
||||||
displayAddr.substr(displayAddr.length() - 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
|
||||||
displayAddr.c_str());
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Balance
|
|
||||||
if (addr.balance > 0) {
|
|
||||||
char balanceStr[64];
|
|
||||||
snprintf(balanceStr, sizeof(balanceStr), "Balance: %.8f DRGX", addr.balance);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), balanceStr);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action buttons
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
if (OutlinedButton(ICON_MD_CONTENT_COPY " COPY")) {
|
|
||||||
if (m_onCopyAddress) m_onCopyAddress(addr.address);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(1));
|
|
||||||
|
|
||||||
if (OutlinedButton(ICON_MD_QR_CODE " QR")) {
|
|
||||||
m_selectedQRAddress = addr.address;
|
|
||||||
m_showQRPopup = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(1));
|
|
||||||
|
|
||||||
if (TextButton(ICON_MD_EDIT " EDIT")) {
|
|
||||||
// Would trigger edit label dialog
|
|
||||||
if (m_onEditLabel) m_onEditLabel(addr.address, addr.label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ReceiveScreen::renderNewAddressButton() {
|
|
||||||
// Floating action button style
|
|
||||||
float buttonWidth = 200.0f;
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - buttonWidth) * 0.5f);
|
|
||||||
|
|
||||||
if (ContainedButton("+ NEW ADDRESS")) {
|
|
||||||
if (m_onNewAddress) {
|
|
||||||
m_onNewAddress(m_selectedTab == 0); // true for shielded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void ReceiveScreen::renderQRCodePopup() {
|
|
||||||
DialogSpec dialogSpec;
|
|
||||||
dialogSpec.title = "QR Code";
|
|
||||||
dialogSpec.maxWidth = 400.0f;
|
|
||||||
|
|
||||||
DialogResult result = BeginDialog("qr_popup", dialogSpec);
|
|
||||||
|
|
||||||
if (result.isOpen) {
|
|
||||||
// QR code placeholder (actual QR generation would be separate)
|
|
||||||
float qrSize = 250.0f;
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - qrSize) * 0.5f);
|
|
||||||
|
|
||||||
// QR placeholder box
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
||||||
pos,
|
|
||||||
ImVec2(pos.x + qrSize, pos.y + qrSize),
|
|
||||||
ImGui::GetColorU32(Surface())
|
|
||||||
);
|
|
||||||
ImGui::GetWindowDrawList()->AddRect(
|
|
||||||
pos,
|
|
||||||
ImVec2(pos.x + qrSize, pos.y + qrSize),
|
|
||||||
ImGui::GetColorU32(Divider()),
|
|
||||||
4.0f
|
|
||||||
);
|
|
||||||
|
|
||||||
// Center text in QR box
|
|
||||||
const char* qrText = "QR CODE";
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(qrText);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(
|
|
||||||
pos.x + (qrSize - textSize.x) * 0.5f,
|
|
||||||
pos.y + (qrSize - textSize.y) * 0.5f
|
|
||||||
));
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), qrText);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(qrSize, qrSize + spacing::dp(2)));
|
|
||||||
|
|
||||||
// Address below QR
|
|
||||||
ImGui::TextWrapped("%s", m_selectedQRAddress.c_str());
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
float btnWidth = 100.0f;
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - btnWidth * 2 - spacing::dp(2)) * 0.5f);
|
|
||||||
|
|
||||||
if (OutlinedButton("COPY")) {
|
|
||||||
if (m_onCopyAddress) m_onCopyAddress(m_selectedQRAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
if (ContainedButton("CLOSE")) {
|
|
||||||
m_showQRPopup = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.dismissed) {
|
|
||||||
m_showQRPopup = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file screens.h
|
|
||||||
* @brief Unified include for all wallet screens
|
|
||||||
*
|
|
||||||
* Phase 7: Component Redesign
|
|
||||||
*
|
|
||||||
* This header provides access to all Material Design wallet screens:
|
|
||||||
* - HomeScreen: Dashboard with balances and quick actions
|
|
||||||
* - SendScreen: Material form for sending DRGX
|
|
||||||
* - ReceiveScreen: Address cards with QR codes
|
|
||||||
* - TransactionsScreen: Transaction history list
|
|
||||||
* - MiningScreen: Mining controls and stats
|
|
||||||
* - PeersScreen: Network peer information
|
|
||||||
* - SettingsScreen: Application settings
|
|
||||||
* - MainLayout: Full application layout with navigation
|
|
||||||
* - ConfirmationDialog: Modal confirmations for critical actions
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* @code
|
|
||||||
* #include "ui/screens/screens.h"
|
|
||||||
* using namespace dragonx::ui::screens;
|
|
||||||
*
|
|
||||||
* // Create main layout
|
|
||||||
* MainLayout layout;
|
|
||||||
*
|
|
||||||
* // Set up data
|
|
||||||
* BalanceInfo balance;
|
|
||||||
* balance.total = 12345.67;
|
|
||||||
* layout.homeScreen().setBalance(balance);
|
|
||||||
*
|
|
||||||
* // Render (in main loop)
|
|
||||||
* layout.render();
|
|
||||||
*
|
|
||||||
* // Handle confirmations
|
|
||||||
* ConfirmationDialog::instance().render();
|
|
||||||
* @endcode
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
// Material Design system
|
|
||||||
#include "../material/material.h"
|
|
||||||
|
|
||||||
// Individual screens
|
|
||||||
#include "home_screen.h"
|
|
||||||
#include "send_screen.h"
|
|
||||||
#include "receive_screen.h"
|
|
||||||
#include "transactions_screen.h"
|
|
||||||
#include "mining_screen.h"
|
|
||||||
#include "peers_screen.h"
|
|
||||||
#include "settings_screen.h"
|
|
||||||
|
|
||||||
// Main layout and navigation
|
|
||||||
#include "main_layout.h"
|
|
||||||
|
|
||||||
// Dialogs
|
|
||||||
#include "confirmation_dialog.h"
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initialize all screen systems
|
|
||||||
*
|
|
||||||
* Call this once at application startup to initialize
|
|
||||||
* any global screen state.
|
|
||||||
*/
|
|
||||||
inline void InitializeScreens() {
|
|
||||||
// Currently no global initialization needed
|
|
||||||
// Reserved for future use
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Shutdown all screen systems
|
|
||||||
*
|
|
||||||
* Call this at application shutdown to clean up
|
|
||||||
* any global screen state.
|
|
||||||
*/
|
|
||||||
inline void ShutdownScreens() {
|
|
||||||
// Currently no global cleanup needed
|
|
||||||
// Reserved for future use
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "../../config/version.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Send Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Material Design form for sending DRGX
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Address book entry
|
|
||||||
*/
|
|
||||||
struct AddressBookEntry {
|
|
||||||
std::string label;
|
|
||||||
std::string address;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Send form data
|
|
||||||
*/
|
|
||||||
struct SendFormData {
|
|
||||||
char toAddress[512] = {0};
|
|
||||||
char amount[32] = {0};
|
|
||||||
char memo[512] = {0};
|
|
||||||
bool useShielded = true;
|
|
||||||
double fee = DRAGONX_DEFAULT_FEE;
|
|
||||||
|
|
||||||
// Validation state
|
|
||||||
bool addressValid = false;
|
|
||||||
bool amountValid = false;
|
|
||||||
std::string addressError;
|
|
||||||
std::string amountError;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Send screen with Material form
|
|
||||||
*/
|
|
||||||
class SendScreen {
|
|
||||||
public:
|
|
||||||
SendScreen() = default;
|
|
||||||
|
|
||||||
void setAvailableBalance(double shielded, double transparent) {
|
|
||||||
m_shieldedBalance = shielded;
|
|
||||||
m_transparentBalance = transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAddressBook(const std::vector<AddressBookEntry>& entries) {
|
|
||||||
m_addressBook = entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnSend(std::function<void(const SendFormData&)> callback) {
|
|
||||||
m_onSend = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnCancel(std::function<void()> callback) {
|
|
||||||
m_onCancel = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnValidateAddress(std::function<bool(const std::string&)> callback) {
|
|
||||||
m_onValidateAddress = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
|
||||||
memset(m_formData.toAddress, 0, sizeof(m_formData.toAddress));
|
|
||||||
memset(m_formData.amount, 0, sizeof(m_formData.amount));
|
|
||||||
memset(m_formData.memo, 0, sizeof(m_formData.memo));
|
|
||||||
m_formData.useShielded = true;
|
|
||||||
m_formData.addressValid = false;
|
|
||||||
m_formData.amountValid = false;
|
|
||||||
m_formData.addressError.clear();
|
|
||||||
m_formData.amountError.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderFromSelector();
|
|
||||||
void renderAddressField();
|
|
||||||
void renderAmountField();
|
|
||||||
void renderMemoField();
|
|
||||||
void renderFeeDisplay();
|
|
||||||
void renderActions();
|
|
||||||
void validateForm();
|
|
||||||
|
|
||||||
SendFormData m_formData;
|
|
||||||
double m_shieldedBalance = 0.0;
|
|
||||||
double m_transparentBalance = 0.0;
|
|
||||||
std::vector<AddressBookEntry> m_addressBook;
|
|
||||||
|
|
||||||
std::function<void(const SendFormData&)> m_onSend;
|
|
||||||
std::function<void()> m_onCancel;
|
|
||||||
std::function<bool(const std::string&)> m_onValidateAddress;
|
|
||||||
|
|
||||||
bool m_showAddressBook = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void SendScreen::render() {
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
float formWidth = std::min(availWidth, 500.0f);
|
|
||||||
float offsetX = (availWidth - formWidth) * 0.5f;
|
|
||||||
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
// Title
|
|
||||||
Typography::instance().text(TypeStyle::H5, "Send DRGX");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Form card
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 2;
|
|
||||||
cardSpec.padding = spacing::dp(3);
|
|
||||||
cardSpec.width = formWidth;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
// From selector (shielded vs transparent)
|
|
||||||
renderFromSelector();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// To address field
|
|
||||||
renderAddressField();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Amount field
|
|
||||||
renderAmountField();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Memo field (only for shielded)
|
|
||||||
if (m_formData.useShielded) {
|
|
||||||
renderMemoField();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fee display
|
|
||||||
renderFeeDisplay();
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Divider
|
|
||||||
ImGui::GetWindowDrawList()->AddLine(
|
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y),
|
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x + formWidth - spacing::dp(6), ImGui::GetCursorScreenPos().y),
|
|
||||||
ImGui::GetColorU32(Divider()),
|
|
||||||
1.0f
|
|
||||||
);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Action buttons
|
|
||||||
renderActions();
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::renderFromSelector() {
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SEND FROM");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
float buttonWidth = 200.0f;
|
|
||||||
|
|
||||||
// Shielded option
|
|
||||||
ImGui::PushID("from_shielded");
|
|
||||||
{
|
|
||||||
bool selected = m_formData.useShielded;
|
|
||||||
ImVec4 bgColor = selected ? colors::withAlpha(Primary(), 0.12f) : Surface();
|
|
||||||
ImVec4 borderColor = selected ? Primary() : Divider();
|
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, bgColor);
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(Primary(), 0.16f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, selected ? 2.0f : 1.0f);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
|
||||||
|
|
||||||
char label[128];
|
|
||||||
snprintf(label, sizeof(label), ICON_MD_SHIELD " Shielded\n%.4f DRGX", m_shieldedBalance);
|
|
||||||
|
|
||||||
if (ImGui::Button(label, ImVec2(buttonWidth, 60))) {
|
|
||||||
m_formData.useShielded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
ImGui::PopStyleColor(3);
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Transparent option
|
|
||||||
ImGui::PushID("from_transparent");
|
|
||||||
{
|
|
||||||
bool selected = !m_formData.useShielded;
|
|
||||||
ImVec4 bgColor = selected ? colors::withAlpha(Secondary(), 0.12f) : Surface();
|
|
||||||
ImVec4 borderColor = selected ? Secondary() : Divider();
|
|
||||||
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, bgColor);
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(Secondary(), 0.16f));
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, selected ? 2.0f : 1.0f);
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
|
||||||
|
|
||||||
char label[128];
|
|
||||||
snprintf(label, sizeof(label), ICON_MD_DESCRIPTION " Transparent\n%.4f DRGX", m_transparentBalance);
|
|
||||||
|
|
||||||
if (ImGui::Button(label, ImVec2(buttonWidth, 60))) {
|
|
||||||
m_formData.useShielded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopStyleVar(2);
|
|
||||||
ImGui::PopStyleColor(3);
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::renderAddressField() {
|
|
||||||
TextFieldSpec spec;
|
|
||||||
spec.label = "Recipient Address";
|
|
||||||
spec.placeholder = "Enter DRGX address...";
|
|
||||||
spec.variant = TextFieldVariant::Outlined;
|
|
||||||
spec.width = -1; // Full width
|
|
||||||
spec.hasError = !m_formData.addressError.empty();
|
|
||||||
spec.helperText = m_formData.addressError.empty() ?
|
|
||||||
"z-address (shielded) or t-address (transparent)" :
|
|
||||||
m_formData.addressError.c_str();
|
|
||||||
spec.leadingIcon = ICON_MD_MARKUNREAD_MAILBOX;
|
|
||||||
spec.trailingIcon = ICON_MD_MENU_BOOK; // Address book
|
|
||||||
|
|
||||||
TextFieldResult result = TextField("send_address", m_formData.toAddress,
|
|
||||||
sizeof(m_formData.toAddress), spec);
|
|
||||||
|
|
||||||
if (result.trailingIconClicked) {
|
|
||||||
m_showAddressBook = !m_showAddressBook;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.changed) {
|
|
||||||
// Validate address
|
|
||||||
if (m_onValidateAddress) {
|
|
||||||
m_formData.addressValid = m_onValidateAddress(m_formData.toAddress);
|
|
||||||
if (!m_formData.addressValid && strlen(m_formData.toAddress) > 10) {
|
|
||||||
m_formData.addressError = "Invalid address format";
|
|
||||||
} else {
|
|
||||||
m_formData.addressError.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address book dropdown
|
|
||||||
if (m_showAddressBook && !m_addressBook.empty()) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 8;
|
|
||||||
cardSpec.padding = spacing::dp(1);
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("address_book_list", false);
|
|
||||||
|
|
||||||
for (const auto& entry : m_addressBook) {
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = "👤";
|
|
||||||
itemSpec.primaryText = entry.label.c_str();
|
|
||||||
itemSpec.secondaryText = entry.address.substr(0, 20).c_str();
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
strncpy(m_formData.toAddress, entry.address.c_str(),
|
|
||||||
sizeof(m_formData.toAddress) - 1);
|
|
||||||
m_formData.addressValid = true;
|
|
||||||
m_formData.addressError.clear();
|
|
||||||
m_showAddressBook = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::renderAmountField() {
|
|
||||||
TextFieldSpec spec;
|
|
||||||
spec.label = "Amount";
|
|
||||||
spec.placeholder = "0.00000000";
|
|
||||||
spec.variant = TextFieldVariant::Outlined;
|
|
||||||
spec.width = 200.0f;
|
|
||||||
spec.hasError = !m_formData.amountError.empty();
|
|
||||||
spec.helperText = m_formData.amountError.c_str();
|
|
||||||
spec.suffix = "DRGX";
|
|
||||||
|
|
||||||
TextFieldResult result = TextField("send_amount", m_formData.amount,
|
|
||||||
sizeof(m_formData.amount), spec);
|
|
||||||
|
|
||||||
if (result.changed) {
|
|
||||||
// Validate amount
|
|
||||||
double amt = atof(m_formData.amount);
|
|
||||||
double maxBalance = m_formData.useShielded ? m_shieldedBalance : m_transparentBalance;
|
|
||||||
|
|
||||||
if (amt <= 0) {
|
|
||||||
m_formData.amountValid = false;
|
|
||||||
if (strlen(m_formData.amount) > 0) {
|
|
||||||
m_formData.amountError = "Amount must be greater than 0";
|
|
||||||
}
|
|
||||||
} else if (amt > maxBalance - m_formData.fee) {
|
|
||||||
m_formData.amountValid = false;
|
|
||||||
m_formData.amountError = "Insufficient balance";
|
|
||||||
} else {
|
|
||||||
m_formData.amountValid = true;
|
|
||||||
m_formData.amountError.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Max button
|
|
||||||
if (TextButton("MAX")) {
|
|
||||||
double maxBalance = m_formData.useShielded ? m_shieldedBalance : m_transparentBalance;
|
|
||||||
double maxAmount = maxBalance - m_formData.fee;
|
|
||||||
if (maxAmount > 0) {
|
|
||||||
snprintf(m_formData.amount, sizeof(m_formData.amount), "%.8f", maxAmount);
|
|
||||||
m_formData.amountValid = true;
|
|
||||||
m_formData.amountError.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::renderMemoField() {
|
|
||||||
TextFieldSpec spec;
|
|
||||||
spec.label = "Memo (encrypted)";
|
|
||||||
spec.placeholder = "Optional private message...";
|
|
||||||
spec.variant = TextFieldVariant::Outlined;
|
|
||||||
spec.width = -1;
|
|
||||||
spec.multiline = true;
|
|
||||||
spec.helperText = "512 characters max. Only visible to recipient.";
|
|
||||||
spec.maxLength = 512;
|
|
||||||
|
|
||||||
TextField("send_memo", m_formData.memo, sizeof(m_formData.memo), spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::renderFeeDisplay() {
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), "Network Fee:");
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
char feeStr[32];
|
|
||||||
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", m_formData.fee);
|
|
||||||
Typography::instance().text(TypeStyle::Body2, feeStr);
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
// Total if amount is valid
|
|
||||||
if (m_formData.amountValid) {
|
|
||||||
double amt = atof(m_formData.amount);
|
|
||||||
double total = amt + m_formData.fee;
|
|
||||||
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "Total:");
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
char totalStr[32];
|
|
||||||
snprintf(totalStr, sizeof(totalStr), "%.8f DRGX", total);
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, Primary(), totalStr);
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::renderActions() {
|
|
||||||
float buttonWidth = 120.0f;
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
// Right-align buttons
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - buttonWidth * 2 - spacing::dp(2));
|
|
||||||
|
|
||||||
// Cancel button
|
|
||||||
if (TextButton("CANCEL")) {
|
|
||||||
if (m_onCancel) m_onCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(2));
|
|
||||||
|
|
||||||
// Send button
|
|
||||||
bool canSend = m_formData.addressValid && m_formData.amountValid;
|
|
||||||
|
|
||||||
ImGui::BeginDisabled(!canSend);
|
|
||||||
if (ContainedButton("SEND")) {
|
|
||||||
if (m_onSend) m_onSend(m_formData);
|
|
||||||
}
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SendScreen::validateForm() {
|
|
||||||
// Address validation
|
|
||||||
if (strlen(m_formData.toAddress) == 0) {
|
|
||||||
m_formData.addressValid = false;
|
|
||||||
m_formData.addressError = "Address is required";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Amount validation
|
|
||||||
if (strlen(m_formData.amount) == 0) {
|
|
||||||
m_formData.amountValid = false;
|
|
||||||
m_formData.amountError = "Amount is required";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,561 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../config/version.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Settings Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Application settings with Material list items
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Setting category
|
|
||||||
*/
|
|
||||||
enum class SettingsCategory {
|
|
||||||
Wallet,
|
|
||||||
Display,
|
|
||||||
Network,
|
|
||||||
Privacy,
|
|
||||||
Advanced
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Settings data
|
|
||||||
*/
|
|
||||||
struct SettingsData {
|
|
||||||
// Wallet
|
|
||||||
bool autoShield = true;
|
|
||||||
double defaultFee = DRAGONX_DEFAULT_FEE;
|
|
||||||
|
|
||||||
// Display
|
|
||||||
std::string theme = "dark";
|
|
||||||
std::string language = "en";
|
|
||||||
std::string fiatCurrency = "USD";
|
|
||||||
bool showBalanceInFiat = true;
|
|
||||||
bool hideEmptyAddresses = true;
|
|
||||||
|
|
||||||
// Network
|
|
||||||
std::string proxyAddress;
|
|
||||||
int proxyPort = 9050;
|
|
||||||
bool useTor = false;
|
|
||||||
int maxConnections = 125;
|
|
||||||
|
|
||||||
// Privacy
|
|
||||||
bool rememberAddresses = true;
|
|
||||||
bool autoLock = true;
|
|
||||||
int autoLockTimeout = 5; // minutes
|
|
||||||
|
|
||||||
// Advanced
|
|
||||||
int confirmations = 10;
|
|
||||||
std::string dataDir;
|
|
||||||
bool debugMode = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Settings screen with Material list
|
|
||||||
*/
|
|
||||||
class SettingsScreen {
|
|
||||||
public:
|
|
||||||
void setSettings(const SettingsData& settings) { m_settings = settings; }
|
|
||||||
const SettingsData& getSettings() const { return m_settings; }
|
|
||||||
|
|
||||||
void setOnSettingsChanged(std::function<void(const SettingsData&)> callback) {
|
|
||||||
m_onSettingsChanged = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnBackupWallet(std::function<void()> callback) {
|
|
||||||
m_onBackupWallet = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnExportKeys(std::function<void()> callback) {
|
|
||||||
m_onExportKeys = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnRescan(std::function<void()> callback) {
|
|
||||||
m_onRescan = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderWalletSettings();
|
|
||||||
void renderDisplaySettings();
|
|
||||||
void renderNetworkSettings();
|
|
||||||
void renderPrivacySettings();
|
|
||||||
void renderAdvancedSettings();
|
|
||||||
void renderAboutSection();
|
|
||||||
|
|
||||||
void notifySettingsChanged();
|
|
||||||
|
|
||||||
SettingsData m_settings;
|
|
||||||
std::function<void(const SettingsData&)> m_onSettingsChanged;
|
|
||||||
std::function<void()> m_onBackupWallet;
|
|
||||||
std::function<void()> m_onExportKeys;
|
|
||||||
std::function<void()> m_onRescan;
|
|
||||||
|
|
||||||
SettingsCategory m_selectedCategory = SettingsCategory::Wallet;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void SettingsScreen::render() {
|
|
||||||
// Title
|
|
||||||
Typography::instance().text(TypeStyle::H5, "Settings");
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Category tabs
|
|
||||||
TabSpec tabSpec;
|
|
||||||
tabSpec.variant = TabVariant::Scrollable;
|
|
||||||
|
|
||||||
if (BeginTabs("settings_tabs", tabSpec)) {
|
|
||||||
if (Tab(ICON_MD_ACCOUNT_BALANCE_WALLET " Wallet", m_selectedCategory == SettingsCategory::Wallet)) {
|
|
||||||
m_selectedCategory = SettingsCategory::Wallet;
|
|
||||||
}
|
|
||||||
if (Tab(ICON_MD_PALETTE " Display", m_selectedCategory == SettingsCategory::Display)) {
|
|
||||||
m_selectedCategory = SettingsCategory::Display;
|
|
||||||
}
|
|
||||||
if (Tab(ICON_MD_PUBLIC " Network", m_selectedCategory == SettingsCategory::Network)) {
|
|
||||||
m_selectedCategory = SettingsCategory::Network;
|
|
||||||
}
|
|
||||||
if (Tab(ICON_MD_LOCK " Privacy", m_selectedCategory == SettingsCategory::Privacy)) {
|
|
||||||
m_selectedCategory = SettingsCategory::Privacy;
|
|
||||||
}
|
|
||||||
if (Tab(ICON_MD_SETTINGS " Advanced", m_selectedCategory == SettingsCategory::Advanced)) {
|
|
||||||
m_selectedCategory = SettingsCategory::Advanced;
|
|
||||||
}
|
|
||||||
EndTabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Settings content
|
|
||||||
switch (m_selectedCategory) {
|
|
||||||
case SettingsCategory::Wallet:
|
|
||||||
renderWalletSettings();
|
|
||||||
break;
|
|
||||||
case SettingsCategory::Display:
|
|
||||||
renderDisplaySettings();
|
|
||||||
break;
|
|
||||||
case SettingsCategory::Network:
|
|
||||||
renderNetworkSettings();
|
|
||||||
break;
|
|
||||||
case SettingsCategory::Privacy:
|
|
||||||
renderPrivacySettings();
|
|
||||||
break;
|
|
||||||
case SettingsCategory::Advanced:
|
|
||||||
renderAdvancedSettings();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// About section (always visible)
|
|
||||||
renderAboutSection();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::renderWalletSettings() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("wallet_settings", false);
|
|
||||||
|
|
||||||
// Auto-shield toggle
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Auto-Shield Transparent Funds";
|
|
||||||
itemSpec.secondaryText = "Automatically move transparent balance to shielded";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ImGui::PushID("auto_shield");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
// Toggle positioned at end
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.autoShield)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fee
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Default Transaction Fee";
|
|
||||||
|
|
||||||
char feeStr[32];
|
|
||||||
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", m_settings.defaultFee);
|
|
||||||
itemSpec.secondaryText = feeStr;
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup wallet
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = ICON_MD_SAVE;
|
|
||||||
itemSpec.primaryText = "Backup Wallet";
|
|
||||||
itemSpec.secondaryText = "Export wallet backup file";
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
if (m_onBackupWallet) m_onBackupWallet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export keys
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = ICON_MD_KEY;
|
|
||||||
itemSpec.primaryText = "Export Private Keys";
|
|
||||||
itemSpec.secondaryText = "Export viewing and spending keys";
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
if (m_onExportKeys) m_onExportKeys();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::renderDisplaySettings() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("display_settings", false);
|
|
||||||
|
|
||||||
// Theme selection
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Theme";
|
|
||||||
itemSpec.secondaryText = m_settings.theme == "dark" ? "Dark" : "Light";
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Language
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Language";
|
|
||||||
itemSpec.secondaryText = "English"; // Would map from code
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fiat currency
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Fiat Currency";
|
|
||||||
itemSpec.secondaryText = m_settings.fiatCurrency.c_str();
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show balance in fiat toggle
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Show Balance in Fiat";
|
|
||||||
itemSpec.secondaryText = "Display fiat equivalent on home screen";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ImGui::PushID("show_fiat");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.showBalanceInFiat)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide empty addresses
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Hide Empty Addresses";
|
|
||||||
itemSpec.secondaryText = "Don't show addresses with zero balance";
|
|
||||||
|
|
||||||
ImGui::PushID("hide_empty");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.hideEmptyAddresses)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::renderNetworkSettings() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("network_settings", false);
|
|
||||||
|
|
||||||
// Use Tor
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = "🧅";
|
|
||||||
itemSpec.primaryText = "Use Tor Network";
|
|
||||||
itemSpec.secondaryText = "Route connections through Tor for privacy";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ImGui::PushID("use_tor");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.useTor)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxy settings (shown if Tor enabled)
|
|
||||||
if (m_settings.useTor) {
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Proxy Address";
|
|
||||||
|
|
||||||
char proxyStr[128];
|
|
||||||
snprintf(proxyStr, sizeof(proxyStr), "%s:%d",
|
|
||||||
m_settings.proxyAddress.empty() ? "127.0.0.1" : m_settings.proxyAddress.c_str(),
|
|
||||||
m_settings.proxyPort);
|
|
||||||
itemSpec.secondaryText = proxyStr;
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max connections
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Maximum Connections";
|
|
||||||
|
|
||||||
char connStr[32];
|
|
||||||
snprintf(connStr, sizeof(connStr), "%d peers", m_settings.maxConnections);
|
|
||||||
itemSpec.secondaryText = connStr;
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::renderPrivacySettings() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("privacy_settings", false);
|
|
||||||
|
|
||||||
// Remember addresses
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Remember Addresses";
|
|
||||||
itemSpec.secondaryText = "Save recently used addresses";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ImGui::PushID("remember_addr");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.rememberAddresses)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-lock
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Auto-Lock Wallet";
|
|
||||||
itemSpec.secondaryText = "Lock wallet after inactivity";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ImGui::PushID("auto_lock");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.autoLock)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock timeout (if auto-lock enabled)
|
|
||||||
if (m_settings.autoLock) {
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Lock Timeout";
|
|
||||||
|
|
||||||
char timeoutStr[32];
|
|
||||||
snprintf(timeoutStr, sizeof(timeoutStr), "%d minutes", m_settings.autoLockTimeout);
|
|
||||||
itemSpec.secondaryText = timeoutStr;
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::renderAdvancedSettings() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("advanced_settings", false);
|
|
||||||
|
|
||||||
// Required confirmations
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.primaryText = "Required Confirmations";
|
|
||||||
|
|
||||||
char confStr[32];
|
|
||||||
snprintf(confStr, sizeof(confStr), "%d blocks", m_settings.confirmations);
|
|
||||||
itemSpec.secondaryText = confStr;
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data directory
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = "📁";
|
|
||||||
itemSpec.primaryText = "Data Directory";
|
|
||||||
itemSpec.secondaryText = m_settings.dataDir.empty() ?
|
|
||||||
"(default)" : m_settings.dataDir.c_str();
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
ListItem(itemSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rescan blockchain
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = ICON_MD_SYNC;
|
|
||||||
itemSpec.primaryText = "Rescan Blockchain";
|
|
||||||
itemSpec.secondaryText = "Rebuild transaction history from blockchain";
|
|
||||||
itemSpec.trailingIcon = ">";
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
if (m_onRescan) m_onRescan();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug mode
|
|
||||||
{
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
itemSpec.leadingIcon = "🐛";
|
|
||||||
itemSpec.primaryText = "Debug Mode";
|
|
||||||
itemSpec.secondaryText = "Enable verbose logging";
|
|
||||||
|
|
||||||
ImGui::PushID("debug_mode");
|
|
||||||
ListItem(itemSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
||||||
if (Switch("##toggle", m_settings.debugMode)) {
|
|
||||||
notifySettingsChanged();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::renderAboutSection() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 0;
|
|
||||||
cardSpec.padding = spacing::dp(3);
|
|
||||||
cardSpec.outlined = true;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
// App name and version centered
|
|
||||||
const char* appName = "ObsidianDragon";
|
|
||||||
ImVec2 nameSize = ImGui::CalcTextSize(appName);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - nameSize.x) * 0.5f);
|
|
||||||
Typography::instance().text(TypeStyle::H6, appName);
|
|
||||||
|
|
||||||
char version[64];
|
|
||||||
snprintf(version, sizeof(version), "Version %s-imgui", DRAGONX_VERSION);
|
|
||||||
ImVec2 versionSize = ImGui::CalcTextSize(version);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - versionSize.x) * 0.5f);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), version);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
const char* copyright = "© 2024-2026 The Hush Developers";
|
|
||||||
ImVec2 copySize = ImGui::CalcTextSize(copyright);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - copySize.x) * 0.5f);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), copyright);
|
|
||||||
|
|
||||||
const char* license = "Released under GPLv3";
|
|
||||||
ImVec2 licenseSize = ImGui::CalcTextSize(license);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - licenseSize.x) * 0.5f);
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), license);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void SettingsScreen::notifySettingsChanged() {
|
|
||||||
if (m_onSettingsChanged) {
|
|
||||||
m_onSettingsChanged(m_settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../material/material.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
namespace screens {
|
|
||||||
|
|
||||||
using namespace material;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Transactions Screen
|
|
||||||
// ============================================================================
|
|
||||||
// Display transaction history with Material list
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Transaction details
|
|
||||||
*/
|
|
||||||
struct Transaction {
|
|
||||||
std::string txid;
|
|
||||||
std::string type; // "sent", "received", "mined", "self"
|
|
||||||
std::string fromAddress;
|
|
||||||
std::string toAddress;
|
|
||||||
double amount;
|
|
||||||
double fee;
|
|
||||||
std::string memo;
|
|
||||||
std::string datetime;
|
|
||||||
int confirmations;
|
|
||||||
int blockHeight;
|
|
||||||
bool isShielded;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Transaction filter options
|
|
||||||
*/
|
|
||||||
enum class TxFilter {
|
|
||||||
All,
|
|
||||||
Sent,
|
|
||||||
Received,
|
|
||||||
Mined
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Transactions screen with Material list
|
|
||||||
*/
|
|
||||||
class TransactionsScreen {
|
|
||||||
public:
|
|
||||||
void setTransactions(const std::vector<Transaction>& txns) {
|
|
||||||
m_transactions = txns;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnTransactionClick(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onTxClick = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnCopyTxid(std::function<void(const std::string&)> callback) {
|
|
||||||
m_onCopyTxid = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOnExport(std::function<void()> callback) {
|
|
||||||
m_onExport = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
void render();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void renderFilters();
|
|
||||||
void renderTransactionList();
|
|
||||||
void renderTransactionItem(const Transaction& tx, int index);
|
|
||||||
void renderTransactionDetails();
|
|
||||||
void renderEmptyState();
|
|
||||||
|
|
||||||
std::vector<Transaction> m_transactions;
|
|
||||||
std::function<void(const std::string&)> m_onTxClick;
|
|
||||||
std::function<void(const std::string&)> m_onCopyTxid;
|
|
||||||
std::function<void()> m_onExport;
|
|
||||||
|
|
||||||
TxFilter m_filter = TxFilter::All;
|
|
||||||
std::string m_searchQuery;
|
|
||||||
char m_searchBuffer[256] = {0};
|
|
||||||
|
|
||||||
std::string m_selectedTxid;
|
|
||||||
bool m_showDetails = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
inline void TransactionsScreen::render() {
|
|
||||||
// Title and export button
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
Typography::instance().text(TypeStyle::H5, "Transactions");
|
|
||||||
|
|
||||||
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100);
|
|
||||||
|
|
||||||
if (OutlinedButton("📥 EXPORT")) {
|
|
||||||
if (m_onExport) m_onExport();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
renderFilters();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Transaction list or empty state
|
|
||||||
std::vector<Transaction> filtered;
|
|
||||||
for (const auto& tx : m_transactions) {
|
|
||||||
// Apply type filter
|
|
||||||
if (m_filter != TxFilter::All) {
|
|
||||||
if (m_filter == TxFilter::Sent && tx.type != "sent") continue;
|
|
||||||
if (m_filter == TxFilter::Received && tx.type != "received") continue;
|
|
||||||
if (m_filter == TxFilter::Mined && tx.type != "mined") continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply search filter
|
|
||||||
if (strlen(m_searchBuffer) > 0) {
|
|
||||||
std::string search = m_searchBuffer;
|
|
||||||
if (tx.txid.find(search) == std::string::npos &&
|
|
||||||
tx.toAddress.find(search) == std::string::npos &&
|
|
||||||
tx.fromAddress.find(search) == std::string::npos &&
|
|
||||||
tx.memo.find(search) == std::string::npos) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered.push_back(tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filtered.empty()) {
|
|
||||||
renderEmptyState();
|
|
||||||
} else {
|
|
||||||
// Scrollable list
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = 0;
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
BeginList("tx_list", false);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < filtered.size(); i++) {
|
|
||||||
renderTransactionItem(filtered[i], static_cast<int>(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
EndList();
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Details popup
|
|
||||||
if (m_showDetails) {
|
|
||||||
renderTransactionDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransactionsScreen::renderFilters() {
|
|
||||||
// Search field
|
|
||||||
TextFieldSpec searchSpec;
|
|
||||||
searchSpec.label = "Search";
|
|
||||||
searchSpec.placeholder = "Search by address, txid, memo...";
|
|
||||||
searchSpec.variant = TextFieldVariant::Outlined;
|
|
||||||
searchSpec.width = 300.0f;
|
|
||||||
searchSpec.leadingIcon = ICON_MD_SEARCH;
|
|
||||||
|
|
||||||
TextField("tx_search", m_searchBuffer, sizeof(m_searchBuffer), searchSpec);
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(3));
|
|
||||||
|
|
||||||
// Filter chips
|
|
||||||
ImGui::BeginGroup();
|
|
||||||
{
|
|
||||||
ChipSpec chipSpec;
|
|
||||||
chipSpec.variant = ChipVariant::Filter;
|
|
||||||
chipSpec.selectable = true;
|
|
||||||
|
|
||||||
chipSpec.selected = (m_filter == TxFilter::All);
|
|
||||||
if (FilterChip("All", chipSpec)) {
|
|
||||||
m_filter = TxFilter::All;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(1));
|
|
||||||
|
|
||||||
chipSpec.selected = (m_filter == TxFilter::Sent);
|
|
||||||
if (FilterChip(ICON_MD_CALL_MADE " Sent", chipSpec)) {
|
|
||||||
m_filter = TxFilter::Sent;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(1));
|
|
||||||
|
|
||||||
chipSpec.selected = (m_filter == TxFilter::Received);
|
|
||||||
if (FilterChip(ICON_MD_CALL_RECEIVED " Received", chipSpec)) {
|
|
||||||
m_filter = TxFilter::Received;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine(0, spacing::dp(1));
|
|
||||||
|
|
||||||
chipSpec.selected = (m_filter == TxFilter::Mined);
|
|
||||||
if (FilterChip(ICON_MD_CONSTRUCTION " Mined", chipSpec)) {
|
|
||||||
m_filter = TxFilter::Mined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransactionsScreen::renderTransactionItem(const Transaction& tx, int index) {
|
|
||||||
ListItemSpec itemSpec;
|
|
||||||
|
|
||||||
// Icon based on type
|
|
||||||
if (tx.type == "received") {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
|
|
||||||
} else if (tx.type == "sent") {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CALL_MADE;
|
|
||||||
} else if (tx.type == "mined") {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CONSTRUCTION;
|
|
||||||
} else {
|
|
||||||
itemSpec.leadingIcon = ICON_MD_SWAP_HORIZ; // Self transfer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Amount formatting
|
|
||||||
char amountStr[64];
|
|
||||||
if (tx.type == "sent") {
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "-%.8f DRGX", tx.amount);
|
|
||||||
} else {
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "+%.8f DRGX", tx.amount);
|
|
||||||
}
|
|
||||||
itemSpec.primaryText = amountStr;
|
|
||||||
|
|
||||||
// Secondary: date + confirmations
|
|
||||||
char secondaryStr[128];
|
|
||||||
if (tx.confirmations == 0) {
|
|
||||||
snprintf(secondaryStr, sizeof(secondaryStr), "%s • " ICON_MD_HOURGLASS_EMPTY " Pending", tx.datetime.c_str());
|
|
||||||
} else if (tx.confirmations < 10) {
|
|
||||||
snprintf(secondaryStr, sizeof(secondaryStr), "%s • %d confirmations",
|
|
||||||
tx.datetime.c_str(), tx.confirmations);
|
|
||||||
} else {
|
|
||||||
snprintf(secondaryStr, sizeof(secondaryStr), "%s • " ICON_MD_CHECK " Confirmed", tx.datetime.c_str());
|
|
||||||
}
|
|
||||||
itemSpec.secondaryText = secondaryStr;
|
|
||||||
|
|
||||||
// Privacy indicator
|
|
||||||
if (tx.isShielded) {
|
|
||||||
itemSpec.trailingIcon = ICON_MD_SHIELD;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemSpec.dividerBelow = true;
|
|
||||||
|
|
||||||
char itemId[32];
|
|
||||||
snprintf(itemId, sizeof(itemId), "tx_%d", index);
|
|
||||||
ImGui::PushID(itemId);
|
|
||||||
|
|
||||||
if (ListItem(itemSpec)) {
|
|
||||||
m_selectedTxid = tx.txid;
|
|
||||||
m_showDetails = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransactionsScreen::renderTransactionDetails() {
|
|
||||||
// Find the selected transaction
|
|
||||||
const Transaction* selected = nullptr;
|
|
||||||
for (const auto& tx : m_transactions) {
|
|
||||||
if (tx.txid == m_selectedTxid) {
|
|
||||||
selected = &tx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selected) {
|
|
||||||
m_showDetails = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogSpec dialogSpec;
|
|
||||||
dialogSpec.title = "Transaction Details";
|
|
||||||
dialogSpec.maxWidth = 500.0f;
|
|
||||||
|
|
||||||
DialogResult result = BeginDialog("tx_details", dialogSpec);
|
|
||||||
|
|
||||||
if (result.isOpen) {
|
|
||||||
const Transaction& tx = *selected;
|
|
||||||
|
|
||||||
// Type badge
|
|
||||||
ChipSpec chipSpec;
|
|
||||||
chipSpec.variant = ChipVariant::Filled;
|
|
||||||
|
|
||||||
if (tx.type == "received") {
|
|
||||||
chipSpec.color = colors::Green500;
|
|
||||||
Chip(ICON_MD_CALL_RECEIVED " Received", chipSpec);
|
|
||||||
} else if (tx.type == "sent") {
|
|
||||||
chipSpec.color = colors::Red500;
|
|
||||||
Chip(ICON_MD_CALL_MADE " Sent", chipSpec);
|
|
||||||
} else if (tx.type == "mined") {
|
|
||||||
chipSpec.color = Secondary();
|
|
||||||
Chip(ICON_MD_CONSTRUCTION " Mined", chipSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tx.isShielded) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
chipSpec.color = Primary();
|
|
||||||
Chip(ICON_MD_SHIELD " Shielded", chipSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
|
|
||||||
char amountStr[64];
|
|
||||||
snprintf(amountStr, sizeof(amountStr), "%.8f DRGX", tx.amount);
|
|
||||||
Typography::instance().text(TypeStyle::H5, amountStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Details grid
|
|
||||||
auto detailRow = [](const char* label, const char* value, bool canCopy = false) {
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), label);
|
|
||||||
Typography::instance().text(TypeStyle::Body2, value);
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
};
|
|
||||||
|
|
||||||
detailRow("DATE", tx.datetime.c_str());
|
|
||||||
|
|
||||||
char confStr[32];
|
|
||||||
if (tx.confirmations == 0) {
|
|
||||||
snprintf(confStr, sizeof(confStr), "Pending");
|
|
||||||
} else {
|
|
||||||
snprintf(confStr, sizeof(confStr), "%d confirmations", tx.confirmations);
|
|
||||||
}
|
|
||||||
detailRow("STATUS", confStr);
|
|
||||||
|
|
||||||
if (tx.blockHeight > 0) {
|
|
||||||
char blockStr[32];
|
|
||||||
snprintf(blockStr, sizeof(blockStr), "%d", tx.blockHeight);
|
|
||||||
detailRow("BLOCK", blockStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
char feeStr[32];
|
|
||||||
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", tx.fee);
|
|
||||||
detailRow("FEE", feeStr);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
||||||
|
|
||||||
// Transaction ID
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "TRANSACTION ID");
|
|
||||||
ImGui::TextWrapped("%s", tx.txid.c_str());
|
|
||||||
|
|
||||||
if (TextButton(ICON_MD_CONTENT_COPY " COPY TXID")) {
|
|
||||||
if (m_onCopyTxid) m_onCopyTxid(tx.txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memo if present
|
|
||||||
if (!tx.memo.empty()) {
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "MEMO");
|
|
||||||
ImGui::TextWrapped("%s", tx.memo.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
||||||
|
|
||||||
// Close button
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 100);
|
|
||||||
|
|
||||||
if (ContainedButton("CLOSE")) {
|
|
||||||
m_showDetails = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.dismissed) {
|
|
||||||
m_showDetails = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void TransactionsScreen::renderEmptyState() {
|
|
||||||
CardSpec cardSpec;
|
|
||||||
cardSpec.elevation = 1;
|
|
||||||
cardSpec.padding = spacing::dp(6);
|
|
||||||
|
|
||||||
if (BeginCard(cardSpec)) {
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
// Icon
|
|
||||||
const char* icon = ICON_MD_RECEIPT;
|
|
||||||
ImVec2 iconSize = ImGui::CalcTextSize(icon);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - iconSize.x) * 0.5f);
|
|
||||||
Typography::instance().text(TypeStyle::H3, icon);
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
||||||
|
|
||||||
// Message
|
|
||||||
const char* message = m_filter == TxFilter::All ?
|
|
||||||
"No transactions yet" : "No matching transactions";
|
|
||||||
ImVec2 textSize = ImGui::CalcTextSize(message);
|
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
|
|
||||||
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), message);
|
|
||||||
|
|
||||||
EndCard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
+45
-5
@@ -12,6 +12,7 @@
|
|||||||
#include "schema/ui_schema.h"
|
#include "schema/ui_schema.h"
|
||||||
#include "../embedded/IconsMaterialDesign.h"
|
#include "../embedded/IconsMaterialDesign.h"
|
||||||
#include "../util/i18n.h"
|
#include "../util/i18n.h"
|
||||||
|
#include "../wallet/wallet_capabilities.h"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -29,7 +30,9 @@ enum class NavPage {
|
|||||||
Market,
|
Market,
|
||||||
// --- separator ---
|
// --- separator ---
|
||||||
Console,
|
Console,
|
||||||
|
LiteConsole, // lite-only diagnostics console (full-node uses the RPC Console); one per variant
|
||||||
Peers,
|
Peers,
|
||||||
|
LiteNetwork, // lite-only "Network" tab (full-node uses Peers); exactly one shows per variant
|
||||||
Explorer,
|
Explorer,
|
||||||
Settings,
|
Settings,
|
||||||
Count_
|
Count_
|
||||||
@@ -51,7 +54,9 @@ inline const NavItem kNavItems[] = {
|
|||||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
||||||
|
{ "Console", NavPage::LiteConsole, "ADVANCED","console", "advanced" },
|
||||||
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
||||||
|
{ "Network", NavPage::LiteNetwork, nullptr, "network", nullptr },
|
||||||
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
|
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
|
||||||
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
||||||
};
|
};
|
||||||
@@ -66,6 +71,30 @@ inline const char* NavSectionLabel(const NavItem& item) {
|
|||||||
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
|
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline wallet::WalletUiSurface NavPageSurface(NavPage page)
|
||||||
|
{
|
||||||
|
switch (page) {
|
||||||
|
case NavPage::Overview: return wallet::WalletUiSurface::Overview;
|
||||||
|
case NavPage::Send: return wallet::WalletUiSurface::Send;
|
||||||
|
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
|
||||||
|
case NavPage::History: return wallet::WalletUiSurface::History;
|
||||||
|
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
||||||
|
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
||||||
|
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
||||||
|
case NavPage::LiteConsole: return wallet::WalletUiSurface::LiteConsole;
|
||||||
|
case NavPage::Peers: return wallet::WalletUiSurface::Peers;
|
||||||
|
case NavPage::LiteNetwork: return wallet::WalletUiSurface::LiteNetwork;
|
||||||
|
case NavPage::Explorer: return wallet::WalletUiSurface::Explorer;
|
||||||
|
case NavPage::Settings: return wallet::WalletUiSurface::Settings;
|
||||||
|
default: return wallet::WalletUiSurface::Overview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool IsNavPageVisible(NavPage page)
|
||||||
|
{
|
||||||
|
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), NavPageSurface(page));
|
||||||
|
}
|
||||||
|
|
||||||
// Get the Material Design icon string for a navigation page.
|
// Get the Material Design icon string for a navigation page.
|
||||||
inline const char* GetNavIconMD(NavPage page)
|
inline const char* GetNavIconMD(NavPage page)
|
||||||
{
|
{
|
||||||
@@ -77,7 +106,9 @@ inline const char* GetNavIconMD(NavPage page)
|
|||||||
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
||||||
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
||||||
case NavPage::Console: return ICON_MD_TERMINAL;
|
case NavPage::Console: return ICON_MD_TERMINAL;
|
||||||
|
case NavPage::LiteConsole: return ICON_MD_TERMINAL;
|
||||||
case NavPage::Peers: return ICON_MD_HUB;
|
case NavPage::Peers: return ICON_MD_HUB;
|
||||||
|
case NavPage::LiteNetwork: return ICON_MD_HUB;
|
||||||
case NavPage::Explorer: return ICON_MD_EXPLORE;
|
case NavPage::Explorer: return ICON_MD_EXPLORE;
|
||||||
case NavPage::Settings: return ICON_MD_SETTINGS;
|
case NavPage::Settings: return ICON_MD_SETTINGS;
|
||||||
default: return ICON_MD_HOME;
|
default: return ICON_MD_HOME;
|
||||||
@@ -446,12 +477,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
// Separate fixed parts (don't scale) from flex parts (scale when tight).
|
// Separate fixed parts (don't scale) from flex parts (scale when tight).
|
||||||
float fixedH = stripH; // collapse strip
|
float fixedH = stripH; // collapse strip
|
||||||
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
||||||
if (kNavItems[i].section_label && showLabels)
|
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
|
||||||
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
|
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
|
||||||
fixedH += bottomPadding + stripH; // exit area
|
fixedH += bottomPadding + stripH; // exit area
|
||||||
|
|
||||||
float baseFlexH = baseNavGap;
|
float baseFlexH = baseNavGap;
|
||||||
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
||||||
|
if (!IsNavPageVisible(kNavItems[i].page)) continue;
|
||||||
if (kNavItems[i].section_label) {
|
if (kNavItems[i].section_label) {
|
||||||
if (showLabels) baseFlexH += baseSectionGap;
|
if (showLabels) baseFlexH += baseSectionGap;
|
||||||
else baseFlexH += baseSectionGap * 0.8f;
|
else baseFlexH += baseSectionGap * 0.8f;
|
||||||
@@ -479,6 +511,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
|
|
||||||
float curY = stripH + navGap; // after collapse strip + gap
|
float curY = stripH + navGap; // after collapse strip + gap
|
||||||
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
||||||
|
if (!IsNavPageVisible(kNavItems[i].page)) continue;
|
||||||
if (kNavItems[i].section_label) {
|
if (kNavItems[i].section_label) {
|
||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
curY += sectionGap;
|
curY += sectionGap;
|
||||||
@@ -560,6 +593,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
float maxSectionW = panelRight - wp.x - sbSectionLabelPadLeft;
|
float maxSectionW = panelRight - wp.x - sbSectionLabelPadLeft;
|
||||||
int si = 0;
|
int si = 0;
|
||||||
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
||||||
|
if (!IsNavPageVisible(kNavItems[i].page)) continue;
|
||||||
if (kNavItems[i].section_label && si < nSectionLabels) {
|
if (kNavItems[i].section_label && si < nSectionLabels) {
|
||||||
float ly = panelTopY + sectionLabelY[si++];
|
float ly = panelTopY + sectionLabelY[si++];
|
||||||
const char* sLabel = NavSectionLabel(kNavItems[i]);
|
const char* sLabel = NavSectionLabel(kNavItems[i]);
|
||||||
@@ -587,6 +621,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded;
|
float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded;
|
||||||
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
||||||
const NavItem& item = kNavItems[i];
|
const NavItem& item = kNavItems[i];
|
||||||
|
if (!IsNavPageVisible(item.page)) continue;
|
||||||
bool selected = (current == item.page);
|
bool selected = (current == item.page);
|
||||||
float btnY = panelTopY + itemY[i];
|
float btnY = panelTopY + itemY[i];
|
||||||
float btnRnd = itemH * 0.22f;
|
float btnRnd = itemH * 0.22f;
|
||||||
@@ -617,6 +652,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
// Click detection
|
// Click detection
|
||||||
bool pageNeedsUnlock = locked &&
|
bool pageNeedsUnlock = locked &&
|
||||||
item.page != NavPage::Console &&
|
item.page != NavPage::Console &&
|
||||||
|
item.page != NavPage::LiteConsole &&
|
||||||
item.page != NavPage::Peers &&
|
item.page != NavPage::Peers &&
|
||||||
item.page != NavPage::Settings;
|
item.page != NavPage::Settings;
|
||||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
||||||
@@ -695,10 +731,14 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||||
int idx = static_cast<int>(current);
|
int idx = static_cast<int>(current);
|
||||||
bool nav = false;
|
bool nav = false;
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K))
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
|
||||||
if (idx > 0) { idx--; nav = true; }
|
do { idx--; } while (idx >= 0 && !IsNavPageVisible(static_cast<NavPage>(idx)));
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J))
|
if (idx >= 0) nav = true;
|
||||||
if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; }
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
|
||||||
|
do { idx++; } while (idx < (int)NavPage::Count_ && !IsNavPageVisible(static_cast<NavPage>(idx)));
|
||||||
|
if (idx < (int)NavPage::Count_) nav = true;
|
||||||
|
}
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket))
|
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket))
|
||||||
collapsed = !collapsed;
|
collapsed = !collapsed;
|
||||||
if (nav) { current = static_cast<NavPage>(idx); changed = true; }
|
if (nav) { current = static_cast<NavPage>(idx); changed = true; }
|
||||||
|
|||||||
@@ -0,0 +1,842 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "balance_components.h"
|
||||||
|
#include "balance_tab_helpers.h"
|
||||||
|
#include "balance_address_list.h"
|
||||||
|
#include "balance_recent_tx.h"
|
||||||
|
#include "key_export_dialog.h"
|
||||||
|
#include "qr_popup_dialog.h"
|
||||||
|
#include "address_label_dialog.h"
|
||||||
|
#include "address_transfer_dialog.h"
|
||||||
|
#include "mining_tab_helpers.h"
|
||||||
|
#include "send_tab.h"
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
|
#include "../../config/version.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
|
#include "../theme.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../effects/imgui_acrylic.h"
|
||||||
|
#include "../sidebar.h"
|
||||||
|
#include "../notifications.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
void UpdateBalanceLerp(App* app) {
|
||||||
|
using namespace material;
|
||||||
|
const auto& S = schema::UISchema::instance();
|
||||||
|
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 = (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; }
|
||||||
|
disp += diff * (double)(dt * spd);
|
||||||
|
if (std::abs(target - disp) < 1e-9) disp = target;
|
||||||
|
};
|
||||||
|
lerp(s_dispTotal, app->getTotalBalance(), dt, speed);
|
||||||
|
lerp(s_dispShielded, app->getShieldedBalance(), dt, speed);
|
||||||
|
lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
|
||||||
|
lerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render compact hero line: logo + balance + USD + mining on one line
|
||||||
|
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) {
|
||||||
|
using namespace material;
|
||||||
|
char buf[64];
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
|
||||||
|
// Coin logo
|
||||||
|
ImTextureID logoTex = app->getCoinLogoTexture();
|
||||||
|
ImFont* sub1 = Type().subtitle1();
|
||||||
|
float logoSz = sub1->LegacySize + 4.0f * dp;
|
||||||
|
float lineH = (heroHeightOverride >= 0.0f) ? heroHeightOverride : logoSz;
|
||||||
|
if (logoTex != 0) {
|
||||||
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||||
|
dl->AddImage(logoTex,
|
||||||
|
ImVec2(pos.x, pos.y),
|
||||||
|
ImVec2(pos.x + lineH, pos.y + lineH),
|
||||||
|
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
|
||||||
|
ImGui::Dummy(ImVec2(lineH + Layout::spacingSm(), lineH));
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total balance
|
||||||
|
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||||
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||||
|
float fontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride : sub1->LegacySize;
|
||||||
|
DrawTextShadow(dl, sub1, fontSize, pos, OnSurface(), buf);
|
||||||
|
ImVec2 sz = sub1->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf);
|
||||||
|
ImGui::Dummy(ImVec2(sz.x, lineH));
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
// Ticker
|
||||||
|
ImFont* capFont = Type().caption();
|
||||||
|
float tickerFontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride * 0.6f : capFont->LegacySize;
|
||||||
|
float tickerY = pos.y + lineH - tickerFontSize;
|
||||||
|
dl->AddText(capFont, tickerFontSize,
|
||||||
|
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||||
|
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||||
|
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
// USD value
|
||||||
|
const auto& state = app->state();
|
||||||
|
double usd_value = state.getBalanceUSD();
|
||||||
|
if (usd_value > 0.0)
|
||||||
|
snprintf(buf, sizeof(buf), "$%.2f", usd_value);
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "$--.--");
|
||||||
|
dl->AddText(capFont, tickerFontSize,
|
||||||
|
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||||
|
OnSurfaceDisabled(), buf);
|
||||||
|
|
||||||
|
ImGui::NewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the shared address list section (used by all layouts)
|
||||||
|
void RenderSharedAddressList(App* app, float listH, float availW,
|
||||||
|
float glassRound, float hs, float vs) {
|
||||||
|
using namespace material;
|
||||||
|
const auto& S = schema::UISchema::instance();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
const auto addrBtn = S.button("tabs.balance", "address-button");
|
||||||
|
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||||
|
const auto searchIn = S.input("tabs.balance", "search-input");
|
||||||
|
const auto addrTable = S.table("tabs.balance", "address-table");
|
||||||
|
const auto& state = app->state();
|
||||||
|
|
||||||
|
Type().text(TypeStyle::H6, TR("your_addresses"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ---- Persistent state ----
|
||||||
|
static char addr_search[128] = "";
|
||||||
|
static bool s_hideZeroBalances = true;
|
||||||
|
static bool s_showHidden = false;
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
static int s_dragIdx = -1; // row being dragged (-1 = none)
|
||||||
|
static float s_dragOffsetY = 0.0f; // mouse offset from row top
|
||||||
|
static float s_dragStartY = 0.0f; // mouse Y at drag start
|
||||||
|
static bool s_dragActive = false; // drag distance threshold passed
|
||||||
|
static int s_dropTargetIdx = -1; // row hovered during drag
|
||||||
|
static int s_dropMode = 0; // 0 = transfer (released over a row's CENTER), 1 = reorder
|
||||||
|
// (released over a row's top/bottom EDGE). Disambiguates the
|
||||||
|
// two drag gestures by WHERE on the target row you release.
|
||||||
|
|
||||||
|
// Copy feedback
|
||||||
|
static int s_copiedRow = -1;
|
||||||
|
static float s_copiedTime = 0.0f;
|
||||||
|
|
||||||
|
// ---- Build and filter address rows ----
|
||||||
|
std::vector<AddressListInput> rowInputs;
|
||||||
|
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||||
|
|
||||||
|
auto addRows = [&](const auto& addrs, bool isZ) {
|
||||||
|
for (const auto& a : addrs) {
|
||||||
|
std::string addrLabel = app->getAddressLabel(a.address);
|
||||||
|
bool isHidden = app->isAddressHidden(a.address);
|
||||||
|
bool isFav = app->isAddressFavorite(a.address);
|
||||||
|
bool isMining = app->isMiningAddress(a.address);
|
||||||
|
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
|
||||||
|
addrLabel, app->getAddressIcon(a.address),
|
||||||
|
app->getAddressSortOrder(a.address)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addRows(state.z_addresses, true);
|
||||||
|
addRows(state.t_addresses, false);
|
||||||
|
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
|
||||||
|
|
||||||
|
// ---- Toolbar: search + checkboxes + create buttons ----
|
||||||
|
float avail = ImGui::GetContentRegionAvail().x;
|
||||||
|
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
|
||||||
|
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
|
||||||
|
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
|
||||||
|
ImGui::SetNextItemWidth(searchW);
|
||||||
|
ImGui::InputTextWithHint("##AddrSearch", TR("filter"), addr_search, sizeof(addr_search));
|
||||||
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
|
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
|
||||||
|
{
|
||||||
|
int hc = app->getHiddenAddressCount();
|
||||||
|
if (hc > 0) {
|
||||||
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
|
char hlbl[64];
|
||||||
|
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
||||||
|
ImGui::Checkbox(hlbl, &s_showHidden);
|
||||||
|
} else {
|
||||||
|
s_showHidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
||||||
|
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
||||||
|
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
||||||
|
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
|
||||||
|
S.drawElement("tabs.balance", "buttons-position").size * hs);
|
||||||
|
ImGui::SameLine(std::max(kMinButtonsPosition,
|
||||||
|
ImGui::GetWindowWidth() - totalButtonsWidth -
|
||||||
|
S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f)));
|
||||||
|
|
||||||
|
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||||
|
ImGui::BeginDisabled(sharedAddrSyncing);
|
||||||
|
if (s_generating_z_address) {
|
||||||
|
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||||
|
const char* dotStr[] = {"", ".", "..", "..."};
|
||||||
|
char genLabel[64];
|
||||||
|
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
|
||||||
|
TactileButton(genLabel, ImVec2(buttonWidth, 0),
|
||||||
|
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||||
|
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
||||||
|
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||||
|
s_generating_z_address = true;
|
||||||
|
app->createNewZAddress([](const std::string& addr) {
|
||||||
|
s_generating_z_address = false;
|
||||||
|
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0),
|
||||||
|
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||||
|
app->createNewTAddress([](const std::string& addr) {
|
||||||
|
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
|
// ---- Glass panel container ----
|
||||||
|
float addrListH = listH;
|
||||||
|
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
|
||||||
|
|
||||||
|
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
||||||
|
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 listPanelMax(listPanelMin.x + availW, listPanelMin.y + addrListH);
|
||||||
|
GlassPanelSpec addrGlassSpec;
|
||||||
|
addrGlassSpec.rounding = glassRound;
|
||||||
|
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
|
||||||
|
|
||||||
|
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
|
||||||
|
int addrParentVtx = dlPanel->VtxBuffer.Size;
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
|
||||||
|
ImGui::BeginChild("AddressList", ImVec2(availW, addrListH), false,
|
||||||
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||||
|
ApplySmoothScroll();
|
||||||
|
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
|
||||||
|
int addrChildVtx = addrChildDL->VtxBuffer.Size;
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
|
// ---- Empty states ----
|
||||||
|
if (!app->isConnected()) {
|
||||||
|
ImGui::Dummy(ImVec2(0, 16 * dp));
|
||||||
|
float cw = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
|
||||||
|
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
|
||||||
|
ImGui::TextDisabled("%s", TR("not_connected"));
|
||||||
|
} else if (rows.empty()) {
|
||||||
|
float cw = ImGui::GetContentRegionAvail().x;
|
||||||
|
float ch = ImGui::GetContentRegionAvail().y;
|
||||||
|
if (ch < 60) ch = 60;
|
||||||
|
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
||||||
|
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
||||||
|
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||||
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||||
|
ImGui::TextDisabled("%s", emptyMsg);
|
||||||
|
} else {
|
||||||
|
// ---- PASS 1: Compute row layout ----
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImFont* capFont = Type().caption();
|
||||||
|
ImFont* body2 = Type().body2();
|
||||||
|
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd() + Layout::spacingMd();
|
||||||
|
static int selected_row = -1;
|
||||||
|
addrScrollY = ImGui::GetScrollY();
|
||||||
|
addrScrollMaxY = ImGui::GetScrollMaxY();
|
||||||
|
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
||||||
|
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
||||||
|
float rowPadLeft = Layout::cardInnerPadding();
|
||||||
|
float rowPadRight = Layout::cardInnerPadding();
|
||||||
|
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
|
||||||
|
S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
||||||
|
float innerW = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
||||||
|
// Theme colors for buttons (resolved once)
|
||||||
|
ImU32 favGoldFill = S.resolveColor("var(--favorite-fill)", IM_COL32(255, 200, 50, 40));
|
||||||
|
ImU32 favGoldBorder = S.resolveColor("var(--favorite-border)", IM_COL32(255, 200, 50, 100));
|
||||||
|
ImU32 favGoldIcon = S.resolveColor("var(--favorite-icon)", IM_COL32(255, 200, 50, 255));
|
||||||
|
ImU32 favGoldOutline= S.resolveColor("var(--favorite-outline)", IM_COL32(255, 200, 50, 120));
|
||||||
|
ImU32 btnFill = S.resolveColor("var(--row-button-fill)", IM_COL32(255, 255, 255, 12));
|
||||||
|
ImU32 btnFillHov = S.resolveColor("var(--row-button-fill-hover)", IM_COL32(255, 255, 255, 25));
|
||||||
|
ImU32 btnBorder = S.resolveColor("var(--row-button-border)", IM_COL32(255, 255, 255, 25));
|
||||||
|
ImU32 btnBorderHov = S.resolveColor("var(--row-button-border-hover)", IM_COL32(255, 255, 255, 50));
|
||||||
|
ImU32 rowHoverCol = S.resolveColor("var(--row-hover)", IM_COL32(255, 255, 255, 15));
|
||||||
|
ImU32 rowSelectCol = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
|
||||||
|
ImU32 dividerCol = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));
|
||||||
|
|
||||||
|
// Compute Y positions for all rows (pass 1)
|
||||||
|
std::vector<float> rowY(rows.size());
|
||||||
|
float cursorStartY = ImGui::GetCursorScreenPos().y;
|
||||||
|
for (int i = 0; i < (int)rows.size(); ++i)
|
||||||
|
rowY[i] = cursorStartY + i * rowH;
|
||||||
|
|
||||||
|
// Drag logic — detect drag start, compute drag position
|
||||||
|
ImVec2 mousePos = ImGui::GetMousePos();
|
||||||
|
bool mouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||||
|
bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||||
|
|
||||||
|
// Reset drop target each frame — it gets set only if mouse is over a row.
|
||||||
|
// Preserve the drop target on the release frame so the drop handler can use it.
|
||||||
|
if (s_dragActive && mouseDown) s_dropTargetIdx = -1;
|
||||||
|
|
||||||
|
if (s_dragIdx >= 0 && !mouseDown) {
|
||||||
|
// Mouse released — copy if it was a click (no drag activated)
|
||||||
|
if (!s_dragActive && s_dragIdx >= 0 && s_dragIdx < (int)rows.size()) {
|
||||||
|
const auto& clickRow = rows[s_dragIdx];
|
||||||
|
ImGui::SetClipboardText(clickRow.info->address.c_str());
|
||||||
|
selected_row = s_dragIdx;
|
||||||
|
s_copiedRow = s_dragIdx;
|
||||||
|
s_copiedTime = (float)ImGui::GetTime();
|
||||||
|
}
|
||||||
|
// Drop. Released over a row's CENTER (transfer mode) and that row has funds -> transfer;
|
||||||
|
// released over an EDGE, in a gap, or on a zero-balance row -> reorder.
|
||||||
|
if (s_dragActive) {
|
||||||
|
bool didTransfer = false;
|
||||||
|
if (s_dropMode == 0 && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx &&
|
||||||
|
s_dragIdx < (int)rows.size()) {
|
||||||
|
const auto& srcRow = rows[s_dragIdx];
|
||||||
|
const auto& dstRow = rows[s_dropTargetIdx];
|
||||||
|
if (srcRow.info->balance > 1e-9) {
|
||||||
|
AddressTransferDialog::TransferInfo ti;
|
||||||
|
ti.fromAddr = srcRow.info->address;
|
||||||
|
ti.toAddr = dstRow.info->address;
|
||||||
|
ti.fromBalance = srcRow.info->balance;
|
||||||
|
ti.toBalance = dstRow.info->balance;
|
||||||
|
ti.fromIsZ = srcRow.isZ;
|
||||||
|
ti.toIsZ = dstRow.isZ;
|
||||||
|
AddressTransferDialog::show(app, ti);
|
||||||
|
didTransfer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!didTransfer) {
|
||||||
|
// Reorder — compute insert position from mouseY.
|
||||||
|
int insertIdx = 0;
|
||||||
|
for (int i = 0; i < (int)rows.size(); ++i) {
|
||||||
|
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
|
||||||
|
}
|
||||||
|
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) {
|
||||||
|
int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx;
|
||||||
|
if (targetIdx >= 0 && targetIdx < (int)rows.size() && s_dragIdx < (int)rows.size()) {
|
||||||
|
app->swapAddressOrder(rows[s_dragIdx].info->address,
|
||||||
|
rows[targetIdx].info->address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_dragIdx = -1;
|
||||||
|
s_dragActive = false;
|
||||||
|
s_dropTargetIdx = -1;
|
||||||
|
s_dropMode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PASS 2: Render rows ----
|
||||||
|
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
|
||||||
|
const auto& row = rows[row_idx];
|
||||||
|
const auto& addr = *row.info;
|
||||||
|
bool isDragged = (s_dragIdx == row_idx && s_dragActive);
|
||||||
|
|
||||||
|
// For dragged row, offset Y to follow mouse
|
||||||
|
float drawY = rowY[row_idx];
|
||||||
|
if (isDragged) {
|
||||||
|
drawY = mousePos.y - s_dragOffsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 rowPos(ImGui::GetCursorScreenPos().x, drawY);
|
||||||
|
ImVec2 rowEnd(rowPos.x + innerW, drawY + rowH);
|
||||||
|
ImU32 typeCol = row.isZ ? greenCol : goldCol;
|
||||||
|
if (row.hidden) typeCol = OnSurfaceDisabled();
|
||||||
|
|
||||||
|
// Dragged row: draw with semi-transparent elevation
|
||||||
|
if (isDragged) {
|
||||||
|
dl->AddRectFilled(ImVec2(rowPos.x - 2*dp, rowPos.y - 2*dp),
|
||||||
|
ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
|
||||||
|
IM_COL32(0, 0, 0, 30), 6.0f * dp);
|
||||||
|
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp);
|
||||||
|
// Tooltip following cursor — reflect the gesture: centre of a row = transfer to it,
|
||||||
|
// edge of a row = reorder (move here).
|
||||||
|
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size() && s_dropMode == 0) {
|
||||||
|
const auto& target = rows[s_dropTargetIdx];
|
||||||
|
ImGui::SetTooltip("%s\n%s\n\n%s %s",
|
||||||
|
truncateAddress(addr.address, 32).c_str(),
|
||||||
|
row.isZ ? TR("shielded") : TR("transparent"),
|
||||||
|
TR("transfer_to"),
|
||||||
|
truncateAddress(target.info->address, 32).c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::SetTooltip("%s\n%s\n\n%s",
|
||||||
|
truncateAddress(addr.address, 32).c_str(),
|
||||||
|
row.isZ ? TR("shielded") : TR("transparent"),
|
||||||
|
TR("address_reorder_hint"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Golden border for favorites — inset so it doesn't touch container
|
||||||
|
if (row.favorite) {
|
||||||
|
float inset = 2.0f * dp;
|
||||||
|
dl->AddRect(ImVec2(rowPos.x + inset, rowPos.y + inset),
|
||||||
|
ImVec2(rowEnd.x - inset, rowEnd.y - inset),
|
||||||
|
favGoldOutline, 4.0f * dp, 0, 1.5f * dp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection highlight
|
||||||
|
if (selected_row == row_idx && !isDragged) {
|
||||||
|
ImDrawFlags accentFlags = 0;
|
||||||
|
float accentRound = 2.0f * dp;
|
||||||
|
if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; }
|
||||||
|
if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; }
|
||||||
|
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
|
||||||
|
dl->AddRectFilled(rowPos, rowEnd, rowSelectCol, 4.0f * dp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover effects
|
||||||
|
bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd);
|
||||||
|
|
||||||
|
// Drop target feedback when dragging another row over this one. The cursor's vertical
|
||||||
|
// position WITHIN the row picks the gesture: top/bottom edge = reorder (show an insertion
|
||||||
|
// line), centre = transfer (highlight the whole row).
|
||||||
|
if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
|
||||||
|
s_dropTargetIdx = row_idx;
|
||||||
|
const float relY = rowH > 0.0f ? (mousePos.y - rowPos.y) / rowH : 0.5f;
|
||||||
|
s_dropMode = (relY < 0.30f || relY > 0.70f) ? 1 : 0;
|
||||||
|
if (s_dropMode == 0) {
|
||||||
|
// Transfer: highlight the whole target row.
|
||||||
|
dl->AddRectFilled(rowPos, rowEnd, WithAlpha(Primary(), 40), 4.0f * dp);
|
||||||
|
dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp);
|
||||||
|
} else {
|
||||||
|
// Reorder: draw an insertion line at the near (top or bottom) edge.
|
||||||
|
const float lineY = (relY < 0.5f) ? rowPos.y : rowEnd.y;
|
||||||
|
dl->AddLine(ImVec2(rowPos.x, lineY), ImVec2(rowEnd.x, lineY),
|
||||||
|
WithAlpha(Primary(), 230), 2.5f * dp);
|
||||||
|
dl->AddCircleFilled(ImVec2(rowPos.x, lineY), 3.0f * dp, WithAlpha(Primary(), 230));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hovered && selected_row != row_idx && !s_dragActive) {
|
||||||
|
dl->AddRectFilled(rowPos, rowEnd, rowHoverCol, 4.0f * dp);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto rowLayout = ComputeAddressRowLayout(
|
||||||
|
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
|
||||||
|
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
|
||||||
|
float cx = rowLayout.contentStartX;
|
||||||
|
float cy = rowLayout.contentStartY;
|
||||||
|
|
||||||
|
// ---- Button zone (right edge): [eye] [star] ----
|
||||||
|
float btnRound = 6.0f * dp;
|
||||||
|
bool btnClicked = false;
|
||||||
|
|
||||||
|
if (!isDragged) {
|
||||||
|
// Star button
|
||||||
|
{
|
||||||
|
const auto& starRect = rowLayout.favoriteButton;
|
||||||
|
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
|
||||||
|
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||||
|
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
|
||||||
|
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
|
||||||
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
||||||
|
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(starRect.x + (starRect.width - iSz.x) * 0.5f,
|
||||||
|
starRect.y + (starRect.height - iSz.y) * 0.5f), starCol, starIcon);
|
||||||
|
if (bHov && mouseClicked) {
|
||||||
|
if (row.favorite) app->unfavoriteAddress(addr.address);
|
||||||
|
else app->favoriteAddress(addr.address);
|
||||||
|
btnClicked = true;
|
||||||
|
}
|
||||||
|
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eye button (zero balance or hidden)
|
||||||
|
bool showEye = true;
|
||||||
|
// Always reserve space for both buttons so content doesn't shift
|
||||||
|
float contentRight = rowLayout.contentRight;
|
||||||
|
|
||||||
|
if (showEye) {
|
||||||
|
const auto& eyeRect = rowLayout.visibilityButton;
|
||||||
|
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
|
||||||
|
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||||
|
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
|
||||||
|
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
||||||
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
||||||
|
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
|
||||||
|
eyeRect.y + (eyeRect.height - iSz.y) * 0.5f), iconCol, hideIcon);
|
||||||
|
if (bHov && mouseClicked) {
|
||||||
|
if (row.hidden) app->unhideAddress(addr.address);
|
||||||
|
else app->hideAddress(addr.address);
|
||||||
|
btnClicked = true;
|
||||||
|
}
|
||||||
|
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Type icon or custom icon ----
|
||||||
|
float iconCx = cx + rowIconSz;
|
||||||
|
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||||
|
{
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
bool drewCustom = false;
|
||||||
|
if (!row.icon.empty()) {
|
||||||
|
drewCustom = AddressLabelDialog::drawIconByName(
|
||||||
|
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
|
||||||
|
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
|
||||||
|
}
|
||||||
|
if (!drewCustom) {
|
||||||
|
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
|
||||||
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Type label (first line) ----
|
||||||
|
float labelX = rowLayout.labelX;
|
||||||
|
{
|
||||||
|
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||||
|
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||||
|
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||||
|
char typeBuf[64];
|
||||||
|
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||||
|
|
||||||
|
// User label next to type
|
||||||
|
if (!row.label.empty()) {
|
||||||
|
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
||||||
|
OnSurfaceMedium(), row.label.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Address (second line) ----
|
||||||
|
{
|
||||||
|
float addrAvailW = contentRight - labelX;
|
||||||
|
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
|
||||||
|
std::string display_addr;
|
||||||
|
if (fullAddrSz.x <= addrAvailW) {
|
||||||
|
display_addr = addr.address;
|
||||||
|
} else {
|
||||||
|
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
|
||||||
|
? addrTable.columns.at("address").truncate : 32;
|
||||||
|
display_addr = truncateAddress(addr.address, addrTruncLen);
|
||||||
|
}
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||||
|
OnSurfaceMedium(), display_addr.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Balance (right-aligned, first line) ----
|
||||||
|
{
|
||||||
|
char balBuf[32];
|
||||||
|
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
|
||||||
|
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
|
||||||
|
float balX = contentRight - balSz.x;
|
||||||
|
ImU32 balCol = addr.balance > 0.0
|
||||||
|
? (row.isZ ? greenCol : OnSurface())
|
||||||
|
: OnSurfaceDisabled();
|
||||||
|
if (row.hidden) balCol = OnSurfaceDisabled();
|
||||||
|
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- USD value (second line, right-aligned) ----
|
||||||
|
{
|
||||||
|
std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
|
||||||
|
if (!usdText.empty()) {
|
||||||
|
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(contentRight - usdSz.x,
|
||||||
|
cy + body2->LegacySize + Layout::spacingXs()),
|
||||||
|
OnSurfaceDisabled(), usdText.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Copy feedback flash ----
|
||||||
|
if (s_copiedRow == row_idx) {
|
||||||
|
float elapsed = (float)ImGui::GetTime() - s_copiedTime;
|
||||||
|
if (elapsed < 1.0f) {
|
||||||
|
float alpha = 1.0f - elapsed;
|
||||||
|
ImFont* capF = Type().caption();
|
||||||
|
const char* copiedTxt = TR("copied");
|
||||||
|
ImVec2 sz = capF->CalcTextSizeA(capF->LegacySize, 1000.0f, 0.0f, copiedTxt);
|
||||||
|
ImVec4 fc = ImGui::ColorConvertU32ToFloat4(Success());
|
||||||
|
fc.w = alpha;
|
||||||
|
float tx = rowPos.x + (innerW - sz.x) * 0.5f;
|
||||||
|
float ty = rowPos.y + (rowH - sz.y) * 0.5f;
|
||||||
|
dl->AddText(capF, capF->LegacySize, ImVec2(tx, ty),
|
||||||
|
ImGui::ColorConvertFloat4ToU32(fc), copiedTxt);
|
||||||
|
} else {
|
||||||
|
s_copiedRow = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Click: begin drag tracking (copy deferred to mouse-up) ----
|
||||||
|
if (hovered && mouseClicked && !btnClicked) {
|
||||||
|
s_dragIdx = row_idx;
|
||||||
|
s_dragOffsetY = mousePos.y - rowPos.y;
|
||||||
|
s_dragStartY = mousePos.y;
|
||||||
|
s_dragActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate drag after a small threshold
|
||||||
|
if (s_dragIdx == row_idx && !s_dragActive && mouseDown) {
|
||||||
|
if (std::abs(mousePos.y - s_dragStartY) > 6.0f * dp) {
|
||||||
|
s_dragActive = true;
|
||||||
|
s_dropTargetIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end !isDragged
|
||||||
|
|
||||||
|
// Advance cursor for interaction area
|
||||||
|
ImGui::PushID(row_idx);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
|
||||||
|
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
||||||
|
if (ImGui::IsItemHovered() && !s_dragActive) {
|
||||||
|
ImGui::SetTooltip("%s", addr.address.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
const auto& acrTheme = GetCurrentAcrylicTheme();
|
||||||
|
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
|
||||||
|
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||||
|
ImGui::SetClipboardText(addr.address.c_str());
|
||||||
|
s_copiedRow = row_idx;
|
||||||
|
s_copiedTime = (float)ImGui::GetTime();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(TR("send_from_this_address"))) {
|
||||||
|
SetSendFromAddress(addr.address);
|
||||||
|
app->setCurrentPage(NavPage::Send);
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::MenuItem(TR("set_label"))) {
|
||||||
|
AddressLabelDialog::show(app, addr.address, row.isZ);
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||||
|
app->setMiningAddress(addr.address, !row.mining);
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(TR("export_private_key")))
|
||||||
|
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||||
|
if (row.isZ) {
|
||||||
|
if (ImGui::MenuItem(TR("export_viewing_key")))
|
||||||
|
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem(TR("show_qr_code")))
|
||||||
|
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
|
||||||
|
ImGui::Separator();
|
||||||
|
if (row.hidden) {
|
||||||
|
if (ImGui::MenuItem(TR("restore_address")))
|
||||||
|
app->unhideAddress(addr.address);
|
||||||
|
} else if (addr.balance < 1e-9) {
|
||||||
|
if (ImGui::MenuItem(TR("hide_address")))
|
||||||
|
app->hideAddress(addr.address);
|
||||||
|
}
|
||||||
|
if (row.favorite) {
|
||||||
|
if (ImGui::MenuItem(TR("remove_favorite")))
|
||||||
|
app->unfavoriteAddress(addr.address);
|
||||||
|
} else {
|
||||||
|
if (ImGui::MenuItem(TR("favorite_address")))
|
||||||
|
app->favoriteAddress(addr.address);
|
||||||
|
}
|
||||||
|
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
|
||||||
|
// Separator between rows
|
||||||
|
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
||||||
|
float sepY = rowY[row_idx] + rowH;
|
||||||
|
dl->AddLine(
|
||||||
|
ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
|
||||||
|
ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
|
||||||
|
dividerCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance cursor past all rows
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
|
||||||
|
cursorStartY + (float)rows.size() * rowH));
|
||||||
|
|
||||||
|
// ---- Keyboard navigation ----
|
||||||
|
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl && !s_dragActive) {
|
||||||
|
bool nav = false;
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
|
||||||
|
if (selected_row > 0) { selected_row--; nav = true; }
|
||||||
|
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
|
||||||
|
if (selected_row < (int)rows.size() - 1) { selected_row++; nav = true; }
|
||||||
|
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_Enter) && selected_row >= 0 && selected_row < (int)rows.size()) {
|
||||||
|
ImGui::SetClipboardText(rows[selected_row].info->address.c_str());
|
||||||
|
s_copiedRow = selected_row;
|
||||||
|
s_copiedTime = (float)ImGui::GetTime();
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_F2) && selected_row >= 0 && selected_row < (int)rows.size()) {
|
||||||
|
const auto& r = rows[selected_row];
|
||||||
|
AddressLabelDialog::show(app, r.info->address, r.isZ);
|
||||||
|
}
|
||||||
|
(void)nav;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
|
||||||
|
// Scroll fade
|
||||||
|
{
|
||||||
|
float fadeZone = std::min(
|
||||||
|
(Type().body2()->LegacySize + Type().caption()->LegacySize +
|
||||||
|
Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
|
||||||
|
addrListH * 0.18f);
|
||||||
|
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
|
||||||
|
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address count
|
||||||
|
{
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
char countBuf[128];
|
||||||
|
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
|
||||||
|
snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"),
|
||||||
|
(int)rows.size(), totalAddrs);
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the shared recent transactions section (used by all layouts)
|
||||||
|
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs) {
|
||||||
|
using namespace material;
|
||||||
|
const auto& S = schema::UISchema::instance();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||||
|
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
|
||||||
|
const auto& state = app->state();
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
|
||||||
|
app->setCurrentPage(NavPage::History);
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||||
|
float availableListH = ImGui::GetContentRegionAvail().y;
|
||||||
|
float listH = std::max({scaledRowH, recentH, availableListH});
|
||||||
|
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
|
||||||
|
ImGuiWindowFlags_NoBackground);
|
||||||
|
|
||||||
|
const auto& txs = state.transactions;
|
||||||
|
int count = (int)txs.size();
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
||||||
|
} else {
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImFont* capFont = Type().caption();
|
||||||
|
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||||
|
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||||
|
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
const auto& tx = txs[i];
|
||||||
|
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||||
|
float rowY = rowPos.y + rowH * 0.5f;
|
||||||
|
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||||
|
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
||||||
|
|
||||||
|
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
||||||
|
|
||||||
|
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
||||||
|
|
||||||
|
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
||||||
|
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||||
|
float amtX = rightEdge - amtSz.x - std::max(
|
||||||
|
S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||||
|
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(amtX, rowPos.y + 2 * dp),
|
||||||
|
recentTxAmountColor(tx.type), display.amountText.c_str());
|
||||||
|
|
||||||
|
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||||
|
OnSurfaceDisabled(), display.timeText.c_str());
|
||||||
|
|
||||||
|
float rowW = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||||
|
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||||
|
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (ImGui::IsMouseClicked(0))
|
||||||
|
app->setCurrentPage(NavPage::History);
|
||||||
|
}
|
||||||
|
ImGui::Dummy(ImVec2(0, rowH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render sync progress bar (used by multiple layouts)
|
||||||
|
void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
|
||||||
|
using namespace material;
|
||||||
|
const auto& S = schema::UISchema::instance();
|
||||||
|
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
||||||
|
const auto& state = app->state();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
|
||||||
|
if (state.sync.syncing && state.sync.headers > 0) {
|
||||||
|
float prog = static_cast<float>(state.sync.verification_progress);
|
||||||
|
if (prog > 1.0f) prog = 1.0f;
|
||||||
|
float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
|
||||||
|
float barW = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImVec2 barPos = ImGui::GetCursorScreenPos();
|
||||||
|
dl->AddRectFilled(barPos,
|
||||||
|
ImVec2(barPos.x + barW, barPos.y + barH),
|
||||||
|
IM_COL32(255, 255, 255, 15), 1.0f * dp);
|
||||||
|
dl->AddRectFilled(barPos,
|
||||||
|
ImVec2(barPos.x + barW * prog, barPos.y + barH),
|
||||||
|
WithAlpha(Warning(), 200), 1.0f * dp);
|
||||||
|
ImGui::Dummy(ImVec2(barW, barH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Shared rendering components used by every balance-tab layout (the animated hero, the address
|
||||||
|
// list, the recent-transactions strip, the sync bar) plus the balance-lerp animation state.
|
||||||
|
// Extracted from balance_tab.cpp so the layouts can be split into their own files later.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Animated balance display values — lerp toward the wallet's real balances each frame. Defined in
|
||||||
|
// balance_tab.cpp (the lerp is advanced by UpdateBalanceLerp), shared with the components.
|
||||||
|
extern double s_dispTotal;
|
||||||
|
extern double s_dispShielded;
|
||||||
|
extern double s_dispTransparent;
|
||||||
|
extern double s_dispUnconfirmed;
|
||||||
|
// True while an async new-address generation is in flight.
|
||||||
|
extern bool s_generating_z_address;
|
||||||
|
|
||||||
|
void UpdateBalanceLerp(App* app);
|
||||||
|
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs,
|
||||||
|
float heroHeightOverride = -1.0f);
|
||||||
|
void RenderSharedAddressList(App* app, float listH, float availW, float glassRound, float hs, float vs);
|
||||||
|
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs);
|
||||||
|
void RenderSyncBar(App* app, ImDrawList* dl, float vs);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "balance_recent_tx.h"
|
#include "balance_recent_tx.h"
|
||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -11,7 +12,7 @@ namespace ui {
|
|||||||
namespace {
|
namespace {
|
||||||
std::string truncateRecentAddress(const std::string& address, int maxLen)
|
std::string truncateRecentAddress(const std::string& address, int maxLen)
|
||||||
{
|
{
|
||||||
if (address.length() <= static_cast<size_t>(maxLen)) return address;
|
if (maxLen <= 3 || address.length() <= static_cast<size_t>(maxLen)) return address;
|
||||||
int half = (maxLen - 3) / 2;
|
int half = (maxLen - 3) / 2;
|
||||||
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
||||||
}
|
}
|
||||||
@@ -27,14 +28,7 @@ std::string formatRecentAmount(const std::string& type, double amount)
|
|||||||
|
|
||||||
std::string recentTimeAgo(int64_t timestamp)
|
std::string recentTimeAgo(int64_t timestamp)
|
||||||
{
|
{
|
||||||
if (timestamp <= 0) return "";
|
return util::formatTimeAgoShort(timestamp);
|
||||||
int64_t now = static_cast<int64_t>(std::time(nullptr));
|
|
||||||
int64_t diff = now - timestamp;
|
|
||||||
if (diff < 0) diff = 0;
|
|
||||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
|
||||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
|
||||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
|
||||||
return std::to_string(diff / 86400) + "d ago";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-798
@@ -6,6 +6,8 @@
|
|||||||
#include "balance_address_list.h"
|
#include "balance_address_list.h"
|
||||||
#include "balance_tab_helpers.h"
|
#include "balance_tab_helpers.h"
|
||||||
#include "balance_recent_tx.h"
|
#include "balance_recent_tx.h"
|
||||||
|
#include "balance_components.h"
|
||||||
|
#include "mining_tab_helpers.h" // FormatHashrate (consistent MH/GH/.. scaling)
|
||||||
#include "key_export_dialog.h"
|
#include "key_export_dialog.h"
|
||||||
#include "qr_popup_dialog.h"
|
#include "qr_popup_dialog.h"
|
||||||
#include "address_label_dialog.h"
|
#include "address_label_dialog.h"
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
#include "../../config/settings.h"
|
#include "../../config/settings.h"
|
||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
@@ -35,11 +38,12 @@
|
|||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
// Animated balance state — lerps smoothly toward target
|
// Animated balance state — lerps smoothly toward target. Non-static (declared extern in
|
||||||
static double s_dispTotal = 0.0;
|
// balance_components.h) so the shared components in balance_components.cpp share the same values.
|
||||||
static double s_dispShielded = 0.0;
|
double s_dispTotal = 0.0;
|
||||||
static double s_dispTransparent = 0.0;
|
double s_dispShielded = 0.0;
|
||||||
static double s_dispUnconfirmed = 0.0;
|
double s_dispTransparent = 0.0;
|
||||||
|
double s_dispUnconfirmed = 0.0;
|
||||||
|
|
||||||
// Forward declarations for all layout functions
|
// Forward declarations for all layout functions
|
||||||
static void RenderBalanceClassic(App* app);
|
static void RenderBalanceClassic(App* app);
|
||||||
@@ -67,7 +71,7 @@ static constexpr int s_legacyLayoutCount = 10;
|
|||||||
static std::vector<BalanceLayoutEntry> s_balanceLayouts;
|
static std::vector<BalanceLayoutEntry> s_balanceLayouts;
|
||||||
static std::string s_defaultLayoutId = "classic";
|
static std::string s_defaultLayoutId = "classic";
|
||||||
static bool s_layoutConfigLoaded = false;
|
static bool s_layoutConfigLoaded = false;
|
||||||
static bool s_generating_z_address = false;
|
bool s_generating_z_address = false; // external linkage — shared with balance_components.cpp
|
||||||
|
|
||||||
static void LoadBalanceLayoutConfig()
|
static void LoadBalanceLayoutConfig()
|
||||||
{
|
{
|
||||||
@@ -430,10 +434,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f),
|
dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f),
|
||||||
S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol);
|
S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol);
|
||||||
double hr = state.mining.localHashrate;
|
double hr = state.mining.localHashrate;
|
||||||
if (hr >= 1000.0)
|
snprintf(buf, sizeof(buf), " Mining %s", FormatHashrate(hr).c_str());
|
||||||
snprintf(buf, sizeof(buf), " Mining %.1f KH/s", hr / 1000.0);
|
|
||||||
else
|
|
||||||
snprintf(buf, sizeof(buf), " Mining %.0f H/s", hr);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(cx + 12 * dp, cy),
|
ImVec2(cx + 12 * dp, cy),
|
||||||
WithAlpha(Success(), 200), buf);
|
WithAlpha(Success(), 200), buf);
|
||||||
@@ -656,10 +657,12 @@ static void RenderBalanceClassic(App* app)
|
|||||||
};
|
};
|
||||||
std::vector<AddrRow> rows;
|
std::vector<AddrRow> rows;
|
||||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||||
|
// The search text is constant across the loop and containsIgnoreCase is now allocation-free,
|
||||||
|
// so build the filter view ONCE (was a per-address std::string + two tolower-copies/frame).
|
||||||
|
const std::string_view addrFilter(addr_search);
|
||||||
for (const auto& a : state.z_addresses) {
|
for (const auto& a : state.z_addresses) {
|
||||||
std::string filter(addr_search);
|
if (!util::containsIgnoreCase(a.address, addrFilter) &&
|
||||||
if (!containsIgnoreCase(a.address, filter) &&
|
!util::containsIgnoreCase(a.label, addrFilter))
|
||||||
!containsIgnoreCase(a.label, filter))
|
|
||||||
continue;
|
continue;
|
||||||
bool isHidden = app->isAddressHidden(a.address);
|
bool isHidden = app->isAddressHidden(a.address);
|
||||||
if (isHidden && !s_showHidden) continue;
|
if (isHidden && !s_showHidden) continue;
|
||||||
@@ -669,9 +672,8 @@ static void RenderBalanceClassic(App* app)
|
|||||||
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
|
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||||
}
|
}
|
||||||
for (const auto& a : state.t_addresses) {
|
for (const auto& a : state.t_addresses) {
|
||||||
std::string filter(addr_search);
|
if (!util::containsIgnoreCase(a.address, addrFilter) &&
|
||||||
if (!containsIgnoreCase(a.address, filter) &&
|
!util::containsIgnoreCase(a.label, addrFilter))
|
||||||
!containsIgnoreCase(a.label, filter))
|
|
||||||
continue;
|
continue;
|
||||||
bool isHidden = app->isAddressHidden(a.address);
|
bool isHidden = app->isAddressHidden(a.address);
|
||||||
if (isHidden && !s_showHidden) continue;
|
if (isHidden && !s_showHidden) continue;
|
||||||
@@ -1175,6 +1177,10 @@ static void RenderBalanceClassic(App* app)
|
|||||||
dl->AddRectFilled(rowPos, rowEnd,
|
dl->AddRectFilled(rowPos, rowEnd,
|
||||||
IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
// Show the full, untruncated address — two z-addresses can truncate to the
|
||||||
|
// same first/last window, so the truncated text alone can't disambiguate.
|
||||||
|
if (!tx.address.empty())
|
||||||
|
ImGui::SetTooltip("%s", tx.address.c_str());
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(NavPage::History);
|
app->setCurrentPage(NavPage::History);
|
||||||
}
|
}
|
||||||
@@ -1186,784 +1192,8 @@ static void RenderBalanceClassic(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// Shared helpers (UpdateBalanceLerp / RenderCompactHero / RenderSharedAddressList /
|
||||||
// Shared helpers used by multiple layouts
|
// RenderSharedRecentTx / RenderSyncBar) now live in balance_components.{h,cpp}.
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Update animated lerp balances — called at the top of every layout
|
|
||||||
static void UpdateBalanceLerp(App* app) {
|
|
||||||
using namespace material;
|
|
||||||
const auto& S = schema::UISchema::instance();
|
|
||||||
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 = (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; }
|
|
||||||
disp += diff * (double)(dt * spd);
|
|
||||||
if (std::abs(target - disp) < 1e-9) disp = target;
|
|
||||||
};
|
|
||||||
lerp(s_dispTotal, app->getTotalBalance(), dt, speed);
|
|
||||||
lerp(s_dispShielded, app->getShieldedBalance(), dt, speed);
|
|
||||||
lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
|
|
||||||
lerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render compact hero line: logo + balance + USD + mining on one line
|
|
||||||
static void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride = -1.0f) {
|
|
||||||
using namespace material;
|
|
||||||
char buf[64];
|
|
||||||
const float dp = Layout::dpiScale();
|
|
||||||
|
|
||||||
// Coin logo
|
|
||||||
ImTextureID logoTex = app->getCoinLogoTexture();
|
|
||||||
ImFont* sub1 = Type().subtitle1();
|
|
||||||
float logoSz = sub1->LegacySize + 4.0f * dp;
|
|
||||||
float lineH = (heroHeightOverride >= 0.0f) ? heroHeightOverride : logoSz;
|
|
||||||
if (logoTex != 0) {
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
dl->AddImage(logoTex,
|
|
||||||
ImVec2(pos.x, pos.y),
|
|
||||||
ImVec2(pos.x + lineH, pos.y + lineH),
|
|
||||||
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
|
|
||||||
ImGui::Dummy(ImVec2(lineH + Layout::spacingSm(), lineH));
|
|
||||||
ImGui::SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total balance
|
|
||||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
float fontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride : sub1->LegacySize;
|
|
||||||
DrawTextShadow(dl, sub1, fontSize, pos, OnSurface(), buf);
|
|
||||||
ImVec2 sz = sub1->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf);
|
|
||||||
ImGui::Dummy(ImVec2(sz.x, lineH));
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Ticker
|
|
||||||
ImFont* capFont = Type().caption();
|
|
||||||
float tickerFontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride * 0.6f : capFont->LegacySize;
|
|
||||||
float tickerY = pos.y + lineH - tickerFontSize;
|
|
||||||
dl->AddText(capFont, tickerFontSize,
|
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
|
||||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
|
||||||
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// USD value
|
|
||||||
const auto& state = app->state();
|
|
||||||
double usd_value = state.getBalanceUSD();
|
|
||||||
if (usd_value > 0.0)
|
|
||||||
snprintf(buf, sizeof(buf), "$%.2f", usd_value);
|
|
||||||
else
|
|
||||||
snprintf(buf, sizeof(buf), "$--.--");
|
|
||||||
dl->AddText(capFont, tickerFontSize,
|
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
|
||||||
OnSurfaceDisabled(), buf);
|
|
||||||
|
|
||||||
ImGui::NewLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the shared address list section (used by all layouts)
|
|
||||||
static void RenderSharedAddressList(App* app, float listH, float availW,
|
|
||||||
float glassRound, float hs, float vs) {
|
|
||||||
using namespace material;
|
|
||||||
const auto& S = schema::UISchema::instance();
|
|
||||||
const float dp = Layout::dpiScale();
|
|
||||||
const auto addrBtn = S.button("tabs.balance", "address-button");
|
|
||||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
|
||||||
const auto searchIn = S.input("tabs.balance", "search-input");
|
|
||||||
const auto addrTable = S.table("tabs.balance", "address-table");
|
|
||||||
const auto& state = app->state();
|
|
||||||
|
|
||||||
Type().text(TypeStyle::H6, TR("your_addresses"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// ---- Persistent state ----
|
|
||||||
static char addr_search[128] = "";
|
|
||||||
static bool s_hideZeroBalances = true;
|
|
||||||
static bool s_showHidden = false;
|
|
||||||
|
|
||||||
// Drag state
|
|
||||||
static int s_dragIdx = -1; // row being dragged (-1 = none)
|
|
||||||
static float s_dragOffsetY = 0.0f; // mouse offset from row top
|
|
||||||
static float s_dragStartY = 0.0f; // mouse Y at drag start
|
|
||||||
static bool s_dragActive = false; // drag distance threshold passed
|
|
||||||
static int s_dropTargetIdx = -1; // row hovered during drag
|
|
||||||
|
|
||||||
// Copy feedback
|
|
||||||
static int s_copiedRow = -1;
|
|
||||||
static float s_copiedTime = 0.0f;
|
|
||||||
|
|
||||||
// ---- Build and filter address rows ----
|
|
||||||
std::vector<AddressListInput> rowInputs;
|
|
||||||
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
|
|
||||||
|
|
||||||
auto addRows = [&](const auto& addrs, bool isZ) {
|
|
||||||
for (const auto& a : addrs) {
|
|
||||||
std::string addrLabel = app->getAddressLabel(a.address);
|
|
||||||
bool isHidden = app->isAddressHidden(a.address);
|
|
||||||
bool isFav = app->isAddressFavorite(a.address);
|
|
||||||
bool isMining = app->isMiningAddress(a.address);
|
|
||||||
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
|
|
||||||
addrLabel, app->getAddressIcon(a.address),
|
|
||||||
app->getAddressSortOrder(a.address)});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addRows(state.z_addresses, true);
|
|
||||||
addRows(state.t_addresses, false);
|
|
||||||
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
|
|
||||||
|
|
||||||
// ---- Toolbar: search + checkboxes + create buttons ----
|
|
||||||
float avail = ImGui::GetContentRegionAvail().x;
|
|
||||||
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
|
|
||||||
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
|
|
||||||
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
|
|
||||||
ImGui::SetNextItemWidth(searchW);
|
|
||||||
ImGui::InputTextWithHint("##AddrSearch", TR("filter"), addr_search, sizeof(addr_search));
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
|
||||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
|
|
||||||
{
|
|
||||||
int hc = app->getHiddenAddressCount();
|
|
||||||
if (hc > 0) {
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
|
||||||
char hlbl[64];
|
|
||||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
|
||||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
|
||||||
} else {
|
|
||||||
s_showHidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
|
||||||
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
|
||||||
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
|
||||||
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
|
|
||||||
S.drawElement("tabs.balance", "buttons-position").size * hs);
|
|
||||||
ImGui::SameLine(std::max(kMinButtonsPosition,
|
|
||||||
ImGui::GetWindowWidth() - totalButtonsWidth -
|
|
||||||
S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f)));
|
|
||||||
|
|
||||||
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
|
|
||||||
ImGui::BeginDisabled(sharedAddrSyncing);
|
|
||||||
if (s_generating_z_address) {
|
|
||||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
||||||
const char* dotStr[] = {"", ".", "..", "..."};
|
|
||||||
char genLabel[64];
|
|
||||||
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
|
|
||||||
TactileButton(genLabel, ImVec2(buttonWidth, 0),
|
|
||||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
|
||||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
|
||||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
|
||||||
s_generating_z_address = true;
|
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
|
||||||
s_generating_z_address = false;
|
|
||||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0),
|
|
||||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
|
||||||
app->createNewTAddress([](const std::string& addr) {
|
|
||||||
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
||||||
|
|
||||||
// ---- Glass panel container ----
|
|
||||||
float addrListH = listH;
|
|
||||||
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
|
|
||||||
|
|
||||||
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
|
||||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
|
||||||
ImVec2 listPanelMax(listPanelMin.x + availW, listPanelMin.y + addrListH);
|
|
||||||
GlassPanelSpec addrGlassSpec;
|
|
||||||
addrGlassSpec.rounding = glassRound;
|
|
||||||
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
|
|
||||||
|
|
||||||
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
|
|
||||||
int addrParentVtx = dlPanel->VtxBuffer.Size;
|
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
|
|
||||||
ImGui::BeginChild("AddressList", ImVec2(availW, addrListH), false,
|
|
||||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
|
||||||
ApplySmoothScroll();
|
|
||||||
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
|
|
||||||
int addrChildVtx = addrChildDL->VtxBuffer.Size;
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
|
|
||||||
// ---- Empty states ----
|
|
||||||
if (!app->isConnected()) {
|
|
||||||
ImGui::Dummy(ImVec2(0, 16 * dp));
|
|
||||||
float cw = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
|
|
||||||
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
|
|
||||||
ImGui::TextDisabled("%s", TR("not_connected"));
|
|
||||||
} else if (rows.empty()) {
|
|
||||||
float cw = ImGui::GetContentRegionAvail().x;
|
|
||||||
float ch = ImGui::GetContentRegionAvail().y;
|
|
||||||
if (ch < 60) ch = 60;
|
|
||||||
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
|
||||||
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
|
||||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
|
||||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
|
||||||
ImGui::TextDisabled("%s", emptyMsg);
|
|
||||||
} else {
|
|
||||||
// ---- PASS 1: Compute row layout ----
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
||||||
ImFont* capFont = Type().caption();
|
|
||||||
ImFont* body2 = Type().body2();
|
|
||||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd() + Layout::spacingMd();
|
|
||||||
static int selected_row = -1;
|
|
||||||
addrScrollY = ImGui::GetScrollY();
|
|
||||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
|
||||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
|
||||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
|
||||||
float rowPadLeft = Layout::cardInnerPadding();
|
|
||||||
float rowPadRight = Layout::cardInnerPadding();
|
|
||||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
|
|
||||||
S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
|
||||||
float innerW = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
// Theme colors for buttons (resolved once)
|
|
||||||
ImU32 favGoldFill = S.resolveColor("var(--favorite-fill)", IM_COL32(255, 200, 50, 40));
|
|
||||||
ImU32 favGoldBorder = S.resolveColor("var(--favorite-border)", IM_COL32(255, 200, 50, 100));
|
|
||||||
ImU32 favGoldIcon = S.resolveColor("var(--favorite-icon)", IM_COL32(255, 200, 50, 255));
|
|
||||||
ImU32 favGoldOutline= S.resolveColor("var(--favorite-outline)", IM_COL32(255, 200, 50, 120));
|
|
||||||
ImU32 btnFill = S.resolveColor("var(--row-button-fill)", IM_COL32(255, 255, 255, 12));
|
|
||||||
ImU32 btnFillHov = S.resolveColor("var(--row-button-fill-hover)", IM_COL32(255, 255, 255, 25));
|
|
||||||
ImU32 btnBorder = S.resolveColor("var(--row-button-border)", IM_COL32(255, 255, 255, 25));
|
|
||||||
ImU32 btnBorderHov = S.resolveColor("var(--row-button-border-hover)", IM_COL32(255, 255, 255, 50));
|
|
||||||
ImU32 rowHoverCol = S.resolveColor("var(--row-hover)", IM_COL32(255, 255, 255, 15));
|
|
||||||
ImU32 rowSelectCol = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
|
|
||||||
ImU32 dividerCol = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));
|
|
||||||
|
|
||||||
// Compute Y positions for all rows (pass 1)
|
|
||||||
std::vector<float> rowY(rows.size());
|
|
||||||
float cursorStartY = ImGui::GetCursorScreenPos().y;
|
|
||||||
for (int i = 0; i < (int)rows.size(); ++i)
|
|
||||||
rowY[i] = cursorStartY + i * rowH;
|
|
||||||
|
|
||||||
// Drag logic — detect drag start, compute drag position
|
|
||||||
ImVec2 mousePos = ImGui::GetMousePos();
|
|
||||||
bool mouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
|
||||||
bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
|
||||||
|
|
||||||
// Reset drop target each frame — it gets set only if mouse is over a row.
|
|
||||||
// Preserve the drop target on the release frame so the drop handler can use it.
|
|
||||||
if (s_dragActive && mouseDown) s_dropTargetIdx = -1;
|
|
||||||
|
|
||||||
if (s_dragIdx >= 0 && !mouseDown) {
|
|
||||||
// Mouse released — copy if it was a click (no drag activated)
|
|
||||||
if (!s_dragActive && s_dragIdx >= 0 && s_dragIdx < (int)rows.size()) {
|
|
||||||
const auto& clickRow = rows[s_dragIdx];
|
|
||||||
ImGui::SetClipboardText(clickRow.info->address.c_str());
|
|
||||||
selected_row = s_dragIdx;
|
|
||||||
s_copiedRow = s_dragIdx;
|
|
||||||
s_copiedTime = (float)ImGui::GetTime();
|
|
||||||
}
|
|
||||||
// Drop
|
|
||||||
if (s_dragActive && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx) {
|
|
||||||
const auto& srcRow = rows[s_dragIdx];
|
|
||||||
const auto& dstRow = rows[s_dropTargetIdx];
|
|
||||||
// Transfer: if dropped on another row and drag was active
|
|
||||||
if (srcRow.info->balance > 1e-9) {
|
|
||||||
AddressTransferDialog::TransferInfo ti;
|
|
||||||
ti.fromAddr = srcRow.info->address;
|
|
||||||
ti.toAddr = dstRow.info->address;
|
|
||||||
ti.fromBalance = srcRow.info->balance;
|
|
||||||
ti.toBalance = dstRow.info->balance;
|
|
||||||
ti.fromIsZ = srcRow.isZ;
|
|
||||||
ti.toIsZ = dstRow.isZ;
|
|
||||||
AddressTransferDialog::show(app, ti);
|
|
||||||
}
|
|
||||||
} else if (s_dragActive && s_dropTargetIdx < 0) {
|
|
||||||
// Reorder: dropped in gap — compute insert position from mouseY
|
|
||||||
int insertIdx = 0;
|
|
||||||
for (int i = 0; i < (int)rows.size(); ++i) {
|
|
||||||
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
|
|
||||||
}
|
|
||||||
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) {
|
|
||||||
int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx;
|
|
||||||
if (targetIdx >= 0 && targetIdx < (int)rows.size()) {
|
|
||||||
app->swapAddressOrder(rows[s_dragIdx].info->address,
|
|
||||||
rows[targetIdx].info->address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s_dragIdx = -1;
|
|
||||||
s_dragActive = false;
|
|
||||||
s_dropTargetIdx = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- PASS 2: Render rows ----
|
|
||||||
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
|
|
||||||
const auto& row = rows[row_idx];
|
|
||||||
const auto& addr = *row.info;
|
|
||||||
bool isDragged = (s_dragIdx == row_idx && s_dragActive);
|
|
||||||
|
|
||||||
// For dragged row, offset Y to follow mouse
|
|
||||||
float drawY = rowY[row_idx];
|
|
||||||
if (isDragged) {
|
|
||||||
drawY = mousePos.y - s_dragOffsetY;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 rowPos(ImGui::GetCursorScreenPos().x, drawY);
|
|
||||||
ImVec2 rowEnd(rowPos.x + innerW, drawY + rowH);
|
|
||||||
ImU32 typeCol = row.isZ ? greenCol : goldCol;
|
|
||||||
if (row.hidden) typeCol = OnSurfaceDisabled();
|
|
||||||
|
|
||||||
// Dragged row: draw with semi-transparent elevation
|
|
||||||
if (isDragged) {
|
|
||||||
dl->AddRectFilled(ImVec2(rowPos.x - 2*dp, rowPos.y - 2*dp),
|
|
||||||
ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
|
|
||||||
IM_COL32(0, 0, 0, 30), 6.0f * dp);
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp);
|
|
||||||
// Tooltip following cursor — show transfer intent if over a target row
|
|
||||||
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size()) {
|
|
||||||
const auto& target = rows[s_dropTargetIdx];
|
|
||||||
ImGui::SetTooltip("%s\n%s\n\n%s %s",
|
|
||||||
truncateAddress(addr.address, 32).c_str(),
|
|
||||||
row.isZ ? TR("shielded") : TR("transparent"),
|
|
||||||
TR("transfer_to"),
|
|
||||||
truncateAddress(target.info->address, 32).c_str());
|
|
||||||
} else {
|
|
||||||
ImGui::SetTooltip("%s\n%s",
|
|
||||||
truncateAddress(addr.address, 32).c_str(),
|
|
||||||
row.isZ ? TR("shielded") : TR("transparent"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Golden border for favorites — inset so it doesn't touch container
|
|
||||||
if (row.favorite) {
|
|
||||||
float inset = 2.0f * dp;
|
|
||||||
dl->AddRect(ImVec2(rowPos.x + inset, rowPos.y + inset),
|
|
||||||
ImVec2(rowEnd.x - inset, rowEnd.y - inset),
|
|
||||||
favGoldOutline, 4.0f * dp, 0, 1.5f * dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selection highlight
|
|
||||||
if (selected_row == row_idx && !isDragged) {
|
|
||||||
ImDrawFlags accentFlags = 0;
|
|
||||||
float accentRound = 2.0f * dp;
|
|
||||||
if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; }
|
|
||||||
if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; }
|
|
||||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, rowSelectCol, 4.0f * dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hover effects
|
|
||||||
bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd);
|
|
||||||
|
|
||||||
// Drop target highlight when dragging another row over this one
|
|
||||||
if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
|
|
||||||
s_dropTargetIdx = row_idx;
|
|
||||||
ImU32 dropCol = WithAlpha(Primary(), 40);
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, dropCol, 4.0f * dp);
|
|
||||||
dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hovered && selected_row != row_idx && !s_dragActive) {
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, rowHoverCol, 4.0f * dp);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto rowLayout = ComputeAddressRowLayout(
|
|
||||||
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
|
|
||||||
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
|
|
||||||
float cx = rowLayout.contentStartX;
|
|
||||||
float cy = rowLayout.contentStartY;
|
|
||||||
|
|
||||||
// ---- Button zone (right edge): [eye] [star] ----
|
|
||||||
float btnRound = 6.0f * dp;
|
|
||||||
bool btnClicked = false;
|
|
||||||
|
|
||||||
if (!isDragged) {
|
|
||||||
// Star button
|
|
||||||
{
|
|
||||||
const auto& starRect = rowLayout.favoriteButton;
|
|
||||||
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
|
|
||||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
|
||||||
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
|
|
||||||
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
|
|
||||||
ImFont* iconFont = Type().iconSmall();
|
|
||||||
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
|
||||||
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(starRect.x + (starRect.width - iSz.x) * 0.5f,
|
|
||||||
starRect.y + (starRect.height - iSz.y) * 0.5f), starCol, starIcon);
|
|
||||||
if (bHov && mouseClicked) {
|
|
||||||
if (row.favorite) app->unfavoriteAddress(addr.address);
|
|
||||||
else app->favoriteAddress(addr.address);
|
|
||||||
btnClicked = true;
|
|
||||||
}
|
|
||||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eye button (zero balance or hidden)
|
|
||||||
bool showEye = true;
|
|
||||||
// Always reserve space for both buttons so content doesn't shift
|
|
||||||
float contentRight = rowLayout.contentRight;
|
|
||||||
|
|
||||||
if (showEye) {
|
|
||||||
const auto& eyeRect = rowLayout.visibilityButton;
|
|
||||||
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
|
|
||||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
|
||||||
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
|
|
||||||
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
|
|
||||||
ImFont* iconFont = Type().iconSmall();
|
|
||||||
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
|
||||||
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
|
|
||||||
eyeRect.y + (eyeRect.height - iSz.y) * 0.5f), iconCol, hideIcon);
|
|
||||||
if (bHov && mouseClicked) {
|
|
||||||
if (row.hidden) app->unhideAddress(addr.address);
|
|
||||||
else app->hideAddress(addr.address);
|
|
||||||
btnClicked = true;
|
|
||||||
}
|
|
||||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Type icon or custom icon ----
|
|
||||||
float iconCx = cx + rowIconSz;
|
|
||||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
|
||||||
{
|
|
||||||
ImFont* iconFont = Type().iconSmall();
|
|
||||||
bool drewCustom = false;
|
|
||||||
if (!row.icon.empty()) {
|
|
||||||
drewCustom = AddressLabelDialog::drawIconByName(
|
|
||||||
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
|
|
||||||
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
|
|
||||||
}
|
|
||||||
if (!drewCustom) {
|
|
||||||
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, glyph);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Type label (first line) ----
|
|
||||||
float labelX = rowLayout.labelX;
|
|
||||||
{
|
|
||||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
|
||||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
|
||||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
|
||||||
char typeBuf[64];
|
|
||||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
|
||||||
|
|
||||||
// User label next to type
|
|
||||||
if (!row.label.empty()) {
|
|
||||||
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
|
||||||
OnSurfaceMedium(), row.label.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Address (second line) ----
|
|
||||||
{
|
|
||||||
float addrAvailW = contentRight - labelX;
|
|
||||||
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
|
|
||||||
std::string display_addr;
|
|
||||||
if (fullAddrSz.x <= addrAvailW) {
|
|
||||||
display_addr = addr.address;
|
|
||||||
} else {
|
|
||||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
|
|
||||||
? addrTable.columns.at("address").truncate : 32;
|
|
||||||
display_addr = truncateAddress(addr.address, addrTruncLen);
|
|
||||||
}
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
|
||||||
OnSurfaceMedium(), display_addr.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Balance (right-aligned, first line) ----
|
|
||||||
{
|
|
||||||
char balBuf[32];
|
|
||||||
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
|
|
||||||
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
|
|
||||||
float balX = contentRight - balSz.x;
|
|
||||||
ImU32 balCol = addr.balance > 0.0
|
|
||||||
? (row.isZ ? greenCol : OnSurface())
|
|
||||||
: OnSurfaceDisabled();
|
|
||||||
if (row.hidden) balCol = OnSurfaceDisabled();
|
|
||||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- USD value (second line, right-aligned) ----
|
|
||||||
{
|
|
||||||
std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
|
|
||||||
if (!usdText.empty()) {
|
|
||||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(contentRight - usdSz.x,
|
|
||||||
cy + body2->LegacySize + Layout::spacingXs()),
|
|
||||||
OnSurfaceDisabled(), usdText.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Copy feedback flash ----
|
|
||||||
if (s_copiedRow == row_idx) {
|
|
||||||
float elapsed = (float)ImGui::GetTime() - s_copiedTime;
|
|
||||||
if (elapsed < 1.0f) {
|
|
||||||
float alpha = 1.0f - elapsed;
|
|
||||||
ImFont* capF = Type().caption();
|
|
||||||
const char* copiedTxt = TR("copied");
|
|
||||||
ImVec2 sz = capF->CalcTextSizeA(capF->LegacySize, 1000.0f, 0.0f, copiedTxt);
|
|
||||||
ImVec4 fc = ImGui::ColorConvertU32ToFloat4(Success());
|
|
||||||
fc.w = alpha;
|
|
||||||
float tx = rowPos.x + (innerW - sz.x) * 0.5f;
|
|
||||||
float ty = rowPos.y + (rowH - sz.y) * 0.5f;
|
|
||||||
dl->AddText(capF, capF->LegacySize, ImVec2(tx, ty),
|
|
||||||
ImGui::ColorConvertFloat4ToU32(fc), copiedTxt);
|
|
||||||
} else {
|
|
||||||
s_copiedRow = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Click: begin drag tracking (copy deferred to mouse-up) ----
|
|
||||||
if (hovered && mouseClicked && !btnClicked) {
|
|
||||||
s_dragIdx = row_idx;
|
|
||||||
s_dragOffsetY = mousePos.y - rowPos.y;
|
|
||||||
s_dragStartY = mousePos.y;
|
|
||||||
s_dragActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate drag after a small threshold
|
|
||||||
if (s_dragIdx == row_idx && !s_dragActive && mouseDown) {
|
|
||||||
if (std::abs(mousePos.y - s_dragStartY) > 6.0f * dp) {
|
|
||||||
s_dragActive = true;
|
|
||||||
s_dropTargetIdx = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // end !isDragged
|
|
||||||
|
|
||||||
// Advance cursor for interaction area
|
|
||||||
ImGui::PushID(row_idx);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
|
|
||||||
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
|
||||||
if (ImGui::IsItemHovered() && !s_dragActive) {
|
|
||||||
ImGui::SetTooltip("%s", addr.address.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context menu
|
|
||||||
const auto& acrTheme = GetCurrentAcrylicTheme();
|
|
||||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
|
|
||||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
|
||||||
ImGui::SetClipboardText(addr.address.c_str());
|
|
||||||
s_copiedRow = row_idx;
|
|
||||||
s_copiedTime = (float)ImGui::GetTime();
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(TR("send_from_this_address"))) {
|
|
||||||
SetSendFromAddress(addr.address);
|
|
||||||
app->setCurrentPage(NavPage::Send);
|
|
||||||
}
|
|
||||||
ImGui::Separator();
|
|
||||||
if (ImGui::MenuItem(TR("set_label"))) {
|
|
||||||
AddressLabelDialog::show(app, addr.address, row.isZ);
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
|
||||||
app->setMiningAddress(addr.address, !row.mining);
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(TR("export_private_key")))
|
|
||||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
|
||||||
if (row.isZ) {
|
|
||||||
if (ImGui::MenuItem(TR("export_viewing_key")))
|
|
||||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(TR("show_qr_code")))
|
|
||||||
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
|
|
||||||
ImGui::Separator();
|
|
||||||
if (row.hidden) {
|
|
||||||
if (ImGui::MenuItem(TR("restore_address")))
|
|
||||||
app->unhideAddress(addr.address);
|
|
||||||
} else if (addr.balance < 1e-9) {
|
|
||||||
if (ImGui::MenuItem(TR("hide_address")))
|
|
||||||
app->hideAddress(addr.address);
|
|
||||||
}
|
|
||||||
if (row.favorite) {
|
|
||||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
|
||||||
app->unfavoriteAddress(addr.address);
|
|
||||||
} else {
|
|
||||||
if (ImGui::MenuItem(TR("favorite_address")))
|
|
||||||
app->favoriteAddress(addr.address);
|
|
||||||
}
|
|
||||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
// Separator between rows
|
|
||||||
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
|
||||||
float sepY = rowY[row_idx] + rowH;
|
|
||||||
dl->AddLine(
|
|
||||||
ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
|
|
||||||
ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
|
|
||||||
dividerCol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance cursor past all rows
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
|
|
||||||
cursorStartY + (float)rows.size() * rowH));
|
|
||||||
|
|
||||||
// ---- Keyboard navigation ----
|
|
||||||
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl && !s_dragActive) {
|
|
||||||
bool nav = false;
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
|
|
||||||
if (selected_row > 0) { selected_row--; nav = true; }
|
|
||||||
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
|
|
||||||
}
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
|
|
||||||
if (selected_row < (int)rows.size() - 1) { selected_row++; nav = true; }
|
|
||||||
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
|
|
||||||
}
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter) && selected_row >= 0 && selected_row < (int)rows.size()) {
|
|
||||||
ImGui::SetClipboardText(rows[selected_row].info->address.c_str());
|
|
||||||
s_copiedRow = selected_row;
|
|
||||||
s_copiedTime = (float)ImGui::GetTime();
|
|
||||||
}
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_F2) && selected_row >= 0 && selected_row < (int)rows.size()) {
|
|
||||||
const auto& r = rows[selected_row];
|
|
||||||
AddressLabelDialog::show(app, r.info->address, r.isZ);
|
|
||||||
}
|
|
||||||
(void)nav;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
ImGui::EndChild();
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
|
|
||||||
// Scroll fade
|
|
||||||
{
|
|
||||||
float fadeZone = std::min(
|
|
||||||
(Type().body2()->LegacySize + Type().caption()->LegacySize +
|
|
||||||
Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
|
|
||||||
addrListH * 0.18f);
|
|
||||||
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
|
|
||||||
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address count
|
|
||||||
{
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
char countBuf[128];
|
|
||||||
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
|
|
||||||
snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"),
|
|
||||||
(int)rows.size(), totalAddrs);
|
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the shared recent transactions section (used by all layouts)
|
|
||||||
static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs) {
|
|
||||||
using namespace material;
|
|
||||||
const auto& S = schema::UISchema::instance();
|
|
||||||
const float dp = Layout::dpiScale();
|
|
||||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
|
||||||
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
|
|
||||||
const auto& state = app->state();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
|
|
||||||
app->setCurrentPage(NavPage::History);
|
|
||||||
}
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
|
||||||
float availableListH = ImGui::GetContentRegionAvail().y;
|
|
||||||
float listH = std::max({scaledRowH, recentH, availableListH});
|
|
||||||
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
|
|
||||||
ImGuiWindowFlags_NoBackground);
|
|
||||||
|
|
||||||
const auto& txs = state.transactions;
|
|
||||||
int count = (int)txs.size();
|
|
||||||
|
|
||||||
if (count == 0) {
|
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
|
||||||
} else {
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
||||||
ImFont* capFont = Type().caption();
|
|
||||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
|
||||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
|
||||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
const auto& tx = txs[i];
|
|
||||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
|
||||||
float rowY = rowPos.y + rowH * 0.5f;
|
|
||||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
|
||||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
|
||||||
|
|
||||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
|
||||||
|
|
||||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
|
||||||
|
|
||||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
|
||||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
|
||||||
float amtX = rightEdge - amtSz.x - std::max(
|
|
||||||
S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
|
||||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
|
||||||
recentTxAmountColor(tx.type), display.amountText.c_str());
|
|
||||||
|
|
||||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
|
||||||
OnSurfaceDisabled(), display.timeText.c_str());
|
|
||||||
|
|
||||||
float rowW = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
|
||||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
if (ImGui::IsMouseClicked(0))
|
|
||||||
app->setCurrentPage(NavPage::History);
|
|
||||||
}
|
|
||||||
ImGui::Dummy(ImVec2(0, rowH));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndChild();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render sync progress bar (used by multiple layouts)
|
|
||||||
static void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
|
|
||||||
using namespace material;
|
|
||||||
const auto& S = schema::UISchema::instance();
|
|
||||||
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
|
||||||
const auto& state = app->state();
|
|
||||||
const float dp = Layout::dpiScale();
|
|
||||||
|
|
||||||
if (state.sync.syncing && state.sync.headers > 0) {
|
|
||||||
float prog = static_cast<float>(state.sync.verification_progress);
|
|
||||||
if (prog > 1.0f) prog = 1.0f;
|
|
||||||
float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
|
|
||||||
float barW = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImVec2 barPos = ImGui::GetCursorScreenPos();
|
|
||||||
dl->AddRectFilled(barPos,
|
|
||||||
ImVec2(barPos.x + barW, barPos.y + barH),
|
|
||||||
IM_COL32(255, 255, 255, 15), 1.0f * dp);
|
|
||||||
dl->AddRectFilled(barPos,
|
|
||||||
ImVec2(barPos.x + barW * prog, barPos.y + barH),
|
|
||||||
WithAlpha(Warning(), 200), 1.0f * dp);
|
|
||||||
ImGui::Dummy(ImVec2(barW, barH));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Layout 1: Donut Chart
|
// Layout 1: Donut Chart
|
||||||
@@ -3202,10 +2432,7 @@ static void RenderBalanceTwoRow(App* app) {
|
|||||||
}
|
}
|
||||||
if (state.mining.generate) {
|
if (state.mining.generate) {
|
||||||
double hr = state.mining.localHashrate;
|
double hr = state.mining.localHashrate;
|
||||||
if (hr >= 1000.0)
|
snprintf(buf, sizeof(buf), "Mining %s", FormatHashrate(hr).c_str());
|
||||||
snprintf(buf, sizeof(buf), "Mining %.1f KH/s", hr / 1000.0);
|
|
||||||
else
|
|
||||||
snprintf(buf, sizeof(buf), "Mining %.0f H/s", hr);
|
|
||||||
Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf);
|
Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/text_format.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
|
|
||||||
@@ -22,31 +23,20 @@ std::string TrId(const char* trKey, const char* id)
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thin wrappers over the shared, allocation-free helpers (kept for existing call sites).
|
||||||
bool containsIgnoreCase(const std::string& value, const std::string& search)
|
bool containsIgnoreCase(const std::string& value, const std::string& search)
|
||||||
{
|
{
|
||||||
if (search.empty()) return true;
|
return util::containsIgnoreCase(value, search);
|
||||||
std::string haystack = value;
|
|
||||||
std::string needle = search;
|
|
||||||
std::transform(haystack.begin(), haystack.end(), haystack.begin(), ::tolower);
|
|
||||||
std::transform(needle.begin(), needle.end(), needle.begin(), ::tolower);
|
|
||||||
return haystack.find(needle) != std::string::npos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string timeAgo(int64_t timestamp)
|
std::string timeAgo(int64_t timestamp)
|
||||||
{
|
{
|
||||||
if (timestamp <= 0) return "";
|
return util::formatTimeAgoShort(timestamp);
|
||||||
int64_t now = static_cast<int64_t>(std::time(nullptr));
|
|
||||||
int64_t diff = now - timestamp;
|
|
||||||
if (diff < 0) diff = 0;
|
|
||||||
if (diff < 60) return std::to_string(diff) + "s ago";
|
|
||||||
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
|
|
||||||
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
|
|
||||||
return std::to_string(diff / 86400) + "d ago";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string truncateAddress(const std::string& address, int maxLen)
|
std::string truncateAddress(const std::string& address, int maxLen)
|
||||||
{
|
{
|
||||||
if (address.length() <= static_cast<size_t>(maxLen)) return address;
|
if (maxLen <= 3 || address.length() <= static_cast<size_t>(maxLen)) return address;
|
||||||
int half = (maxLen - 3) / 2;
|
int half = (maxLen - 3) / 2;
|
||||||
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
return address.substr(0, half) + "..." + address.substr(address.length() - half);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "block_info_dialog.h"
|
#include "block_info_dialog.h"
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../rpc/rpc_client.h"
|
#include "../../rpc/rpc_client.h"
|
||||||
|
#include "../../rpc/rpc_worker.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
@@ -124,14 +125,31 @@ void BlockInfoDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
||||||
if (rpc && rpc->isConnected()) {
|
if (rpc && rpc->isConnected() && app->worker()) {
|
||||||
s_loading = true;
|
s_loading = true;
|
||||||
s_error.clear();
|
s_error.clear();
|
||||||
s_has_data = false;
|
s_has_data = false;
|
||||||
s_pending_app = app;
|
s_pending_app = app;
|
||||||
|
|
||||||
// Use getBlock(height) which uses UnifiedCallback
|
// Run the two chained RPCs (getblockhash → getblock) on the worker thread;
|
||||||
rpc->getBlock(s_height, handleBlockResponseUnified);
|
// doing them inline froze the UI for two round-trips. Guard the hash type.
|
||||||
|
int height = s_height;
|
||||||
|
app->worker()->post([rpc, height]() -> rpc::RPCWorker::MainCb {
|
||||||
|
json block;
|
||||||
|
std::string error;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
||||||
|
auto hashResult = rpc->call("getblockhash", {height});
|
||||||
|
if (!hashResult.is_string()) {
|
||||||
|
error = "unexpected getblockhash result";
|
||||||
|
} else {
|
||||||
|
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
error = e.what();
|
||||||
|
}
|
||||||
|
return [block, error]() { handleBlockResponseUnified(block, error); };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace ui {
|
|||||||
class BootstrapDownloadDialog {
|
class BootstrapDownloadDialog {
|
||||||
public:
|
public:
|
||||||
static void show(App* app) {
|
static void show(App* app) {
|
||||||
|
if (!app || !app->supportsFullNodeLifecycleActions()) return;
|
||||||
s_open = true;
|
s_open = true;
|
||||||
s_app = app;
|
s_app = app;
|
||||||
s_state = State::Confirm;
|
s_state = State::Confirm;
|
||||||
@@ -285,6 +286,7 @@ private:
|
|||||||
|
|
||||||
// ---- Shared: kick off download ----
|
// ---- Shared: kick off download ----
|
||||||
static void startDownload(const std::string& url) {
|
static void startDownload(const std::string& url) {
|
||||||
|
if (!s_app || !s_app->supportsFullNodeLifecycleActions()) return;
|
||||||
s_wasDaemonRunning = s_app->stopDaemonForBootstrap();
|
s_wasDaemonRunning = s_app->stopDaemonForBootstrap();
|
||||||
s_app->setBootstrapDownloading(true);
|
s_app->setBootstrapDownloading(true);
|
||||||
s_bootstrap = std::make_unique<util::Bootstrap>();
|
s_bootstrap = std::make_unique<util::Bootstrap>();
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ bool consoleLinePassesFilter(const std::string& lineText,
|
|||||||
{
|
{
|
||||||
if (!filter.daemonMessagesEnabled && lineColor == filter.daemonColor) return false;
|
if (!filter.daemonMessagesEnabled && lineColor == filter.daemonColor) return false;
|
||||||
if (!filter.rpcTraceEnabled && lineColor == filter.rpcTraceColor) return false;
|
if (!filter.rpcTraceEnabled && lineColor == filter.rpcTraceColor) return false;
|
||||||
|
// "[app] ..." lines share COLOR_INFO with other info text, so match them by prefix.
|
||||||
|
if (!filter.appMessagesEnabled && lineText.rfind("[app] ", 0) == 0) return false;
|
||||||
if (filter.errorsOnly && lineColor != filter.errorColor) return false;
|
if (filter.errorsOnly && lineColor != filter.errorColor) return false;
|
||||||
if (!filter.text.empty()) {
|
if (!filter.text.empty()) {
|
||||||
std::string needle = lowerCopy(filter.text);
|
std::string needle = lowerCopy(filter.text);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct ConsoleOutputFilter {
|
|||||||
bool daemonMessagesEnabled = true;
|
bool daemonMessagesEnabled = true;
|
||||||
bool errorsOnly = false;
|
bool errorsOnly = false;
|
||||||
bool rpcTraceEnabled = false;
|
bool rpcTraceEnabled = false;
|
||||||
|
bool appMessagesEnabled = true; // "[app] ..." wallet log lines (matched by prefix, not color)
|
||||||
ImU32 daemonColor = 0;
|
ImU32 daemonColor = 0;
|
||||||
ImU32 errorColor = 0;
|
ImU32 errorColor = 0;
|
||||||
ImU32 rpcTraceColor = 0;
|
ImU32 rpcTraceColor = 0;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ float ConsoleTab::s_console_zoom = 1.0f;
|
|||||||
bool ConsoleTab::s_daemon_messages_enabled = true;
|
bool ConsoleTab::s_daemon_messages_enabled = true;
|
||||||
bool ConsoleTab::s_errors_only_enabled = false;
|
bool ConsoleTab::s_errors_only_enabled = false;
|
||||||
bool ConsoleTab::s_rpc_trace_enabled = false;
|
bool ConsoleTab::s_rpc_trace_enabled = false;
|
||||||
|
bool ConsoleTab::s_app_messages_enabled = true;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -536,6 +537,23 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
// App messages toggle — "[app] ..." wallet log lines
|
||||||
|
{
|
||||||
|
static bool s_prev_app_enabled = true;
|
||||||
|
ImGui::Checkbox(TR("console_app"), &s_app_messages_enabled);
|
||||||
|
if (s_prev_app_enabled != s_app_messages_enabled && auto_scroll_) {
|
||||||
|
scroll_to_bottom_ = true;
|
||||||
|
}
|
||||||
|
s_prev_app_enabled = s_app_messages_enabled;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("%s", TR("console_show_app_output"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
// Clear button
|
// Clear button
|
||||||
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||||
clear();
|
clear();
|
||||||
@@ -651,10 +669,12 @@ void ConsoleTab::renderOutput()
|
|||||||
// Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
|
// Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
|
||||||
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
|
||||||
s_errors_only_enabled, s_rpc_trace_enabled,
|
s_errors_only_enabled, s_rpc_trace_enabled,
|
||||||
|
s_app_messages_enabled,
|
||||||
COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
|
COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
|
||||||
bool has_text_filter = !outputFilter.text.empty();
|
bool has_text_filter = !outputFilter.text.empty();
|
||||||
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
|
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
|
||||||
!outputFilter.rpcTraceEnabled || outputFilter.errorsOnly;
|
!outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled ||
|
||||||
|
outputFilter.errorsOnly;
|
||||||
visible_indices_.clear();
|
visible_indices_.clear();
|
||||||
if (has_filter) {
|
if (has_filter) {
|
||||||
for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
|
for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ public:
|
|||||||
// Show app RPC calls made through RPCClient (method/source only)
|
// Show app RPC calls made through RPCClient (method/source only)
|
||||||
static bool s_rpc_trace_enabled;
|
static bool s_rpc_trace_enabled;
|
||||||
|
|
||||||
|
// Show "[app] ..." wallet log lines
|
||||||
|
static bool s_app_messages_enabled;
|
||||||
|
|
||||||
/// Refresh console text colors for current theme (call after theme switch)
|
/// Refresh console text colors for current theme (call after theme switch)
|
||||||
static void refreshColors();
|
static void refreshColors();
|
||||||
|
|
||||||
|
|||||||
+140
-63
@@ -41,12 +41,35 @@ using namespace material;
|
|||||||
static char s_search_buf[128] = {};
|
static char s_search_buf[128] = {};
|
||||||
static bool s_search_loading = false;
|
static bool s_search_loading = false;
|
||||||
static std::string s_search_error;
|
static std::string s_search_error;
|
||||||
|
static double s_search_change_time = -1.0; // GetTime() of the last edit (debounce for live search)
|
||||||
|
static std::string s_search_last_query; // last query actually run (avoids re-running it)
|
||||||
|
static constexpr double kSearchDebounceSeconds = 0.35;
|
||||||
|
// When > 0, the recent-blocks list is anchored here (a search result) instead of the chain tip.
|
||||||
|
// Searching re-anchors the LIST rather than opening the detail modal — that stays for row clicks.
|
||||||
|
static int s_search_target_height = -1;
|
||||||
|
|
||||||
|
// A query that performSearch() can resolve to a concrete result: a block height (all digits) or a
|
||||||
|
// complete 64-char block hash / txid. Partial hex is intentionally NOT resolvable, so live search
|
||||||
|
// stays quiet (no "invalid query") until the user finishes typing or pasting a full hash.
|
||||||
|
static bool isResolvableQuery(const std::string& q) {
|
||||||
|
if (q.empty()) return false;
|
||||||
|
if (std::all_of(q.begin(), q.end(), ::isdigit)) return true;
|
||||||
|
return q.size() == 64 && std::all_of(q.begin(), q.end(), [](char c) {
|
||||||
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
using BlockSummary = ExplorerBlockSummary;
|
using BlockSummary = ExplorerBlockSummary;
|
||||||
static ExplorerBlockCache s_recent_block_cache;
|
static ExplorerBlockCache s_recent_block_cache;
|
||||||
static std::set<int> s_pending_block_heights;
|
static std::set<int> s_pending_block_heights;
|
||||||
static int s_recent_page = 0;
|
static int s_recent_page = 0;
|
||||||
static int s_recent_max_page = 0;
|
static int s_recent_max_page = 0;
|
||||||
|
// Fuzzy search: a non-numeric / partial query filters the list to cached blocks whose hash (or
|
||||||
|
// height text) contains it. Results are memoized per query so we don't hit SQLite every frame.
|
||||||
|
static std::string s_active_search_query;
|
||||||
|
static std::string s_fuzzy_query_cached;
|
||||||
|
static std::vector<BlockSummary> s_fuzzy_results;
|
||||||
|
static constexpr int kMaxFuzzyResults = 100;
|
||||||
static std::atomic<int> s_pending_block_fetches{0};
|
static std::atomic<int> s_pending_block_fetches{0};
|
||||||
static bool s_recent_disk_cache_validation_pending = false;
|
static bool s_recent_disk_cache_validation_pending = false;
|
||||||
|
|
||||||
@@ -263,53 +286,6 @@ static void fetchBlockDetail(App* app, int height) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fetchBlockDetailByHash(App* app, const std::string& hash) {
|
|
||||||
auto* worker = app->worker();
|
|
||||||
auto* rpc = app->rpc();
|
|
||||||
if (!worker || !rpc) return;
|
|
||||||
s_detail_loading = true;
|
|
||||||
s_search_loading = true;
|
|
||||||
s_search_error.clear();
|
|
||||||
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
|
|
||||||
json blockResult;
|
|
||||||
json txResult;
|
|
||||||
std::string blockErr;
|
|
||||||
bool gotBlock = false;
|
|
||||||
bool gotTx = false;
|
|
||||||
// Try as block hash first
|
|
||||||
try {
|
|
||||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
|
||||||
blockResult = rpc->call("getblock", {hash});
|
|
||||||
gotBlock = true;
|
|
||||||
} catch (...) {}
|
|
||||||
// If not a block hash, try as txid
|
|
||||||
if (!gotBlock) {
|
|
||||||
try {
|
|
||||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
|
||||||
txResult = rpc->call("getrawtransaction", {hash, 1});
|
|
||||||
gotTx = true;
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
return [blockResult, txResult, gotBlock, gotTx]() {
|
|
||||||
if (gotBlock) {
|
|
||||||
applyBlockDetailResult(blockResult, "");
|
|
||||||
} else if (gotTx && !txResult.is_null()) {
|
|
||||||
s_detail_loading = false;
|
|
||||||
s_search_loading = false;
|
|
||||||
s_tx_detail = txResult;
|
|
||||||
s_tx_loading = false;
|
|
||||||
s_expanded_tx_idx = 0;
|
|
||||||
s_search_error.clear();
|
|
||||||
s_show_detail_modal = false;
|
|
||||||
} else {
|
|
||||||
s_detail_loading = false;
|
|
||||||
s_search_loading = false;
|
|
||||||
s_search_error = "No block or transaction found for this hash";
|
|
||||||
s_show_detail_modal = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool fetchRecentBlock(App* app, int height) {
|
static bool fetchRecentBlock(App* app, int height) {
|
||||||
auto* worker = app->worker();
|
auto* worker = app->worker();
|
||||||
@@ -391,24 +367,72 @@ static void fetchTxDetail(App* app, const std::string& txid) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static void performSearch(App* app, const std::string& query) {
|
// Resolve a 64-char hash to a block height and re-anchor the list there (no modal). If the hash is a
|
||||||
if (query.empty()) return;
|
// txid rather than a block, fall back to the inline transaction view (also not a modal).
|
||||||
|
static void navigateToHash(App* app, const std::string& hash) {
|
||||||
|
auto* worker = app->worker();
|
||||||
|
auto* rpc = app->rpc();
|
||||||
|
if (!worker || !rpc || !rpc->isConnected()) return;
|
||||||
|
s_search_loading = true;
|
||||||
|
s_search_error.clear();
|
||||||
|
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
|
||||||
|
json blockResult, txResult;
|
||||||
|
bool gotBlock = false, gotTx = false;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||||
|
blockResult = rpc->call("getblock", {hash});
|
||||||
|
gotBlock = true;
|
||||||
|
} catch (...) {}
|
||||||
|
if (!gotBlock) {
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||||
|
txResult = rpc->call("getrawtransaction", {hash, 1});
|
||||||
|
gotTx = true;
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
return [blockResult, txResult, gotBlock, gotTx]() {
|
||||||
|
s_search_loading = false;
|
||||||
|
if (gotBlock) {
|
||||||
|
int h = blockResult.value("height", -1);
|
||||||
|
if (h > 0) { s_search_target_height = h; s_recent_page = 0; }
|
||||||
|
s_show_detail_modal = false;
|
||||||
|
} else if (gotTx && !txResult.is_null()) {
|
||||||
|
// A txid: show it inline (existing tx detail view), not a modal.
|
||||||
|
s_tx_detail = txResult;
|
||||||
|
s_tx_loading = false;
|
||||||
|
s_expanded_tx_idx = 0;
|
||||||
|
s_show_detail_modal = false;
|
||||||
|
} else {
|
||||||
|
s_search_error = "No block or transaction found for this hash";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Integer = block height
|
// Search re-anchors the recent-blocks LIST to the queried block instead of popping the detail modal.
|
||||||
bool isNumeric = std::all_of(query.begin(), query.end(), ::isdigit);
|
static void performSearch(App* app, const std::string& query) {
|
||||||
if (isNumeric && !query.empty()) {
|
if (query.empty()) { s_search_target_height = -1; return; }
|
||||||
int height = std::stoi(query);
|
|
||||||
fetchBlockDetail(app, height);
|
// Integer = block height → just point the list at it.
|
||||||
|
if (std::all_of(query.begin(), query.end(), ::isdigit)) {
|
||||||
|
long h = 0;
|
||||||
|
try { h = std::stol(query); } catch (...) { return; }
|
||||||
|
int tip = app->getWalletState().sync.blocks;
|
||||||
|
if (h < 1) h = 1;
|
||||||
|
if (tip > 0 && h > tip) h = tip;
|
||||||
|
s_search_target_height = static_cast<int>(h);
|
||||||
|
s_recent_page = 0;
|
||||||
|
s_search_error.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 64-char hex = block hash or txid
|
// 64-char hex = block hash or txid → resolve to a height and re-anchor (or inline tx).
|
||||||
bool isHex64 = query.size() == 64 &&
|
bool isHex64 = query.size() == 64 &&
|
||||||
std::all_of(query.begin(), query.end(), [](char c) {
|
std::all_of(query.begin(), query.end(), [](char c) {
|
||||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||||
});
|
});
|
||||||
if (isHex64) {
|
if (isHex64) {
|
||||||
fetchBlockDetailByHash(app, query);
|
navigateToHash(app, query);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +486,7 @@ static void renderSearchBar(App* app, float availWidth) {
|
|||||||
ImGui::SetNextItemWidth(inputW);
|
ImGui::SetNextItemWidth(inputW);
|
||||||
bool enter = ImGui::InputText("##ExplorerSearch", s_search_buf, sizeof(s_search_buf),
|
bool enter = ImGui::InputText("##ExplorerSearch", s_search_buf, sizeof(s_search_buf),
|
||||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
|
bool edited = ImGui::IsItemEdited(); // must read immediately after the input item
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -471,11 +496,35 @@ static void renderSearchBar(App* app, float availWidth) {
|
|||||||
bool clicked = material::StyledButton(TR("explorer_search"),
|
bool clicked = material::StyledButton(TR("explorer_search"),
|
||||||
ImVec2(btnW, barH));
|
ImVec2(btnW, barH));
|
||||||
|
|
||||||
if ((enter || clicked) && app->rpc() && app->rpc()->isConnected()) {
|
|
||||||
std::string query(s_search_buf);
|
std::string query(s_search_buf);
|
||||||
while (!query.empty() && query.front() == ' ') query.erase(query.begin());
|
while (!query.empty() && query.front() == ' ') query.erase(query.begin());
|
||||||
while (!query.empty() && query.back() == ' ') query.pop_back();
|
while (!query.empty() && query.back() == ' ') query.pop_back();
|
||||||
performSearch(app, query);
|
s_active_search_query = query; // drives the fuzzy list filter in renderRecentBlocks
|
||||||
|
|
||||||
|
if (edited) {
|
||||||
|
s_search_change_time = ImGui::GetTime(); // (re)start the debounce on each keystroke
|
||||||
|
s_search_error.clear(); // don't show a stale error mid-type
|
||||||
|
if (query.empty()) {
|
||||||
|
// Cleared the box → drop the anchor and return to recent (tip) blocks immediately.
|
||||||
|
s_search_target_height = -1;
|
||||||
|
s_search_last_query.clear();
|
||||||
|
s_search_change_time = -1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool doSearch = enter || clicked;
|
||||||
|
// Live search: once the user pauses and the query is resolvable, re-anchor the list (no modal)
|
||||||
|
// without waiting for Enter.
|
||||||
|
if (!doSearch && s_search_change_time >= 0.0 &&
|
||||||
|
(ImGui::GetTime() - s_search_change_time) >= kSearchDebounceSeconds &&
|
||||||
|
isResolvableQuery(query) && query != s_search_last_query) {
|
||||||
|
doSearch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doSearch && !query.empty()) {
|
||||||
|
s_search_change_time = -1.0; // consume the debounce
|
||||||
|
s_search_last_query = query;
|
||||||
|
performSearch(app, query); // numeric is offline-friendly; hash lookup checks rpc itself
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading spinner
|
// Loading spinner
|
||||||
@@ -746,12 +795,16 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
int rowsPerPage = std::max(1, (int)std::ceil(rowAreaH / std::max(1.0f, baseRowH)));
|
int rowsPerPage = std::max(1, (int)std::ceil(rowAreaH / std::max(1.0f, baseRowH)));
|
||||||
float rowH = baseRowH;
|
float rowH = baseRowH;
|
||||||
|
|
||||||
int maxPage = (state.sync.blocks > 0) ? (state.sync.blocks - 1) / rowsPerPage : 0;
|
// Anchor the list at a search result when one is active, otherwise at the chain tip.
|
||||||
|
int anchorTip = (s_search_target_height > 0 && s_search_target_height <= state.sync.blocks)
|
||||||
|
? s_search_target_height : state.sync.blocks;
|
||||||
|
|
||||||
|
int maxPage = (anchorTip > 0) ? (anchorTip - 1) / rowsPerPage : 0;
|
||||||
s_recent_max_page = maxPage;
|
s_recent_max_page = maxPage;
|
||||||
if (s_recent_page > maxPage) s_recent_page = maxPage;
|
if (s_recent_page > maxPage) s_recent_page = maxPage;
|
||||||
if (s_recent_page < 0) s_recent_page = 0;
|
if (s_recent_page < 0) s_recent_page = 0;
|
||||||
|
|
||||||
int startHeight = state.sync.blocks - s_recent_page * rowsPerPage;
|
int startHeight = anchorTip - s_recent_page * rowsPerPage;
|
||||||
std::vector<int> pageHeights;
|
std::vector<int> pageHeights;
|
||||||
pageHeights.reserve(rowsPerPage);
|
pageHeights.reserve(rowsPerPage);
|
||||||
for (int i = 0; i < rowsPerPage; ++i) {
|
for (int i = 0; i < rowsPerPage; ++i) {
|
||||||
@@ -765,7 +818,24 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
|
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canFetchBlocks = state.sync.blocks > 0 &&
|
// Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
|
||||||
|
// (or height text) contains it. Exact queries (a height or a full 64-char hash) still navigate
|
||||||
|
// via performSearch/the anchor above. Results are memoized per query (no per-frame SQLite).
|
||||||
|
const std::string& fq = s_active_search_query;
|
||||||
|
bool fqDigits = !fq.empty() && std::all_of(fq.begin(), fq.end(), ::isdigit);
|
||||||
|
bool fqFullHash = fq.size() == 64 && std::all_of(fq.begin(), fq.end(), [](char c) {
|
||||||
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||||
|
});
|
||||||
|
bool fuzzyMode = !fq.empty() && !fqDigits && !fqFullHash;
|
||||||
|
if (fuzzyMode) {
|
||||||
|
if (fq != s_fuzzy_query_cached) {
|
||||||
|
s_fuzzy_results = s_recent_block_cache.searchBlocks(fq, kMaxFuzzyResults);
|
||||||
|
s_fuzzy_query_cached = fq;
|
||||||
|
}
|
||||||
|
s_recent_max_page = 0; // fuzzy results aren't paginated; they scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canFetchBlocks = !fuzzyMode && state.sync.blocks > 0 &&
|
||||||
!s_show_detail_modal && !s_detail_loading && !s_tx_loading;
|
!s_show_detail_modal && !s_detail_loading && !s_tx_loading;
|
||||||
if (canFetchBlocks) {
|
if (canFetchBlocks) {
|
||||||
bool currentPageComplete = true;
|
bool currentPageComplete = true;
|
||||||
@@ -781,7 +851,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
if (currentPageComplete && s_recent_page < maxPage) {
|
if (currentPageComplete && s_recent_page < maxPage) {
|
||||||
std::vector<int> nextPageHeights;
|
std::vector<int> nextPageHeights;
|
||||||
nextPageHeights.reserve(rowsPerPage);
|
nextPageHeights.reserve(rowsPerPage);
|
||||||
int nextStartHeight = state.sync.blocks - (s_recent_page + 1) * rowsPerPage;
|
int nextStartHeight = anchorTip - (s_recent_page + 1) * rowsPerPage;
|
||||||
for (int i = 0; i < rowsPerPage; ++i) {
|
for (int i = 0; i < rowsPerPage; ++i) {
|
||||||
int height = nextStartHeight - i;
|
int height = nextStartHeight - i;
|
||||||
if (height < 1) break;
|
if (height < 1) break;
|
||||||
@@ -809,12 +879,19 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<RecentBlockRow> blocks;
|
std::vector<RecentBlockRow> blocks;
|
||||||
|
if (fuzzyMode) {
|
||||||
|
blocks.reserve(s_fuzzy_results.size());
|
||||||
|
for (const auto& b : s_fuzzy_results) {
|
||||||
|
blocks.push_back({b.height, &b});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
blocks.reserve(pageHeights.size());
|
blocks.reserve(pageHeights.size());
|
||||||
for (int height : pageHeights) {
|
for (int height : pageHeights) {
|
||||||
auto it = cachedBlocks.find(height);
|
auto it = cachedBlocks.find(height);
|
||||||
blocks.push_back({height,
|
blocks.push_back({height,
|
||||||
it != cachedBlocks.end() ? &it->second : nullptr});
|
it != cachedBlocks.end() ? &it->second : nullptr});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Responsive column layout — give height more room, use remaining for data
|
// Responsive column layout — give height more room, use remaining for data
|
||||||
float innerW = availWidth - pad * 2;
|
float innerW = availWidth - pad * 2;
|
||||||
float colHeight = pad;
|
float colHeight = pad;
|
||||||
@@ -849,7 +926,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
|
|
||||||
if (blocks.empty()) {
|
if (blocks.empty()) {
|
||||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||||
ImGui::TextDisabled("%s", TR("loading"));
|
ImGui::TextDisabled("%s", fuzzyMode ? TR("explorer_no_results") : TR("loading"));
|
||||||
} else {
|
} else {
|
||||||
for (size_t i = 0; i < blocks.size(); i++) {
|
for (size_t i = 0; i < blocks.size(); i++) {
|
||||||
const auto& row = blocks[i];
|
const auto& row = blocks[i];
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../widgets/qr_code.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -18,16 +21,27 @@ namespace ui {
|
|||||||
bool KeyExportDialog::s_open = false;
|
bool KeyExportDialog::s_open = false;
|
||||||
bool KeyExportDialog::s_fetching = false;
|
bool KeyExportDialog::s_fetching = false;
|
||||||
bool KeyExportDialog::s_show_key = false;
|
bool KeyExportDialog::s_show_key = false;
|
||||||
|
bool KeyExportDialog::s_show_qr = false;
|
||||||
|
std::uintptr_t KeyExportDialog::s_qr_tex = 0;
|
||||||
|
std::string KeyExportDialog::s_qr_cached;
|
||||||
KeyExportDialog::KeyType KeyExportDialog::s_key_type = KeyExportDialog::KeyType::Private;
|
KeyExportDialog::KeyType KeyExportDialog::s_key_type = KeyExportDialog::KeyType::Private;
|
||||||
std::string KeyExportDialog::s_address;
|
std::string KeyExportDialog::s_address;
|
||||||
std::string KeyExportDialog::s_key;
|
std::string KeyExportDialog::s_key;
|
||||||
std::string KeyExportDialog::s_error;
|
std::string KeyExportDialog::s_error;
|
||||||
|
|
||||||
|
void KeyExportDialog::releaseQr()
|
||||||
|
{
|
||||||
|
if (s_qr_tex) { FreeQRTexture(s_qr_tex); s_qr_tex = 0; }
|
||||||
|
s_qr_cached.clear();
|
||||||
|
s_show_qr = false;
|
||||||
|
}
|
||||||
|
|
||||||
void KeyExportDialog::show(const std::string& address, KeyType type)
|
void KeyExportDialog::show(const std::string& address, KeyType type)
|
||||||
{
|
{
|
||||||
s_open = true;
|
s_open = true;
|
||||||
s_fetching = false;
|
s_fetching = false;
|
||||||
s_show_key = false;
|
s_show_key = false;
|
||||||
|
releaseQr();
|
||||||
s_key_type = type;
|
s_key_type = type;
|
||||||
s_address = address;
|
s_address = address;
|
||||||
s_key.clear();
|
s_key.clear();
|
||||||
@@ -52,7 +66,6 @@ void KeyExportDialog::render(App* app)
|
|||||||
auto addrInput = S.input("dialogs.key-export", "address-input");
|
auto addrInput = S.input("dialogs.key-export", "address-input");
|
||||||
auto revealBtn = S.button("dialogs.key-export", "reveal-button");
|
auto revealBtn = S.button("dialogs.key-export", "reveal-button");
|
||||||
auto keyDisplay = S.drawElement("dialogs.key-export", "key-display");
|
auto keyDisplay = S.drawElement("dialogs.key-export", "key-display");
|
||||||
auto toggleBtn = S.button("dialogs.key-export", "toggle-button");
|
|
||||||
auto copyBtn = S.button("dialogs.key-export", "copy-button");
|
auto copyBtn = S.button("dialogs.key-export", "copy-button");
|
||||||
auto closeBtn = S.button("dialogs.key-export", "close-button");
|
auto closeBtn = S.button("dialogs.key-export", "close-button");
|
||||||
|
|
||||||
@@ -61,7 +74,10 @@ void KeyExportDialog::render(App* app)
|
|||||||
|
|
||||||
// Warning section with colored background
|
// Warning section with colored background
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
|
||||||
ImGui::BeginChild("WarningBox", ImVec2(-1, warningBox.height > 0 ? warningBox.height : 80), true);
|
// Auto-size the warning box to its (wrapping, font-scaled) text so it never clips it.
|
||||||
|
(void)warningBox;
|
||||||
|
ImGui::BeginChild("WarningBox", ImVec2(-1, 0),
|
||||||
|
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders);
|
||||||
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " %s", TR("warning_upper"));
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " %s", TR("warning_upper"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -90,8 +106,14 @@ void KeyExportDialog::render(App* app)
|
|||||||
char addr_buf[512];
|
char addr_buf[512];
|
||||||
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
||||||
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
addr_buf[sizeof(addr_buf) - 1] = '\0';
|
||||||
|
// Fit the field to the wrapped address (no excess empty space below it).
|
||||||
|
(void)addrInput;
|
||||||
|
const float addrFieldH =
|
||||||
|
ImGui::CalcTextSize(addr_buf, nullptr, false,
|
||||||
|
ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2.0f).y
|
||||||
|
+ ImGui::GetStyle().FramePadding.y * 2.0f + 4.0f;
|
||||||
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
|
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
|
||||||
ImVec2(-1, addrInput.height > 0 ? addrInput.height : 60), ImGuiInputTextFlags_ReadOnly);
|
ImVec2(-1, addrFieldH), ImGuiInputTextFlags_ReadOnly);
|
||||||
} else {
|
} else {
|
||||||
char addr_buf[128];
|
char addr_buf[128];
|
||||||
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
|
||||||
@@ -181,35 +203,61 @@ void KeyExportDialog::render(App* app)
|
|||||||
} else {
|
} else {
|
||||||
// Key has been fetched - display it
|
// Key has been fetched - display it
|
||||||
|
|
||||||
if (s_show_key) {
|
// Fit the field to the wrapped key (same length whether shown or masked).
|
||||||
// Show the actual key
|
(void)keyDisplay;
|
||||||
char key_buf[1024];
|
char key_buf[1024];
|
||||||
|
if (s_show_key) {
|
||||||
strncpy(key_buf, s_key.c_str(), sizeof(key_buf) - 1);
|
strncpy(key_buf, s_key.c_str(), sizeof(key_buf) - 1);
|
||||||
key_buf[sizeof(key_buf) - 1] = '\0';
|
|
||||||
ImGui::InputTextMultiline("##Key", key_buf, sizeof(key_buf),
|
|
||||||
ImVec2(-1, keyDisplay.height > 0 ? keyDisplay.height : 80), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
} else {
|
} else {
|
||||||
// Show masked
|
|
||||||
std::string masked(s_key.length(), '*');
|
std::string masked(s_key.length(), '*');
|
||||||
char masked_buf[1024];
|
strncpy(key_buf, masked.c_str(), sizeof(key_buf) - 1);
|
||||||
strncpy(masked_buf, masked.c_str(), sizeof(masked_buf) - 1);
|
|
||||||
masked_buf[sizeof(masked_buf) - 1] = '\0';
|
|
||||||
ImGui::InputTextMultiline("##Key", masked_buf, sizeof(masked_buf),
|
|
||||||
ImVec2(-1, keyDisplay.height > 0 ? keyDisplay.height : 80), ImGuiInputTextFlags_ReadOnly);
|
|
||||||
}
|
}
|
||||||
|
key_buf[sizeof(key_buf) - 1] = '\0';
|
||||||
|
const float keyFieldH =
|
||||||
|
ImGui::CalcTextSize(key_buf, nullptr, false,
|
||||||
|
ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2.0f).y
|
||||||
|
+ ImGui::GetStyle().FramePadding.y * 2.0f + 4.0f;
|
||||||
|
ImGui::InputTextMultiline("##Key", key_buf, sizeof(key_buf),
|
||||||
|
ImVec2(-1, keyFieldH), ImGuiInputTextFlags_ReadOnly);
|
||||||
|
|
||||||
// Show/Hide and Copy buttons
|
// Action row: Show/Hide · Copy · QR. Auto-width buttons (size 0) so the label text never
|
||||||
|
// clips at the user's font scale, and ONE shared font so they all match.
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
ImFont* actionFont = S.resolveFont(copyBtn.font);
|
||||||
|
|
||||||
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
|
if (material::StyledButton(s_show_key ? TR("hide") : TR("show"), ImVec2(0, 0), actionFont)) {
|
||||||
s_show_key = !s_show_key;
|
s_show_key = !s_show_key;
|
||||||
|
if (!s_show_key) s_show_qr = false; // hiding the key also hides its QR
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
if (material::StyledButton(TR("copy_to_clipboard"), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
if (material::StyledButton(TR("copy_to_clipboard"), ImVec2(0, 0), actionFont)) {
|
||||||
ImGui::SetClipboardText(s_key.c_str());
|
// Auto-clearing clipboard: the key (as sensitive as the seed) is wiped after ~45s.
|
||||||
// Could add a notification here
|
app->copySecretToClipboard(s_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR (only once revealed) — for scanning the key into another wallet.
|
||||||
|
if (s_show_key) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (material::StyledButton(s_show_qr ? TR("hide_qr") : TR("show_qr"), ImVec2(0, 0), actionFont)) {
|
||||||
|
s_show_qr = !s_show_qr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_show_qr && s_show_key && !s_key.empty()) {
|
||||||
|
if (s_qr_cached != s_key) { // (re)generate the QR texture when the key changes
|
||||||
|
if (s_qr_tex) { FreeQRTexture(s_qr_tex); s_qr_tex = 0; }
|
||||||
|
int qw = 0, qh = 0;
|
||||||
|
s_qr_tex = GenerateQRTexture(s_key.c_str(), &qw, &qh);
|
||||||
|
s_qr_cached = s_key;
|
||||||
|
}
|
||||||
|
if (s_qr_tex) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
const float qrSize = 180.0f;
|
||||||
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - qrSize) * 0.5f);
|
||||||
|
RenderQRCode(s_qr_tex, qrSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,10 +275,18 @@ void KeyExportDialog::render(App* app)
|
|||||||
// Clear sensitive data
|
// Clear sensitive data
|
||||||
s_key.clear();
|
s_key.clear();
|
||||||
s_show_key = false;
|
s_show_key = false;
|
||||||
|
releaseQr();
|
||||||
}
|
}
|
||||||
|
|
||||||
material::EndOverlayDialog();
|
material::EndOverlayDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dialog dismissed any other way (scrim click / Esc): drop the key + its QR texture.
|
||||||
|
if (!s_open) {
|
||||||
|
if (!s_key.empty()) s_key.clear();
|
||||||
|
s_show_key = false;
|
||||||
|
releaseQr();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -47,10 +48,15 @@ private:
|
|||||||
static bool s_open;
|
static bool s_open;
|
||||||
static bool s_fetching;
|
static bool s_fetching;
|
||||||
static bool s_show_key;
|
static bool s_show_key;
|
||||||
|
static bool s_show_qr; // inline QR of the revealed key (for scanning into another wallet)
|
||||||
|
static std::uintptr_t s_qr_tex; // cached QR texture handle (0 = none)
|
||||||
|
static std::string s_qr_cached; // key the cached QR was generated for
|
||||||
static KeyType s_key_type;
|
static KeyType s_key_type;
|
||||||
static std::string s_address;
|
static std::string s_address;
|
||||||
static std::string s_key;
|
static std::string s_key;
|
||||||
static std::string s_error;
|
static std::string s_error;
|
||||||
|
|
||||||
|
static void releaseQr(); // free the QR texture + reset its state
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "lite_console_tab.h"
|
||||||
|
#include "console_input_model.h" // reuse the generic command-history helpers
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../wallet/lite_diagnostics.h"
|
||||||
|
#include "../../wallet/lite_wallet_controller.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ConsoleLine {
|
||||||
|
std::string text;
|
||||||
|
ImU32 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unified, in-memory console buffer: it interleaves the automatic diagnostics log (lifecycle /
|
||||||
|
// connection / sync events) with the user's interactive commands and their responses, the same
|
||||||
|
// way the full-node Console interleaves the daemon log with RPC I/O. NOTE: this buffer can hold
|
||||||
|
// secret command output (e.g. `seed`/`export`) and is NEVER persisted to LiteDiagnostics.
|
||||||
|
std::vector<ConsoleLine> s_lines;
|
||||||
|
std::uint64_t s_diagGen = static_cast<std::uint64_t>(-1); // last consumed diagnostics generation
|
||||||
|
char s_input[512] = "";
|
||||||
|
std::vector<std::string> s_history;
|
||||||
|
int s_historyIndex = -1;
|
||||||
|
bool s_autoScroll = true;
|
||||||
|
bool s_scrollToBottom = false;
|
||||||
|
bool s_focusInput = false;
|
||||||
|
|
||||||
|
// Colour error/success lines for at-a-glance scanning (substring match on the messages the
|
||||||
|
// controller emits). Anything else renders in the muted default colour.
|
||||||
|
ImU32 logColor(const std::string& line)
|
||||||
|
{
|
||||||
|
const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; };
|
||||||
|
if (has("failed") || has(" unreachable") || has("blocked") || has("could not") ||
|
||||||
|
has("Error") || has("error"))
|
||||||
|
return Error();
|
||||||
|
if (has(": connected") || has("opened") || has("wallet ready") || has("Ready"))
|
||||||
|
return Success();
|
||||||
|
return OnSurfaceMedium();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append text as one or more lines (splitting on '\n' so multi-line JSON responses render row by row).
|
||||||
|
void appendLines(const std::string& text, ImU32 color)
|
||||||
|
{
|
||||||
|
std::size_t start = 0;
|
||||||
|
while (start <= text.size()) {
|
||||||
|
std::size_t nl = text.find('\n', start);
|
||||||
|
std::string line = text.substr(start, nl == std::string::npos ? std::string::npos : nl - start);
|
||||||
|
while (!line.empty() && (line.back() == '\r')) line.pop_back();
|
||||||
|
s_lines.push_back({std::move(line), color});
|
||||||
|
if (nl == std::string::npos) break;
|
||||||
|
start = nl + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lowerFirstToken(const std::string& cmd)
|
||||||
|
{
|
||||||
|
std::size_t b = cmd.find_first_not_of(" \t");
|
||||||
|
if (b == std::string::npos) return {};
|
||||||
|
std::size_t e = cmd.find_first_of(" \t", b);
|
||||||
|
std::string tok = cmd.substr(b, e == std::string::npos ? std::string::npos : e - b);
|
||||||
|
std::transform(tok.begin(), tok.end(), tok.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputText history navigation (Up/Down) — reuses the generic console history helpers.
|
||||||
|
int inputCallback(ImGuiInputTextCallbackData* data)
|
||||||
|
{
|
||||||
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
|
||||||
|
const bool up = (data->EventKey == ImGuiKey_UpArrow);
|
||||||
|
s_historyIndex = NavigateConsoleHistoryIndex(s_historyIndex, s_history.size(), up);
|
||||||
|
const std::string entry = ConsoleHistoryEntry(s_history, s_historyIndex);
|
||||||
|
data->DeleteChars(0, data->BufTextLen);
|
||||||
|
if (!entry.empty()) data->InsertChars(0, entry.c_str());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RenderLiteConsoleTab(App* app)
|
||||||
|
{
|
||||||
|
if (!app) return;
|
||||||
|
wallet::LiteWalletController* lw = app->liteWallet();
|
||||||
|
|
||||||
|
// ── Header ──────────────────────────────────────────────────────────────────
|
||||||
|
Type().text(TypeStyle::H6, TR("lite_console_title"));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_console_intro"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Live status (read straight from the controller — always meaningful) ───────
|
||||||
|
{
|
||||||
|
const char* connText;
|
||||||
|
ImU32 connCol;
|
||||||
|
if (!lw) {
|
||||||
|
connText = "Lite backend not linked in this build (rebuild with --lite-backend)";
|
||||||
|
connCol = Error();
|
||||||
|
} else if (lw->walletOpen()) {
|
||||||
|
connText = "Connected";
|
||||||
|
connCol = Success();
|
||||||
|
} else if (lw->openInProgress()) {
|
||||||
|
connText = "Connecting\xE2\x80\xA6"; // ellipsis
|
||||||
|
connCol = Warning();
|
||||||
|
} else {
|
||||||
|
connText = "Disconnected";
|
||||||
|
connCol = Error();
|
||||||
|
}
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_console_status"));
|
||||||
|
ImGui::SameLine();
|
||||||
|
Type().textColored(TypeStyle::Body2, connCol, connText);
|
||||||
|
|
||||||
|
if (lw && lw->walletOpen()) {
|
||||||
|
const SyncInfo& sync = app->state().sync;
|
||||||
|
char buf[96];
|
||||||
|
if (sync.syncing && !sync.isSynced()) {
|
||||||
|
double vp = sync.verification_progress;
|
||||||
|
if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0;
|
||||||
|
std::snprintf(buf, sizeof(buf), "Syncing %.1f%% (block %d / %d)",
|
||||||
|
vp * 100.0, sync.blocks, sync.headers);
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "Synced (block %d)", sync.blocks);
|
||||||
|
}
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
|
||||||
|
}
|
||||||
|
const std::string& openErr = app->liteOpenError();
|
||||||
|
if (!openErr.empty() && (!lw || !lw->walletOpen())) {
|
||||||
|
Type().textColored(TypeStyle::Caption, Error(),
|
||||||
|
(std::string("Last error: ") + openErr).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Ingest new automatic diagnostics events into the unified buffer ───────────
|
||||||
|
auto& diag = wallet::LiteDiagnostics::instance();
|
||||||
|
const std::uint64_t gen = diag.generation();
|
||||||
|
if (gen != s_diagGen) {
|
||||||
|
const auto snap = diag.snapshot();
|
||||||
|
// First pass dumps the whole ring; later passes append only the delta (generation counts
|
||||||
|
// total events ever added; the ring is bounded, so cap the delta at the snapshot size).
|
||||||
|
std::size_t startIdx = 0;
|
||||||
|
if (s_diagGen != static_cast<std::uint64_t>(-1)) {
|
||||||
|
const std::uint64_t added = gen - s_diagGen;
|
||||||
|
startIdx = (added >= snap.size()) ? 0 : snap.size() - static_cast<std::size_t>(added);
|
||||||
|
}
|
||||||
|
for (std::size_t i = startIdx; i < snap.size(); ++i)
|
||||||
|
s_lines.push_back({snap[i], logColor(snap[i])});
|
||||||
|
if (gen != s_diagGen && !snap.empty()) s_scrollToBottom = true;
|
||||||
|
s_diagGen = gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drain a completed interactive command ────────────────────────────────────
|
||||||
|
if (lw) {
|
||||||
|
wallet::LiteConsoleResult res;
|
||||||
|
if (lw->takeConsoleResult(res)) {
|
||||||
|
appendLines(res.response.empty() ? "(no output)" : res.response,
|
||||||
|
res.ok ? OnSurface() : Error());
|
||||||
|
s_scrollToBottom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ─────────────────────────────────────────────────────────────────
|
||||||
|
if (ImGui::Button(TR("lite_console_clear"))) {
|
||||||
|
s_lines.clear();
|
||||||
|
s_diagGen = diag.generation(); // don't re-dump the whole ring on the next frame
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TR("lite_console_copy"))) {
|
||||||
|
std::string all;
|
||||||
|
for (const auto& l : s_lines) { all += l.text; all.push_back('\n'); }
|
||||||
|
ImGui::SetClipboardText(all.c_str());
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox(TR("lite_console_autoscroll"), &s_autoScroll);
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Output log (terminal-styled scroll region) ───────────────────────────────
|
||||||
|
const float inputRowH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingSm();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 90));
|
||||||
|
ImGui::BeginChild("##LiteConsoleLog", ImVec2(0, -inputRowH), true,
|
||||||
|
ImGuiWindowFlags_HorizontalScrollbar);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
if (s_lines.empty()) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceDisabled());
|
||||||
|
ImGui::TextUnformatted(TR("lite_console_empty"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else {
|
||||||
|
for (const auto& line : s_lines) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, line.color);
|
||||||
|
ImGui::TextUnformatted(line.text.c_str()); // not format-interpreted — safe for any content
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s_scrollToBottom && s_autoScroll) ImGui::SetScrollHereY(1.0f);
|
||||||
|
else if (s_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ImGui::SetScrollHereY(1.0f);
|
||||||
|
s_scrollToBottom = false;
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
// ── Command input row ────────────────────────────────────────────────────────
|
||||||
|
const bool busy = lw && lw->consoleCommandInProgress();
|
||||||
|
auto submit = [&]() {
|
||||||
|
std::string cmd = s_input;
|
||||||
|
// trim
|
||||||
|
const std::size_t b = cmd.find_first_not_of(" \t");
|
||||||
|
if (b == std::string::npos) { s_input[0] = '\0'; return; }
|
||||||
|
cmd = cmd.substr(b);
|
||||||
|
while (!cmd.empty() && (cmd.back() == ' ' || cmd.back() == '\t')) cmd.pop_back();
|
||||||
|
|
||||||
|
s_lines.push_back({"> " + cmd, Primary()});
|
||||||
|
AppendConsoleHistory(s_history, cmd);
|
||||||
|
s_historyIndex = -1;
|
||||||
|
|
||||||
|
const std::string first = lowerFirstToken(cmd);
|
||||||
|
if (first == "clear" || first == "cls") {
|
||||||
|
// The backend's `clear` wipes wallet tx history — NOT the screen. Intercept it as a
|
||||||
|
// view-clear (what the user expects) so a stray "clear" can't destroy history.
|
||||||
|
s_lines.clear();
|
||||||
|
s_diagGen = diag.generation();
|
||||||
|
} else if (first == "quit" || first == "exit") {
|
||||||
|
appendLines(TR("lite_console_quit_note"), OnSurfaceMedium());
|
||||||
|
} else if (!lw) {
|
||||||
|
appendLines("Lite backend not linked in this build.", Error());
|
||||||
|
} else if (!lw->runConsoleCommand(cmd)) {
|
||||||
|
appendLines(TR("lite_console_busy"), Warning());
|
||||||
|
}
|
||||||
|
s_input[0] = '\0';
|
||||||
|
s_scrollToBottom = true;
|
||||||
|
s_focusInput = true; // keep focus for the next command
|
||||||
|
};
|
||||||
|
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
Type().textColored(TypeStyle::Caption, busy ? Warning() : Primary(),
|
||||||
|
busy ? TR("lite_console_running") : "\xE2\x80\xBA"); // ‹running…› / "›"
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
if (busy) ImGui::BeginDisabled();
|
||||||
|
const ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
|
||||||
|
ImGuiInputTextFlags_CallbackHistory;
|
||||||
|
if (s_focusInput) { ImGui::SetKeyboardFocusHere(); s_focusInput = false; }
|
||||||
|
if (ImGui::InputTextWithHint("##LiteConsoleInput", TR("lite_console_input_hint"),
|
||||||
|
s_input, sizeof(s_input), flags, inputCallback)) {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
if (busy) ImGui::EndDisabled();
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Lite-wallet "Console" tab: a read-only, terminal-styled view of the lite diagnostics log
|
||||||
|
// (server connection attempts, wallet open/create/restore, sync milestones, errors). Lite
|
||||||
|
// builds only. See src/ui/windows/lite_console_tab.cpp.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
void RenderLiteConsoleTab(App* app);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -29,8 +29,7 @@ namespace ui {
|
|||||||
using namespace material;
|
using namespace material;
|
||||||
|
|
||||||
// ---- Market tab persistent state ----
|
// ---- Market tab persistent state ----
|
||||||
static std::vector<double> s_price_history;
|
static std::vector<double> s_price_history; // mirror of WalletState market.price_history
|
||||||
static std::vector<double> s_time_history;
|
|
||||||
static bool s_history_initialized = false;
|
static bool s_history_initialized = false;
|
||||||
static double s_last_refresh_time = 0.0;
|
static double s_last_refresh_time = 0.0;
|
||||||
|
|
||||||
@@ -322,18 +321,11 @@ void RenderMarketTab(App* app)
|
|||||||
// PRICE CHART — Custom drawn inside glass panel (matches app design)
|
// PRICE CHART — Custom drawn inside glass panel (matches app design)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
{
|
||||||
// Initialize history with simulated data if not set
|
// Plot the REAL accumulated price history (one point per refresh, oldest→newest).
|
||||||
if (!s_history_initialized && market.price_usd > 0) {
|
// No synthetic data: until at least two real samples exist, the empty-state below
|
||||||
s_price_history.clear();
|
// ("not enough history yet") is shown rather than inventing a price curve.
|
||||||
s_time_history.clear();
|
s_price_history = market.price_history;
|
||||||
double base = market.price_usd;
|
|
||||||
for (int i = 0; i < 24; i++) {
|
|
||||||
double variance = ((rand() % 1000) - 500) / 10000.0 * base;
|
|
||||||
s_price_history.push_back(base + variance);
|
|
||||||
s_time_history.push_back(static_cast<double>(i));
|
|
||||||
}
|
|
||||||
s_history_initialized = true;
|
s_history_initialized = true;
|
||||||
}
|
|
||||||
|
|
||||||
// Chart height from schema
|
// Chart height from schema
|
||||||
float chartH = std::max(60.0f, chartElem.height * vs);
|
float chartH = std::max(60.0f, chartElem.height * vs);
|
||||||
@@ -411,19 +403,14 @@ void RenderMarketTab(App* app)
|
|||||||
dl->AddCircleFilled(points[i], dotR, dotCol);
|
dl->AddCircleFilled(points[i], dotR, dotCol);
|
||||||
}
|
}
|
||||||
|
|
||||||
// X-axis labels
|
// X-axis: samples are per-refresh, not hourly-timestamped, so only the most
|
||||||
const int xlabels[] = {0, 6, 12, 18, 23};
|
// recent point can be labelled truthfully ("Now"). The series simply runs
|
||||||
const char* xlabelText[] = {TR("market_24h"), TR("market_18h"), TR("market_12h"), TR("market_6h"), TR("market_now")};
|
// older→newer, left→right. (Avoids the old fake "24h…6h" hourly gradations.)
|
||||||
for (int xi = 0; xi < 5; xi++) {
|
{
|
||||||
int idx = xlabels[xi];
|
const char* nowText = TR("market_now");
|
||||||
float t = (float)idx / (float)(n - 1);
|
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, nowText);
|
||||||
float x = plotLeft + t * plotW;
|
|
||||||
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xlabelText[xi]);
|
|
||||||
float lx = x - lblSz.x * 0.5f;
|
|
||||||
if (lx < plotLeft) lx = plotLeft;
|
|
||||||
if (lx + lblSz.x > plotRight) lx = plotRight - lblSz.x;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(lx, plotBottom + 2), OnSurfaceDisabled(), xlabelText[xi]);
|
ImVec2(plotRight - lblSz.x, plotBottom + 2), OnSurfaceDisabled(), nowText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover crosshair + tooltip
|
// Hover crosshair + tooltip
|
||||||
@@ -450,8 +437,9 @@ void RenderMarketTab(App* app)
|
|||||||
dl->AddCircleFilled(ImVec2(px, py), hoverDotR, dotCol);
|
dl->AddCircleFilled(ImVec2(px, py), hoverDotR, dotCol);
|
||||||
dl->AddCircle(ImVec2(px, py), hoverRingR, IM_COL32(255, 255, 255, 80), 0, 1.5f);
|
dl->AddCircle(ImVec2(px, py), hoverRingR, IM_COL32(255, 255, 255, 80), 0, 1.5f);
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "%dh ago: %s",
|
// Samples are per-refresh, not timestamped, so we don't claim a specific
|
||||||
24 - idx, FormatPrice(s_price_history[idx]).c_str());
|
// "Xh ago" — just show the price at the hovered point.
|
||||||
|
snprintf(buf, sizeof(buf), "%s", FormatPrice(s_price_history[idx]).c_str());
|
||||||
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
||||||
float tipX = px + 10;
|
float tipX = px + 10;
|
||||||
@@ -843,8 +831,10 @@ void RenderMarketTab(App* app)
|
|||||||
WithAlpha(Warning(), 200),
|
WithAlpha(Warning(), 200),
|
||||||
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
|
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
|
||||||
|
|
||||||
int pct = (int)(shieldedRatio * 100.0f + 0.5f);
|
// market_pct_shielded is "%.0f%% Shielded" — pass a double, not an int (passing
|
||||||
snprintf(buf, sizeof(buf), TR("market_pct_shielded"), pct);
|
// an int to a %f conversion is UB and renders garbage on x86-64).
|
||||||
|
snprintf(buf, sizeof(buf), TR("market_pct_shielded"),
|
||||||
|
static_cast<double>(shieldedRatio) * 100.0);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf);
|
ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,902 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "mining_controls.h"
|
||||||
|
#include "mining_tab_helpers.h"
|
||||||
|
#include "mining_benchmark.h"
|
||||||
|
#include "xmrig_download_dialog.h"
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/version.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/platform.h"
|
||||||
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/components/buttons.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../notifications.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float hs, float vs, float gap, float pad, float availWidth,
|
||||||
|
const GlassPanelSpec& glassSpec, float controlsBudgetH,
|
||||||
|
int max_threads, bool isMiningActive, bool poolMode,
|
||||||
|
const char* poolWorker, const std::string& xmrigLatestTag,
|
||||||
|
ThreadBenchmark& benchmark, int& selectedThreads,
|
||||||
|
bool& dragActive, int& dragAnchorThread)
|
||||||
|
{
|
||||||
|
// Aliases so the moved body keeps its original identifiers verbatim. Mutated state is a
|
||||||
|
// reference to the real static; read-only state is a const copy/ref.
|
||||||
|
const bool s_pool_mode = poolMode;
|
||||||
|
const char* const s_pool_worker = poolWorker;
|
||||||
|
const std::string& s_xmrig_latest_tag = xmrigLatestTag;
|
||||||
|
ThreadBenchmark& s_benchmark = benchmark;
|
||||||
|
int& s_selected_threads = selectedThreads;
|
||||||
|
bool& s_drag_active = dragActive;
|
||||||
|
int& s_drag_anchor_thread = dragAnchorThread;
|
||||||
|
char buf[128];
|
||||||
|
float miningBtnGap = gap;
|
||||||
|
float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size;
|
||||||
|
|
||||||
|
// --- Compute thread grid layout based on controls card width ---
|
||||||
|
// Estimate controlsW first to compute cols correctly
|
||||||
|
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
|
||||||
|
float innerW = estControlsW - pad * 2;
|
||||||
|
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
|
||||||
|
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
|
||||||
|
int cols = std::max(1, std::min(max_threads, (int)(innerW / (cellSz + cellGap))));
|
||||||
|
int rows = (max_threads + cols - 1) / cols;
|
||||||
|
float gridW = cols * cellSz + (cols - 1) * cellGap;
|
||||||
|
float gridH = rows * cellSz + (rows - 1) * cellGap;
|
||||||
|
|
||||||
|
// Card sections: header(label+info+RAM inline) | grid
|
||||||
|
float headerH = capFont->LegacySize + Layout::spacingXs();
|
||||||
|
float secGap = Layout::spacingLg();
|
||||||
|
|
||||||
|
// Card height from actual content, capped by proportional budget
|
||||||
|
float cardH = pad + headerH + secGap + gridH + pad;
|
||||||
|
cardH = std::clamp(cardH, schema::UI().drawElement("tabs.mining", "control-card-min-height").size, controlsBudgetH);
|
||||||
|
|
||||||
|
// Mining button — sized to match card height (square)
|
||||||
|
float miningBtnSz = cardH;
|
||||||
|
if (miningBtnSz + miningBtnGap > miningBtnMaxW)
|
||||||
|
miningBtnSz = miningBtnMaxW;
|
||||||
|
float controlsW = availWidth - miningBtnSz - miningBtnGap;
|
||||||
|
|
||||||
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 cardMax(cardMin.x + controlsW, cardMin.y + cardH);
|
||||||
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||||
|
|
||||||
|
float curY = cardMin.y + pad;
|
||||||
|
|
||||||
|
// --- Header row: "THREADS 4 / 16" + RAM est + active indicator ---
|
||||||
|
{
|
||||||
|
ImVec2 labelPos(cardMin.x + pad, curY);
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), TR("mining_threads"));
|
||||||
|
|
||||||
|
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_threads")).x;
|
||||||
|
snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads);
|
||||||
|
ImVec2 countPos(labelPos.x + labelW, curY);
|
||||||
|
dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf);
|
||||||
|
|
||||||
|
// RAM estimate inline (after thread count)
|
||||||
|
// Model matches DragonX RandomX: shared ~2080MB dataset + ~256MB cache (allocated once),
|
||||||
|
// plus ~2MB scratchpad per mining thread VM.
|
||||||
|
{
|
||||||
|
float countW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x;
|
||||||
|
double ram_estimate_mb = schema::UI().drawElement("business", "ram-base-mb").size
|
||||||
|
+ schema::UI().drawElement("business", "ram-dataset-mb").size
|
||||||
|
+ schema::UI().drawElement("business", "ram-cache-mb").size
|
||||||
|
+ s_selected_threads * schema::UI().drawElement("business", "ram-per-thread-mb").size;
|
||||||
|
double ram_estimate_gb = ram_estimate_mb / 1024.0;
|
||||||
|
snprintf(buf, sizeof(buf), " RAM ~%.1fGB", ram_estimate_gb);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(countPos.x + countW, curY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
||||||
|
OnSurfaceDisabled(), buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle mining toggle (top-right corner of card)
|
||||||
|
float idleRightEdge = cardMax.x - pad;
|
||||||
|
{
|
||||||
|
bool idleOn = app->settings()->getMineWhenIdle();
|
||||||
|
bool threadScaling = app->settings()->getIdleThreadScaling();
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
const char* idleIcon = ICON_MD_SCHEDULE;
|
||||||
|
float icoH = icoFont->LegacySize;
|
||||||
|
float btnSz = icoH + 8.0f * dp;
|
||||||
|
float btnX = idleRightEdge - btnSz;
|
||||||
|
float btnY = curY + (headerH - btnSz) * 0.5f;
|
||||||
|
|
||||||
|
// Pill background when active
|
||||||
|
if (idleOn) {
|
||||||
|
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
||||||
|
WithAlpha(Primary(), 60), btnSz * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon centered in button
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
|
||||||
|
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
|
||||||
|
icoCol, idleIcon);
|
||||||
|
|
||||||
|
// Click target (save/restore cursor so layout is unaffected)
|
||||||
|
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||||
|
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
app->settings()->setMineWhenIdle(!idleOn);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
idleRightEdge = btnX - 4.0f * dp;
|
||||||
|
|
||||||
|
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
|
||||||
|
if (idleOn) {
|
||||||
|
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
|
||||||
|
float sBtnX = idleRightEdge - btnSz;
|
||||||
|
float sBtnY = btnY;
|
||||||
|
|
||||||
|
if (threadScaling) {
|
||||||
|
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
|
||||||
|
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
|
||||||
|
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
|
||||||
|
sIcoCol, scaleIcon);
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
|
||||||
|
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
app->settings()->setIdleThreadScaling(!threadScaling);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", threadScaling
|
||||||
|
? TR("mining_idle_scale_on_tooltip")
|
||||||
|
: TR("mining_idle_scale_off_tooltip"));
|
||||||
|
}
|
||||||
|
idleRightEdge = sBtnX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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; };
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread scaling controls: idle delay + active threads / idle threads combos
|
||||||
|
if (idleOn && threadScaling) {
|
||||||
|
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||||
|
|
||||||
|
// Idle delay combo
|
||||||
|
{
|
||||||
|
struct DelayOption { int seconds; const char* label; };
|
||||||
|
static const DelayOption delays[] = {
|
||||||
|
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
||||||
|
};
|
||||||
|
int curDelay = app->settings()->getMineIdleDelay();
|
||||||
|
const char* previewLabel = "2m";
|
||||||
|
for (const auto& d : delays) {
|
||||||
|
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
||||||
|
}
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (const auto& d : delays) {
|
||||||
|
bool selected = (d.seconds == curDelay);
|
||||||
|
if (ImGui::Selectable(d.label, selected)) {
|
||||||
|
app->settings()->setMineIdleDelay(d.seconds);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle threads combo (threads when system is idle)
|
||||||
|
{
|
||||||
|
int curVal = app->settings()->getIdleThreadsIdle();
|
||||||
|
if (curVal <= 0) curVal = hwThreads;
|
||||||
|
char previewBuf[16];
|
||||||
|
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (int t = 1; t <= hwThreads; t++) {
|
||||||
|
char lbl[16];
|
||||||
|
snprintf(lbl, sizeof(lbl), "%d", t);
|
||||||
|
bool selected = (t == curVal);
|
||||||
|
if (ImGui::Selectable(lbl, selected)) {
|
||||||
|
app->settings()->setIdleThreadsIdle(t);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator arrow icon
|
||||||
|
{
|
||||||
|
const char* arrowIcon = ICON_MD_ARROW_BACK;
|
||||||
|
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
|
||||||
|
float arrX = idleRightEdge - arrSz.x;
|
||||||
|
float arrY = curY + (headerH - arrSz.y) * 0.5f;
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
|
||||||
|
idleRightEdge = arrX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active threads combo (threads when user is active)
|
||||||
|
{
|
||||||
|
int curVal = app->settings()->getIdleThreadsActive();
|
||||||
|
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
|
||||||
|
char previewBuf[16];
|
||||||
|
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (int t = 1; t <= hwThreads; t++) {
|
||||||
|
char lbl[16];
|
||||||
|
snprintf(lbl, sizeof(lbl), "%d", t);
|
||||||
|
bool selected = (t == curVal);
|
||||||
|
if (ImGui::Selectable(lbl, selected)) {
|
||||||
|
app->settings()->setIdleThreadsActive(t);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(savedCur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Miner update: "Update <latest>" button + current version (left of benchmark) ---
|
||||||
|
if (s_pool_mode && !benchRunning) {
|
||||||
|
const std::string curVer = app->settings()->getXmrigVersion();
|
||||||
|
char xbtn[64];
|
||||||
|
if (!s_xmrig_latest_tag.empty())
|
||||||
|
snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str());
|
||||||
|
else
|
||||||
|
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_update_button"));
|
||||||
|
|
||||||
|
const bool minerBusy = state.pool_mining.xmrig_running;
|
||||||
|
ImVec2 xlblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xbtn);
|
||||||
|
float xpadX = 8.0f * dp;
|
||||||
|
float xbtnW = xlblSz.x + xpadX * 2.0f;
|
||||||
|
float xbtnH = capFont->LegacySize + 6.0f * dp;
|
||||||
|
float xbtnX = idleRightEdge - xbtnW;
|
||||||
|
float xbtnY = curY + (headerH - xbtnH) * 0.5f;
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(xbtnX, xbtnY));
|
||||||
|
ImGui::InvisibleButton("##XmrigUpdateBtn", ImVec2(xbtnW, xbtnH));
|
||||||
|
bool xhov = ImGui::IsItemHovered();
|
||||||
|
bool xclk = ImGui::IsItemClicked();
|
||||||
|
ImU32 xpill = minerBusy ? WithAlpha(OnSurface(), 12)
|
||||||
|
: (xhov ? StateHover() : WithAlpha(OnSurface(), 28));
|
||||||
|
dl->AddRectFilled(ImVec2(xbtnX, xbtnY), ImVec2(xbtnX + xbtnW, xbtnY + xbtnH),
|
||||||
|
xpill, xbtnH * 0.3f);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f),
|
||||||
|
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
|
||||||
|
if (xhov) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
|
||||||
|
: TR("xmrig_update_title"));
|
||||||
|
}
|
||||||
|
if (xclk && !minerBusy) XmrigDownloadDialog::show(app);
|
||||||
|
|
||||||
|
// Current installed version, as text to the LEFT of the button.
|
||||||
|
char xcur[64];
|
||||||
|
snprintf(xcur, sizeof(xcur), "%s %s", TR("xmrig_current"),
|
||||||
|
curVer.empty() ? TR("xmrig_none") : curVer.c_str());
|
||||||
|
ImVec2 xcurSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xcur);
|
||||||
|
float xcurX = xbtnX - 6.0f * dp - xcurSz.x;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(xcurX, curY + (headerH - xcurSz.y) * 0.5f),
|
||||||
|
OnSurfaceDisabled(), xcur);
|
||||||
|
idleRightEdge = xcurX - 8.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(benchSavedCur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active mining indicator (left of idle toggle)
|
||||||
|
if (mining.generate) {
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
||||||
|
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
||||||
|
ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse));
|
||||||
|
float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs;
|
||||||
|
dl->AddCircleFilled(ImVec2(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY),
|
||||||
|
WithAlpha(Success(), 200), TR("mining_active"));
|
||||||
|
}
|
||||||
|
curY += headerH + secGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Thread Grid (drag-to-select) ---
|
||||||
|
{
|
||||||
|
// Center the grid horizontally within the card
|
||||||
|
float gridX = cardMin.x + pad + (innerW - gridW) * 0.5f;
|
||||||
|
float gridY = curY;
|
||||||
|
|
||||||
|
bool threads_changed = false;
|
||||||
|
|
||||||
|
// Track which thread the mouse is currently over (-1 = none)
|
||||||
|
int hovered_thread = -1;
|
||||||
|
|
||||||
|
// First pass: hit-test all cells to find hovered thread
|
||||||
|
for (int i = 0; i < max_threads; i++) {
|
||||||
|
int row = i / cols;
|
||||||
|
int col = i % cols;
|
||||||
|
float cx = gridX + col * (cellSz + cellGap);
|
||||||
|
float cy = gridY + row * (cellSz + cellGap);
|
||||||
|
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + cellSz, cy + cellSz))) {
|
||||||
|
hovered_thread = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pointer cursor when hovering the thread grid
|
||||||
|
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 (disabled during benchmark)
|
||||||
|
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||||
|
// Begin drag
|
||||||
|
s_drag_active = true;
|
||||||
|
s_drag_anchor_thread = hovered_thread;
|
||||||
|
s_selected_threads = hovered_thread;
|
||||||
|
threads_changed = true;
|
||||||
|
}
|
||||||
|
if (s_drag_active) {
|
||||||
|
if (ImGui::IsMouseDown(0)) {
|
||||||
|
if (hovered_thread > 0 && hovered_thread != s_selected_threads) {
|
||||||
|
// Drag selects up to the hovered thread in either direction
|
||||||
|
s_selected_threads = hovered_thread;
|
||||||
|
threads_changed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Mouse released — end drag
|
||||||
|
s_drag_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render cells
|
||||||
|
for (int i = 0; i < max_threads; i++) {
|
||||||
|
int row = i / cols;
|
||||||
|
int col = i % cols;
|
||||||
|
float cx = gridX + col * (cellSz + cellGap);
|
||||||
|
float cy = gridY + row * (cellSz + cellGap);
|
||||||
|
ImVec2 cMin(cx, cy);
|
||||||
|
ImVec2 cMax(cx + cellSz, cy + cellSz);
|
||||||
|
|
||||||
|
int threadNum = i + 1;
|
||||||
|
bool active = threadNum <= s_selected_threads;
|
||||||
|
bool hovered = (threadNum == hovered_thread);
|
||||||
|
|
||||||
|
// Determine visual state
|
||||||
|
float rounding = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
|
||||||
|
if (active && mining.generate) {
|
||||||
|
// Mining + active: animated heat glow using theme colors
|
||||||
|
float t = (float)ImGui::GetTime();
|
||||||
|
float phase = t * schema::UI().drawElement("animations", "heat-glow-speed").size + i * schema::UI().drawElement("animations", "heat-glow-phase-offset").size;
|
||||||
|
float glow = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin(phase);
|
||||||
|
|
||||||
|
// Base color from theme (--mining-heat-glow) falling back to Primary
|
||||||
|
ImU32 heatBase = schema::UI().resolveColor("var(--mining-heat-glow)", Primary());
|
||||||
|
int baseR = (heatBase >> 0) & 0xFF;
|
||||||
|
int baseG = (heatBase >> 8) & 0xFF;
|
||||||
|
int baseB = (heatBase >> 16) & 0xFF;
|
||||||
|
// Brighten toward white as glow increases
|
||||||
|
int r = std::min(255, (int)(baseR + (255 - baseR) * 0.3f * glow));
|
||||||
|
int g = std::min(255, (int)(baseG + (255 - baseG) * 0.3f * glow));
|
||||||
|
int b = std::min(255, (int)(baseB + (255 - baseB) * 0.3f * glow));
|
||||||
|
int a = (int)(180 + 60 * glow);
|
||||||
|
ImU32 fillCol = IM_COL32(r, g, b, a);
|
||||||
|
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||||
|
// Bright border
|
||||||
|
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
|
||||||
|
} else if (active) {
|
||||||
|
// Active but not mining: solid primary fill
|
||||||
|
ImU32 pri = Primary();
|
||||||
|
int priR = (pri >> 0) & 0xFF;
|
||||||
|
int priG = (pri >> 8) & 0xFF;
|
||||||
|
int priB = (pri >> 16) & 0xFF;
|
||||||
|
ImU32 fillCol = hovered
|
||||||
|
? IM_COL32(priR, priG, priB, 220)
|
||||||
|
: IM_COL32(priR, priG, priB, 180);
|
||||||
|
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||||
|
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
|
||||||
|
} else {
|
||||||
|
// Inactive: dim outline
|
||||||
|
ImU32 fillCol = hovered
|
||||||
|
? WithAlpha(OnSurface(), 25)
|
||||||
|
: WithAlpha(OnSurface(), 8);
|
||||||
|
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||||
|
dl->AddRect(cMin, cMax, WithAlpha(OnSurface(), hovered ? 80 : 35), rounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread number label (centered)
|
||||||
|
snprintf(buf, sizeof(buf), "%d", threadNum);
|
||||||
|
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
ImVec2 txtPos(cx + (cellSz - txtSz.x) * 0.5f, cy + (cellSz - txtSz.y) * 0.5f);
|
||||||
|
ImU32 txtCol = active ? schema::UI().resolveColor("var(--on-primary)", IM_COL32(255, 255, 255, 230)) : WithAlpha(OnSurface(), 80);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threads_changed) {
|
||||||
|
app->settings()->setPoolThreads(s_selected_threads);
|
||||||
|
app->settings()->save();
|
||||||
|
if (mining.generate) {
|
||||||
|
app->startMining(s_selected_threads);
|
||||||
|
}
|
||||||
|
if (s_pool_mode && state.pool_mining.xmrig_running) {
|
||||||
|
app->stopPoolMining();
|
||||||
|
app->startPoolMining(s_selected_threads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curY += gridH + secGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Large square mining button — right of the controls card
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
float btnX = cardMin.x + controlsW + miningBtnGap;
|
||||||
|
float btnY = cardMin.y;
|
||||||
|
ImVec2 bMin(btnX, btnY);
|
||||||
|
ImVec2 bMax(btnX + miningBtnSz, btnY + cardH);
|
||||||
|
|
||||||
|
bool btnHovered = material::IsRectHovered(bMin, bMax);
|
||||||
|
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
|
||||||
|
bool isSyncing = state.sync.syncing;
|
||||||
|
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
|
||||||
|
bool isToggling = app->isMiningToggleInProgress();
|
||||||
|
// Pool mining connects to an external pool via xmrig — it does not
|
||||||
|
// need the local blockchain synced or even the daemon connected.
|
||||||
|
// If pool mining is still shutting down after switching to solo,
|
||||||
|
// keep the button enabled so user can stop it.
|
||||||
|
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
|
||||||
|
bool disabled = s_pool_mode
|
||||||
|
? (isToggling || poolBlockedBySolo)
|
||||||
|
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
|
||||||
|
|
||||||
|
// Glass panel background with state-dependent tint
|
||||||
|
GlassPanelSpec btnGlass;
|
||||||
|
btnGlass.rounding = Layout::glassRounding();
|
||||||
|
if (isToggling) {
|
||||||
|
// Toggling: subtle pulsing glow to indicate activity
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? 0.5f
|
||||||
|
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0);
|
||||||
|
int glowA = (int)(15 + 25 * pulse);
|
||||||
|
btnGlass.fillAlpha = glowA;
|
||||||
|
} else if (isMiningActive) {
|
||||||
|
// Active mining: warm glow
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? schema::UI().drawElement("animations", "pulse-base-glow").size
|
||||||
|
: schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size);
|
||||||
|
int glowA = (int)(20 + 30 * pulse);
|
||||||
|
btnGlass.fillAlpha = glowA;
|
||||||
|
} else {
|
||||||
|
btnGlass.fillAlpha = btnHovered ? 30 : 18;
|
||||||
|
}
|
||||||
|
DrawGlassPanel(dl, bMin, bMax, btnGlass);
|
||||||
|
|
||||||
|
// Hover highlight
|
||||||
|
if (btnHovered && !disabled) {
|
||||||
|
dl->AddRectFilled(bMin, bMax,
|
||||||
|
isMiningActive ? WithAlpha(Error(), 25) : WithAlpha(Success(), 20),
|
||||||
|
btnGlass.rounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw mining icon centered in button — Material Design ICON_MD_CONSTRUCTION
|
||||||
|
{
|
||||||
|
float btnW = bMax.x - bMin.x;
|
||||||
|
float btnH = bMax.y - bMin.y;
|
||||||
|
float cx = bMin.x + btnW * 0.5f;
|
||||||
|
float cy = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-icon-y-ratio").size; // shift up to leave room for label
|
||||||
|
|
||||||
|
if (isToggling) {
|
||||||
|
// Draw a spinning arc spinner to indicate progress
|
||||||
|
float spinnerR = std::min(btnW, btnH) * 0.18f;
|
||||||
|
float thickness = std::max(2.5f, spinnerR * 0.15f);
|
||||||
|
float time = (float)ImGui::GetTime();
|
||||||
|
|
||||||
|
// Track circle (faint)
|
||||||
|
ImU32 trackCol = WithAlpha(Primary(), 40);
|
||||||
|
dl->AddCircle(ImVec2(cx, cy), spinnerR, trackCol, 0, thickness);
|
||||||
|
|
||||||
|
// Animated arc
|
||||||
|
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f);
|
||||||
|
float cycleTime = fmodf(time, 1.333f);
|
||||||
|
float arcLength = (cycleTime < 0.666f)
|
||||||
|
? (cycleTime / 0.666f) * 0.75f + 0.1f
|
||||||
|
: ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
|
||||||
|
|
||||||
|
float startAngle = rotation - IM_PI * 0.5f;
|
||||||
|
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
|
||||||
|
int segments = (int)(32 * arcLength) + 1;
|
||||||
|
float angleStep = (endAngle - startAngle) / segments;
|
||||||
|
ImU32 arcCol = Primary();
|
||||||
|
|
||||||
|
for (int i = 0; i < segments; i++) {
|
||||||
|
float a1 = startAngle + angleStep * i;
|
||||||
|
float a2 = startAngle + angleStep * (i + 1);
|
||||||
|
ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR);
|
||||||
|
ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR);
|
||||||
|
dl->AddLine(p1, p2, arcCol, thickness);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImU32 iconCol;
|
||||||
|
if (disabled) {
|
||||||
|
iconCol = OnSurfaceDisabled();
|
||||||
|
} else if (isMiningActive) {
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
||||||
|
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
||||||
|
iconCol = WithAlpha(Error(), (int)(200 + 55 * pulse));
|
||||||
|
} else {
|
||||||
|
iconCol = btnHovered ? WithAlpha(Success(), 255) : OnSurfaceMedium();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use XL icon for the large mining button
|
||||||
|
ImFont* iconFont = Type().iconXL();
|
||||||
|
const char* mineIcon = isMiningActive ? ICON_MD_CLOSE : ICON_MD_CONSTRUCTION;
|
||||||
|
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mineIcon);
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), iconCol, mineIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label below icon
|
||||||
|
{
|
||||||
|
float btnW = bMax.x - bMin.x;
|
||||||
|
float btnH = bMax.y - bMin.y;
|
||||||
|
const char* label;
|
||||||
|
ImU32 lblCol;
|
||||||
|
if (isToggling) {
|
||||||
|
label = isMiningActive ? TR("mining_stopping") : TR("mining_starting");
|
||||||
|
// Animated dots effect via alpha pulse
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? 0.7f
|
||||||
|
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
|
||||||
|
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
|
||||||
|
} else if (isMiningActive) {
|
||||||
|
label = TR("mining_stop");
|
||||||
|
lblCol = WithAlpha(Error(), 220);
|
||||||
|
} else if (disabled) {
|
||||||
|
label = TR("mining_mine");
|
||||||
|
lblCol = WithAlpha(OnSurface(), 50);
|
||||||
|
} else {
|
||||||
|
label = TR("mining_mine");
|
||||||
|
lblCol = WithAlpha(OnSurface(), 160);
|
||||||
|
}
|
||||||
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||||
|
float lblX = bMin.x + (btnW - lblSz.x) * 0.5f;
|
||||||
|
float lblY = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-label-y-ratio").size;
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip + pointer cursor
|
||||||
|
if (btnHovered) {
|
||||||
|
if (!disabled)
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (isToggling)
|
||||||
|
ImGui::SetTooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
||||||
|
else if (isSyncing && !s_pool_mode)
|
||||||
|
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
||||||
|
else if (poolBlockedBySolo)
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||||
|
else
|
||||||
|
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click action — pool or solo
|
||||||
|
if (btnClicked && !disabled) {
|
||||||
|
if (s_pool_mode) {
|
||||||
|
if (state.pool_mining.xmrig_running)
|
||||||
|
app->stopPoolMining();
|
||||||
|
else
|
||||||
|
app->startPoolMining(s_selected_threads);
|
||||||
|
} else {
|
||||||
|
// If pool mining is still running (user just switched from pool to solo),
|
||||||
|
// stop pool mining first
|
||||||
|
if (state.pool_mining.xmrig_running)
|
||||||
|
app->stopPoolMining();
|
||||||
|
else if (mining.generate)
|
||||||
|
app->stopMining();
|
||||||
|
else
|
||||||
|
app->startMining(s_selected_threads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "../material/draw_helpers.h" // GlassPanelSpec
|
||||||
|
#include "mining_benchmark.h" // ThreadBenchmark
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Renders the mining "Controls" card (CPU-core grid + mining start/stop button + benchmark /
|
||||||
|
// miner-update controls). Extracted verbatim from the monolithic mining tab. State the section
|
||||||
|
// MUTATES is passed BY REFERENCE (the benchmark, selected thread count, and drag state) so the
|
||||||
|
// interactions behave exactly as before; read-only context is passed by value/const.
|
||||||
|
void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float hs, float vs, float gap, float pad, float availWidth,
|
||||||
|
const material::GlassPanelSpec& glassSpec, float controlsBudgetH,
|
||||||
|
int max_threads, bool isMiningActive, bool poolMode,
|
||||||
|
const char* poolWorker, const std::string& xmrigLatestTag,
|
||||||
|
ThreadBenchmark& benchmark, int& selectedThreads,
|
||||||
|
bool& dragActive, int& dragAnchorThread);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,683 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "mining_earnings.h"
|
||||||
|
#include "mining_tab_helpers.h" // EstimateHoursToBlock
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/version.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/platform.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../notifications.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool. (Moved here with the card it belongs to.)
|
||||||
|
static int s_earnings_filter = 0;
|
||||||
|
|
||||||
|
void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float vs, float gap, float pad, bool isMiningActive,
|
||||||
|
bool poolMode, float availWidth,
|
||||||
|
const GlassPanelSpec& glassSpec, float sHdr, float gapOver)
|
||||||
|
{
|
||||||
|
const bool s_pool_mode = poolMode; // alias: keep the moved body's identifier verbatim
|
||||||
|
char buf[128]; // section-local scratch (was shared in the parent)
|
||||||
|
// Gather mining transactions from state
|
||||||
|
double minedToday = 0.0, minedYesterday = 0.0, minedAllTime = 0.0;
|
||||||
|
int minedTodayCount = 0, minedYesterdayCount = 0, minedAllTimeCount = 0;
|
||||||
|
int64_t now = (int64_t)std::time(nullptr);
|
||||||
|
|
||||||
|
// Calendar-day boundaries (local time)
|
||||||
|
time_t nowT = (time_t)now;
|
||||||
|
struct tm local;
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(&local, &nowT);
|
||||||
|
#else
|
||||||
|
localtime_r(&nowT, &local);
|
||||||
|
#endif
|
||||||
|
local.tm_hour = 0;
|
||||||
|
local.tm_min = 0;
|
||||||
|
local.tm_sec = 0;
|
||||||
|
int64_t todayStart = (int64_t)mktime(&local);
|
||||||
|
int64_t yesterdayStart = todayStart - 86400;
|
||||||
|
|
||||||
|
struct MinedTx {
|
||||||
|
int64_t timestamp;
|
||||||
|
double amount;
|
||||||
|
int confirmations;
|
||||||
|
bool mature;
|
||||||
|
std::string txid;
|
||||||
|
bool isPoolPayout;
|
||||||
|
};
|
||||||
|
std::vector<MinedTx> recentMined;
|
||||||
|
|
||||||
|
for (const auto& tx : state.transactions) {
|
||||||
|
bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined");
|
||||||
|
bool isPoolPayout = (tx.type == "receive"
|
||||||
|
&& !tx.memo.empty()
|
||||||
|
&& tx.memo.find("Mining Pool payout") != std::string::npos);
|
||||||
|
if (isSoloMined || isPoolPayout) {
|
||||||
|
// Apply earnings filter
|
||||||
|
if (!app->supportsSoloMining()) {
|
||||||
|
if (s_earnings_filter == 1 && !isPoolPayout) continue;
|
||||||
|
} else {
|
||||||
|
if (s_earnings_filter == 1 && !isSoloMined) continue;
|
||||||
|
if (s_earnings_filter == 2 && !isPoolPayout) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double amt = std::abs(tx.amount);
|
||||||
|
minedAllTime += amt;
|
||||||
|
minedAllTimeCount++;
|
||||||
|
if (tx.timestamp >= todayStart) {
|
||||||
|
minedToday += amt;
|
||||||
|
minedTodayCount++;
|
||||||
|
} else if (tx.timestamp >= yesterdayStart) {
|
||||||
|
minedYesterday += amt;
|
||||||
|
minedYesterdayCount++;
|
||||||
|
}
|
||||||
|
// Separate solo blocks from pool payouts based on current mode
|
||||||
|
bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined;
|
||||||
|
if (showInCurrentMode && recentMined.size() < 4) {
|
||||||
|
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use pool hashrate for EST. DAILY when in pool mode
|
||||||
|
double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate;
|
||||||
|
double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty);
|
||||||
|
double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0;
|
||||||
|
double blockReward = schema::UI().drawElement("business", "block-reward").size;
|
||||||
|
double estDaily = estDailyBlocks * blockReward;
|
||||||
|
bool estActive = isMiningActive && estDaily > 0;
|
||||||
|
|
||||||
|
ImU32 greenCol2 = Success();
|
||||||
|
|
||||||
|
// --- Combined Earnings + Details card ---
|
||||||
|
float earningsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad * 1.5f;
|
||||||
|
float detailsContentH = pad * 0.5f + capFont->LegacySize + pad * 0.5f;
|
||||||
|
float barH_est = capFont->LegacySize + Layout::spacingMd() * 2.0f;
|
||||||
|
float combinedCardH = earningsRowH + detailsContentH + barH_est + pad;
|
||||||
|
combinedCardH = std::max(combinedCardH,
|
||||||
|
schema::UI().drawElement("tabs.mining", "details-card-min-height").size + earningsRowH);
|
||||||
|
|
||||||
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + combinedCardH);
|
||||||
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||||
|
|
||||||
|
// === Earnings filter toggle button (top-right of card) ===
|
||||||
|
{
|
||||||
|
const bool soloMiningAvailable = app->supportsSoloMining();
|
||||||
|
const char* filterLabels[] = { TR("mining_filter_all"), soloMiningAvailable ? TR("mining_solo") : TR("mining_pool"), TR("mining_pool") };
|
||||||
|
const char* filterIcons[] = { ICON_MD_FUNCTIONS, soloMiningAvailable ? ICON_MD_MEMORY : ICON_MD_CLOUD, ICON_MD_CLOUD };
|
||||||
|
const int filterCount = soloMiningAvailable ? 3 : 2;
|
||||||
|
const char* curIcon = filterIcons[s_earnings_filter];
|
||||||
|
const char* curLabel = filterLabels[s_earnings_filter];
|
||||||
|
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
float icoH = icoFont->LegacySize;
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, curIcon);
|
||||||
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, curLabel);
|
||||||
|
float padH = Layout::spacingSm();
|
||||||
|
float btnW = padH + icoSz.x + Layout::spacingXs() + lblSz.x + padH;
|
||||||
|
float btnH = icoH + 8.0f * dp;
|
||||||
|
float btnX = cardMax.x - pad - btnW;
|
||||||
|
float btnY = cardMin.y + (earningsRowH - btnH) * 0.5f;
|
||||||
|
|
||||||
|
ImVec2 bMin(btnX, btnY), bMax(btnX + btnW, btnY + btnH);
|
||||||
|
bool hov = material::IsRectHovered(bMin, bMax);
|
||||||
|
|
||||||
|
// Pill background
|
||||||
|
ImU32 pillBg = s_earnings_filter != 0
|
||||||
|
? WithAlpha(Primary(), 60)
|
||||||
|
: WithAlpha(OnSurface(), hov ? 25 : 12);
|
||||||
|
dl->AddRectFilled(bMin, bMax, pillBg, btnH * 0.5f);
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
ImU32 icoCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceDisabled());
|
||||||
|
float cx = bMin.x + padH;
|
||||||
|
float cy = bMin.y + (btnH - icoSz.y) * 0.5f;
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cx, cy), icoCol, curIcon);
|
||||||
|
|
||||||
|
// Label
|
||||||
|
ImU32 lblCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceMedium());
|
||||||
|
float lx = cx + icoSz.x + Layout::spacingXs();
|
||||||
|
float ly = bMin.y + (btnH - lblSz.y) * 0.5f;
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), lblCol, curLabel);
|
||||||
|
|
||||||
|
// Click target
|
||||||
|
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::SetCursorScreenPos(bMin);
|
||||||
|
ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
s_earnings_filter = (s_earnings_filter + 1) % filterCount;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
const char* tips[] = {
|
||||||
|
TR("mining_filter_tip_all"),
|
||||||
|
soloMiningAvailable ? TR("mining_filter_tip_solo") : TR("mining_filter_tip_pool"),
|
||||||
|
TR("mining_filter_tip_pool")
|
||||||
|
};
|
||||||
|
ImGui::SetTooltip("%s", tips[s_earnings_filter]);
|
||||||
|
}
|
||||||
|
ImGui::SetCursorScreenPos(savedCur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Earnings section (top of combined card) ===
|
||||||
|
{
|
||||||
|
const int numCols = 4;
|
||||||
|
float colW = (availWidth - pad * 2) / (float)numCols;
|
||||||
|
float ey = cardMin.y + pad * 0.5f;
|
||||||
|
char valBuf[64], subBuf2[64];
|
||||||
|
|
||||||
|
struct EarningsEntry {
|
||||||
|
const char* label;
|
||||||
|
const char* value;
|
||||||
|
const char* sub;
|
||||||
|
ImU32 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
|
||||||
|
snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount);
|
||||||
|
char todayVal[64], todaySub[64];
|
||||||
|
strncpy(todayVal, valBuf, sizeof(todayVal));
|
||||||
|
strncpy(todaySub, subBuf2, sizeof(todaySub));
|
||||||
|
|
||||||
|
char yesterdayVal[64], yesterdaySub[64];
|
||||||
|
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
|
||||||
|
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount);
|
||||||
|
|
||||||
|
char allVal[64], allSub[64];
|
||||||
|
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
|
||||||
|
snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount);
|
||||||
|
|
||||||
|
char estVal[64];
|
||||||
|
if (estActive)
|
||||||
|
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
||||||
|
else
|
||||||
|
snprintf(estVal, sizeof(estVal), "N/A");
|
||||||
|
|
||||||
|
EarningsEntry entries[] = {
|
||||||
|
{ TR("mining_today"), todayVal, todaySub, greenCol2 },
|
||||||
|
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() },
|
||||||
|
{ TR("mining_all_time"), allVal, allSub, OnSurface() },
|
||||||
|
{ TR("mining_est_daily"), estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int ei = 0; ei < numCols; ei++) {
|
||||||
|
float sx = cardMin.x + pad + ei * colW;
|
||||||
|
float centerX = sx + colW * 0.5f;
|
||||||
|
|
||||||
|
// Overline label (centered)
|
||||||
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, entries[ei].label);
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize,
|
||||||
|
ImVec2(centerX - lblSz.x * 0.5f, ey), OnSurfaceMedium(), entries[ei].label);
|
||||||
|
|
||||||
|
// Value (centered)
|
||||||
|
float valY = ey + ovFont->LegacySize + Layout::spacingXs();
|
||||||
|
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, entries[ei].value);
|
||||||
|
|
||||||
|
if (entries[ei].sub) {
|
||||||
|
// Value + sub annotation side by side, centered together
|
||||||
|
ImVec2 subSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, entries[ei].sub);
|
||||||
|
float totalW = valSz.x + 4 * dp + subSz.x;
|
||||||
|
float startX = centerX - totalW * 0.5f;
|
||||||
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(startX, valY), entries[ei].col, entries[ei].value);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(startX + valSz.x + 4 * dp, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
||||||
|
OnSurfaceDisabled(), entries[ei].sub);
|
||||||
|
} else {
|
||||||
|
dl->AddText(sub1, sub1->LegacySize,
|
||||||
|
ImVec2(centerX - valSz.x * 0.5f, valY), entries[ei].col, entries[ei].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Separator between earnings & details ===
|
||||||
|
float earningsSepY = cardMin.y + earningsRowH;
|
||||||
|
{
|
||||||
|
float rnd = glassSpec.rounding;
|
||||||
|
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, earningsSepY),
|
||||||
|
ImVec2(cardMax.x - rnd * 0.5f, earningsSepY),
|
||||||
|
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Details section (below separator) ===
|
||||||
|
{
|
||||||
|
float cx = cardMin.x + pad;
|
||||||
|
float cy = earningsSepY + pad * 0.5f;
|
||||||
|
|
||||||
|
// Three equal columns: Difficulty | Block | Mining Address
|
||||||
|
float colW = availWidth / 3.0f;
|
||||||
|
float valOffX = availWidth * schema::UI().drawElement("tabs.mining", "stats-col1-value-x-ratio").size;
|
||||||
|
|
||||||
|
float col1X = cx;
|
||||||
|
float col2X = cx + colW;
|
||||||
|
float col3X = cx + colW * 2.0f;
|
||||||
|
|
||||||
|
// -- Difficulty --
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), TR("difficulty"));
|
||||||
|
if (mining.difficulty > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
|
||||||
|
ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(col1X + valOffX, cy));
|
||||||
|
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_click_copy_difficulty"));
|
||||||
|
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||||
|
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||||
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
ImGui::SetClipboardText(buf);
|
||||||
|
Notifications::instance().info(TR("mining_difficulty_copied"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Block --
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), TR("block"));
|
||||||
|
if (mining.blocks > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "%d", mining.blocks);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
|
||||||
|
ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(col2X + valOffX, cy));
|
||||||
|
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_click_copy_block"));
|
||||||
|
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||||
|
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||||
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
ImGui::SetClipboardText(buf);
|
||||||
|
Notifications::instance().info(TR("mining_block_copied"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mining Address --
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr"));
|
||||||
|
std::string mining_address = "";
|
||||||
|
for (const auto& addr : state.addresses) {
|
||||||
|
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||||
|
mining_address = addr.address;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mining_address.empty()) {
|
||||||
|
float addrAvailW = colW - pad - valOffX;
|
||||||
|
float charW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "M").x;
|
||||||
|
if (charW <= 0.0f) charW = 8.0f;
|
||||||
|
int maxChars = std::max(8, (int)(addrAvailW / charW));
|
||||||
|
std::string truncAddr = mining_address;
|
||||||
|
if ((int)truncAddr.length() > maxChars && maxChars > 5) {
|
||||||
|
int half = (maxChars - 3) / 2;
|
||||||
|
if (half > 0 && (size_t)half < truncAddr.length())
|
||||||
|
truncAddr = truncAddr.substr(0, half) + "..." + truncAddr.substr(truncAddr.length() - half);
|
||||||
|
}
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurface(), truncAddr.c_str());
|
||||||
|
|
||||||
|
ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str());
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(col3X + valOffX, cy));
|
||||||
|
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_click_copy_address"));
|
||||||
|
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
|
||||||
|
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
|
||||||
|
WithAlpha(OnSurface(), 60), 1.0f * dp);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
ImGui::SetClipboardText(mining_address.c_str());
|
||||||
|
Notifications::instance().info(TR("mining_address_copied"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Memory bar — centered in remaining space below details ----
|
||||||
|
{
|
||||||
|
double totalRAM = dragonx::util::Platform::getTotalSystemRAM_MB();
|
||||||
|
double usedRAM = dragonx::util::Platform::getUsedSystemRAM_MB();
|
||||||
|
double selfRAM = dragonx::util::Platform::getSelfMemoryUsageMB();
|
||||||
|
double daemonRAM = app->getDaemonMemoryUsageMB();
|
||||||
|
// Include xmrig memory when pool mining
|
||||||
|
double xmrigRAM = state.pool_mining.memory_used / (1024.0 * 1024.0); // bytes -> MB
|
||||||
|
double appRAM = selfRAM + daemonRAM + xmrigRAM; // App + daemon + xmrig combined
|
||||||
|
|
||||||
|
// Fixed bar height (text + padding)
|
||||||
|
float barH = capFont->LegacySize + Layout::spacingMd() * 2.0f;
|
||||||
|
float barRnd = barH * 0.5f; // fully rounded corners
|
||||||
|
|
||||||
|
// Details content ends here
|
||||||
|
float detailsEndY = earningsSepY + detailsContentH;
|
||||||
|
|
||||||
|
// Subtle top separator at the boundary
|
||||||
|
float rnd = glassSpec.rounding;
|
||||||
|
float stripY = detailsEndY;
|
||||||
|
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, stripY),
|
||||||
|
ImVec2(cardMax.x - rnd * 0.5f, stripY),
|
||||||
|
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||||
|
float remainingH = cardMax.y - stripY;
|
||||||
|
float barX = cardMin.x + pad;
|
||||||
|
float barW = cardMax.x - pad - barX;
|
||||||
|
float barY = stripY + (remainingH - barH) * 0.5f;
|
||||||
|
float textY = barY + (barH - capFont->LegacySize) * 0.5f;
|
||||||
|
float textPadX = Layout::spacingMd();
|
||||||
|
|
||||||
|
// Background track
|
||||||
|
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||||
|
WithAlpha(OnSurface(), 20), barRnd);
|
||||||
|
|
||||||
|
float sysFrac = 0.0f, appFrac = 0.0f;
|
||||||
|
float sysFillW = 0.0f, appFillW = 0.0f;
|
||||||
|
|
||||||
|
// Helper: draw a fill bar that perfectly matches the track's rounded
|
||||||
|
// left edge regardless of fill width. We draw a full-width rounded
|
||||||
|
// rect (same shape as the track) but clip it to just the fill portion.
|
||||||
|
auto drawFillBar = [&](float fillW, ImU32 col) {
|
||||||
|
if (fillW <= 1.0f) return;
|
||||||
|
dl->PushClipRect(ImVec2(barX, barY), ImVec2(barX + fillW, barY + barH), true);
|
||||||
|
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||||
|
col, barRnd);
|
||||||
|
dl->PopClipRect();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usedRAM > 0 && totalRAM > 0) {
|
||||||
|
sysFrac = std::clamp((float)(usedRAM / totalRAM), 0.0f, 1.0f);
|
||||||
|
sysFillW = barW * sysFrac;
|
||||||
|
|
||||||
|
// System memory bar (subtle fill)
|
||||||
|
ImU32 ramBarCol = schema::UI().resolveColor("var(--ram-bar-system)",
|
||||||
|
IsDarkTheme() ? IM_COL32(255, 255, 255, 46) : IM_COL32(0, 0, 0, 46));
|
||||||
|
drawFillBar(sysFillW, ramBarCol);
|
||||||
|
|
||||||
|
// App+daemon memory bar (saturated accent)
|
||||||
|
if (appRAM > 0) {
|
||||||
|
appFrac = std::clamp((float)(appRAM / totalRAM), 0.0f, sysFrac);
|
||||||
|
appFillW = barW * appFrac;
|
||||||
|
ImU32 appBarCol = schema::UI().resolveColor("var(--ram-bar-app)",
|
||||||
|
ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]));
|
||||||
|
drawFillBar(appFillW, appBarCol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Text overlaying the bar ---
|
||||||
|
float accentEdge = barX + appFillW;
|
||||||
|
float whiteEdge = barX + sysFillW;
|
||||||
|
|
||||||
|
// Determine text colors for bar segments
|
||||||
|
// On dark themes: white text on empty, dark on filled bars (both system and app)
|
||||||
|
// On light themes: dark text on empty/system, white on app accent bar
|
||||||
|
ImU32 barTextOnEmpty = IsDarkTheme() ? IM_COL32(255, 255, 255, 230)
|
||||||
|
: IM_COL32(30, 30, 30, 230);
|
||||||
|
ImU32 barTextOnFill = IsDarkTheme() ? IM_COL32(30, 30, 30, 230)
|
||||||
|
: IM_COL32(255, 255, 255, 230);
|
||||||
|
ImU32 barTextOnAccent = IsDarkTheme() ? IM_COL32(0, 0, 0, 210)
|
||||||
|
: IM_COL32(255, 255, 255, 230);
|
||||||
|
|
||||||
|
// App usage on the left
|
||||||
|
char appBuf[64] = {};
|
||||||
|
if (appRAM > 0) {
|
||||||
|
if (appRAM >= 1024.0)
|
||||||
|
snprintf(appBuf, sizeof(appBuf), "%.1f GB", appRAM / 1024.0);
|
||||||
|
else
|
||||||
|
snprintf(appBuf, sizeof(appBuf), "%.0f MB", appRAM);
|
||||||
|
|
||||||
|
float appTextX = barX + textPadX;
|
||||||
|
float appTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, appBuf).x;
|
||||||
|
float appTextEnd = appTextX + appTextW;
|
||||||
|
|
||||||
|
// Inside accent bar: white | inside white bar: dark | outside: white
|
||||||
|
if (appTextEnd <= accentEdge) {
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
|
||||||
|
barTextOnAccent, appBuf);
|
||||||
|
} else if (appTextX >= whiteEdge) {
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
|
||||||
|
barTextOnEmpty, appBuf);
|
||||||
|
} else {
|
||||||
|
// Part in accent bar: white
|
||||||
|
if (accentEdge > appTextX) {
|
||||||
|
dl->PushClipRect(ImVec2(appTextX, barY), ImVec2(accentEdge, barY + barH));
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
|
||||||
|
barTextOnAccent, appBuf);
|
||||||
|
dl->PopClipRect();
|
||||||
|
}
|
||||||
|
// Part in white bar (past accent): dark
|
||||||
|
float dkS = std::max(appTextX, accentEdge);
|
||||||
|
float dkE = std::min(appTextEnd, whiteEdge);
|
||||||
|
if (dkE > dkS) {
|
||||||
|
dl->PushClipRect(ImVec2(dkS, barY), ImVec2(dkE, barY + barH));
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
|
||||||
|
barTextOnFill, appBuf);
|
||||||
|
dl->PopClipRect();
|
||||||
|
}
|
||||||
|
// Part outside white bar: white
|
||||||
|
if (appTextEnd > whiteEdge) {
|
||||||
|
dl->PushClipRect(ImVec2(whiteEdge, barY), ImVec2(appTextEnd + 1, barY + barH));
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
|
||||||
|
barTextOnEmpty, appBuf);
|
||||||
|
dl->PopClipRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System usage on the right
|
||||||
|
char sysBuf[64] = {};
|
||||||
|
if (usedRAM > 0 && totalRAM > 0)
|
||||||
|
snprintf(sysBuf, sizeof(sysBuf), "%.1f / %.0f GB", usedRAM / 1024.0, totalRAM / 1024.0);
|
||||||
|
else if (totalRAM > 0)
|
||||||
|
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
|
||||||
|
else
|
||||||
|
snprintf(sysBuf, sizeof(sysBuf), "N/A");
|
||||||
|
|
||||||
|
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
|
||||||
|
float sysTextX = barX + barW - textPadX - sysTextW;
|
||||||
|
|
||||||
|
if (sysTextX >= whiteEdge) {
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
|
||||||
|
barTextOnEmpty, sysBuf);
|
||||||
|
} else if (sysTextX + sysTextW <= whiteEdge) {
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
|
||||||
|
barTextOnFill, sysBuf);
|
||||||
|
} else {
|
||||||
|
dl->PushClipRect(ImVec2(sysTextX, barY), ImVec2(whiteEdge, barY + barH));
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
|
||||||
|
barTextOnFill, sysBuf);
|
||||||
|
dl->PopClipRect();
|
||||||
|
dl->PushClipRect(ImVec2(whiteEdge, barY), ImVec2(sysTextX + sysTextW + 1, barY + barH));
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
|
||||||
|
barTextOnEmpty, sysBuf);
|
||||||
|
dl->PopClipRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invisible button over the bar for tooltip interaction
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(barX, barY));
|
||||||
|
ImGui::InvisibleButton("##rambar", ImVec2(barW, barH));
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
if (selfRAM >= 1024.0)
|
||||||
|
ImGui::Text(TR("ram_wallet_gb"), selfRAM / 1024.0);
|
||||||
|
else
|
||||||
|
ImGui::Text(TR("ram_wallet_mb"), selfRAM);
|
||||||
|
if (daemonRAM >= 1024.0)
|
||||||
|
ImGui::Text(TR("ram_daemon_gb"), daemonRAM / 1024.0, app->getDaemonMemDiag().c_str());
|
||||||
|
else
|
||||||
|
ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str());
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text(TR("ram_system_gb"), usedRAM / 1024.0, totalRAM / 1024.0);
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// RECENT BLOCKS — last 4 mined blocks (always shown in pool mode)
|
||||||
|
// ============================================================
|
||||||
|
if (!recentMined.empty() || s_pool_mode) {
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
|
||||||
|
s_pool_mode ? TR("mining_recent_payouts") : TR("mining_recent_blocks"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
|
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
|
||||||
|
// Size to remaining space — proportional budget ensures fit
|
||||||
|
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
|
||||||
|
float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size();
|
||||||
|
float contentH_blocks = rowH_blocks * minRows + pad * 2.5f;
|
||||||
|
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
|
||||||
|
|
||||||
|
// Glass panel wrapping the list + scroll-edge mask state
|
||||||
|
ImVec2 recentPanelMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 recentPanelMax(recentPanelMin.x + availWidth, recentPanelMin.y + recentH);
|
||||||
|
GlassPanelSpec recentGlass;
|
||||||
|
recentGlass.rounding = Layout::glassRounding();
|
||||||
|
DrawGlassPanel(dl, recentPanelMin, recentPanelMax, recentGlass);
|
||||||
|
|
||||||
|
float miningScrollY = 0.0f, miningScrollMaxY = 0.0f;
|
||||||
|
int miningParentVtx = dl->VtxBuffer.Size;
|
||||||
|
|
||||||
|
ImGui::BeginChild("##RecentBlocks", ImVec2(availWidth, recentH), false,
|
||||||
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||||
|
ImDrawList* miningChildDL = ImGui::GetWindowDrawList();
|
||||||
|
int miningChildVtx = miningChildDL->VtxBuffer.Size;
|
||||||
|
|
||||||
|
miningScrollY = ImGui::GetScrollY();
|
||||||
|
miningScrollMaxY = ImGui::GetScrollMaxY();
|
||||||
|
|
||||||
|
// Top padding inside glass card
|
||||||
|
ImGui::Dummy(ImVec2(0, pad * 0.5f));
|
||||||
|
|
||||||
|
if (recentMined.empty()) {
|
||||||
|
// Empty state — card is visible but no rows yet
|
||||||
|
float emptyY = ImGui::GetCursorScreenPos().y;
|
||||||
|
float emptyX = ImGui::GetCursorScreenPos().x;
|
||||||
|
float centerX = emptyX + availWidth * 0.5f;
|
||||||
|
ImFont* icoFont = Type().iconMed();
|
||||||
|
const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY;
|
||||||
|
ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon);
|
||||||
|
miningChildDL->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(centerX - iSz.x * 0.5f, emptyY),
|
||||||
|
OnSurfaceDisabled(), emptyIcon);
|
||||||
|
const char* emptyMsg = s_pool_mode
|
||||||
|
? TR("mining_no_payouts_yet")
|
||||||
|
: TR("mining_no_blocks_yet");
|
||||||
|
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg);
|
||||||
|
miningChildDL->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()),
|
||||||
|
OnSurfaceDisabled(), emptyMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t mi = 0; mi < recentMined.size(); mi++) {
|
||||||
|
const auto& mtx = recentMined[mi];
|
||||||
|
|
||||||
|
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||||
|
float rH = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
|
||||||
|
ImVec2 rMax(rMin.x + availWidth, rMin.y + rH);
|
||||||
|
|
||||||
|
// Subtle background on hover (inset from card edges)
|
||||||
|
bool hovered = material::IsRectHovered(rMin, rMax);
|
||||||
|
bool isClickable = !mtx.txid.empty();
|
||||||
|
if (hovered) {
|
||||||
|
dl->AddRectFilled(ImVec2(rMin.x + pad * 0.5f, rMin.y),
|
||||||
|
ImVec2(rMax.x - pad * 0.5f, rMax.y),
|
||||||
|
IM_COL32(255, 255, 255, 8), 3.0f * dp);
|
||||||
|
if (isClickable) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float rx = rMin.x + pad;
|
||||||
|
float ry = rMin.y + Layout::spacingXs();
|
||||||
|
|
||||||
|
// Mining icon — Material Design
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
const char* mIcon = ICON_MD_CONSTRUCTION;
|
||||||
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mIcon);
|
||||||
|
float iconX = rx + 2 * dp, iconY = ry + 2 * dp;
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(iconX, iconY),
|
||||||
|
WithAlpha(Warning(), 200), mIcon);
|
||||||
|
|
||||||
|
// Time
|
||||||
|
int64_t diff = now - mtx.timestamp;
|
||||||
|
if (diff < 60)
|
||||||
|
snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff);
|
||||||
|
else if (diff < 3600)
|
||||||
|
snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60));
|
||||||
|
else if (diff < 86400)
|
||||||
|
snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600));
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400));
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + iSz.x + 8 * dp, ry), OnSurfaceDisabled(), buf);
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
snprintf(buf, sizeof(buf), "+%.8f %s", mtx.amount, DRAGONX_TICKER);
|
||||||
|
float amtX = rMin.x + pad + (availWidth - pad * 2) * 0.35f;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(amtX, ry), greenCol2, buf);
|
||||||
|
|
||||||
|
// Maturity badge — inset from right edge
|
||||||
|
float badgeX = rMax.x - pad - Layout::spacingXl() * 3.5f;
|
||||||
|
if (mtx.mature) {
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
|
||||||
|
WithAlpha(Success(), 180), TR("mature"));
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), TR("conf_count"), mtx.confirmations);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
|
||||||
|
WithAlpha(Warning(), 200), buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click to open in block explorer
|
||||||
|
ImGui::SetCursorScreenPos(rMin);
|
||||||
|
char blockBtnId[32];
|
||||||
|
snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi);
|
||||||
|
ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH));
|
||||||
|
if (ImGui::IsItemClicked() && !mtx.txid.empty()) {
|
||||||
|
std::string url = app->settings()->getTxExplorerUrl() + mtx.txid;
|
||||||
|
dragonx::util::Platform::openUrl(url);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_open_in_explorer"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndChild(); // ##RecentBlocks
|
||||||
|
|
||||||
|
// CSS-style clipping mask
|
||||||
|
{
|
||||||
|
float fadeZone = std::min(capFont->LegacySize * 3.0f, recentH * 0.18f);
|
||||||
|
ApplyScrollEdgeMask(dl, miningParentVtx, miningChildDL, miningChildVtx,
|
||||||
|
recentPanelMin.y, recentPanelMax.y, fadeZone, miningScrollY, miningScrollMaxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "../material/draw_helpers.h" // GlassPanelSpec
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Renders the mining "Earnings" card (Today | Yesterday | All Time | Est. Daily). Extracted
|
||||||
|
// verbatim from the monolithic mining tab; the immediate-mode layout context (draw list, fonts,
|
||||||
|
// scale/spacing, the glass-panel spec, and the parent's pool-mode flag) is passed in so the
|
||||||
|
// rendering flows in sequence exactly as before.
|
||||||
|
void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float vs, float gap, float pad, bool isMiningActive,
|
||||||
|
bool poolMode, float availWidth,
|
||||||
|
const material::GlassPanelSpec& glassSpec, float sHdr, float gapOver);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "mining_mode_toggle.h"
|
||||||
|
#include "mining_tab_helpers.h"
|
||||||
|
#include "mining_pool_panel.h"
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/version.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/platform.h"
|
||||||
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/components/buttons.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../notifications.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* ovFont,
|
||||||
|
float dp, float hs, float gap, float availWidth,
|
||||||
|
bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256],
|
||||||
|
bool& s_pool_settings_dirty)
|
||||||
|
{
|
||||||
|
const bool soloMiningAvailable = app->supportsSoloMining();
|
||||||
|
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
|
||||||
|
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
|
||||||
|
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size;
|
||||||
|
float totalW = soloMiningAvailable ? (toggleW * 2) : toggleW;
|
||||||
|
|
||||||
|
ImVec2 tMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH);
|
||||||
|
|
||||||
|
// Glass background for the segmented control
|
||||||
|
dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd);
|
||||||
|
dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd);
|
||||||
|
|
||||||
|
ImVec2 soloMin = tMin;
|
||||||
|
ImVec2 soloMax(tMin.x + toggleW, tMax.y);
|
||||||
|
bool soloHov = material::IsRectHovered(soloMin, soloMax);
|
||||||
|
if (soloMiningAvailable) {
|
||||||
|
// SOLO button (left half)
|
||||||
|
if (!s_pool_mode) {
|
||||||
|
dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd);
|
||||||
|
} else if (soloHov) {
|
||||||
|
dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd);
|
||||||
|
}
|
||||||
|
const char* label = TR("mining_solo");
|
||||||
|
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||||
|
float lx = soloMin.x + (toggleW - sz.x) * 0.5f;
|
||||||
|
float ly = soloMin.y + (toggleH - sz.y) * 0.5f;
|
||||||
|
ImU32 col = !s_pool_mode ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium();
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POOL button (right half) — disabled when solo mining is active
|
||||||
|
bool soloMiningActive = mining.generate;
|
||||||
|
ImVec2 poolMin(tMin.x + (soloMiningAvailable ? toggleW : 0.0f), tMin.y);
|
||||||
|
ImVec2 poolMax = tMax;
|
||||||
|
bool poolHov = material::IsRectHovered(poolMin, poolMax);
|
||||||
|
if (s_pool_mode) {
|
||||||
|
dl->AddRectFilled(poolMin, poolMax, WithAlpha(Primary(), 180), toggleRnd);
|
||||||
|
} else if (soloMiningActive) {
|
||||||
|
// Dimmed — solo mining blocks pool mode
|
||||||
|
} else if (poolHov) {
|
||||||
|
dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const char* label = TR("mining_pool");
|
||||||
|
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||||
|
float lx = poolMin.x + (toggleW - sz.x) * 0.5f;
|
||||||
|
float ly = poolMin.y + (toggleH - sz.y) * 0.5f;
|
||||||
|
ImU32 col = s_pool_mode ? IM_COL32(255, 255, 255, 230)
|
||||||
|
: (soloMiningActive ? OnSurfaceDisabled() : OnSurfaceMedium());
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invisible buttons for click targets
|
||||||
|
if (soloMiningAvailable) {
|
||||||
|
ImGui::SetCursorScreenPos(soloMin);
|
||||||
|
ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH));
|
||||||
|
if (ImGui::IsItemClicked() && s_pool_mode) {
|
||||||
|
s_pool_mode = false;
|
||||||
|
app->settings()->setPoolMode(false);
|
||||||
|
app->settings()->save();
|
||||||
|
app->stopPoolMining();
|
||||||
|
}
|
||||||
|
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(poolMin);
|
||||||
|
ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH));
|
||||||
|
if (soloMiningAvailable && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) {
|
||||||
|
s_pool_mode = true;
|
||||||
|
app->settings()->setPoolMode(true);
|
||||||
|
app->settings()->save();
|
||||||
|
// Note: soloMiningActive is already false (checked above),
|
||||||
|
// so no need to call stopMining() — it would just set the
|
||||||
|
// toggle-in-progress flag and make the button show "STARTING".
|
||||||
|
}
|
||||||
|
if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (poolHov && soloMiningActive && !s_pool_mode) {
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(totalW, 0));
|
||||||
|
|
||||||
|
// Pool URL + Worker inputs inline next to toggle (pool mode only)
|
||||||
|
if (s_pool_mode && soloMiningActive) {
|
||||||
|
// Solo mining is active — show disabled message instead of inputs
|
||||||
|
float inputFrameH = ImGui::GetFrameHeight();
|
||||||
|
float vertOff = (toggleH - inputFrameH) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(tMax.x + Layout::spacingLg(), tMin.y + vertOff));
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::PushFont(Type().iconSmall());
|
||||||
|
ImGui::TextUnformatted(ICON_MD_INFO);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
ImGui::PushFont(capFont);
|
||||||
|
ImGui::TextUnformatted(TR("mining_stop_solo_for_pool_settings"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else if (s_pool_mode) {
|
||||||
|
// Position inputs to the right of the toggle
|
||||||
|
float inputFrameH = ImGui::GetFrameHeight();
|
||||||
|
float vertOff = (toggleH - inputFrameH) * 0.5f;
|
||||||
|
float inputsStartX = tMax.x + Layout::spacingLg();
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff));
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp));
|
||||||
|
|
||||||
|
float inputFrameH2 = ImGui::GetFrameHeight();
|
||||||
|
float iconBtnW = inputFrameH2;
|
||||||
|
float resetBtnW = iconBtnW;
|
||||||
|
float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
|
||||||
|
// Each input group: [input][▼][bookmark]
|
||||||
|
// Layout: [URL group] [spacing] [Worker group] [spacing] [reset]
|
||||||
|
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
|
||||||
|
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
|
||||||
|
- Layout::spacingSm() - perGroupExtra * 2;
|
||||||
|
float urlW = std::max(60.0f, remainW * 0.30f);
|
||||||
|
float wrkW = std::max(40.0f, remainW * 0.70f);
|
||||||
|
|
||||||
|
// Track positions for popup alignment
|
||||||
|
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||||
|
float urlGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||||
|
float urlGroupW = urlW + perGroupExtra;
|
||||||
|
|
||||||
|
// === Pool URL input ===
|
||||||
|
ImGui::SetNextItemWidth(urlW);
|
||||||
|
if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) {
|
||||||
|
s_pool_settings_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- URL: Dropdown arrow button ---
|
||||||
|
ImGui::SameLine(0, 0);
|
||||||
|
{
|
||||||
|
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||||
|
ImGui::InvisibleButton("##PoolDropdown", btnSize);
|
||||||
|
bool btnHov = ImGui::IsItemHovered();
|
||||||
|
bool btnClk = ImGui::IsItemClicked();
|
||||||
|
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||||
|
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||||
|
if (btnHov) {
|
||||||
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
|
StateHover(), 4.0f * dp);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_saved_pools"));
|
||||||
|
}
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
|
||||||
|
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||||
|
OnSurfaceMedium(), dropIcon);
|
||||||
|
if (btnClk) {
|
||||||
|
ImGui::OpenPopup("##SavedPoolsPopup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- URL: Bookmark button ---
|
||||||
|
ImGui::SameLine(0, 0);
|
||||||
|
{
|
||||||
|
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||||
|
ImGui::InvisibleButton("##SavePoolUrl", btnSize);
|
||||||
|
bool btnHov = ImGui::IsItemHovered();
|
||||||
|
bool btnClk = ImGui::IsItemClicked();
|
||||||
|
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||||
|
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||||
|
std::string currentUrl(s_pool_url);
|
||||||
|
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
|
||||||
|
if (btnHov) {
|
||||||
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
|
StateHover(), 4.0f * dp);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
|
||||||
|
}
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
||||||
|
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||||
|
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
||||||
|
if (btnClk && !currentUrl.empty() && !alreadySaved) {
|
||||||
|
app->settings()->addSavedPoolUrl(currentUrl);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- URL: Popup positioned below the input group ---
|
||||||
|
// Match popup width to input group; zero horizontal padding so
|
||||||
|
// item highlights are flush with the popup container edges.
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||||
|
if (ImGui::BeginPopup("##SavedPoolsPopup")) {
|
||||||
|
const auto& savedUrls = app->settings()->getSavedPoolUrls();
|
||||||
|
if (savedUrls.empty()) {
|
||||||
|
ImGui::SetCursorPosX(8 * dp);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
ImGui::TextDisabled("%s", TR("mining_no_saved_pools"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SetCursorPosX(8 * dp);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
ImGui::TextDisabled("%s", TR("mining_click"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, 2 * dp);
|
||||||
|
ImGui::PushFont(Type().iconSmall());
|
||||||
|
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, 2 * dp);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
ImGui::TextDisabled("%s", TR("mining_to_save"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
} else {
|
||||||
|
std::string urlToRemove;
|
||||||
|
float popupInnerW = ImGui::GetContentRegionAvail().x;
|
||||||
|
float xZoneW = ImGui::GetFrameHeight();
|
||||||
|
float textPadX = 8 * dp;
|
||||||
|
ImFont* rowFont = ImGui::GetFont();
|
||||||
|
float rowFontSz = ImGui::GetFontSize();
|
||||||
|
float rowH = ImGui::GetFrameHeight();
|
||||||
|
for (const auto& url : savedUrls) {
|
||||||
|
ImGui::PushID(url.c_str());
|
||||||
|
bool isCurrent = (std::string(s_pool_url) == url);
|
||||||
|
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
|
||||||
|
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
|
||||||
|
bool rowHov = ImGui::IsItemHovered();
|
||||||
|
bool rowClk = ImGui::IsItemClicked();
|
||||||
|
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||||
|
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW;
|
||||||
|
// Row background — flush with popup edges
|
||||||
|
if (isCurrent)
|
||||||
|
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||||
|
if (rowHov && !inXZone)
|
||||||
|
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||||
|
// Item text with internal padding
|
||||||
|
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
|
||||||
|
pdl->AddText(rowFont, rowFontSz,
|
||||||
|
ImVec2(rowMin.x + textPadX, textY),
|
||||||
|
isCurrent ? Primary() : OnSurface(), url.c_str());
|
||||||
|
// X button — flush with right edge, icon centered
|
||||||
|
{
|
||||||
|
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
|
||||||
|
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||||
|
if (inXZone) {
|
||||||
|
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_remove"));
|
||||||
|
} else if (rowHov) {
|
||||||
|
// Show faint X when row is hovered
|
||||||
|
ImFont* icoF = Type().iconSmall();
|
||||||
|
const char* xIcon = ICON_MD_CLOSE;
|
||||||
|
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||||
|
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||||
|
pdl->AddText(icoF, icoF->LegacySize,
|
||||||
|
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||||
|
OnSurfaceDisabled(), xIcon);
|
||||||
|
}
|
||||||
|
// Always draw icon when hovering X zone
|
||||||
|
if (inXZone) {
|
||||||
|
ImFont* icoF = Type().iconSmall();
|
||||||
|
const char* xIcon = ICON_MD_CLOSE;
|
||||||
|
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||||
|
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||||
|
pdl->AddText(icoF, icoF->LegacySize,
|
||||||
|
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||||
|
Error(), xIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Click handling
|
||||||
|
if (rowClk) {
|
||||||
|
if (inXZone) {
|
||||||
|
urlToRemove = url;
|
||||||
|
} else {
|
||||||
|
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
|
||||||
|
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
|
||||||
|
s_pool_settings_dirty = true;
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rowHov && !inXZone)
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
if (!urlToRemove.empty()) {
|
||||||
|
app->settings()->removeSavedPoolUrl(urlToRemove);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||||
|
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||||
|
float wrkGroupW = wrkW + perGroupExtra;
|
||||||
|
|
||||||
|
ImGui::SetNextItemWidth(wrkW);
|
||||||
|
if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) {
|
||||||
|
s_pool_settings_dirty = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
std::string currentWorkerStr(s_pool_worker);
|
||||||
|
if (currentWorkerStr.empty()) {
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
|
||||||
|
} else {
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Worker: Dropdown arrow button ---
|
||||||
|
ImGui::SameLine(0, 0);
|
||||||
|
{
|
||||||
|
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||||
|
ImGui::InvisibleButton("##WorkerDropdown", btnSize);
|
||||||
|
bool btnHov = ImGui::IsItemHovered();
|
||||||
|
bool btnClk = ImGui::IsItemClicked();
|
||||||
|
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||||
|
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||||
|
if (btnHov) {
|
||||||
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
|
StateHover(), 4.0f * dp);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_saved_addresses"));
|
||||||
|
}
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
|
||||||
|
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||||
|
OnSurfaceMedium(), dropIcon);
|
||||||
|
if (btnClk) {
|
||||||
|
ImGui::OpenPopup("##SavedWorkersPopup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Worker: Bookmark button ---
|
||||||
|
ImGui::SameLine(0, 0);
|
||||||
|
{
|
||||||
|
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 btnSize(iconBtnW, inputFrameH2);
|
||||||
|
ImGui::InvisibleButton("##SaveWorkerAddr", btnSize);
|
||||||
|
bool btnHov = ImGui::IsItemHovered();
|
||||||
|
bool btnClk = ImGui::IsItemClicked();
|
||||||
|
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||||
|
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||||
|
std::string currentWorker(s_pool_worker);
|
||||||
|
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
|
||||||
|
if (btnHov) {
|
||||||
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
|
StateHover(), 4.0f * dp);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
|
||||||
|
}
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
|
||||||
|
dl2->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
|
||||||
|
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
|
||||||
|
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
|
||||||
|
app->settings()->addSavedPoolWorker(currentWorker);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Worker: Popup positioned below the input group ---
|
||||||
|
// Popup sized to fit full z-addresses without truncation;
|
||||||
|
// zero horizontal padding so item highlights are flush with edges.
|
||||||
|
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||||
|
if (ImGui::BeginPopup("##SavedWorkersPopup")) {
|
||||||
|
const auto& savedWorkers = app->settings()->getSavedPoolWorkers();
|
||||||
|
if (savedWorkers.empty()) {
|
||||||
|
ImGui::SetCursorPosX(8 * dp);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
ImGui::TextDisabled("%s", TR("mining_no_saved_addresses"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SetCursorPosX(8 * dp);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
ImGui::TextDisabled("%s", TR("mining_click"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, 2 * dp);
|
||||||
|
ImGui::PushFont(Type().iconSmall());
|
||||||
|
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, 2 * dp);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
ImGui::TextDisabled("%s", TR("mining_to_save"));
|
||||||
|
ImGui::PopFont();
|
||||||
|
} else {
|
||||||
|
std::string addrToRemove;
|
||||||
|
float wPopupInnerW = ImGui::GetContentRegionAvail().x;
|
||||||
|
float wXZoneW = ImGui::GetFrameHeight();
|
||||||
|
float wTextPadX = 8 * dp;
|
||||||
|
ImFont* wRowFont = ImGui::GetFont();
|
||||||
|
float wRowFontSz = ImGui::GetFontSize();
|
||||||
|
float wRowH = ImGui::GetFrameHeight();
|
||||||
|
for (const auto& addr : savedWorkers) {
|
||||||
|
ImGui::PushID(addr.c_str());
|
||||||
|
bool isCurrent = (std::string(s_pool_worker) == addr);
|
||||||
|
ImVec2 rowMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
|
||||||
|
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
|
||||||
|
bool rowHov = ImGui::IsItemHovered();
|
||||||
|
bool rowClk = ImGui::IsItemClicked();
|
||||||
|
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||||
|
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW;
|
||||||
|
// Row background — flush with popup edges
|
||||||
|
if (isCurrent)
|
||||||
|
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
|
||||||
|
if (rowHov && !inXZone)
|
||||||
|
pdl->AddRectFilled(rowMin, rowMax, StateHover());
|
||||||
|
// Full address text with internal padding
|
||||||
|
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
|
||||||
|
pdl->AddText(wRowFont, wRowFontSz,
|
||||||
|
ImVec2(rowMin.x + wTextPadX, textY),
|
||||||
|
isCurrent ? Primary() : OnSurface(), addr.c_str());
|
||||||
|
// Tooltip for long addresses
|
||||||
|
if (rowHov && !inXZone)
|
||||||
|
ImGui::SetTooltip("%s", addr.c_str());
|
||||||
|
// X button — flush with right edge, icon centered
|
||||||
|
{
|
||||||
|
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
|
||||||
|
ImVec2 xMax(rowMax.x, rowMax.y);
|
||||||
|
if (inXZone) {
|
||||||
|
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_remove"));
|
||||||
|
} else if (rowHov) {
|
||||||
|
ImFont* icoF = Type().iconSmall();
|
||||||
|
const char* xIcon = ICON_MD_CLOSE;
|
||||||
|
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||||
|
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||||
|
pdl->AddText(icoF, icoF->LegacySize,
|
||||||
|
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||||
|
OnSurfaceDisabled(), xIcon);
|
||||||
|
}
|
||||||
|
if (inXZone) {
|
||||||
|
ImFont* icoF = Type().iconSmall();
|
||||||
|
const char* xIcon = ICON_MD_CLOSE;
|
||||||
|
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
|
||||||
|
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
|
||||||
|
pdl->AddText(icoF, icoF->LegacySize,
|
||||||
|
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
|
||||||
|
Error(), xIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Click handling
|
||||||
|
if (rowClk) {
|
||||||
|
if (inXZone) {
|
||||||
|
addrToRemove = addr;
|
||||||
|
} else {
|
||||||
|
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
|
||||||
|
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||||
|
s_pool_settings_dirty = true;
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rowHov && !inXZone)
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
if (!addrToRemove.empty()) {
|
||||||
|
app->settings()->removeSavedPoolWorker(addrToRemove);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup
|
||||||
|
|
||||||
|
// === Reset to defaults button ===
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
{
|
||||||
|
ImVec2 btnPos = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 btnSize(inputFrameH2, inputFrameH2);
|
||||||
|
ImGui::InvisibleButton("##ResetPoolDefaults", btnSize);
|
||||||
|
bool btnHov = ImGui::IsItemHovered();
|
||||||
|
bool btnClk = ImGui::IsItemClicked();
|
||||||
|
|
||||||
|
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||||
|
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||||
|
|
||||||
|
// Hover highlight
|
||||||
|
if (btnHov) {
|
||||||
|
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||||
|
StateHover(), 4.0f * dp);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_reset_defaults"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
const char* resetIcon = ICON_MD_REFRESH;
|
||||||
|
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, resetIcon);
|
||||||
|
dl2->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||||
|
OnSurfaceMedium(), resetIcon);
|
||||||
|
|
||||||
|
if (btnClk) {
|
||||||
|
strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1);
|
||||||
|
// Default to user's first shielded (z) address for pool payouts.
|
||||||
|
// Leave blank if no z-address exists yet.
|
||||||
|
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
|
||||||
|
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||||
|
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||||
|
s_pool_settings_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure cursor Y is at toggle bottom regardless of pool input widgets,
|
||||||
|
// so the cards below stay at the same position in both solo and pool modes.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(0, gap * 0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Renders the mining "Mode toggle" (SOLO | POOL segmented control + pool URL/worker inputs).
|
||||||
|
// Extracted verbatim from the monolithic mining tab. The state it mutates is passed BY REFERENCE:
|
||||||
|
// the pool-mode flag, the pool URL / worker text buffers, and the settings-dirty flag (named with
|
||||||
|
// their original identifiers so the moved body is byte-identical).
|
||||||
|
void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* ovFont,
|
||||||
|
float dp, float hs, float gap, float availWidth,
|
||||||
|
bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256],
|
||||||
|
bool& s_pool_settings_dirty);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "mining_stats.h"
|
||||||
|
#include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock
|
||||||
|
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
// Chart/log view toggles (false = chart, true = live log). Moved here with the card they belong to.
|
||||||
|
static bool s_show_pool_log = false;
|
||||||
|
static bool s_show_solo_log = false;
|
||||||
|
|
||||||
|
void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float vs, float gap, float pad, float availWidth,
|
||||||
|
const GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode)
|
||||||
|
{
|
||||||
|
const bool s_pool_mode = poolMode; // alias: keep the moved body's identifier verbatim
|
||||||
|
ImU32 greenCol = Success();
|
||||||
|
|
||||||
|
// Determine view mode first
|
||||||
|
bool showLogView = s_pool_mode
|
||||||
|
? (s_show_pool_log && !state.pool_mining.log_lines.empty())
|
||||||
|
: (s_show_solo_log && !mining.log_lines.empty());
|
||||||
|
bool hasLogContent = s_pool_mode
|
||||||
|
? !state.pool_mining.log_lines.empty()
|
||||||
|
: !mining.log_lines.empty();
|
||||||
|
// Use pool hashrate history when in pool mode, solo otherwise
|
||||||
|
const std::vector<double>& chartHistory = s_pool_mode
|
||||||
|
? state.pool_mining.hashrate_history
|
||||||
|
: mining.hashrate_history;
|
||||||
|
bool hasChartContent = chartHistory.size() >= 2;
|
||||||
|
|
||||||
|
// Stat row height (single line: overline + value)
|
||||||
|
float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
|
||||||
|
float totalCardH = statRowH + chartBudgetH + pad;
|
||||||
|
|
||||||
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
|
||||||
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||||
|
|
||||||
|
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
|
||||||
|
|
||||||
|
if (showLogView) {
|
||||||
|
// --- Full-card log view (selectable + copyable) ---
|
||||||
|
const std::vector<std::string>& logLines = s_pool_mode
|
||||||
|
? state.pool_mining.log_lines
|
||||||
|
: mining.log_lines;
|
||||||
|
|
||||||
|
// Build a single string buffer for InputTextMultiline
|
||||||
|
static std::string s_log_buf;
|
||||||
|
s_log_buf.clear();
|
||||||
|
for (const auto& line : logLines) {
|
||||||
|
if (!line.empty()) {
|
||||||
|
s_log_buf += line;
|
||||||
|
s_log_buf += '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float logPad = pad * 0.5f;
|
||||||
|
float logW = availWidth - logPad * 2;
|
||||||
|
float logH = totalCardH - logPad * 2;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
|
||||||
|
ImFont* monoFont = Type().body2();
|
||||||
|
ImGui::PushFont(monoFont);
|
||||||
|
|
||||||
|
const char* inputId = s_pool_mode ? "##PoolLogText" : "##SoloLogText";
|
||||||
|
ImGui::InputTextMultiline(inputId,
|
||||||
|
const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
|
||||||
|
ImVec2(logW, logH),
|
||||||
|
ImGuiInputTextFlags_ReadOnly);
|
||||||
|
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::PopStyleColor(2);
|
||||||
|
|
||||||
|
// Reset cursor to end of card after the input
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system
|
||||||
|
} else {
|
||||||
|
// --- Stats + Chart view ---
|
||||||
|
|
||||||
|
// Pool vs Solo stats — different columns
|
||||||
|
std::string col1Str, col2Str, col3Str, col4Str;
|
||||||
|
const char* col1Label;
|
||||||
|
const char* col2Label;
|
||||||
|
const char* col3Label;
|
||||||
|
const char* col4Label = nullptr;
|
||||||
|
ImU32 col1Col, col2Col, col3Col, col4Col = OnSurface();
|
||||||
|
int numStats = 3;
|
||||||
|
|
||||||
|
if (s_pool_mode) {
|
||||||
|
col1Label = TR("mining_local_hashrate");
|
||||||
|
col1Str = FormatHashrate(state.pool_mining.hashrate_10s);
|
||||||
|
col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled();
|
||||||
|
|
||||||
|
col2Label = TR("mining_pool_hashrate");
|
||||||
|
col2Str = FormatHashrate(state.pool_mining.pool_hashrate);
|
||||||
|
col2Col = state.pool_mining.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled();
|
||||||
|
|
||||||
|
col3Label = TR("mining_shares");
|
||||||
|
char sharesBuf[64];
|
||||||
|
snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld",
|
||||||
|
(long long)state.pool_mining.accepted,
|
||||||
|
(long long)state.pool_mining.rejected);
|
||||||
|
col3Str = sharesBuf;
|
||||||
|
col3Col = OnSurface();
|
||||||
|
|
||||||
|
col4Label = TR("mining_uptime");
|
||||||
|
int64_t up = state.pool_mining.uptime_sec;
|
||||||
|
char uptBuf[64];
|
||||||
|
if (up <= 0)
|
||||||
|
snprintf(uptBuf, sizeof(uptBuf), "N/A");
|
||||||
|
else if (up < 3600)
|
||||||
|
snprintf(uptBuf, sizeof(uptBuf), "%lldm %llds", (long long)(up / 60), (long long)(up % 60));
|
||||||
|
else
|
||||||
|
snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60));
|
||||||
|
col4Str = uptBuf;
|
||||||
|
col4Col = OnSurface();
|
||||||
|
numStats = 4;
|
||||||
|
} else {
|
||||||
|
double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty);
|
||||||
|
|
||||||
|
col1Label = TR("mining_local_hashrate");
|
||||||
|
col1Str = FormatHashrate(mining.localHashrate);
|
||||||
|
col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
|
||||||
|
|
||||||
|
col2Label = TR("mining_network");
|
||||||
|
col2Str = FormatHashrate(mining.networkHashrate);
|
||||||
|
col2Col = OnSurface();
|
||||||
|
|
||||||
|
col3Label = TR("mining_est_block");
|
||||||
|
col3Str = FormatEstTime(est_hours);
|
||||||
|
col3Col = OnSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw stat values as inline columns at top of card
|
||||||
|
{
|
||||||
|
float statColW = (availWidth - pad * 2) / (float)numStats;
|
||||||
|
float sy = cardMin.y + pad * 0.5f;
|
||||||
|
|
||||||
|
struct StatEntry { const char* label; const char* value; ImU32 col; };
|
||||||
|
char c1Buf[64], c2Buf[64], c3Buf[64], c4Buf[64];
|
||||||
|
snprintf(c1Buf, sizeof(c1Buf), "%s", col1Str.c_str());
|
||||||
|
snprintf(c2Buf, sizeof(c2Buf), "%s", col2Str.c_str());
|
||||||
|
snprintf(c3Buf, sizeof(c3Buf), "%s", col3Str.c_str());
|
||||||
|
if (numStats > 3) snprintf(c4Buf, sizeof(c4Buf), "%s", col4Str.c_str());
|
||||||
|
StatEntry stats[] = {
|
||||||
|
{ col1Label, c1Buf, col1Col },
|
||||||
|
{ col2Label, c2Buf, col2Col },
|
||||||
|
{ col3Label, c3Buf, col3Col },
|
||||||
|
{ col4Label ? col4Label : "", c4Buf, col4Col },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int si = 0; si < numStats; si++) {
|
||||||
|
float sx = cardMin.x + pad + si * statColW;
|
||||||
|
float centerX = sx + statColW * 0.5f;
|
||||||
|
|
||||||
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[si].label);
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize,
|
||||||
|
ImVec2(centerX - lblSz.x * 0.5f, sy), OnSurfaceMedium(), stats[si].label);
|
||||||
|
|
||||||
|
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[si].value);
|
||||||
|
float valY = sy + ovFont->LegacySize + Layout::spacingXs();
|
||||||
|
dl->AddText(sub1, sub1->LegacySize,
|
||||||
|
ImVec2(centerX - valSz.x * 0.5f, valY), stats[si].col, stats[si].value);
|
||||||
|
|
||||||
|
// Trend arrow for hashrate (first column only)
|
||||||
|
if (si == 0 && chartHistory.size() >= 6) {
|
||||||
|
size_t hn = chartHistory.size();
|
||||||
|
double recent = (chartHistory[hn-1] + chartHistory[hn-2] + chartHistory[hn-3]) / 3.0;
|
||||||
|
double older = (chartHistory[hn-4] + chartHistory[hn-5] + chartHistory[hn-6]) / 3.0;
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
float arrowX = centerX + valSz.x * 0.5f + Layout::spacingSm();
|
||||||
|
if (recent > older * 1.02) {
|
||||||
|
const char* icon = ICON_MD_TRENDING_UP;
|
||||||
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f),
|
||||||
|
WithAlpha(Success(), 220), icon);
|
||||||
|
} else if (recent < older * 0.98) {
|
||||||
|
const char* icon = ICON_MD_TRENDING_DOWN;
|
||||||
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f),
|
||||||
|
WithAlpha(Error(), 220), icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparkline chart below stats
|
||||||
|
if (hasChartContent) {
|
||||||
|
float chartTop = cardMin.y + statRowH;
|
||||||
|
float chartBot = cardMax.y;
|
||||||
|
|
||||||
|
// Compute Y range
|
||||||
|
double yMin = *std::min_element(chartHistory.begin(), chartHistory.end());
|
||||||
|
double yMax = *std::max_element(chartHistory.begin(), chartHistory.end());
|
||||||
|
if (yMax <= yMin) { yMax = yMin + 1.0; }
|
||||||
|
double yRange = yMax - yMin;
|
||||||
|
double yPad2 = yRange * 0.1;
|
||||||
|
yMin -= yPad2;
|
||||||
|
yMax += yPad2;
|
||||||
|
|
||||||
|
float plotLeft = cardMin.x + pad;
|
||||||
|
float plotRight = cardMax.x - pad;
|
||||||
|
float plotTop = chartTop + capFont->LegacySize + 4 * dp;
|
||||||
|
float plotBottom = chartBot - capFont->LegacySize * 2 - 16 * dp;
|
||||||
|
float plotW = plotRight - plotLeft;
|
||||||
|
float plotH = std::max(1.0f, plotBottom - plotTop);
|
||||||
|
|
||||||
|
// Build raw data points — evenly spaced across the plot.
|
||||||
|
// No smooth-scroll animation: the chart updates in-place
|
||||||
|
// when new data arrives without any interim compression.
|
||||||
|
size_t n = chartHistory.size();
|
||||||
|
float stepW = (n > 1) ? plotW / (float)(n - 1) : plotW;
|
||||||
|
|
||||||
|
std::vector<ImVec2> rawPts(n);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
float x = plotLeft + (float)i * stepW;
|
||||||
|
float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH;
|
||||||
|
rawPts[i] = ImVec2(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catmull-Rom spline interpolation for smooth curve
|
||||||
|
// Subdivisions are adaptive: more when points are far apart,
|
||||||
|
// none when points are already sub-2px apart.
|
||||||
|
std::vector<ImVec2> points;
|
||||||
|
if (n <= 2) {
|
||||||
|
points = rawPts;
|
||||||
|
} else {
|
||||||
|
points.reserve(n * 4); // conservative estimate
|
||||||
|
for (size_t i = 0; i + 1 < n; i++) {
|
||||||
|
ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0];
|
||||||
|
ImVec2 p1 = rawPts[i];
|
||||||
|
ImVec2 p2 = rawPts[i + 1];
|
||||||
|
ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1];
|
||||||
|
|
||||||
|
// Adaptive subdivision: ~1 segment per 3px of distance
|
||||||
|
float dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
float dist = sqrtf(dx * dx + dy * dy);
|
||||||
|
int subdivs = std::clamp((int)(dist / 3.0f), 1, 16);
|
||||||
|
|
||||||
|
for (int s = 0; s < subdivs; s++) {
|
||||||
|
float t = (float)s / (float)subdivs;
|
||||||
|
float t2 = t * t;
|
||||||
|
float t3 = t2 * t;
|
||||||
|
float q0 = -t3 + 2.0f * t2 - t;
|
||||||
|
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f;
|
||||||
|
float q2 = -3.0f * t3 + 4.0f * t2 + t;
|
||||||
|
float q3 = t3 - t2;
|
||||||
|
float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3);
|
||||||
|
float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3);
|
||||||
|
// Clamp Y to plot bounds to prevent Catmull-Rom overshoot
|
||||||
|
sy = std::clamp(sy, plotTop, plotBottom);
|
||||||
|
points.push_back(ImVec2(sx, sy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points.push_back(rawPts[n - 1]); // final point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill under curve (single concave polygon to avoid AA seam shimmer)
|
||||||
|
if (points.size() >= 2) {
|
||||||
|
for (size_t i = 0; i < points.size(); i++)
|
||||||
|
dl->PathLineTo(points[i]);
|
||||||
|
dl->PathLineTo(ImVec2(points.back().x, plotBottom));
|
||||||
|
dl->PathLineTo(ImVec2(points.front().x, plotBottom));
|
||||||
|
dl->PathFillConcave(WithAlpha(Success(), 25));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Green line
|
||||||
|
dl->AddPolyline(points.data(), (int)points.size(),
|
||||||
|
WithAlpha(Success(), 200), ImDrawFlags_None, schema::UI().drawElement("tabs.mining", "chart-line-thickness").size);
|
||||||
|
|
||||||
|
// Y-axis labels
|
||||||
|
std::string yMaxStr = FormatHashrate(yMax);
|
||||||
|
std::string yMinStr = FormatHashrate(yMin);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotTop - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), yMaxStr.c_str());
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotBottom + 4 * dp), OnSurfaceDisabled(), yMinStr.c_str());
|
||||||
|
|
||||||
|
// X-axis labels
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp),
|
||||||
|
OnSurfaceDisabled(),
|
||||||
|
chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") :
|
||||||
|
chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start"));
|
||||||
|
std::string nowLbl = TR("mining_chart_now");
|
||||||
|
ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str());
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp),
|
||||||
|
OnSurfaceDisabled(), nowLbl.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance cursor past the card (stats/chart view only)
|
||||||
|
ImGui::Dummy(ImVec2(availWidth, totalCardH));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toggle button in top-right corner ---
|
||||||
|
// Rendered after content so the Hand cursor takes priority over
|
||||||
|
// the InputTextMultiline text-cursor when hovering the button.
|
||||||
|
if (hasLogContent || hasChartContent) {
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
||||||
|
const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log");
|
||||||
|
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
|
||||||
|
float btnSize = iconSz.y + 8 * dp;
|
||||||
|
float btnX = cardMax.x - pad - btnSize;
|
||||||
|
float btnY = cardMin.y + pad * 0.5f;
|
||||||
|
ImVec2 btnMin(btnX, btnY);
|
||||||
|
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||||
|
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
||||||
|
|
||||||
|
bool hov = IsRectHovered(btnMin, btnMax);
|
||||||
|
if (hov) {
|
||||||
|
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", toggleTip);
|
||||||
|
}
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
|
||||||
|
OnSurfaceMedium(), toggleIcon);
|
||||||
|
if (hov && ImGui::IsMouseClicked(0)) {
|
||||||
|
showLogFlag = !showLogFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "../material/draw_helpers.h" // GlassPanelSpec
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Renders the mining "Hashrate + Stats" card (stat values on top, hashrate chart or live log
|
||||||
|
// below). Extracted verbatim from the monolithic mining tab; the immediate-mode layout context is
|
||||||
|
// passed in so the rendering flows in sequence exactly as before. `poolMode` is the parent's
|
||||||
|
// solo/pool toggle.
|
||||||
|
void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float vs, float gap, float pad, float availWidth,
|
||||||
|
const material::GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
+41
-2274
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,358 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "network_tab.h"
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/lite_server_probe.h"
|
||||||
|
#include "../../wallet/lite_connection_service.h"
|
||||||
|
#include "../theme.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../effects/theme_effects.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
using LiteMode = config::Settings::LiteServerSelectionPreferenceMode;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
util::LiteServerProbe s_probe;
|
||||||
|
bool s_probeStarted = false;
|
||||||
|
bool s_showHidden = false;
|
||||||
|
char s_addUrl[256] = "";
|
||||||
|
char s_addLabel[128] = "";
|
||||||
|
std::string s_addError;
|
||||||
|
|
||||||
|
// (Re)probe every server in the list (visible + hidden), so latency is ready when unhidden too.
|
||||||
|
void startProbe(config::Settings* st)
|
||||||
|
{
|
||||||
|
std::vector<std::string> urls;
|
||||||
|
for (const auto& s : st->getLiteServers()) urls.push_back(s.url);
|
||||||
|
s_probe.start(std::move(urls));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImU32 latencyColor(int ms)
|
||||||
|
{
|
||||||
|
if (ms < 100) return Success();
|
||||||
|
if (ms < 500) return Warning();
|
||||||
|
return Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RenderLiteNetworkTab(App* app)
|
||||||
|
{
|
||||||
|
if (!app || !app->settings()) return;
|
||||||
|
config::Settings* st = app->settings();
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
|
||||||
|
if (!s_probeStarted) { startProbe(st); s_probeStarted = true; }
|
||||||
|
const auto probe = s_probe.results();
|
||||||
|
|
||||||
|
const bool randomMode = st->getLiteServerSelectionMode() == LiteMode::Random;
|
||||||
|
const std::string sticky = st->getLiteStickyServerUrl();
|
||||||
|
|
||||||
|
auto applyAndRebuild = [&]() { st->save(); app->rebuildLiteWallet(/*force=*/true); };
|
||||||
|
auto pinServer = [&](const std::string& url) {
|
||||||
|
st->setLiteServerSelectionMode(LiteMode::Sticky);
|
||||||
|
st->setLiteStickyServerUrl(url);
|
||||||
|
applyAndRebuild();
|
||||||
|
};
|
||||||
|
|
||||||
|
ImFont* sub2 = Type().subtitle2();
|
||||||
|
ImFont* body2 = Type().body2();
|
||||||
|
ImFont* cap = Type().caption();
|
||||||
|
|
||||||
|
// ── Header ───────────────────────────────────────────────────────────────
|
||||||
|
Type().text(TypeStyle::H6, TR("lite_net_title"));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_net_intro"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Connection + sync status panel ─────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const WalletState& ws = app->state();
|
||||||
|
const SyncInfo& sync = ws.sync;
|
||||||
|
const bool connected = ws.connected;
|
||||||
|
ImDrawList* sdl = ImGui::GetWindowDrawList();
|
||||||
|
const float panelH = 56.0f * dp;
|
||||||
|
const float panelW = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float pad2 = 12.0f * dp;
|
||||||
|
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 pMax(pMin.x + panelW, pMin.y + panelH);
|
||||||
|
GlassPanelSpec sspec; sspec.rounding = 8.0f * dp;
|
||||||
|
DrawGlassPanel(sdl, pMin, pMax, sspec);
|
||||||
|
|
||||||
|
// Top row: status dot + connection label (left); in-use server + latency (right).
|
||||||
|
ImU32 dotCol; const char* connLabel;
|
||||||
|
if (!connected) { dotCol = OnSurfaceDisabled(); connLabel = TR("lite_net_disconnected"); }
|
||||||
|
else if (sync.syncing) { dotCol = Warning(); connLabel = TR("lite_net_syncing"); }
|
||||||
|
else { dotCol = Success(); connLabel = TR("lite_net_synced"); }
|
||||||
|
const float dotR = 5.0f * dp;
|
||||||
|
sdl->AddCircleFilled(ImVec2(pMin.x + pad2 + dotR, pMin.y + 17.0f * dp), dotR, dotCol);
|
||||||
|
sdl->AddText(sub2, sub2->LegacySize, ImVec2(pMin.x + pad2 + dotR * 2 + 8.0f * dp, pMin.y + 9.0f * dp),
|
||||||
|
OnSurface(), connLabel);
|
||||||
|
|
||||||
|
std::string inUse = randomMode ? std::string(TR("lite_net_random_server"))
|
||||||
|
: (sticky.empty() ? std::string("—") : util::liteServerHost(sticky));
|
||||||
|
if (!randomMode && !sticky.empty()) {
|
||||||
|
const auto it = probe.find(sticky);
|
||||||
|
if (it != probe.end() && it->second.online)
|
||||||
|
inUse += " · " + std::to_string(it->second.latencyMs) + "ms";
|
||||||
|
}
|
||||||
|
const ImVec2 inUseSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse.c_str());
|
||||||
|
sdl->AddText(cap, cap->LegacySize, ImVec2(pMax.x - pad2 - inUseSz.x, pMin.y + 12.0f * dp),
|
||||||
|
OnSurfaceMedium(), inUse.c_str());
|
||||||
|
|
||||||
|
// Bottom row: sync detail + (while syncing) a thin progress bar.
|
||||||
|
char syncBuf[160];
|
||||||
|
if (!connected) {
|
||||||
|
// If an auto-open actually failed, show why (e.g. server unreachable) instead of a
|
||||||
|
// bare "no wallet open" — otherwise the disconnected state is silent/confusing.
|
||||||
|
const std::string& openErr = app->liteOpenError();
|
||||||
|
snprintf(syncBuf, sizeof(syncBuf), "%s: %s", TR("lite_net_sync_label"),
|
||||||
|
openErr.empty() ? TR("lite_net_no_wallet") : openErr.c_str());
|
||||||
|
} else if (sync.syncing)
|
||||||
|
snprintf(syncBuf, sizeof(syncBuf), "%s: %.1f%% · %d / %d", TR("lite_net_sync_label"),
|
||||||
|
sync.verification_progress * 100.0, sync.blocks, sync.headers);
|
||||||
|
else
|
||||||
|
snprintf(syncBuf, sizeof(syncBuf), "%s: %s · block %d", TR("lite_net_sync_label"),
|
||||||
|
TR("lite_net_synced"), sync.blocks);
|
||||||
|
sdl->AddText(cap, cap->LegacySize, ImVec2(pMin.x + pad2, pMin.y + 32.0f * dp),
|
||||||
|
OnSurfaceMedium(), syncBuf);
|
||||||
|
if (connected && sync.syncing) {
|
||||||
|
const float barY = pMin.y + panelH - 6.0f * dp;
|
||||||
|
const float barX = pMin.x + pad2;
|
||||||
|
const float barW = panelW - pad2 * 2;
|
||||||
|
const float barH = 2.5f * dp;
|
||||||
|
const float pct = static_cast<float>(std::max(0.0, std::min(1.0, sync.verification_progress)));
|
||||||
|
sdl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||||
|
WithAlpha(OnSurface(), 25), barH * 0.5f);
|
||||||
|
if (pct > 0)
|
||||||
|
sdl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH),
|
||||||
|
Primary(), barH * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(panelW, panelH)); // reserve the panel (proper boundary growth)
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useRandom = randomMode;
|
||||||
|
if (ImGui::Checkbox(TR("lite_net_use_random"), &useRandom)) {
|
||||||
|
st->setLiteServerSelectionMode(useRandom ? LiteMode::Random : LiteMode::Sticky);
|
||||||
|
applyAndRebuild();
|
||||||
|
}
|
||||||
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 110.0f * dp);
|
||||||
|
if (TactileButton(TR("lite_net_refresh"), ImVec2(100.0f * dp, 0))) startProbe(st);
|
||||||
|
if (randomMode)
|
||||||
|
Type().textColored(TypeStyle::Caption, Primary(), TR("lite_net_random_active"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Add custom server ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
const float availW = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float addBtnW = 80.0f * dp;
|
||||||
|
const float urlW = (availW - addBtnW - 16.0f * dp) * 0.6f;
|
||||||
|
const float lblW = (availW - addBtnW - 16.0f * dp) * 0.4f;
|
||||||
|
ImGui::SetNextItemWidth(urlW);
|
||||||
|
ImGui::InputTextWithHint("##LiteAddUrl", TR("lite_net_add_url_hint"), s_addUrl, sizeof(s_addUrl));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(lblW);
|
||||||
|
ImGui::InputTextWithHint("##LiteAddLabel", TR("lite_net_add_label_hint"), s_addLabel, sizeof(s_addLabel));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileButton(TR("lite_net_add"), ImVec2(addBtnW, 0))) {
|
||||||
|
std::string url = s_addUrl;
|
||||||
|
if (!wallet::isLiteServerUrlUsable(url)) {
|
||||||
|
s_addError = TR("lite_net_invalid_url");
|
||||||
|
} else {
|
||||||
|
auto servers = st->getLiteServers();
|
||||||
|
bool exists = false;
|
||||||
|
for (const auto& s : servers) if (s.url == url) { exists = true; break; }
|
||||||
|
if (!exists) {
|
||||||
|
config::Settings::LiteServerPreference p;
|
||||||
|
p.url = url;
|
||||||
|
p.label = s_addLabel[0] ? std::string(s_addLabel) : url;
|
||||||
|
p.enabled = true;
|
||||||
|
servers.push_back(p);
|
||||||
|
st->setLiteServers(servers);
|
||||||
|
st->save();
|
||||||
|
startProbe(st);
|
||||||
|
}
|
||||||
|
s_addUrl[0] = '\0'; s_addLabel[0] = '\0'; s_addError.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!s_addError.empty())
|
||||||
|
Type().textColored(TypeStyle::Caption, Error(), s_addError.c_str());
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Card renderer ──────────────────────────────────────────────────────────
|
||||||
|
const float cardH = 58.0f * dp;
|
||||||
|
const float pad = 12.0f * dp;
|
||||||
|
const float gap = 8.0f * dp;
|
||||||
|
const float hideW = 44.0f * dp;
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
|
||||||
|
auto drawCard = [&](const config::Settings::LiteServerPreference& sv, bool hiddenList) {
|
||||||
|
const std::string host = util::liteServerHost(sv.url);
|
||||||
|
const bool official = wallet::isOfficialLiteServer(sv.url);
|
||||||
|
const bool selected = !randomMode && sv.url == sticky;
|
||||||
|
|
||||||
|
util::LiteServerProbeResult pr;
|
||||||
|
auto it = probe.find(sv.url);
|
||||||
|
if (it != probe.end()) pr = it->second;
|
||||||
|
|
||||||
|
const float cardW = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||||
|
const float rnd = 8.0f * dp;
|
||||||
|
const float cardBtnW = cardW - hideW;
|
||||||
|
|
||||||
|
// Background + selection/official emphasis.
|
||||||
|
GlassPanelSpec spec;
|
||||||
|
spec.rounding = rnd;
|
||||||
|
spec.fillAlpha = selected ? 34 : 18;
|
||||||
|
DrawGlassPanel(dl, cardMin, cardMax, spec);
|
||||||
|
if (official) {
|
||||||
|
auto& fx = effects::ThemeEffects::instance();
|
||||||
|
if (fx.isEnabled()) {
|
||||||
|
fx.drawGlowPulse(dl, cardMin, cardMax, rnd);
|
||||||
|
}
|
||||||
|
// Always-visible static outline so officials are distinguishable even without effects.
|
||||||
|
float pulse = 0.75f + 0.25f * (float)std::sin(ImGui::GetTime() * 2.0);
|
||||||
|
dl->AddRect(cardMin, cardMax, WithAlpha(Primary(), (int)(150 * pulse)), rnd, 0, 1.6f * dp);
|
||||||
|
}
|
||||||
|
if (selected)
|
||||||
|
dl->AddRectFilled(cardMin, ImVec2(cardMin.x + 3.0f * dp, cardMax.y), Primary(), rnd);
|
||||||
|
|
||||||
|
// Main click area selects the server (left of the hide strip).
|
||||||
|
ImGui::SetCursorScreenPos(cardMin);
|
||||||
|
ImGui::InvisibleButton(("##litecard_" + sv.url).c_str(), ImVec2(cardBtnW, cardH));
|
||||||
|
const bool cardHovered = ImGui::IsItemHovered();
|
||||||
|
const bool cardClicked = ImGui::IsItemClicked();
|
||||||
|
if (cardHovered && !hiddenList) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (!selected) dl->AddRectFilled(cardMin, cardMax, WithAlpha(OnSurface(), 8), rnd);
|
||||||
|
}
|
||||||
|
if (cardClicked && !hiddenList) pinServer(sv.url);
|
||||||
|
|
||||||
|
// Left: label + host + ip.
|
||||||
|
float tx = cardMin.x + pad + 4.0f * dp;
|
||||||
|
dl->AddText(sub2, sub2->LegacySize, ImVec2(tx, cardMin.y + 9.0f * dp),
|
||||||
|
OnSurface(), sv.label.c_str());
|
||||||
|
std::string sub = host;
|
||||||
|
if (!pr.ip.empty() && pr.ip != host) sub += " · " + pr.ip;
|
||||||
|
dl->AddText(cap, cap->LegacySize, ImVec2(tx, cardMin.y + 9.0f * dp + sub2->LegacySize + 3.0f * dp),
|
||||||
|
OnSurfaceMedium(), sub.c_str());
|
||||||
|
|
||||||
|
// Right (within the card-button area): latency dot + text, official/custom pill.
|
||||||
|
float rx = cardMin.x + cardBtnW - pad;
|
||||||
|
// latency text
|
||||||
|
char lat[24];
|
||||||
|
ImU32 latCol;
|
||||||
|
if (!pr.probed) { snprintf(lat, sizeof(lat), "%s", TR("lite_net_checking")); latCol = OnSurfaceDisabled(); }
|
||||||
|
else if (!pr.online) { snprintf(lat, sizeof(lat), "%s", TR("lite_net_offline")); latCol = Error(); }
|
||||||
|
else { snprintf(lat, sizeof(lat), "%dms", pr.latencyMs); latCol = latencyColor(pr.latencyMs); }
|
||||||
|
ImVec2 latSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, lat);
|
||||||
|
float latX = rx - latSz.x;
|
||||||
|
dl->AddText(cap, cap->LegacySize, ImVec2(latX, cardMin.y + cardH * 0.5f + 2.0f * dp), latCol, lat);
|
||||||
|
// status dot left of latency text
|
||||||
|
if (pr.probed) {
|
||||||
|
float dotR = 3.5f * dp;
|
||||||
|
dl->AddCircleFilled(ImVec2(latX - dotR - 4.0f * dp, cardMin.y + cardH * 0.5f + 2.0f * dp + cap->LegacySize * 0.5f),
|
||||||
|
dotR, pr.online ? latencyColor(pr.latencyMs) : Error());
|
||||||
|
}
|
||||||
|
// official/custom + in-use pills (top-right)
|
||||||
|
const char* tag = official ? TR("lite_net_official") : TR("lite_net_custom");
|
||||||
|
ImU32 tagBg = official ? WithAlpha(Primary(), 40) : WithAlpha(Secondary(), 30);
|
||||||
|
ImU32 tagFg = official ? WithAlpha(Primary(), 230) : WithAlpha(Secondary(), 220);
|
||||||
|
ImVec2 tagSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, tag);
|
||||||
|
float tagX = rx - tagSz.x;
|
||||||
|
float tagY = cardMin.y + 9.0f * dp;
|
||||||
|
dl->AddRectFilled(ImVec2(tagX - 6 * dp, tagY - 1 * dp), ImVec2(rx + 6 * dp, tagY + tagSz.y + 1 * dp),
|
||||||
|
tagBg, 4.0f * dp);
|
||||||
|
dl->AddText(cap, cap->LegacySize, ImVec2(tagX, tagY), tagFg, tag);
|
||||||
|
if (selected) {
|
||||||
|
const char* inUse = TR("lite_net_in_use");
|
||||||
|
ImVec2 uSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse);
|
||||||
|
dl->AddText(cap, cap->LegacySize, ImVec2(tagX - uSz.x - 10 * dp, tagY), Success(), inUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide / unhide button (far-right strip).
|
||||||
|
ImVec2 hideMin(cardMin.x + cardBtnW, cardMin.y);
|
||||||
|
ImGui::SetCursorScreenPos(hideMin);
|
||||||
|
ImGui::InvisibleButton(("##litehide_" + sv.url).c_str(), ImVec2(hideW, cardH));
|
||||||
|
bool hideHov = ImGui::IsItemHovered();
|
||||||
|
if (hideHov) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"));
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
if (hiddenList) st->unhideLiteServer(sv.url);
|
||||||
|
else st->hideLiteServer(sv.url);
|
||||||
|
st->save();
|
||||||
|
}
|
||||||
|
ImFont* ico = Type().iconSmall();
|
||||||
|
const char* eye = hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
||||||
|
ImVec2 icoSz = ico->CalcTextSizeA(ico->LegacySize, FLT_MAX, 0, eye);
|
||||||
|
dl->AddText(ico, ico->LegacySize,
|
||||||
|
ImVec2(hideMin.x + (hideW - icoSz.x) * 0.5f, cardMin.y + (cardH - icoSz.y) * 0.5f),
|
||||||
|
hideHov ? OnSurface() : OnSurfaceDisabled(), eye);
|
||||||
|
|
||||||
|
// Advance to the next card via a real item (Dummy) below the card, so the scroll region's
|
||||||
|
// content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(cardW, gap));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Visible servers ────────────────────────────────────────────────────────
|
||||||
|
ImVec2 scrollAvail = ImGui::GetContentRegionAvail();
|
||||||
|
ImGui::BeginChild("##LiteServerScroll", scrollAvail, false,
|
||||||
|
ImGuiWindowFlags_NoBackground);
|
||||||
|
for (const auto& sv : st->getLiteServers()) {
|
||||||
|
if (st->isLiteServerHidden(sv.url)) continue;
|
||||||
|
drawCard(sv, /*hiddenList=*/false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hidden servers (toggle) ─────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
bool anyHidden = false;
|
||||||
|
for (const auto& sv : st->getLiteServers())
|
||||||
|
if (st->isLiteServerHidden(sv.url)) { anyHidden = true; break; }
|
||||||
|
if (anyHidden) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Checkbox(TR("lite_net_show_hidden"), &s_showHidden);
|
||||||
|
if (s_showHidden) {
|
||||||
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("lite_net_hidden_section"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
for (const auto& sv : st->getLiteServers()) {
|
||||||
|
if (!st->isLiteServerHidden(sv.url)) continue;
|
||||||
|
drawCard(sv, /*hiddenList=*/true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
(void)body2;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user