Compare commits
290 Commits
e66f2b6c8c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c252e60f7f | |||
| fffee9f0b5 | |||
| 00ffc959e5 | |||
| b561406c23 | |||
| 5a0743a17b | |||
| a7b0770ad0 | |||
| 7d8323a622 | |||
| 320944fd18 | |||
| 6d5e0ac614 | |||
| 02554d523d | |||
| 21da9e75fc | |||
| c53b7f771e | |||
| bcee4bfe72 | |||
| e1870c3b23 | |||
| 7cf0bb8bc7 | |||
| 541a9e1fd5 | |||
| 07b8e0b2cb | |||
| de48414c65 | |||
| f0c681b0b5 | |||
| fdf2502ca8 | |||
| e393b0d847 | |||
| 3b041f20c1 | |||
| 77a0ad64b0 | |||
| d0ca2fdf52 | |||
| 9c07b6d33d | |||
| fedfe3d60c | |||
| 95ff9ce9ae | |||
| 203967411a | |||
| 3d0305a9ca | |||
| e7f38c2a45 | |||
| 3a2be661ea | |||
| 17525003c5 | |||
| 267d839d5b | |||
| 987d9b93c7 | |||
| 4c43f2e082 | |||
| 57fa6470b7 | |||
| c68889d276 | |||
| b0a4333cdf | |||
| 4471f54842 | |||
| e08121af2b | |||
| 31ebfc782e | |||
| f3776bcbe5 | |||
| 38f2aa2f8f | |||
| 40f8425d94 | |||
| 2157a30192 | |||
| bef3c0c47d | |||
| 5c570613c8 | |||
| 567732206e | |||
| 0dcc09fc5f | |||
| 82d3178119 | |||
| 7d47c6e14e | |||
| 973bc0d338 | |||
| e4c9b98ca2 | |||
| 1c38f68781 | |||
| 818c29fca7 | |||
| 0a43742897 | |||
| defe14d913 | |||
| 8b55a90102 | |||
| 0fd3d214a4 | |||
| 996e10ed02 | |||
| 2c909d35ea | |||
| 3b5db02e09 | |||
| 8d8cd337cf | |||
| acde8c0833 | |||
| a78b44246e | |||
| 63ed31d2ec | |||
| 64b27db2ff | |||
| 03fe6e077c | |||
| 125ffa8863 | |||
| 3465acc57b | |||
| 30bd0d99ff | |||
| 29fbe46cce | |||
| fc414eeed4 | |||
| fce873936e | |||
| 7351d8a06d | |||
| 76093fe82d | |||
| ef247c95ff | |||
| c291e8a587 | |||
| 06e55d6394 | |||
| 2072f70a60 | |||
| ee9ea15233 | |||
| 11de117331 | |||
| fac0245297 | |||
| 9cb91415d9 | |||
| 24c661f743 | |||
| fecc9015fb | |||
| bce69362eb | |||
| 7f46d9e2d5 | |||
| 100ed01469 | |||
| 055685a4c4 | |||
| 46f93e380b | |||
| 6f6ad89c9f | |||
| d9911a9bc9 | |||
| faf77de9ce | |||
| 7f414b3dcf | |||
| 994ddea6cd | |||
| 41e4e73e63 | |||
| 88091bd77c | |||
| 02839dc415 | |||
| 17b70a1388 | |||
| 42d59709f9 | |||
| 30bc80b2f2 | |||
| 3808b3ee82 | |||
| bb5124c1ae | |||
| e6c78da062 | |||
| 5874142186 | |||
| 928335dd6b | |||
| 63dcda0ad1 | |||
| 122db9d903 | |||
| fc509a9340 | |||
| f546d3e2b1 | |||
| 6f0f95f4fb | |||
| a7c50ff19b | |||
| e7f2d2e3c7 | |||
| 137147921c | |||
| fd0cab41f0 | |||
| 4b8d80b2fc | |||
| 923d086092 | |||
| 6ac8f66644 | |||
| 62fc202557 | |||
| f8034b843a | |||
| 731d4e04ff | |||
| 9649654c3c | |||
| e1df5ea798 | |||
| a53b13b6b6 | |||
| bc3c6037fc | |||
| a338fac208 | |||
| 9bc2a0b62a | |||
| 1b3446e43c | |||
| 19dea53ef8 | |||
| 3d2b734541 | |||
| 2d4ba89c00 | |||
| 2249bc31d5 | |||
| 1ce37aa0fa | |||
| 33317c4e78 | |||
| 03dde25339 | |||
| 3fbb64d14c | |||
| 1b0006a4af | |||
| ba13875eaf | |||
| a5d44c8aca | |||
| 1a41dec8d8 | |||
| 88e10f3e06 | |||
| d29283a9b7 | |||
| ca65aa8bf7 | |||
| dae677acd8 | |||
| 4ef09d5546 | |||
| 21be39c725 | |||
| abbdf3f4f7 | |||
| 28288d7197 | |||
| 304f65c3b4 | |||
| 04a5078133 | |||
| b6b4fdacb5 | |||
| f6024557d5 | |||
| d82821ba06 | |||
| 8290e96ec4 | |||
| 46f7da4fac | |||
| 5bd07b0505 | |||
| 90df19b8b6 | |||
| 5985f05fb2 | |||
| 60ee73bf1d | |||
| 625df8abe6 | |||
| 5dd68dc9bd | |||
| 8a30578872 | |||
| 63d5c817aa | |||
| 3662df550c | |||
| 067c96c425 | |||
| a2c4a8df73 | |||
| 822891d4ef | |||
| 22638b094c | |||
| bf81ec1702 | |||
| 56cd9b6273 | |||
| 8b9cd5bf80 | |||
| 2b191cea34 | |||
| 6ecbf835ec | |||
| a13190a6df | |||
| ffc24ee99b | |||
| 3274c72a58 | |||
| b7b32bfde8 | |||
| 0b47134112 | |||
| 828018de2b | |||
| 2a96c41e07 | |||
| c25da0d26d | |||
| 0c55ed0fe4 | |||
| 1fc3e03e6f | |||
| f428292ea4 | |||
| 46f3001360 | |||
| 8293f02e36 | |||
| b34069f57c | |||
| 4416e01f9c | |||
| 44a17f3ad0 | |||
| c9e9c9f979 | |||
| ab4dd370c0 | |||
| 675d434958 | |||
| e8055888a5 | |||
| 48eceb6593 | |||
| 4f71c18884 | |||
| 232b81f8ee | |||
| c40c252c9a | |||
| d156dddcd0 | |||
| 0e1957c5f9 | |||
| 3f95765dcc | |||
| 62c92cc862 | |||
| 3aeb847657 | |||
| 2a9d32e78b | |||
| 97a54f0e0c | |||
| 82da4af857 | |||
| bfe8b4d77d | |||
| 78c00daf92 | |||
| e82514f46a | |||
| 76708e9191 | |||
| a767702ff6 | |||
| 40e8128a30 | |||
| 326192e75e | |||
| a107fda9b8 | |||
| d1ff58c374 | |||
| 156735eb06 | |||
| 2f86bec98e | |||
| 6310f51f65 | |||
| 09e0b962c3 | |||
| 758ea06ab7 | |||
| d189ee5ddb | |||
| 32d6e34c45 | |||
| 088e3371d9 | |||
| 49b5ef6b20 | |||
| 6aaab9e01f | |||
| b7da6335d1 | |||
| b765d62e00 | |||
| 3dc4d2d860 | |||
| fe40d38f0d | |||
| 1d3a919ac6 | |||
| d549127e41 | |||
| 54a14485a5 | |||
| 8eefab99ec | |||
| 4fdf281230 | |||
| c4f69b5a85 | |||
| 171b705429 | |||
| 175950a2ec | |||
| 36efe99a0f | |||
| fadbd6a879 | |||
| 112c581247 | |||
| 57147fd06b | |||
| aa60188665 | |||
| cff958c21e | |||
| c50125523c | |||
| 063f1fa4be | |||
| ad7a150ad6 | |||
| 433c7ae46f | |||
| a5efbaf83d | |||
| 46fb0029b8 | |||
| aa587d1d84 | |||
| 3a9edf2200 | |||
| c25a4e4150 | |||
| 54945c94bf | |||
| 96a1c54556 | |||
| bedb482855 | |||
| c352364bdc | |||
| 6b7438b529 | |||
| c6624913de | |||
| e6bea8eb04 | |||
| 5e5f167fb0 | |||
| 1fd794ccc1 | |||
| ed4d61c364 | |||
| 3df516be70 | |||
| f8fbc32463 | |||
| 06c8b99bce | |||
| 5eff3adc3c | |||
| e010822a74 | |||
| b2882095bb | |||
| a80feb708e | |||
| 010465f835 | |||
| eb08b81706 | |||
| 8cbd0cab9d | |||
| a9f0d544cc | |||
| 13ff20d791 | |||
| d8cc1e9ee6 | |||
| e7a3f90102 | |||
| b70f1ae643 | |||
| 76a2ef51f3 | |||
| 8846a96e3d | |||
| 5db7941fbc | |||
| a09adb14fb | |||
| d23ee9b75f | |||
| e8a8ce68b2 | |||
| 72bf59149d | |||
| 82e61da62f | |||
| 48c79567d7 | |||
| 6c19fac6f5 | |||
| 0d9908019d | |||
| a86cb8f0f3 | |||
| 5228da707f |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
|
||||
!prebuilt-binaries/dragonxd-win/.gitkeep
|
||||
prebuilt-binaries/dragonxd-mac/*
|
||||
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
||||
prebuilt-binaries/xmrig-hac/*
|
||||
!prebuilt-binaries/xmrig-hac/.gitkeep
|
||||
prebuilt-binaries/drg-xmrig/*
|
||||
!prebuilt-binaries/drg-xmrig/.gitkeep
|
||||
|
||||
|
||||
# External sources / toolchains (created by scripts/setup.sh)
|
||||
@@ -33,7 +33,7 @@ imgui.ini
|
||||
*.bak*
|
||||
*.params
|
||||
asmap.dat
|
||||
/external/xmrig-hac
|
||||
/external/drg-xmrig
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
@@ -54,3 +54,7 @@ third_party/silentdragonxlite/lib/vendor/
|
||||
|
||||
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
|
||||
res/ObsidianDragon.manifest
|
||||
|
||||
# Cross-built mingw FreeType (color emoji) — regenerated by scripts/build-freetype-mingw.sh
|
||||
third_party/freetype-mingw/
|
||||
third_party/.freetype-mingw-build/
|
||||
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -79,16 +79,24 @@ The detailed milestone plan and design history (the v2 plan, backend artifact/AB
|
||||
|
||||
## 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. A **"Browse all releases…"** button (the `/releases` list, newest first, pre-releases included) lets users pin an older or pre-release build — same verify/install path via `startInstallRelease()`; the picker UI is shared with the daemon updater (`ui/windows/release_list_view.h`).
|
||||
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. The dialog is a two-pane version picker (every `/releases` entry on the left, newest first, pre-releases included) so users can pin an older or pre-release build — same verify/install path via `startInstallRelease()`. It shares only the `ReleaseRow` row model with the daemon updater (`ui/windows/release_list_view.h`); each dialog renders its own tactile Material list.
|
||||
|
||||
**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).
|
||||
|
||||
## Daemon updater (dragonxd)
|
||||
|
||||
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). A **"Browse all releases…"** button (shared `release_list_view.h` picker) lets users pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data.
|
||||
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). The dialog is a two-pane version picker (every `/releases` entry on the left) so users can pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. It shares only the `ReleaseRow` row model (`ui/windows/release_list_view.h`) with the miner updater; each renders its own tactile Material list.
|
||||
|
||||
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
|
||||
|
||||
## Seed phrase & migrate-to-seed (full node)
|
||||
|
||||
Full-node wallets are **BIP39-mnemonic-backed** and can **migrate a legacy (non-mnemonic) wallet into a seed wallet**. All of this **requires the `hd-transparent-keys`/`dev` daemon** (`z_exportmnemonic` + `-usemnemonic`); the older bundled binary lacks those RPCs, so these features degrade gracefully (chat falls back to a `z_exportkey` identity; the backup screen shows a "legacy wallet" note). The daemon source is vendored at `external/dragonx/` (build with its own `./build.sh`); deploy the built `dragonxd`/`dragonx-cli`/`dragonx-tx` into the wallet's daemon dir (`build/*/bin`) or via the daemon updater.
|
||||
|
||||
- **New wallets get a phrase.** `EmbeddedDaemon::getChainParams()` always passes `-usemnemonic=1`. The daemon reads it **only inside `GenerateNewSeed()` when a wallet has no seed yet**, so it is inert on existing wallets (safe to pass unconditionally) and makes every fresh wallet mnemonic-backed.
|
||||
- **Back up seed phrase.** Settings → Backup & Data → "Seed phrase" opens `renderSeedBackupDialog` (`App::exportSeedPhrase` → `z_exportmnemonic`, wiped via `sodium_memzero`). A one-time nudge (`maybeRemindSeedBackup`, settings flag `seed_backup_reminded`) reminds mnemonic-wallet users to back up.
|
||||
- **Migrate-to-seed** (`showSeedMigrationDialog` / `renderSeedMigrationDialog`, state machine `SeedMigrationStep`). **Phase 1 — create:** `daemon::SeedWalletCreator` runs a **second, isolated `dragonxd`** on its own port + throwaway datadir (`<config>/seed-migrate/DRAGONX`, basename MUST be the acname; `-usemnemonic=1 -connect=0`; RPC is plaintext http, not TLS), mints a mnemonic wallet, exports its seed + a sweep-target z-address, then stops. Uses the one-shot `EmbeddedDaemon::setNextStartOverride` + `setSkipPortCheck`. **Phase 2 — sweep + adopt:** `z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]` sweeps all funds to the new address; a **Confirming gate** (`pollSweepStatus`) only allows adopt once the sweep tx is **mined (≥1 conf) AND the legacy balance is ~0** (offering a remainder re-sweep); adopt (`beginAdoptSeedWallet`, background) stops the daemon, moves `wallet.dat` aside to a **timestamped `.bak` (never deleted, restored on failure)**, installs the new wallet, and restarts with `-rescan`. Fund-moving code — **two rounds of adversarial review + a live mainnet run** gate it; the pending stage persists (`seed_migration_*` settings) so a restart resumes at sweep/confirm.
|
||||
|
||||
## 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`.
|
||||
@@ -98,5 +106,6 @@ The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` p
|
||||
- **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.
|
||||
- **DPI / display scaling:** `schema::UI()` returns **logical (raw) px**; `Layout::dpiScale()` (= OS DPI × the in-app font-scale) is the single scale factor. The `Layout::k*()` helpers and `BeginOverlayDialog` already fold it in, and ImGui auto-layout scales via the DPI-rebuilt font atlas + `ScaleAllSizes` — so tabs/standard widgets scale for free. But **hand-drawn absolute geometry** (`dl->AddText` at manual `cy += 24.0f` offsets, `SetNextWindowSize`, explicit `ImVec2(width,0)` button sizes, `SameLine(x)` column strides) is immune to all of that and must be multiplied by `ui::Layout::dpiScale()` yourself, or it renders native-size (tiny) on a HiDPI display. Font metrics (`font->LegacySize`, `CalcTextSize`) are already scaled — don't double-scale those. Verify at scale with a full sweep run at `font_scale: 1.5` (same `dpiScale()==1.5` code path as OS 150%).
|
||||
- **i18n:** user-facing strings are translated via `src/util/i18n`; the English source of truth is the `strings_[...]` map in `src/util/i18n.cpp`, and the per-language translations live as the **source of truth** in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). **Edit those JSONs directly and additively** — write with `json.dump(..., indent=4, sort_keys=True, ensure_ascii=False)`; never bulk-regenerate/overwrite a whole file (that path silently dropped ~285 keys/language before). `scripts/add_missing_translations.py` back-fills only the keys missing from a JSON (non-destructive), and `scripts/build_cjk_subset.py` rebuilds the CJK subset font (`res/fonts/NotoSansCJK-Subset.ttf`) after new CJK glyphs are added.
|
||||
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.
|
||||
|
||||
123
CMakeLists.txt
123
CMakeLists.txt
@@ -53,7 +53,6 @@ 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
|
||||
@@ -126,36 +125,24 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
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()
|
||||
# Note (F15-1): the former signature-metadata gate was removed. It trusted a
|
||||
# "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
|
||||
# self-attested with no cryptographic check (the "verified" SHA was just the artifact's
|
||||
# own SHA). The trust root is now build-from-source: that script builds the backend from
|
||||
# the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
|
||||
# is the one built from reviewed source. The required-symbol inventory check above stays.
|
||||
|
||||
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
||||
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
||||
)
|
||||
if(APPLE)
|
||||
# The Rust backend's TLS stack (security-framework / core-foundation crates)
|
||||
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
|
||||
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
|
||||
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
|
||||
endif()
|
||||
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}")
|
||||
@@ -295,6 +282,38 @@ else()
|
||||
set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
# libwebp - WebP decode (still + animated via WebPAnimDecoder). Built from source, static, decode-only
|
||||
# so Linux / mingw-Windows / macOS-osxcross all build it identically (the mingw/osx sysroots have no
|
||||
# webp). Encode tools are disabled to avoid pulling in libpng/zlib that the cross sysroots lack.
|
||||
message(STATUS "Fetching libwebp (decode-only, static)...")
|
||||
FetchContent_Declare(
|
||||
libwebp
|
||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||
GIT_TAG v1.4.0
|
||||
GIT_SHALLOW TRUE
|
||||
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
|
||||
# files when it can't probe SSE support. Under a macOS universal build
|
||||
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
|
||||
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
|
||||
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
|
||||
PATCH_COMMAND ${CMAKE_COMMAND}
|
||||
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
|
||||
)
|
||||
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_CWEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_DWEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_GIF2WEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_IMG2WEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_VWEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_WEBPINFO OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_LIBWEBPMUX OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_WEBPMUX OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_EXTRAS OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(libwebp)
|
||||
|
||||
# libsodium - platform-specific
|
||||
# Search order per platform:
|
||||
# 1. Local pre-built in libs/libsodium{-mac,-win}/ (downloaded by scripts/fetch-libsodium.sh)
|
||||
@@ -383,6 +402,35 @@ else()
|
||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/backends/imgui_impl_opengl3.h)
|
||||
endif()
|
||||
|
||||
# Optional FreeType font loader — enables color-emoji rendering (COLR/CPAL Twemoji) when the chat
|
||||
# "color emoji" setting is on; otherwise the wallet falls back to the monochrome emoji subset.
|
||||
# - Native Linux/macOS: use the system FreeType via find_package.
|
||||
# - Windows (mingw cross): the toolchain ships no FreeType, so build.sh --win-release cross-builds a
|
||||
# static one (scripts/build-freetype-mingw.sh) and passes -DDRAGONX_MINGW_FREETYPE_PREFIX here.
|
||||
# - Other cross builds (osxcross) without FreeType: silently fall back to monochrome.
|
||||
set(DRAGONX_FREETYPE OFF)
|
||||
set(DRAGONX_FREETYPE_LIB "")
|
||||
set(DRAGONX_FREETYPE_INC "")
|
||||
if(DEFINED DRAGONX_MINGW_FREETYPE_PREFIX AND EXISTS "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||
set(DRAGONX_FREETYPE ON)
|
||||
set(DRAGONX_FREETYPE_LIB "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||
set(DRAGONX_FREETYPE_INC "${DRAGONX_MINGW_FREETYPE_PREFIX}/include/freetype2")
|
||||
message(STATUS "FreeType (mingw cross-built) found — chat color emoji enabled")
|
||||
elseif(NOT CMAKE_CROSSCOMPILING)
|
||||
find_package(Freetype QUIET)
|
||||
if(FREETYPE_FOUND)
|
||||
set(DRAGONX_FREETYPE ON)
|
||||
set(DRAGONX_FREETYPE_LIB Freetype::Freetype) # imported target carries include dirs
|
||||
message(STATUS "FreeType ${FREETYPE_VERSION_STRING} found — chat color emoji enabled")
|
||||
endif()
|
||||
endif()
|
||||
if(DRAGONX_FREETYPE)
|
||||
list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/misc/freetype/imgui_freetype.cpp)
|
||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/misc/freetype/imgui_freetype.h)
|
||||
else()
|
||||
message(STATUS "FreeType not found — chat color emoji falls back to monochrome")
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# QR Code library (bundled)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -404,6 +452,7 @@ set(APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_network.cpp
|
||||
src/app_security.cpp
|
||||
src/app_sweep.cpp
|
||||
src/app_wizard.cpp
|
||||
src/services/network_refresh_service.cpp
|
||||
src/services/refresh_scheduler.cpp
|
||||
@@ -489,6 +538,7 @@ set(APP_SOURCES
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/data/exchange_info.cpp
|
||||
src/util/logger.cpp
|
||||
src/util/async_task_manager.cpp
|
||||
@@ -501,6 +551,7 @@ set(APP_SOURCES
|
||||
src/util/platform.cpp
|
||||
src/util/payment_uri.cpp
|
||||
src/util/texture_loader.cpp
|
||||
src/util/svg_texture.cpp
|
||||
src/util/noise_texture.cpp
|
||||
src/daemon/embedded_daemon.cpp
|
||||
src/daemon/seed_wallet_creator.cpp
|
||||
@@ -705,7 +756,9 @@ ${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoEmoji-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/TwemojiMozilla-Color.ttf"
|
||||
)
|
||||
|
||||
add_executable(ObsidianDragon
|
||||
@@ -733,6 +786,7 @@ target_include_directories(ObsidianDragon PRIVATE
|
||||
${GLAD_INCLUDE}
|
||||
${CURL_INCLUDE_DIRS}
|
||||
${MINIZ_DIR}
|
||||
${libwebp_SOURCE_DIR}/src # <webp/decode.h>, <webp/demux.h> (FetchContent build tree)
|
||||
)
|
||||
|
||||
target_link_libraries(ObsidianDragon PRIVATE
|
||||
@@ -742,6 +796,8 @@ target_link_libraries(ObsidianDragon PRIVATE
|
||||
sqlite3_amalgamation
|
||||
${CURL_LIBRARIES}
|
||||
${SODIUM_LIBRARY}
|
||||
webp
|
||||
webpdemux # WebPAnimDecoder (animated WebP); transitively pulls in webp + sharpyuv
|
||||
)
|
||||
|
||||
if(DRAGONX_LITE_BACKEND_READY)
|
||||
@@ -835,6 +891,15 @@ else()
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
||||
endif()
|
||||
|
||||
# Color-emoji font loader (FreeType) — linked + flagged only when found (see DRAGONX_FREETYPE above).
|
||||
if(DRAGONX_FREETYPE)
|
||||
target_link_libraries(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_LIB})
|
||||
if(DRAGONX_FREETYPE_INC)
|
||||
target_include_directories(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_INC})
|
||||
endif()
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAVE_FREETYPE)
|
||||
endif()
|
||||
|
||||
add_executable(HushChatFixtureCheck
|
||||
tools/hushchat_fixture_check.cpp
|
||||
src/chat/chat_protocol.cpp
|
||||
@@ -1061,6 +1126,8 @@ if(BUILD_TESTING)
|
||||
src/util/text_format.cpp
|
||||
src/data/wallet_state.cpp
|
||||
src/data/transaction_history_cache.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/daemon/lifecycle_adapters.cpp
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
@@ -1133,5 +1200,5 @@ 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 " Lite trust: built-from-source (vendored third_party/silentdragonxlite)")
|
||||
message(STATUS "")
|
||||
|
||||
@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
|
||||
- prebuilt-binaries/dragonxd-win/
|
||||
- prebuilt-binaries/dragonxd-mac/
|
||||
|
||||
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
||||
- prebuilt-binaries/xmrig-hac/
|
||||
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
|
||||
- prebuilt-binaries/drg-xmrig/
|
||||
|
||||
|
||||
## Build Steps
|
||||
|
||||
105
build.sh
105
build.sh
@@ -131,7 +131,7 @@ fi
|
||||
# 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_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[: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)
|
||||
@@ -386,7 +386,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
@@ -419,7 +419,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||
|
||||
# Desktop entry
|
||||
@@ -478,18 +478,28 @@ APPRUN
|
||||
done
|
||||
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
|
||||
|
||||
# appimagetool
|
||||
# appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
|
||||
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
|
||||
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
local APPIMAGETOOL=""
|
||||
if command -v appimagetool &>/dev/null; then
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
else
|
||||
info "Downloading appimagetool ..."
|
||||
wget -q -O "$bd/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
local at="$bd/appimagetool-x86_64.AppImage"
|
||||
# Re-verify any cached copy too; a stale unverified download must not be trusted.
|
||||
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
info "Downloading appimagetool 1.9.0 (pinned) ..."
|
||||
wget -q -O "$at" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
err "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$at"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$at"
|
||||
fi
|
||||
APPIMAGETOOL="$at"
|
||||
fi
|
||||
|
||||
local ARCH
|
||||
@@ -628,8 +638,8 @@ HDR
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
# 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
|
||||
@@ -725,12 +735,27 @@ HDR
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
|
||||
fi
|
||||
|
||||
# ── FreeType for Windows (color-emoji rendering) ───────────────────────
|
||||
# The mingw toolchain ships no FreeType; cross-build a minimal static one (COLR/CPAL, no external
|
||||
# deps). Failure is non-fatal — the wallet just falls back to monochrome emoji.
|
||||
local FT_MINGW_PREFIX="$SCRIPT_DIR/third_party/freetype-mingw"
|
||||
if [[ ! -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||
info "Cross-building FreeType for Windows (color emoji) ..."
|
||||
"$SCRIPT_DIR/scripts/build-freetype-mingw.sh" "$FT_MINGW_PREFIX" \
|
||||
|| warn "FreeType cross-build failed — Windows build will use monochrome emoji"
|
||||
fi
|
||||
local FT_CMAKE_ARG=()
|
||||
if [[ -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||
FT_CMAKE_ARG=(-DDRAGONX_MINGW_FREETYPE_PREFIX="$FT_MINGW_PREFIX")
|
||||
fi
|
||||
|
||||
# ── CMake + build ────────────────────────────────────────────────────────
|
||||
info "Configuring (cross-compile) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
"${FT_CMAKE_ARG[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
@@ -766,7 +791,7 @@ HDR
|
||||
fi
|
||||
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
|
||||
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
@@ -876,8 +901,26 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
MAC_ARCH="universal"
|
||||
# Native macOS: build universal (arm64 + x86_64) by default. Override with
|
||||
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
|
||||
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
|
||||
# When linking the real lite backend, the app can only include architectures
|
||||
# the backend static library actually provides. Its pinned ring 0.16.11 has no
|
||||
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
|
||||
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
|
||||
# otherwise the arm64 slice fails to link.
|
||||
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
|
||||
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
|
||||
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
|
||||
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
|
||||
MAC_ARCHS="$_backend_archs"
|
||||
fi
|
||||
fi
|
||||
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
|
||||
MAC_ARCH="universal"
|
||||
else
|
||||
MAC_ARCH="$MAC_ARCHS"
|
||||
fi
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
@@ -965,7 +1008,7 @@ TOOLCHAIN
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
@@ -976,13 +1019,13 @@ TOOLCHAIN
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native universal arm64+x86_64) ..."
|
||||
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -1012,8 +1055,12 @@ TOOLCHAIN
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# Clean only THIS variant's prior artifacts so full-node and lite releases can
|
||||
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
|
||||
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
|
||||
# and the ".app" names are exact.
|
||||
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
|
||||
|
||||
local APP="$out/${APP_BASENAME}.app"
|
||||
local CONTENTS="$APP/Contents"
|
||||
@@ -1063,8 +1110,8 @@ TOOLCHAIN
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
if [[ -f "$XMRIG_MAC" ]]; then
|
||||
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||
chmod +x "$MACOS/xmrig"
|
||||
@@ -1213,8 +1260,10 @@ PLIST
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
local DMG_BASENAME="DragonX_Wallet"
|
||||
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
|
||||
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
|
||||
# (APP_DISPLAY_NAME above).
|
||||
local DMG_BASENAME="${APP_BASENAME}"
|
||||
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||
|
||||
if command -v create-dmg &>/dev/null; then
|
||||
@@ -1296,3 +1345,9 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then
|
||||
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
|
||||
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
|
||||
fi
|
||||
|
||||
# Reaching here means the build completed (real failures exit 1 at their point of failure).
|
||||
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
|
||||
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
|
||||
# become the script's exit code and make a successful build report failure (e.g. to CI).
|
||||
exit 0
|
||||
|
||||
31
cmake/patch-libwebp-simd.cmake
Normal file
31
cmake/patch-libwebp-simd.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
|
||||
#
|
||||
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
|
||||
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
|
||||
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
|
||||
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
|
||||
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
|
||||
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
|
||||
# functions unconditionally — so any TU including <math.h> fails to compile with
|
||||
# "_Float16 is not supported on this target".
|
||||
#
|
||||
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
|
||||
# is unnecessary on every platform we target. Blanking the SSE entries (indices
|
||||
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
|
||||
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
|
||||
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
|
||||
endif()
|
||||
|
||||
file(READ "${CPU_CMAKE}" _contents)
|
||||
string(REPLACE
|
||||
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
|
||||
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
|
||||
_patched "${_contents}")
|
||||
|
||||
if(_patched STREQUAL _contents)
|
||||
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
|
||||
else()
|
||||
file(WRITE "${CPU_CMAKE}" "${_patched}")
|
||||
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
|
||||
endif()
|
||||
61
docs/release-notes/lite-v1.0.0.md
Normal file
61
docs/release-notes/lite-v1.0.0.md
Normal file
@@ -0,0 +1,61 @@
|
||||
**DragonX (DRGX) Lite Desktop Wallet — v1.0.0**
|
||||
|
||||
ObsidianDragonLite is a lightweight companion to the ObsidianDragon full-node wallet. It skips the embedded full node entirely — **no multi-gigabyte blockchain download and near-instant startup** — by connecting to a DragonX lite-wallet (lightwalletd-style) server, while keeping the same native ImGui interface and shielded-first workflow.
|
||||
|
||||
This is the **first release** of the Lite variant.
|
||||
|
||||
---
|
||||
|
||||
## Why Lite?
|
||||
|
||||
- **No blockchain to download.** Sync in seconds instead of hours; a few MB of state instead of many GB.
|
||||
- **Same wallet, lighter footprint.** The familiar ObsidianDragon UI, shielded and transparent addresses, and address book — without running a node or miner.
|
||||
- **Portable.** A single self-contained binary that stores its data in its own `ObsidianDragonLite` folder, so it coexists cleanly alongside the full-node wallet.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast, node-free operation** — connect to a DragonX lite-wallet server and sync in seconds; minimal disk and RAM.
|
||||
- **Wallet lifecycle** — create a new wallet, restore from a seed phrase, or open an existing one; wallets auto-open on startup, with a first-run welcome prompt and **guided seed backup** on creation.
|
||||
- **Shielded + transparent** — send, shield, and receive DRGX; per-address balances computed from unspent notes and UTXOs.
|
||||
- **Keys & seed** — export your seed phrase and keys, and import keys, from Settings.
|
||||
- **Passphrase encryption** — encrypt, unlock, lock, and decrypt the wallet; you're prompted to unlock at send time and on startup when the wallet is locked.
|
||||
- **Encrypted messaging (HushChat)** — built-in **Contacts** and **Chat** with a seed-derived, **SilentDragonXLite-compatible** identity and a seed-encrypted local message store, so you can message other DragonX users end-to-end.
|
||||
- **Server management** — a built-in **server browser with automatic failover**; switch servers with live reconnect/recovery, and a **"Redownload blocks"** action to rescan from the server.
|
||||
- **Status & diagnostics** — a **Network tab** showing connection and sync status, plus an **interactive Console** for running backend commands and copyable error output.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (DMG + .app), x86-64.
|
||||
- QR code generation for receiving addresses
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## How it differs from the full-node wallet
|
||||
|
||||
- **No embedded `dragonxd`, no mining, no local blockchain explorer.** Chain data comes from the lite-wallet server rather than being verified locally by a full node — a lighter footprint in exchange for trusting the server for chain state.
|
||||
- Everything key-related stays **on your device**: seed, keys, and (when enabled) the encryption passphrase never leave the machine.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragonLite-1.0.0.AppImage | `a1459d6081124a6b8df47aa898b60c0237875e8cbb132e4fd8ae637673055ed4` |
|
||||
| ObsidianDragonLite-1.0.0-Linux-x64.zip | `573af502a6372334572e4dc88bd26e0cf36ed543b9df1e835fd8e4abd295108d` |
|
||||
| ObsidianDragonLite-1.0.0.exe | `28659efb770fa573bd1b1d0fcab1c1a3f9c7e46574185c3916cc9f669684852c` |
|
||||
| ObsidianDragonLite-1.0.0-Windows-x64.zip | `f6434a931e873e5936ebe249b044d6c7a413694767c667374c91ee438259b50c` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.dmg | `10f976f1453e6b1978cb7a85026fd033c85a10c1e66f436475aa628379a298c7` |
|
||||
| ObsidianDragonLite-1.0.0-macOS-x86_64.app.zip | `3e7259c363f7be2e9027c5282c94b742f4be280c998203752dc0a0551f6ab68b` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** x86-64 (Intel; runs on Apple Silicon via Rosetta 2), macOS 10.15 Catalina or later
|
||||
- Network access to a DragonX lite-wallet server.
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
86
docs/release-notes/v2.0.0.md
Normal file
86
docs/release-notes/v2.0.0.md
Normal file
@@ -0,0 +1,86 @@
|
||||
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.0**
|
||||
|
||||
This is ObsidianDragon, a native ImGui-based wallet for the DragonX network. It ships with an embedded full-node daemon (`dragonxd`) and an integrated CPU miner (DRG-XMRig), giving users a complete, self-contained experience on Linux, Windows, and macOS.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v2.0.0
|
||||
|
||||
v2.0.0 is a major release. It adds **end-to-end encrypted HushChat messaging** and **BIP39 seed-phrase wallets** (with migrate-to-seed for legacy wallets), lets the wallet **update its own node and miner** with each install cryptographically verified, and ships a **security-hardening pass** across the updater, RPC, and secret-storage surfaces — plus full support for the **new multi-threaded `dragonxd`** and a large stability and UX overhaul.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Encrypted messaging (HushChat)** — new **Contacts** and **Chat** tabs bring DRGX-native, end-to-end encrypted messaging. Your chat identity is derived from your wallet seed, messages are encrypted with libsodium (XChaCha20-Poly1305 / secretstream) and stored in a **seed-encrypted local database**, and the wire format is interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **BIP39 seed-phrase wallets & migrate-to-seed** — new wallets are backed by a mnemonic **seed phrase** you can back up from Settings. Existing **legacy wallets can be migrated into a seed wallet** — the wallet mints a new mnemonic wallet, sweeps your funds to it, and adopts it only once the sweep is mined, keeping a timestamped `wallet.dat` backup throughout. (The bundled `dragonxd` supports the required mnemonic RPCs; older nodes degrade gracefully.)
|
||||
- **In-app daemon updater** — Settings → **Node & Security → Daemon binary → "Check for updates…"** downloads the latest `dragonxd` from the project Gitea, verifies its **SHA-256 and a detached ed25519 signature** before installing, and replaces the binary **atomically while the node keeps running** (the new build takes effect on the next daemon start). A two-pane version picker lets you pin, downgrade, or install any published/pre-release build.
|
||||
- **In-app miner updater** — an **"Update miner…"** action in the Mining tab fetches, verifies (SHA-256 + ed25519), and installs the latest DRG-XMRig, with the same version picker and a display of current vs. latest.
|
||||
- **Daemon binary management** — the wallet no longer auto-overwrites a node you've dropped in; a dedicated Settings panel shows the managed binary and gathers all node actions (restart, rescan, repair) onto one toolbar.
|
||||
- **Repair Wallet** — a one-click `-zapwallettxes=2` recovery in Settings, plus automatic wallet reconciliation after a bootstrap and runtime rescan support for pruned nodes.
|
||||
- **Explorer search** — fuzzy, live (debounced) filtering of the block list by partial hash or height.
|
||||
- **History sorting & fast load** — sort transactions by date or amount, a "Loading older history (N%)" indicator during the initial bulk load, and persisted history that surfaces pending sends immediately.
|
||||
- **Smarter mining** — the thread benchmark now measures **sustained (thermally-throttled) hashrate** instead of an inflated initial burst, with GPU-aware idle mining and corrected idle thread scaling.
|
||||
|
||||
### Security & Integrity
|
||||
|
||||
- **Mandatory signature verification** — both the daemon and miner updaters **require** a valid ed25519 signature (checked against a key pinned in the wallet) in addition to the SHA-256; an install is refused if the signature is missing or invalid.
|
||||
- **Hardened secret handling** — RPC credentials are wiped from memory after use, RPC responses are size-capped and scrubbed of secrets in logs, generated node config is written **owner-only (0600)**, and secrets copied to the clipboard **auto-clear**.
|
||||
- **Safe on-disk writes** — settings, address book, and secret files are written **atomically with owner-only permissions**; the PIN vault fsyncs its secure-delete overwrite; SQLite cache growth is bounded.
|
||||
- **Recipient validation** — sends validate recipient address checksums (Base58Check + Bech32) before building a transaction.
|
||||
- **Path-traversal protection** — archive extraction (chain bootstrap and the updaters) rejects any entry that would escape the target directory (zip-slip), and the bootstrap fails closed on a missing checksum rather than trusting an unverified archive.
|
||||
- **Robustness** — malformed RPC error JSON is guarded and sends are single-flight, preventing duplicate/ill-formed submissions.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **New multi-threaded daemon support** — live Sapling note-witness rebuild progress, accurate sync-speed display, reliable rescan-completion detection, and RPC polling throttled during sync so block download isn't slowed.
|
||||
- **Networking** — DragonX DNS seed nodes, `-maxconnections` passed to the daemon, and a peer count that stays current on every tab.
|
||||
- **Node resilience** — non-blocking warmup so you can connect while the daemon is still initializing, a live daemon-console tail on the startup overlay, fast-failing connect probes, and mid-session disconnect detection that keeps the UI responsive (in-flight RPC calls abort cleanly on disconnect/shutdown).
|
||||
- **Address list** — modernized with drag-to-transfer, labels, and view-only handling.
|
||||
- **UI / DPI** — overlay dialogs scale correctly with the font/DPI setting, improved CJK font rendering, native language names in the language picker, and format-incompatible translations are rejected.
|
||||
- **Settings** — an "Open data folder" button and confirmation modals for rescan and restart-daemon.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Extensive fixes across history (shielded-tx ordering, stuck "refreshing history" banners, unconfirmed-badge stickiness), sends (correct fee passed to `z_sendmany`, fast-lane worker restart on reconnect, note-selection fee-gap workaround), rescan (accurate completion detection, no false "complete", no per-second error flood), RPC (mid-session disconnect handling, stale-refresh invalidation), and UI layout/i18n. See the commit history for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Full-node wallet** — send, receive, and verify DRGX transactions with a bundled `dragonxd` daemon; no external setup required.
|
||||
- **Self-updating** — verify-and-install the latest node and miner from within the app (SHA-256 + ed25519 signature enforced).
|
||||
- **Encrypted messaging** — built-in HushChat with a seed-derived identity, end-to-end encryption, and a seed-encrypted local message store, interoperable with SilentDragonX / SilentDragonXLite.
|
||||
- **Seed-phrase wallets** — BIP39 mnemonic backup, and migrate-to-seed to upgrade a legacy wallet into a seed wallet.
|
||||
- **Built-in CPU mining** — start/stop DRG-XMRig from the Mining tab with real-time hashrate and pool statistics, mine-when-idle support with configurable delay, and sustained-hashrate benchmarking.
|
||||
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
|
||||
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (**universal** DMG + .app); Linux/Windows x86-64, macOS Intel + Apple Silicon.
|
||||
- Shielded (z-address) and transparent (t-address) send/receive
|
||||
- QR code generation for receiving addresses
|
||||
- Transaction history with memo support, date/amount sorting, and pending-send tracking
|
||||
- Blockchain explorer with live block/transaction search
|
||||
- Blockchain rescan and Sapling witness-rebuild with status-bar progress
|
||||
- Repair Wallet and daemon-binary management in Settings
|
||||
- Built-in console
|
||||
- Extensive theme and appearance options
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragon-2.0.0.AppImage | `34c9ea57ec27bf415e59d2890d995ed9069bfce04d979d7d8d58aa18f4570aa1` |
|
||||
| ObsidianDragon-2.0.0-Linux-x64.zip | `563004b4e45650dbfa9411d62fa1c59a3ca2199bd43a647b1614f2683bbdb5fa` |
|
||||
| ObsidianDragon-2.0.0.exe | `56100ae59dc3c67445d015cac5412ca40465bf459ba182c5ad2477a3b95ff548` |
|
||||
| ObsidianDragon-2.0.0-Windows-x64.zip | `ce9d067f36c5296288a473d3c2cceca7fcf807672a93a2084afc53c3b6e780d9` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.dmg | `8a01b6c0c2b5bebd64a222ba4a5ad04a1b8851138f28458b5c7f767fbb65db66` |
|
||||
| ObsidianDragon-2.0.0-macOS-universal.app.zip | `1bce1e5be15d9a2f3c6f42c95b23a0e89df142a73d65895252e4d8f50d19f541` |
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
|
||||
- **Windows:** x86-64, Windows 10 or later
|
||||
- **macOS:** Intel & Apple Silicon (universal binary), macOS 10.15 Catalina or later
|
||||
|
||||
## License
|
||||
|
||||
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.
|
||||
@@ -67,7 +67,8 @@
|
||||
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
//---- Enabled so chat can render emoji (U+1F300+, above the BMP) — see Typography::loadFont emoji merge (Q12).
|
||||
#define IMGUI_USE_WCHAR32
|
||||
|
||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||
|
||||
744
libs/imgui/misc/freetype/imgui_freetype.cpp
Normal file
744
libs/imgui/misc/freetype/imgui_freetype.cpp
Normal file
@@ -0,0 +1,744 @@
|
||||
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||
// (code)
|
||||
|
||||
// Get the latest version at https://github.com/ocornut/imgui/tree/master/misc/freetype
|
||||
// Original code by @vuhdo (Aleksei Skriabin) in 2017, with improvements by @mikesart.
|
||||
// Maintained since 2019 by @ocornut.
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2025/06/11: refactored for the new ImFontLoader architecture, and ImGuiBackendFlags_RendererHasTextures support.
|
||||
// 2024/10/17: added plutosvg support for SVG Fonts (seems faster/better than lunasvg). Enable by using '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG'. (#7927)
|
||||
// 2023/11/13: added support for ImFontConfig::RasterizationDensity field for scaling render density without scaling metrics.
|
||||
// 2023/08/01: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG'. (#6591)
|
||||
// 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly.
|
||||
// 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns nullptr.
|
||||
// 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs.
|
||||
// 2021/03/02: set 'atlas->TexPixelsUseColors = true' to help some backends with deciding of a preferred texture format.
|
||||
// 2021/01/28: added support for color-layered glyphs via ImGuiFreeTypeBuilderFlags_LoadColor (require Freetype 2.10+).
|
||||
// 2021/01/26: simplified integration by using '#define IMGUI_ENABLE_FREETYPE'. renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. removed ImGuiFreeType::BuildFontAtlas().
|
||||
// 2020/06/04: fix for rare case where FT_Get_Char_Index() succeed but FT_Load_Glyph() fails.
|
||||
// 2019/02/09: added RasterizerFlags::Monochrome flag to disable font anti-aliasing (combine with ::MonoHinting for best results!)
|
||||
// 2019/01/15: added support for imgui allocators + added FreeType only override function SetAllocatorFunctions().
|
||||
// 2019/01/10: re-factored to match big update in STB builder. fixed texture height waste. fixed redundant glyphs when merging. support for glyph padding.
|
||||
// 2018/06/08: added support for ImFontConfig::GlyphMinAdvanceX, GlyphMaxAdvanceX.
|
||||
// 2018/02/04: moved to main imgui repository (away from http://www.github.com/ocornut/imgui_club)
|
||||
// 2018/01/22: fix for addition of ImFontAtlas::TexUvscale member.
|
||||
// 2017/10/22: minor inconsequential change to match change in master (removed an unnecessary statement).
|
||||
// 2017/09/26: fixes for imgui internal changes.
|
||||
// 2017/08/26: cleanup, optimizations, support for ImFontConfig::RasterizerFlags, ImFontConfig::RasterizerMultiply.
|
||||
// 2017/08/16: imported from https://github.com/Vuhdo/imgui_freetype into http://www.github.com/ocornut/imgui_club, updated for latest changes in ImFontAtlas, minor tweaks.
|
||||
|
||||
// About Gamma Correct Blending:
|
||||
// - FreeType assumes blending in linear space rather than gamma space.
|
||||
// - See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Render_Glyph
|
||||
// - For correct results you need to be using sRGB and convert to linear space in the pixel shader output.
|
||||
// - The default dear imgui styles will be impacted by this change (alpha values will need tweaking).
|
||||
|
||||
// FIXME: cfg.OversampleH, OversampleV are not supported, but generally not necessary with this rasterizer because Hinting makes everything look better.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_freetype.h"
|
||||
#include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*,
|
||||
#include <stdint.h>
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H // <freetype/freetype.h>
|
||||
#include FT_MODULE_H // <freetype/ftmodapi.h>
|
||||
#include FT_GLYPH_H // <freetype/ftglyph.h>
|
||||
#include FT_SIZES_H // <freetype/ftsizes.h>
|
||||
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
|
||||
|
||||
// Handle LunaSVG and PlutoSVG
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
#error "Cannot enable both IMGUI_ENABLE_FREETYPE_LUNASVG and IMGUI_ENABLE_FREETYPE_PLUTOSVG"
|
||||
#endif
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
#include FT_OTSVG_H // <freetype/otsvg.h>
|
||||
#include FT_BBOX_H // <freetype/ftbbox.h>
|
||||
#include <lunasvg.h>
|
||||
#endif
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
#include <plutosvg.h>
|
||||
#endif
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
#if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||
#error IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning (push)
|
||||
#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff)
|
||||
#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
|
||||
#endif
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||
#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used
|
||||
#ifndef __clang__
|
||||
#pragma GCC diagnostic ignored "-Wsubobject-linkage" // warning: 'xxxx' has a field 'xxxx' whose type uses the anonymous namespace
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Data
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
// Default memory allocators
|
||||
static void* ImGuiFreeTypeDefaultAllocFunc(size_t size, void* user_data) { IM_UNUSED(user_data); return IM_ALLOC(size); }
|
||||
static void ImGuiFreeTypeDefaultFreeFunc(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_FREE(ptr); }
|
||||
|
||||
// Current memory allocators
|
||||
static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFreeTypeDefaultAllocFunc;
|
||||
static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc;
|
||||
static void* GImGuiFreeTypeAllocatorUserData = nullptr;
|
||||
|
||||
// Lunasvg support
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* state);
|
||||
static void ImGuiLunasvgPortFree(FT_Pointer* state);
|
||||
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state);
|
||||
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state);
|
||||
#endif
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Code
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
#define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point
|
||||
#define FT_SCALEFACTOR 64.0f
|
||||
|
||||
// Glyph metrics:
|
||||
// --------------
|
||||
//
|
||||
// xmin xmax
|
||||
// | |
|
||||
// |<-------- width -------->|
|
||||
// | |
|
||||
// | +-------------------------+----------------- ymax
|
||||
// | | ggggggggg ggggg | ^ ^
|
||||
// | | g:::::::::ggg::::g | | |
|
||||
// | | g:::::::::::::::::g | | |
|
||||
// | | g::::::ggggg::::::gg | | |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// offsetX -|-------->| g:::::g g:::::g | offsetY |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// | | g::::::g g:::::g | | |
|
||||
// | | g:::::::ggggg:::::g | | |
|
||||
// | | g::::::::::::::::g | | height
|
||||
// | | gg::::::::::::::g | | |
|
||||
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
|
||||
// / | | g:::::g | |
|
||||
// origin | | gggggg g:::::g | |
|
||||
// | | g:::::gg gg:::::g | |
|
||||
// | | g::::::ggg:::::::g | |
|
||||
// | | gg:::::::::::::g | |
|
||||
// | | ggg::::::ggg | |
|
||||
// | | gggggg | v
|
||||
// | +-------------------------+----------------- ymin
|
||||
// | |
|
||||
// |------------- advanceX ----------->|
|
||||
|
||||
// Stored in ImFontAtlas::FontLoaderData. ALLOCATED BY US.
|
||||
struct ImGui_ImplFreeType_Data
|
||||
{
|
||||
FT_Library Library;
|
||||
FT_MemoryRec_ MemoryManager;
|
||||
ImGui_ImplFreeType_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
// Stored in ImFontConfig::FontLoaderData. ALLOCATED BY US.
|
||||
struct ImGui_ImplFreeType_FontSrcData
|
||||
{
|
||||
// Initialize from an external data buffer. Doesn't copy data, and you must ensure it stays valid up to this object lifetime.
|
||||
bool InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_user_flags);
|
||||
void CloseFont();
|
||||
ImGui_ImplFreeType_FontSrcData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
~ImGui_ImplFreeType_FontSrcData() { CloseFont(); }
|
||||
|
||||
// Members
|
||||
FT_Face FtFace;
|
||||
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
|
||||
FT_Int32 LoadFlags;
|
||||
ImFontBaked* BakedLastActivated;
|
||||
};
|
||||
|
||||
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
|
||||
struct ImGui_ImplFreeType_FontSrcBakedData
|
||||
{
|
||||
FT_Size FtSize; // This represent a FT_Face with a given size.
|
||||
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
|
||||
{
|
||||
FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace);
|
||||
if (error != 0)
|
||||
return false;
|
||||
error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE);
|
||||
if (error != 0)
|
||||
return false;
|
||||
|
||||
// Convert to FreeType flags (NB: Bold and Oblique are processed separately)
|
||||
UserFlags = (ImGuiFreeTypeLoaderFlags)(src->FontLoaderFlags | extra_font_loader_flags);
|
||||
|
||||
LoadFlags = 0;
|
||||
if ((UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) == 0)
|
||||
LoadFlags |= FT_LOAD_NO_BITMAP;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoHinting)
|
||||
LoadFlags |= FT_LOAD_NO_HINTING;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoAutoHint)
|
||||
LoadFlags |= FT_LOAD_NO_AUTOHINT;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_ForceAutoHint)
|
||||
LoadFlags |= FT_LOAD_FORCE_AUTOHINT;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LightHinting)
|
||||
LoadFlags |= FT_LOAD_TARGET_LIGHT;
|
||||
else if (UserFlags & ImGuiFreeTypeLoaderFlags_MonoHinting)
|
||||
LoadFlags |= FT_LOAD_TARGET_MONO;
|
||||
else
|
||||
LoadFlags |= FT_LOAD_TARGET_NORMAL;
|
||||
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
|
||||
LoadFlags |= FT_LOAD_COLOR;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplFreeType_FontSrcData::CloseFont()
|
||||
{
|
||||
if (FtFace)
|
||||
{
|
||||
FT_Done_Face(FtFace);
|
||||
FtFace = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint)
|
||||
{
|
||||
uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint);
|
||||
if (glyph_index == 0)
|
||||
return nullptr;
|
||||
|
||||
// If this crash for you: FreeType 2.11.0 has a crash bug on some bitmap/colored fonts.
|
||||
// - https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076
|
||||
// - https://github.com/ocornut/imgui/issues/4567
|
||||
// - https://github.com/ocornut/imgui/issues/4566
|
||||
// You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version.
|
||||
FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags);
|
||||
if (error)
|
||||
return nullptr;
|
||||
|
||||
// Need an outline for this to work
|
||||
FT_GlyphSlot slot = src_data->FtFace->glyph;
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG);
|
||||
#else
|
||||
#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||
IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG in imconfig.h and install required libraries in order to use this font");
|
||||
#endif
|
||||
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP);
|
||||
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
// Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting)
|
||||
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold)
|
||||
FT_GlyphSlot_Embolden(slot);
|
||||
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique)
|
||||
{
|
||||
FT_GlyphSlot_Oblique(slot);
|
||||
//FT_BBox bbox;
|
||||
//FT_Outline_Get_BBox(&slot->outline, &bbox);
|
||||
//slot->metrics.width = bbox.xMax - bbox.xMin;
|
||||
//slot->metrics.height = bbox.yMax - bbox.yMin;
|
||||
}
|
||||
|
||||
return &slot->metrics;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_BlitGlyph(const FT_Bitmap* ft_bitmap, uint32_t* dst, uint32_t dst_pitch)
|
||||
{
|
||||
IM_ASSERT(ft_bitmap != nullptr);
|
||||
const uint32_t w = ft_bitmap->width;
|
||||
const uint32_t h = ft_bitmap->rows;
|
||||
const uint8_t* src = ft_bitmap->buffer;
|
||||
const uint32_t src_pitch = ft_bitmap->pitch;
|
||||
|
||||
switch (ft_bitmap->pixel_mode)
|
||||
{
|
||||
case FT_PIXEL_MODE_GRAY: // Grayscale image, 1 byte per pixel.
|
||||
{
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
dst[x] = IM_COL32(255, 255, 255, src[x]);
|
||||
break;
|
||||
}
|
||||
case FT_PIXEL_MODE_MONO: // Monochrome image, 1 bit per pixel. The bits in each byte are ordered from MSB to LSB.
|
||||
{
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
{
|
||||
uint8_t bits = 0;
|
||||
const uint8_t* bits_ptr = src;
|
||||
for (uint32_t x = 0; x < w; x++, bits <<= 1)
|
||||
{
|
||||
if ((x & 7) == 0)
|
||||
bits = *bits_ptr++;
|
||||
dst[x] = IM_COL32(255, 255, 255, (bits & 0x80) ? 255 : 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FT_PIXEL_MODE_BGRA:
|
||||
{
|
||||
// FIXME: Converting pre-multiplied alpha to straight. Doesn't smell good.
|
||||
#define DE_MULTIPLY(color, alpha) ImMin((ImU32)(255.0f * (float)color / (float)(alpha + FLT_MIN) + 0.5f), 255u)
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
{
|
||||
uint8_t r = src[x * 4 + 2], g = src[x * 4 + 1], b = src[x * 4], a = src[x * 4 + 3];
|
||||
dst[x] = IM_COL32(DE_MULTIPLY(r, a), DE_MULTIPLY(g, a), DE_MULTIPLY(b, a), a);
|
||||
}
|
||||
#undef DE_MULTIPLY
|
||||
break;
|
||||
}
|
||||
default:
|
||||
IM_ASSERT(0 && "FreeTypeFont::BlitGlyph(): Unknown bitmap pixel mode!");
|
||||
}
|
||||
}
|
||||
|
||||
// FreeType memory allocation callbacks
|
||||
static void* FreeType_Alloc(FT_Memory /*memory*/, long size)
|
||||
{
|
||||
return GImGuiFreeTypeAllocFunc((size_t)size, GImGuiFreeTypeAllocatorUserData);
|
||||
}
|
||||
|
||||
static void FreeType_Free(FT_Memory /*memory*/, void* block)
|
||||
{
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
}
|
||||
|
||||
static void* FreeType_Realloc(FT_Memory /*memory*/, long cur_size, long new_size, void* block)
|
||||
{
|
||||
// Implement realloc() as we don't ask user to provide it.
|
||||
if (block == nullptr)
|
||||
return GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||
|
||||
if (new_size == 0)
|
||||
{
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (new_size > cur_size)
|
||||
{
|
||||
void* new_block = GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||
memcpy(new_block, block, (size_t)cur_size);
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
return new_block;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas)
|
||||
{
|
||||
IM_ASSERT(atlas->FontLoaderData == nullptr);
|
||||
ImGui_ImplFreeType_Data* bd = IM_NEW(ImGui_ImplFreeType_Data)();
|
||||
|
||||
// FreeType memory management: https://www.freetype.org/freetype2/docs/design/design-4.html
|
||||
bd->MemoryManager.user = nullptr;
|
||||
bd->MemoryManager.alloc = &FreeType_Alloc;
|
||||
bd->MemoryManager.free = &FreeType_Free;
|
||||
bd->MemoryManager.realloc = &FreeType_Realloc;
|
||||
|
||||
// https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library
|
||||
FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library);
|
||||
if (error != 0)
|
||||
{
|
||||
IM_DELETE(bd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator.
|
||||
FT_Add_Default_Modules(bd->Library);
|
||||
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
// Install svg hooks for FreeType
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts
|
||||
SVG_RendererHooks hooks = { ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot };
|
||||
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", &hooks);
|
||||
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
// With plutosvg, use provided hooks
|
||||
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks());
|
||||
#endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
|
||||
// Store our data
|
||||
atlas->FontLoaderData = (void*)bd;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas)
|
||||
{
|
||||
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||
IM_ASSERT(bd != nullptr);
|
||||
FT_Done_Library(bd->Library);
|
||||
IM_DELETE(bd);
|
||||
atlas->FontLoaderData = nullptr;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
|
||||
{
|
||||
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData);
|
||||
IM_ASSERT(src->FontLoaderData == nullptr);
|
||||
src->FontLoaderData = bd_font_data;
|
||||
|
||||
if (!bd_font_data->InitFont(bd->Library, src, (ImGuiFreeTypeLoaderFlags)atlas->FontLoaderFlags))
|
||||
{
|
||||
IM_DELETE(bd_font_data);
|
||||
src->FontLoaderData = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
IM_DELETE(bd_font_data);
|
||||
src->FontLoaderData = nullptr;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
float size = baked->Size;
|
||||
if (src->MergeMode && src->SizePixels != 0.0f)
|
||||
size *= (src->SizePixels / baked->OwnerFont->Sources[0]->SizePixels);
|
||||
size *= src->ExtraSizeScale;
|
||||
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
|
||||
// We use one FT_Size per (source + baked) combination.
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
IM_ASSERT(bd_baked_data != nullptr);
|
||||
IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData();
|
||||
|
||||
FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize);
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
|
||||
// Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height'
|
||||
// is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me.
|
||||
// FT_Set_Pixel_Sizes() doesn't seem to get us the same result."
|
||||
// (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL)
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
FT_Size_RequestRec req;
|
||||
req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM;
|
||||
req.width = 0;
|
||||
req.height = (uint32_t)(size * 64 * rasterizer_density);
|
||||
req.horiResolution = 0;
|
||||
req.vertResolution = 0;
|
||||
FT_Request_Size(bd_font_data->FtFace, &req);
|
||||
|
||||
// Output
|
||||
if (src->MergeMode == false)
|
||||
{
|
||||
// Read metrics
|
||||
FT_Size_Metrics metrics = bd_baked_data->FtSize->metrics;
|
||||
const float scale = 1.0f / (rasterizer_density * src->ExtraSizeScale);
|
||||
baked->Ascent = (float)FT_CEIL(metrics.ascender) * scale; // The pixel extents above the baseline in pixels (typically positive).
|
||||
baked->Descent = (float)FT_CEIL(metrics.descender) * scale; // The extents below the baseline in pixels (typically negative).
|
||||
//LineSpacing = (float)FT_CEIL(metrics.height) * scale; // The baseline-to-baseline distance. Note that it usually is larger than the sum of the ascender and descender taken as absolute values. There is also no guarantee that no glyphs extend above or below subsequent baselines when using this distance. Think of it as a value the designer of the font finds appropriate.
|
||||
//LineGap = (float)FT_CEIL(metrics.height - metrics.ascender + metrics.descender) * scale; // The spacing in pixels between one row's descent and the next row's ascent.
|
||||
//MaxAdvanceWidth = (float)FT_CEIL(metrics.max_advance) * scale; // This field gives the maximum horizontal cursor advance for all glyphs in the font.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
IM_UNUSED(baked);
|
||||
IM_UNUSED(src);
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
IM_ASSERT(bd_baked_data != nullptr);
|
||||
FT_Done_Size(bd_baked_data->FtSize);
|
||||
bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE()
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
|
||||
{
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||
if (glyph_index == 0)
|
||||
return false;
|
||||
|
||||
if (bd_font_data->BakedLastActivated != baked) // <-- could use id
|
||||
{
|
||||
// Activate current size
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
}
|
||||
|
||||
const FT_Glyph_Metrics* metrics = ImGui_ImplFreeType_LoadGlyph(bd_font_data, codepoint);
|
||||
if (metrics == nullptr)
|
||||
return false;
|
||||
|
||||
FT_Face face = bd_font_data->FtFace;
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
|
||||
// Load metrics only mode
|
||||
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
|
||||
if (out_advance_x != NULL)
|
||||
{
|
||||
IM_ASSERT(out_glyph == NULL);
|
||||
*out_advance_x = advance_x;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Render glyph into a bitmap (currently held by FreeType)
|
||||
FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL;
|
||||
FT_Error error = FT_Render_Glyph(slot, render_mode);
|
||||
const FT_Bitmap* ft_bitmap = &slot->bitmap;
|
||||
if (error != 0 || ft_bitmap == nullptr)
|
||||
return false;
|
||||
|
||||
const int w = (int)ft_bitmap->width;
|
||||
const int h = (int)ft_bitmap->rows;
|
||||
const bool is_visible = (w != 0 && h != 0);
|
||||
|
||||
// Prepare glyph
|
||||
out_glyph->Codepoint = codepoint;
|
||||
out_glyph->AdvanceX = advance_x;
|
||||
|
||||
// Pack and retrieve position inside texture atlas
|
||||
if (is_visible)
|
||||
{
|
||||
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
|
||||
if (pack_id == ImFontAtlasRectId_Invalid)
|
||||
{
|
||||
// Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
|
||||
IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
|
||||
return false;
|
||||
}
|
||||
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
|
||||
|
||||
// Render pixels to our temporary buffer
|
||||
atlas->Builder->TempBuffer.resize(w * h * 4);
|
||||
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
|
||||
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
|
||||
|
||||
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
|
||||
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
|
||||
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); // Snap scaled offset.
|
||||
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
|
||||
float recip_h = 1.0f / rasterizer_density;
|
||||
float recip_v = 1.0f / rasterizer_density;
|
||||
|
||||
// Register glyph
|
||||
float glyph_off_x = (float)face->glyph->bitmap_left;
|
||||
float glyph_off_y = (float)-face->glyph->bitmap_top;
|
||||
out_glyph->X0 = glyph_off_x * recip_h + font_off_x;
|
||||
out_glyph->Y0 = glyph_off_y * recip_v + font_off_y;
|
||||
out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x;
|
||||
out_glyph->Y1 = (glyph_off_y + h) * recip_v + font_off_y;
|
||||
out_glyph->Visible = true;
|
||||
out_glyph->Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
|
||||
out_glyph->PackId = pack_id;
|
||||
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||
return glyph_index != 0;
|
||||
}
|
||||
|
||||
const ImFontLoader* ImGuiFreeType::GetFontLoader()
|
||||
{
|
||||
static ImFontLoader loader;
|
||||
loader.Name = "FreeType";
|
||||
loader.LoaderInit = ImGui_ImplFreeType_LoaderInit;
|
||||
loader.LoaderShutdown = ImGui_ImplFreeType_LoaderShutdown;
|
||||
loader.FontSrcInit = ImGui_ImplFreeType_FontSrcInit;
|
||||
loader.FontSrcDestroy = ImGui_ImplFreeType_FontSrcDestroy;
|
||||
loader.FontSrcContainsGlyph = ImGui_ImplFreetype_FontSrcContainsGlyph;
|
||||
loader.FontBakedInit = ImGui_ImplFreeType_FontBakedInit;
|
||||
loader.FontBakedDestroy = ImGui_ImplFreeType_FontBakedDestroy;
|
||||
loader.FontBakedLoadGlyph = ImGui_ImplFreeType_FontBakedLoadGlyph;
|
||||
loader.FontBakedSrcLoaderDataSize = sizeof(ImGui_ImplFreeType_FontSrcBakedData);
|
||||
return &loader;
|
||||
}
|
||||
|
||||
void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data)
|
||||
{
|
||||
GImGuiFreeTypeAllocFunc = alloc_func;
|
||||
GImGuiFreeTypeFreeFunc = free_func;
|
||||
GImGuiFreeTypeAllocatorUserData = user_data;
|
||||
}
|
||||
|
||||
bool ImGuiFreeType::DebugEditFontLoaderFlags(unsigned int* p_font_loader_flags)
|
||||
{
|
||||
bool edited = false;
|
||||
edited |= ImGui::CheckboxFlags("NoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoHinting);
|
||||
edited |= ImGui::CheckboxFlags("NoAutoHint", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoAutoHint);
|
||||
edited |= ImGui::CheckboxFlags("ForceAutoHint",p_font_loader_flags, ImGuiFreeTypeLoaderFlags_ForceAutoHint);
|
||||
edited |= ImGui::CheckboxFlags("LightHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LightHinting);
|
||||
edited |= ImGui::CheckboxFlags("MonoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_MonoHinting);
|
||||
edited |= ImGui::CheckboxFlags("Bold", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bold);
|
||||
edited |= ImGui::CheckboxFlags("Oblique", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Oblique);
|
||||
edited |= ImGui::CheckboxFlags("Monochrome", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Monochrome);
|
||||
edited |= ImGui::CheckboxFlags("LoadColor", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LoadColor);
|
||||
edited |= ImGui::CheckboxFlags("Bitmap", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bitmap);
|
||||
return edited;
|
||||
}
|
||||
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c
|
||||
// The original code from the demo is licensed under CeCILL-C Free Software License Agreement (https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT)
|
||||
struct LunasvgPortState
|
||||
{
|
||||
FT_Error err = FT_Err_Ok;
|
||||
lunasvg::Matrix matrix;
|
||||
std::unique_ptr<lunasvg::Document> svg = nullptr;
|
||||
};
|
||||
|
||||
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state)
|
||||
{
|
||||
*_state = IM_NEW(LunasvgPortState)();
|
||||
return FT_Err_Ok;
|
||||
}
|
||||
|
||||
static void ImGuiLunasvgPortFree(FT_Pointer* _state)
|
||||
{
|
||||
IM_DELETE(*(LunasvgPortState**)_state);
|
||||
}
|
||||
|
||||
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state)
|
||||
{
|
||||
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||
|
||||
// If there was an error while loading the svg in ImGuiLunasvgPortPresetSlot(), the renderer hook still get called, so just returns the error.
|
||||
if (state->err != FT_Err_Ok)
|
||||
return state->err;
|
||||
|
||||
// rows is height, pitch (or stride) equals to width * sizeof(int32)
|
||||
lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch);
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||
#else
|
||||
state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value
|
||||
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||
#endif
|
||||
state->err = FT_Err_Ok;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state)
|
||||
{
|
||||
FT_SVG_Document document = (FT_SVG_Document)slot->other;
|
||||
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||
FT_Size_Metrics& metrics = document->metrics;
|
||||
|
||||
// This function is called twice, once in the FT_Load_Glyph() and another right before ImGuiLunasvgPortRender().
|
||||
// If it's the latter, don't do anything because it's // already done in the former.
|
||||
if (cache)
|
||||
return state->err;
|
||||
|
||||
state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length);
|
||||
if (state->svg == nullptr)
|
||||
{
|
||||
state->err = FT_Err_Invalid_SVG_Document;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
lunasvg::Box box = state->svg->boundingBox();
|
||||
#else
|
||||
lunasvg::Box box = state->svg->box();
|
||||
#endif
|
||||
double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h);
|
||||
double xx = (double)document->transform.xx / (1 << 16);
|
||||
double xy = -(double)document->transform.xy / (1 << 16);
|
||||
double yx = -(double)document->transform.yx / (1 << 16);
|
||||
double yy = (double)document->transform.yy / (1 << 16);
|
||||
double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem;
|
||||
double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem;
|
||||
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
// Scale, transform and pre-translate the matrix for the rendering step
|
||||
state->matrix = lunasvg::Matrix::translated(-box.x, -box.y);
|
||||
state->matrix.multiply(lunasvg::Matrix(xx, xy, yx, yy, x0, y0));
|
||||
state->matrix.scale(scale, scale);
|
||||
|
||||
// Apply updated transformation to the bounding box
|
||||
box.transform(state->matrix);
|
||||
#else
|
||||
// Scale and transform, we don't translate the svg yet
|
||||
state->matrix.identity();
|
||||
state->matrix.scale(scale, scale);
|
||||
state->matrix.transform(xx, xy, yx, yy, x0, y0);
|
||||
state->svg->setMatrix(state->matrix);
|
||||
|
||||
// Pre-translate the matrix for the rendering step
|
||||
state->matrix.translate(-box.x, -box.y);
|
||||
|
||||
// Get the box again after the transformation
|
||||
box = state->svg->box();
|
||||
#endif
|
||||
|
||||
// Calculate the bitmap size
|
||||
slot->bitmap_left = FT_Int(box.x);
|
||||
slot->bitmap_top = FT_Int(-box.y);
|
||||
slot->bitmap.rows = (unsigned int)(ImCeil((float)box.h));
|
||||
slot->bitmap.width = (unsigned int)(ImCeil((float)box.w));
|
||||
slot->bitmap.pitch = slot->bitmap.width * 4;
|
||||
slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA;
|
||||
|
||||
// Compute all the bearings and set them correctly. The outline is scaled already, we just need to use the bounding box.
|
||||
double metrics_width = box.w;
|
||||
double metrics_height = box.h;
|
||||
double horiBearingX = box.x;
|
||||
double horiBearingY = -box.y;
|
||||
double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0;
|
||||
double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0;
|
||||
slot->metrics.width = FT_Pos(IM_ROUND(metrics_width * 64.0)); // Using IM_ROUND() assume width and height are positive
|
||||
slot->metrics.height = FT_Pos(IM_ROUND(metrics_height * 64.0));
|
||||
slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64);
|
||||
slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64);
|
||||
slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64);
|
||||
slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64);
|
||||
|
||||
if (slot->metrics.vertAdvance == 0)
|
||||
slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0);
|
||||
|
||||
state->err = FT_Err_Ok;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
#endif // #ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning (pop)
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
83
libs/imgui/misc/freetype/imgui_freetype.h
Normal file
83
libs/imgui/misc/freetype/imgui_freetype.h
Normal file
@@ -0,0 +1,83 @@
|
||||
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||
// (headers)
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
// Usage:
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE' in your imconfig to automatically enable support
|
||||
// for imgui_freetype in imgui. It is equivalent to selecting the default loader with:
|
||||
// io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())
|
||||
|
||||
// Optional support for OpenType SVG fonts:
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG' to use plutosvg (not provided). See #7927.
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE_LUNASVG' to use lunasvg (not provided). See #6591.
|
||||
|
||||
// Forward declarations
|
||||
struct ImFontAtlas;
|
||||
struct ImFontLoader;
|
||||
|
||||
// Hinting greatly impacts visuals (and glyph sizes).
|
||||
// - By default, hinting is enabled and the font's native hinter is preferred over the auto-hinter.
|
||||
// - When disabled, FreeType generates blurrier glyphs, more or less matches the stb_truetype.h
|
||||
// - The Default hinting mode usually looks good, but may distort glyphs in an unusual way.
|
||||
// - The Light hinting mode generates fuzzier glyphs but better matches Microsoft's rasterizer.
|
||||
// You can set those flags globally in ImFontAtlas::FontLoaderFlags
|
||||
// You can set those flags on a per font basis in ImFontConfig::FontLoaderFlags
|
||||
typedef unsigned int ImGuiFreeTypeLoaderFlags;
|
||||
enum ImGuiFreeTypeLoaderFlags_
|
||||
{
|
||||
ImGuiFreeTypeLoaderFlags_NoHinting = 1 << 0, // Disable hinting. This generally generates 'blurrier' bitmap glyphs when the glyph are rendered in any of the anti-aliased modes.
|
||||
ImGuiFreeTypeLoaderFlags_NoAutoHint = 1 << 1, // Disable auto-hinter.
|
||||
ImGuiFreeTypeLoaderFlags_ForceAutoHint = 1 << 2, // Indicates that the auto-hinter is preferred over the font's native hinter.
|
||||
ImGuiFreeTypeLoaderFlags_LightHinting = 1 << 3, // A lighter hinting algorithm for gray-level modes. Many generated glyphs are fuzzier but better resemble their original shape. This is achieved by snapping glyphs to the pixel grid only vertically (Y-axis), as is done by Microsoft's ClearType and Adobe's proprietary font renderer. This preserves inter-glyph spacing in horizontal text.
|
||||
ImGuiFreeTypeLoaderFlags_MonoHinting = 1 << 4, // Strong hinting algorithm that should only be used for monochrome output.
|
||||
ImGuiFreeTypeLoaderFlags_Bold = 1 << 5, // Styling: Should we artificially embolden the font?
|
||||
ImGuiFreeTypeLoaderFlags_Oblique = 1 << 6, // Styling: Should we slant the font, emulating italic style?
|
||||
ImGuiFreeTypeLoaderFlags_Monochrome = 1 << 7, // Disable anti-aliasing. Combine this with MonoHinting for best results!
|
||||
ImGuiFreeTypeLoaderFlags_LoadColor = 1 << 8, // Enable FreeType color-layered glyphs
|
||||
ImGuiFreeTypeLoaderFlags_Bitmap = 1 << 9, // Enable FreeType bitmap glyphs
|
||||
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
ImGuiFreeTypeBuilderFlags_NoHinting = ImGuiFreeTypeLoaderFlags_NoHinting,
|
||||
ImGuiFreeTypeBuilderFlags_NoAutoHint = ImGuiFreeTypeLoaderFlags_NoAutoHint,
|
||||
ImGuiFreeTypeBuilderFlags_ForceAutoHint = ImGuiFreeTypeLoaderFlags_ForceAutoHint,
|
||||
ImGuiFreeTypeBuilderFlags_LightHinting = ImGuiFreeTypeLoaderFlags_LightHinting,
|
||||
ImGuiFreeTypeBuilderFlags_MonoHinting = ImGuiFreeTypeLoaderFlags_MonoHinting,
|
||||
ImGuiFreeTypeBuilderFlags_Bold = ImGuiFreeTypeLoaderFlags_Bold,
|
||||
ImGuiFreeTypeBuilderFlags_Oblique = ImGuiFreeTypeLoaderFlags_Oblique,
|
||||
ImGuiFreeTypeBuilderFlags_Monochrome = ImGuiFreeTypeLoaderFlags_Monochrome,
|
||||
ImGuiFreeTypeBuilderFlags_LoadColor = ImGuiFreeTypeLoaderFlags_LoadColor,
|
||||
ImGuiFreeTypeBuilderFlags_Bitmap = ImGuiFreeTypeLoaderFlags_Bitmap,
|
||||
#endif
|
||||
};
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
typedef ImGuiFreeTypeLoaderFlags_ ImGuiFreeTypeBuilderFlags_;
|
||||
#endif
|
||||
|
||||
namespace ImGuiFreeType
|
||||
{
|
||||
// This is automatically assigned when using '#define IMGUI_ENABLE_FREETYPE'.
|
||||
// If you need to dynamically select between multiple builders:
|
||||
// - you can manually assign this builder with 'atlas->SetFontLoader(ImGuiFreeType::GetFontLoader())'
|
||||
// - prefer deep-copying this into your own ImFontLoader instance if you use hot-reloading that messes up static data.
|
||||
IMGUI_API const ImFontLoader* GetFontLoader();
|
||||
|
||||
// Override allocators. By default ImGuiFreeType will use IM_ALLOC()/IM_FREE()
|
||||
// However, as FreeType does lots of allocations we provide a way for the user to redirect it to a separate memory heap if desired.
|
||||
IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = nullptr);
|
||||
|
||||
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
|
||||
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
|
||||
//static inline bool BuildFontAtlas(ImFontAtlas* atlas, unsigned int flags = 0) { atlas->FontBuilderIO = GetBuilderForFreeType(); atlas->FontLoaderFlags = flags; return atlas->Build(); } // Prefer using '#define IMGUI_ENABLE_FREETYPE'
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
3278
libs/nanosvg/nanosvg.h
Normal file
3278
libs/nanosvg/nanosvg.h
Normal file
File diff suppressed because it is too large
Load Diff
1472
libs/nanosvg/nanosvgrast.h
Normal file
1472
libs/nanosvg/nanosvgrast.h
Normal file
File diff suppressed because it is too large
Load Diff
270
overview_mockup.html
Normal file
270
overview_mockup.html
Normal file
File diff suppressed because one or more lines are too long
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
BIN
res/fonts/NotoEmoji-Subset.ttf
Normal file
BIN
res/fonts/NotoEmoji-Subset.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
res/fonts/TwemojiMozilla-Color.ttf
Normal file
BIN
res/fonts/TwemojiMozilla-Color.ttf
Normal file
Binary file not shown.
BIN
res/img/backgrounds/texture/jade_bg.png
Normal file
BIN
res/img/backgrounds/texture/jade_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
23
res/img/logos/logo_dragonx.svg
Normal file
23
res/img/logos/logo_dragonx.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
802
res/lang/de.json
802
res/lang/de.json
File diff suppressed because it is too large
Load Diff
802
res/lang/es.json
802
res/lang/es.json
File diff suppressed because it is too large
Load Diff
802
res/lang/fr.json
802
res/lang/fr.json
File diff suppressed because it is too large
Load Diff
802
res/lang/ja.json
802
res/lang/ja.json
File diff suppressed because it is too large
Load Diff
802
res/lang/ko.json
802
res/lang/ko.json
File diff suppressed because it is too large
Load Diff
802
res/lang/pt.json
802
res/lang/pt.json
File diff suppressed because it is too large
Load Diff
802
res/lang/ru.json
802
res/lang/ru.json
File diff suppressed because it is too large
Load Diff
802
res/lang/zh.json
802
res/lang/zh.json
File diff suppressed because it is too large
Load Diff
190
res/themes/jade.toml
Normal file
190
res/themes/jade.toml
Normal file
@@ -0,0 +1,190 @@
|
||||
[theme]
|
||||
name = "Jade"
|
||||
author = "The Hush Developers"
|
||||
dark = true
|
||||
elevation = { --elevation-0 = "#071210", --elevation-1 = "#0C1A16", --elevation-2 = "#16261F", --elevation-3 = "#1D3128", --elevation-4 = "#243B30" }
|
||||
images = { background_image = "backgrounds/texture/jade_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||
|
||||
[theme.palette]
|
||||
--primary = "#2FA07A"
|
||||
--primary-variant = "#1E7357"
|
||||
--primary-light = "#7FD1B5"
|
||||
--secondary = "#C9A24E"
|
||||
--secondary-variant = "#A8842F"
|
||||
--secondary-light = "#E0C583"
|
||||
--background = "#071210"
|
||||
--surface = "#0C1A16"
|
||||
--surface-variant = "#16261F"
|
||||
--on-primary = "#FFFFFF"
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#DCEDE4"
|
||||
--on-surface = "#DCEDE4"
|
||||
--on-surface-medium = "rgba(220,237,228,0.85)"
|
||||
--on-surface-disabled = "rgba(220,237,228,0.58)"
|
||||
--error = "#CF6679"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
--on-success = "#000000"
|
||||
--warning = "#FFB74D"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(130,205,170,0.14)"
|
||||
--outline = "rgba(130,205,170,0.16)"
|
||||
--scrim = "rgba(0,0,0,0.6)"
|
||||
--surface-hover = "rgba(130,205,170,0.07)"
|
||||
--surface-alt = "rgba(130,205,170,0.05)"
|
||||
--surface-active = "rgba(130,205,170,0.10)"
|
||||
--glass-button = "rgba(130,205,170,0.06)"
|
||||
--glass-button-hover = "rgba(130,205,170,0.12)"
|
||||
--card-border = "rgba(130,205,170,0.26)"
|
||||
--text-shadow = "rgba(0,0,0,0.50)"
|
||||
--input-overlay-text = "rgba(220,237,228,0.30)"
|
||||
--slider-text = "rgba(220,237,228,0.85)"
|
||||
--thumb-fill = "rgba(130,205,170,0.15)"
|
||||
--thumb-border = "rgba(130,205,170,0.50)"
|
||||
--disabled-label = "rgba(130,205,170,0.18)"
|
||||
--chart-grid = "rgba(130,205,170,0.05)"
|
||||
--chart-crosshair = "rgba(130,205,170,0.15)"
|
||||
--chart-hover-ring = "rgba(130,205,170,0.30)"
|
||||
--tooltip-bg = "rgba(9,20,16,0.92)"
|
||||
--tooltip-border = "rgba(130,205,170,0.12)"
|
||||
--glass-fill = "rgba(130,205,170,0.08)"
|
||||
--glass-border = "rgba(47,160,122,0.30)"
|
||||
--glass-noise-tint = "rgba(130,205,170,0.03)"
|
||||
--tactile-top = "rgba(130,205,170,0.06)"
|
||||
--tactile-bottom = "rgba(130,205,170,0.0)"
|
||||
--hover-overlay = "rgba(130,205,170,0.05)"
|
||||
--active-overlay = "rgba(130,205,170,0.10)"
|
||||
--rim-light = "rgba(130,205,170,0.14)"
|
||||
--status-divider = "rgba(130,205,170,0.08)"
|
||||
--sidebar-hover = "rgba(130,205,170,0.10)"
|
||||
--sidebar-icon = "rgba(130,205,170,0.42)"
|
||||
--sidebar-badge = "rgba(220,237,228,1.0)"
|
||||
--sidebar-divider = "rgba(130,205,170,0.06)"
|
||||
--chart-line = "rgba(130,205,170,0.10)"
|
||||
--window-control = "rgba(220,237,228,0.78)"
|
||||
--window-control-hover = "rgba(130,205,170,0.12)"
|
||||
--window-close-hover = "rgba(232,17,35,0.78)"
|
||||
--spinner-track = "rgba(130,205,170,0.10)"
|
||||
--spinner-active = "rgba(79,184,154,0.85)"
|
||||
--shutdown-panel-bg = "rgba(7,18,14,0.90)"
|
||||
--shutdown-panel-border = "rgba(130,205,170,0.07)"
|
||||
--ram-bar-app = "#2FA07A"
|
||||
--ram-bar-system = "rgba(255,255,255,0.18)"
|
||||
--accent-total = "#7FD1B5"
|
||||
--accent-shielded = "#4FB89A"
|
||||
--accent-transparent = "#C9A24E"
|
||||
--accent-action = "#2FA07A"
|
||||
--accent-market = "#4FB89A"
|
||||
--accent-portfolio = "#7FD1B5"
|
||||
--toast-info-accent = "#2FA07A"
|
||||
--toast-info-text = "#7FD1B5"
|
||||
--toast-success-accent = "rgba(50,180,80,1.0)"
|
||||
--toast-success-text = "rgba(180,255,180,1.0)"
|
||||
--toast-warning-accent = "rgba(204,166,50,1.0)"
|
||||
--toast-warning-text = "rgba(255,230,130,1.0)"
|
||||
--toast-error-accent = "rgba(204,64,64,1.0)"
|
||||
--toast-error-text = "rgba(255,153,153,1.0)"
|
||||
--snackbar-bg = "rgba(24,40,34,0.95)"
|
||||
--snackbar-text = "rgba(220,237,228,0.87)"
|
||||
--snackbar-action = "rgba(79,184,154,1.0)"
|
||||
--snackbar-action-hover = "rgba(127,209,181,1.0)"
|
||||
--switch-track-off = "rgba(130,205,170,0.12)"
|
||||
--switch-track-on = "rgba(47,160,122,0.50)"
|
||||
--switch-thumb-off = "#A0C0B4"
|
||||
--switch-thumb-on = "#DCEDE4"
|
||||
--control-shadow = "rgba(0,0,0,0.24)"
|
||||
--checkbox-check = "#000000"
|
||||
--app-bar-shadow = "rgba(0,0,0,0.25)"
|
||||
|
||||
[backdrop]
|
||||
base-color-top = "rgba(14,32,26,210)"
|
||||
base-color-bottom = "rgba(6,18,14,210)"
|
||||
texture-tint-alpha = 120
|
||||
gradient-top-r = 10
|
||||
gradient-top-g = 30
|
||||
gradient-top-b = 22
|
||||
gradient-top-a = 90
|
||||
gradient-bottom-r = 5
|
||||
gradient-bottom-g = 16
|
||||
gradient-bottom-b = 12
|
||||
gradient-bottom-a = 70
|
||||
background-alpha = 0.42
|
||||
surface-alpha = 0.56
|
||||
frame-alpha = 0.78
|
||||
surface-inline-alpha = 0.58
|
||||
background-inline-alpha = 0.40
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Theme Visual Effects — Jade (veins of gold shifting through the stone)
|
||||
# Jade's signature is a slow jade→gold color-shifting border on every glass
|
||||
# panel + the active nav button — a vein of gold surfacing through nephrite.
|
||||
# It's drawn via AddRect so it hugs the real rounded corners (no polygonal
|
||||
# edge-trace). Sparse jade motes drift up the viewport. No other theme turns
|
||||
# gradient-border-panels on, so the panel-wide vein is Jade's own —
|
||||
# deliberately NOT Obsidian's specular glare.
|
||||
# ---------------------------------------------------------------------------
|
||||
[effects]
|
||||
hue-cycle-enabled = { size = 0.0 }
|
||||
rainbow-border-enabled = { size = 0.0 }
|
||||
|
||||
# No shimmer sweep — replaced by specular glare
|
||||
shimmer-enabled = { size = 0.0 }
|
||||
|
||||
positional-hue-enabled = { size = 0.0 }
|
||||
|
||||
glow-pulse-enabled = { size = 0.0 }
|
||||
|
||||
# Edge-trace OFF — its hand-walked perimeter chamfers rounded corners.
|
||||
# Jade's vein is the gradient-border below (corner-clean via AddRect).
|
||||
edge-trace-enabled = { size = 0.0 }
|
||||
edge-trace-speed = { size = 0.16 }
|
||||
edge-trace-length = { size = 0.34 }
|
||||
edge-trace-thickness = { size = 1.6 }
|
||||
edge-trace-alpha = { size = 0.55 }
|
||||
edge-trace-color = { color = "#C9A24E" }
|
||||
|
||||
# Specular glare OFF — that's Obsidian's signature; Jade shouldn't echo it.
|
||||
specular-glare-enabled = { size = 0.0 }
|
||||
specular-glare-speed = { size = 0.018 }
|
||||
specular-glare-intensity = { size = 0.008 }
|
||||
specular-glare-radius = { size = 0.65 }
|
||||
specular-glare-count = { size = 1.0 }
|
||||
specular-glare-color = { color = "rgba(150,220,180,1.0)" }
|
||||
|
||||
# HERO — vein of gold: a slow jade→gold color-shifting border on the active
|
||||
# nav button AND (via gradient-border-panels) every glass panel. Drawn with
|
||||
# AddRect so it follows the rounded corners exactly. Panels drift at a softer
|
||||
# alpha and position-phased offset, so a screenful reads like veins at
|
||||
# different depths rather than one synchronized pulse.
|
||||
gradient-border-enabled = { size = 1.0 }
|
||||
gradient-border-panels = { size = 1.0 }
|
||||
gradient-border-speed = { size = 0.10 }
|
||||
gradient-border-thickness = { size = 1.5 }
|
||||
gradient-border-alpha = { size = 0.55 }
|
||||
gradient-border-color-a = { color = "#7FD1B5" }
|
||||
gradient-border-color-b = { color = "#C9A24E" }
|
||||
|
||||
# Ambient jade motes — sparse, slow, cool green particles drifting up the
|
||||
# viewport (recolored ember-rise; a different mood from dragonx's fire embers).
|
||||
ember-rise-enabled = { size = 1.0 }
|
||||
ember-rise-count = { size = 5.0 }
|
||||
ember-rise-speed = { size = 0.18 }
|
||||
ember-rise-particle-size = { size = 1.4 }
|
||||
ember-rise-alpha = { size = 0.26 }
|
||||
ember-rise-color = { color = "#7FD1B5" }
|
||||
|
||||
# Shader-like viewport overlay — deep green stone atmosphere
|
||||
viewport-wash-enabled = { size = 1.0 }
|
||||
viewport-wash-alpha = { size = 0.05 }
|
||||
viewport-wash-tl = { color = "#12402E" }
|
||||
viewport-wash-tr = { color = "#0E3828" }
|
||||
viewport-wash-bl = { color = "#16442E" }
|
||||
viewport-wash-br = { color = "#1A4A34" }
|
||||
viewport-wash-rotate = { size = 0.015 }
|
||||
viewport-wash-pulse = { size = 0.0 }
|
||||
viewport-wash-pulse-depth = { size = 0.0 }
|
||||
|
||||
viewport-vignette-enabled = { size = 1.0 }
|
||||
viewport-vignette-color = { color = "#04140D" }
|
||||
viewport-vignette-radius = { size = 0.22 }
|
||||
viewport-vignette-alpha = { size = 0.15 }
|
||||
@@ -2100,6 +2100,178 @@ TRANSLATIONS = {
|
||||
"pt": "EXPLORADOR", "ru": "ОБОЗРЕВАТЕЛЬ", "zh": "浏览器",
|
||||
"ja": "エクスプローラー", "ko": "탐색기"
|
||||
},
|
||||
|
||||
# --- Wallets dialog: metadata counts + status badges ---
|
||||
"wallets_col_txs": {
|
||||
"es": "tx", "de": "Tx", "fr": "tx", "pt": "tx",
|
||||
"ru": "трз", "zh": "笔交易", "ja": "件", "ko": "건"
|
||||
},
|
||||
"wallets_col_keys": {
|
||||
"es": "claves", "de": "Schlüssel", "fr": "clés", "pt": "chaves",
|
||||
"ru": "ключей", "zh": "个密钥", "ja": "個の鍵", "ko": "개 키"
|
||||
},
|
||||
"wallets_badge_encrypted": {
|
||||
"es": "Cifrada (protegida con contraseña)", "de": "Verschlüsselt (passphrasengeschützt)",
|
||||
"fr": "Chiffré (protégé par phrase secrète)", "pt": "Encriptada (protegida por senha)",
|
||||
"ru": "Зашифрован (защищён паролем)", "zh": "已加密(密码保护)",
|
||||
"ja": "暗号化済み(パスフレーズ保護)", "ko": "암호화됨 (암호로 보호됨)"
|
||||
},
|
||||
"wallets_badge_seed": {
|
||||
"es": "Billetera con frase semilla (HD)", "de": "Seed-Phrase-Wallet (HD)",
|
||||
"fr": "Portefeuille à phrase de récupération (HD)", "pt": "Carteira com frase semente (HD)",
|
||||
"ru": "Кошелёк с seed-фразой (HD)", "zh": "助记词钱包 (HD)",
|
||||
"ja": "シードフレーズウォレット (HD)", "ko": "시드 문구 지갑 (HD)"
|
||||
},
|
||||
"wallets_badge_legacy": {
|
||||
"es": "Billetera heredada (sin frase semilla)", "de": "Legacy-Wallet (keine Seed-Phrase)",
|
||||
"fr": "Portefeuille hérité (sans phrase de récupération)", "pt": "Carteira legada (sem frase semente)",
|
||||
"ru": "Устаревший кошелёк (без seed-фразы)", "zh": "旧版钱包(无助记词)",
|
||||
"ja": "レガシーウォレット(シードフレーズなし)", "ko": "레거시 지갑 (시드 문구 없음)"
|
||||
},
|
||||
"wallets_badge_unknown": {
|
||||
"es": "Tipo de billetera no determinado por completo (archivo grande — abra para confirmar)",
|
||||
"de": "Wallet-Typ nicht vollständig ermittelt (große Datei — zum Bestätigen öffnen)",
|
||||
"fr": "Type de portefeuille non entièrement déterminé (fichier volumineux — ouvrir pour confirmer)",
|
||||
"pt": "Tipo de carteira não totalmente determinado (arquivo grande — abra para confirmar)",
|
||||
"ru": "Тип кошелька определён не полностью (большой файл — откройте для подтверждения)",
|
||||
"zh": "钱包类型未完全确定(文件较大——打开以确认)",
|
||||
"ja": "ウォレットの種類を完全に判定できません(大きなファイル — 開いて確認)",
|
||||
"ko": "지갑 유형을 완전히 확인하지 못함 (큰 파일 — 열어서 확인)"
|
||||
},
|
||||
"wallets_badge_seed_short": {
|
||||
"es": "Frase semilla", "de": "Seed-Phrase", "fr": "Phrase secrète", "pt": "Frase semente",
|
||||
"ru": "Seed-фраза", "zh": "助记词", "ja": "シードフレーズ", "ko": "시드 문구"
|
||||
},
|
||||
"wallets_badge_encrypted_short": {
|
||||
"es": "Cifrada", "de": "Verschlüsselt", "fr": "Chiffré", "pt": "Encriptada",
|
||||
"ru": "Зашифрован", "zh": "已加密", "ja": "暗号化", "ko": "암호화됨"
|
||||
},
|
||||
"wallets_badge_legacy_short": {
|
||||
"es": "Heredada", "de": "Legacy", "fr": "Hérité", "pt": "Legada",
|
||||
"ru": "Устаревший", "zh": "旧版", "ja": "レガシー", "ko": "레거시"
|
||||
},
|
||||
"wallets_badge_unknown_short": {
|
||||
"es": "Desconocido", "de": "Unbekannt", "fr": "Inconnu", "pt": "Desconhecido",
|
||||
"ru": "Неизвестно", "zh": "未知", "ja": "不明", "ko": "알 수 없음"
|
||||
},
|
||||
|
||||
# --- Wallets dialog: created date + sort control ---
|
||||
"wallets_created": {
|
||||
"es": "creada", "de": "erstellt", "fr": "créé", "pt": "criada",
|
||||
"ru": "создан", "zh": "创建于", "ja": "作成", "ko": "생성"
|
||||
},
|
||||
"wallets_sort_by": {
|
||||
"es": "Ordenar:", "de": "Sortieren:", "fr": "Trier :", "pt": "Ordenar:",
|
||||
"ru": "Сортировка:", "zh": "排序:", "ja": "並べ替え:", "ko": "정렬:"
|
||||
},
|
||||
"wallets_sort_created": {
|
||||
"es": "Creado", "de": "Erstellt", "fr": "Créé", "pt": "Criado",
|
||||
"ru": "Создан", "zh": "创建", "ja": "作成", "ko": "생성"
|
||||
},
|
||||
"wallets_sort_addresses": {
|
||||
"es": "Direcciones", "de": "Adressen", "fr": "Adresses", "pt": "Endereços",
|
||||
"ru": "Адреса", "zh": "地址", "ja": "アドレス", "ko": "주소"
|
||||
},
|
||||
"wallets_sort_txs": {
|
||||
"es": "Txs", "de": "Txs", "fr": "Txs", "pt": "Txs",
|
||||
"ru": "Транз.", "zh": "交易", "ja": "取引", "ko": "거래"
|
||||
},
|
||||
"wallets_sort_size": {
|
||||
"es": "Tamaño", "de": "Größe", "fr": "Taille", "pt": "Tamanho",
|
||||
"ru": "Размер", "zh": "大小", "ja": "サイズ", "ko": "크기"
|
||||
},
|
||||
"wallets_sort_asc": {
|
||||
"es": "Ascendente (más antiguo / menos / más pequeño primero)",
|
||||
"de": "Aufsteigend (ältestes / wenigste / kleinstes zuerst)",
|
||||
"fr": "Croissant (plus ancien / moins / plus petit d'abord)",
|
||||
"pt": "Crescente (mais antigo / menos / menor primeiro)",
|
||||
"ru": "По возрастанию (сначала старые / меньше / меньший)",
|
||||
"zh": "升序(最早/最少/最小优先)", "ja": "昇順(古い/少ない/小さい順)", "ko": "오름차순 (오래된/적은/작은 순)"
|
||||
},
|
||||
"wallets_sort_desc": {
|
||||
"es": "Descendente (más reciente / más / más grande primero)",
|
||||
"de": "Absteigend (neuestes / meiste / größtes zuerst)",
|
||||
"fr": "Décroissant (plus récent / plus / plus grand d'abord)",
|
||||
"pt": "Decrescente (mais recente / mais / maior primeiro)",
|
||||
"ru": "По убыванию (сначала новые / больше / больший)",
|
||||
"zh": "降序(最新/最多/最大优先)", "ja": "降順(新しい/多い/大きい順)", "ko": "내림차순 (최신/많은/큰 순)"
|
||||
},
|
||||
"wallets_open_folder": {
|
||||
"es": "Abrir ubicación de la carpeta", "de": "Ordnerpfad öffnen",
|
||||
"fr": "Ouvrir l'emplacement du dossier", "pt": "Abrir local da pasta",
|
||||
"ru": "Открыть расположение папки", "zh": "打开文件夹位置",
|
||||
"ja": "フォルダーの場所を開く", "ko": "폴더 위치 열기"
|
||||
},
|
||||
"wallets_open_inplace_tt": {
|
||||
"es": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
|
||||
"de": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
|
||||
"fr": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
|
||||
"pt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
|
||||
"ru": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
|
||||
"zh": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"ja": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"ko": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)"
|
||||
},
|
||||
"wallets_open_failed": {
|
||||
"es": "No se pudo abrir esta cartera en su ubicación. Probablemente está en una unidad distinta a tu directorio de datos: muévela a la misma unidad (en Windows, activar el Modo de desarrollador también permite enlazar entre unidades).",
|
||||
"de": "Diese Wallet konnte nicht an ihrem Ort geöffnet werden. Sie liegt vermutlich auf einem anderen Laufwerk als dein Datenverzeichnis — verschiebe sie auf dasselbe Laufwerk (unter Windows erlaubt auch der aktivierte Entwicklermodus laufwerkübergreifende Verknüpfungen).",
|
||||
"fr": "Impossible d'ouvrir ce portefeuille à son emplacement. Il se trouve probablement sur un lecteur différent de votre répertoire de données — déplacez-le sur le même lecteur (sous Windows, activer le mode développeur permet aussi de créer des liens entre lecteurs).",
|
||||
"pt": "Não foi possível abrir esta carteira no lugar. Provavelmente está em uma unidade diferente do seu diretório de dados — mova-a para a mesma unidade (no Windows, ativar o Modo de Desenvolvedor também permite vincular entre unidades).",
|
||||
"ru": "Не удалось открыть этот кошелёк на месте. Вероятно, он на другом диске, чем каталог данных — переместите его на тот же диск (в Windows включённый режим разработчика также позволяет создавать ссылки между дисками).",
|
||||
"zh": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
|
||||
"ja": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してください(Windows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
|
||||
"ko": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다)."
|
||||
},
|
||||
"wallets_external_tt": {
|
||||
"es": "Fuera de tu directorio de datos — Abrir lo enlaza en su lugar (sin copiar).",
|
||||
"de": "Außerhalb deines Datenverzeichnisses — Öffnen verlinkt sie an ihrem Ort (keine Kopie).",
|
||||
"fr": "Hors de votre répertoire de données — Ouvrir le lie sur place (sans copie).",
|
||||
"pt": "Fora do seu diretório de dados — Abrir o vincula no lugar (sem cópia).",
|
||||
"ru": "Вне каталога данных — «Открыть» создаёт ссылку на месте (без копирования).",
|
||||
"zh": "在数据目录之外 —「打开」会就地链接(不复制)。",
|
||||
"ja": "データディレクトリの外 —「開く」はその場でリンクします(コピーなし)。",
|
||||
"ko": "데이터 디렉터리 밖 — '열기'는 제자리에 링크합니다 (복사 없음)."
|
||||
},
|
||||
"wallets_scanned_folders": {
|
||||
"es": "Carpetas escaneadas:", "de": "Durchsuchte Ordner:",
|
||||
"fr": "Dossiers analysés :", "pt": "Pastas verificadas:",
|
||||
"ru": "Просканированные папки:", "zh": "已扫描的文件夹:",
|
||||
"ja": "スキャン対象フォルダー:", "ko": "스캔한 폴더:"
|
||||
},
|
||||
"wallets_remove_folder": {
|
||||
"es": "Dejar de escanear esta carpeta", "de": "Diesen Ordner nicht mehr durchsuchen",
|
||||
"fr": "Ne plus analyser ce dossier", "pt": "Parar de verificar esta pasta",
|
||||
"ru": "Больше не сканировать эту папку", "zh": "停止扫描此文件夹",
|
||||
"ja": "このフォルダーのスキャンを停止", "ko": "이 폴더 스캔 중지"
|
||||
},
|
||||
"wallets_empty_hint": {
|
||||
"es": "Escanea una carpeta para encontrar más carteras",
|
||||
"de": "Ordner durchsuchen, um weitere Wallets zu finden",
|
||||
"fr": "Analysez un dossier pour trouver d'autres portefeuilles",
|
||||
"pt": "Verifique uma pasta para encontrar mais carteiras",
|
||||
"ru": "Просканируйте папку, чтобы найти другие кошельки",
|
||||
"zh": "扫描文件夹以查找更多钱包",
|
||||
"ja": "フォルダーをスキャンして他のウォレットを探す",
|
||||
"ko": "폴더를 스캔하여 다른 지갑 찾기"
|
||||
},
|
||||
"market_chart_loading": {
|
||||
"es": "Cargando historial de precios", "de": "Preisverlauf wird geladen",
|
||||
"fr": "Chargement de l'historique des prix", "pt": "Carregando histórico de preços",
|
||||
"ru": "Загрузка истории цен", "zh": "正在加载价格历史",
|
||||
"ja": "価格履歴を読み込み中", "ko": "가격 기록 불러오는 중"
|
||||
},
|
||||
"market_style_line": {
|
||||
"es": "Cambiar a gráfico de líneas", "de": "Zum Liniendiagramm wechseln",
|
||||
"fr": "Passer au graphique en ligne", "pt": "Mudar para gráfico de linhas",
|
||||
"ru": "Переключить на линейный график", "zh": "切换到折线图",
|
||||
"ja": "折れ線チャートに切り替え", "ko": "선형 차트로 전환"
|
||||
},
|
||||
"market_style_candle": {
|
||||
"es": "Cambiar a velas", "de": "Zu Kerzenchart wechseln",
|
||||
"fr": "Passer aux chandeliers", "pt": "Mudar para velas",
|
||||
"ru": "Переключить на свечи", "zh": "切换到蜡烛图",
|
||||
"ja": "ローソク足に切り替え", "ko": "캔들차트로 전환"
|
||||
},
|
||||
}
|
||||
|
||||
def main():
|
||||
|
||||
94
scripts/build-freetype-mingw.sh
Executable file
94
scripts/build-freetype-mingw.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-build a MINIMAL static FreeType for the mingw-w64 (Windows) target.
|
||||
#
|
||||
# Why: the wallet's optional color-emoji rendering needs FreeType (to rasterize the COLR/CPAL Twemoji
|
||||
# font). Native Linux/macOS pick up the system FreeType via find_package; the Debian/Ubuntu mingw-w64
|
||||
# cross toolchain ships no FreeType, so we build one here. The Twemoji font is COLRv0 (layered vector),
|
||||
# which FreeType renders WITHOUT libpng / harfbuzz / brotli / zlib — so this is a dependency-free static
|
||||
# build (no external libs to also cross-compile), producing a self-contained libfreetype.a.
|
||||
#
|
||||
# Output: <prefix>/include/freetype2/... + <prefix>/lib/libfreetype.a (default prefix: third_party/freetype-mingw)
|
||||
# build.sh --win-release runs this automatically and passes -DDRAGONX_MINGW_FREETYPE_PREFIX to CMake.
|
||||
set -euo pipefail
|
||||
|
||||
FT_VERSION="2.13.3"
|
||||
FT_SHA256="5c3a8e78f7b24c20b25b54ee575d6daa40007a5f4eea2845861c3409b3021747" # freetype-2.13.3.tar.gz
|
||||
FT_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FT_VERSION}.tar.gz"
|
||||
FT_URL_MIRROR="https://downloads.sourceforge.net/project/freetype/freetype2/${FT_VERSION}/freetype-${FT_VERSION}.tar.gz"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PREFIX="${1:-$SCRIPT_DIR/third_party/freetype-mingw}"
|
||||
WORK="$SCRIPT_DIR/third_party/.freetype-mingw-build"
|
||||
|
||||
# Already built? (libfreetype.a present) → nothing to do.
|
||||
if [[ -f "$PREFIX/lib/libfreetype.a" && -d "$PREFIX/include/freetype2" ]]; then
|
||||
echo "FreeType (mingw) already built at: $PREFIX"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pick the mingw compilers (posix threads variant preferred, matching build.sh).
|
||||
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
|
||||
MINGW_GCC=x86_64-w64-mingw32-gcc-posix; MINGW_GXX=x86_64-w64-mingw32-g++-posix
|
||||
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
|
||||
MINGW_GCC=x86_64-w64-mingw32-gcc; MINGW_GXX=x86_64-w64-mingw32-g++
|
||||
else
|
||||
echo "ERROR: x86_64-w64-mingw32-gcc not found (install mingw-w64)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$WORK"
|
||||
cd "$WORK"
|
||||
|
||||
TARBALL="freetype-${FT_VERSION}.tar.gz"
|
||||
if [[ ! -f "$TARBALL" ]]; then
|
||||
echo "Downloading FreeType ${FT_VERSION} ..."
|
||||
curl -fsSL -o "$TARBALL" "$FT_URL" || curl -fsSL -o "$TARBALL" "$FT_URL_MIRROR"
|
||||
fi
|
||||
|
||||
echo "Verifying SHA-256 ..."
|
||||
echo "${FT_SHA256} ${TARBALL}" | sha256sum -c - || {
|
||||
echo "ERROR: FreeType tarball checksum mismatch (expected ${FT_SHA256})." >&2
|
||||
echo " got: $(sha256sum "$TARBALL" | cut -d' ' -f1)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf "freetype-${FT_VERSION}"
|
||||
tar xf "$TARBALL"
|
||||
SRC="$WORK/freetype-${FT_VERSION}"
|
||||
|
||||
# Minimal mingw toolchain for FreeType's own CMake.
|
||||
cat > "$WORK/ft-mingw-toolchain.cmake" <<TOOLCHAIN
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(CMAKE_C_COMPILER ${MINGW_GCC})
|
||||
set(CMAKE_CXX_COMPILER ${MINGW_GXX})
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
TOOLCHAIN
|
||||
|
||||
echo "Configuring FreeType (static, no external deps) ..."
|
||||
rm -rf "$WORK/build"
|
||||
cmake -S "$SRC" -B "$WORK/build" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$WORK/ft-mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DFT_DISABLE_ZLIB=ON \
|
||||
-DFT_DISABLE_BZIP2=ON \
|
||||
-DFT_DISABLE_PNG=ON \
|
||||
-DFT_DISABLE_HARFBUZZ=ON \
|
||||
-DFT_DISABLE_BROTLI=ON
|
||||
|
||||
echo "Building + installing FreeType ..."
|
||||
cmake --build "$WORK/build" -j "$(nproc)"
|
||||
cmake --install "$WORK/build"
|
||||
|
||||
if [[ -f "$PREFIX/lib/libfreetype.a" ]]; then
|
||||
echo "OK: mingw FreeType -> $PREFIX/lib/libfreetype.a"
|
||||
else
|
||||
echo "ERROR: build did not produce libfreetype.a" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,5 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
|
||||
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
|
||||
# present (Homebrew), and fail with a clear message otherwise.
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
|
||||
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
|
||||
done
|
||||
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
|
||||
echo " On macOS: brew install bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -67,25 +79,9 @@ Options:
|
||||
--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.
|
||||
@@ -95,9 +91,13 @@ Outputs:
|
||||
<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.
|
||||
The lite backend is always built from the vendored in-tree source
|
||||
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
|
||||
and self-attested signature metadata are NOT accepted (F15-1) — the previous
|
||||
scheme only recorded an unverified "verified" claim. The script captures the
|
||||
freshly-built artifact's symbols and checksum, and records build provenance.
|
||||
It does not load the library, resolve function pointers, call SDXL, sign,
|
||||
upload, or publish artifacts.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -166,15 +166,8 @@ while [[ $# -gt 0 ]]; do
|
||||
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
|
||||
--artifact|--no-build)
|
||||
die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
|
||||
;;
|
||||
--reproducible)
|
||||
REPRODUCIBLE=true
|
||||
@@ -191,54 +184,11 @@ while [[ $# -gt 0 ]]; do
|
||||
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
|
||||
--signature-required|--signature-file|--signature-path|--signature-format|\
|
||||
--signature-verification-tool|--signature-tool|--signature-verification-command|\
|
||||
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
|
||||
--signature-transparency-log-url|--signature-verified-sha256)
|
||||
die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
|
||||
;;
|
||||
-j|--jobs)
|
||||
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
||||
@@ -374,6 +324,9 @@ prepare_backend_source() {
|
||||
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"
|
||||
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
|
||||
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
|
||||
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
|
||||
# "vendor" relative to the build root, so expose it inside the prepared root too.
|
||||
@@ -766,6 +719,7 @@ MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
|
||||
printf ' },\n'
|
||||
printf ' "provenance": {\n'
|
||||
printf ' "owner_ready": true,\n'
|
||||
printf ' "built_from_source": 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'
|
||||
|
||||
87
scripts/build_emoji_subset.py
Normal file
87
scripts/build_emoji_subset.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build a monochrome Noto Emoji subset for the chat/message UI.
|
||||
|
||||
Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome
|
||||
(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's
|
||||
*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed)
|
||||
and merge it into the text fonts (see Typography::loadFont, the block after the
|
||||
CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ
|
||||
sequences / regional-indicator flags won't compose — single-codepoint emoji
|
||||
(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use.
|
||||
|
||||
Source is the variable font pinned to wght=400 → static, then subset to the
|
||||
emoji planes plus the higher symbol/star ranges the base UI font doesn't cover.
|
||||
The base Ubuntu font already owns U+2600–26FF etc.; ImGui's MergeMode gives the
|
||||
first-loaded glyph precedence, so those stay text-styled and only the codepoints
|
||||
the base lacks fall through to this font.
|
||||
|
||||
Get the source once (OFL, redistributable):
|
||||
curl -fsSL -o /tmp/NotoEmoji-VF.ttf \
|
||||
'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf'
|
||||
|
||||
Then: python3 scripts/build_emoji_subset.py
|
||||
Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN)
|
||||
"""
|
||||
import os
|
||||
from fontTools import ttLib, subset
|
||||
from fontTools.varLib.instancer import instantiateVariableFont
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SOURCE_VF = '/tmp/NotoEmoji-VF.ttf'
|
||||
STATIC = '/tmp/NotoEmoji-Static.ttf'
|
||||
OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf')
|
||||
|
||||
# Emoji codepoints to keep. Ranges are inclusive.
|
||||
RANGES = [
|
||||
(0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended)
|
||||
(0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats
|
||||
(0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows
|
||||
(0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style)
|
||||
(0x2194, 0x21AA), # arrows used as emoji
|
||||
(0x231A, 0x231B), # ⌚ ⌛
|
||||
(0x23E9, 0x23FA), # media-control emoji
|
||||
(0x25AA, 0x25FE), # small squares
|
||||
]
|
||||
SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299,
|
||||
0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2]
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(SOURCE_VF):
|
||||
raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command")
|
||||
|
||||
# 1. Pin the weight axis so stb_truetype rasterizes a clean static instance.
|
||||
f = ttLib.TTFont(SOURCE_VF)
|
||||
if 'fvar' in f:
|
||||
instantiateVariableFont(f, {'wght': 400}, inplace=True)
|
||||
f.save(STATIC)
|
||||
|
||||
unicodes = list(SINGLES)
|
||||
for lo, hi in RANGES:
|
||||
unicodes.extend(range(lo, hi + 1))
|
||||
|
||||
opts = subset.Options()
|
||||
opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS
|
||||
opts.name_IDs = []
|
||||
opts.notdef_outline = True
|
||||
opts.glyph_names = False
|
||||
opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern']
|
||||
|
||||
font = subset.load_font(STATIC, opts)
|
||||
ss = subset.Subsetter(options=opts)
|
||||
ss.populate(unicodes=unicodes)
|
||||
ss.subset(font)
|
||||
subset.save_font(font, OUTPUT, opts)
|
||||
|
||||
out = ttLib.TTFont(OUTPUT)
|
||||
cmap = out.getBestCmap()
|
||||
color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR'))
|
||||
print(f"Output: {OUTPUT}")
|
||||
print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}")
|
||||
if color:
|
||||
raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -24,18 +24,27 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for appimagetool
|
||||
# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a moving, unverified network download that runs on the release
|
||||
# builder; verify it or refuse to package.
|
||||
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
APPIMAGETOOL=""
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
else
|
||||
print_status "Downloading appimagetool..."
|
||||
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_status "Downloading appimagetool 1.9.0 (pinned)..."
|
||||
wget -q -O "$AT" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_error "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$AT"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$AT"
|
||||
fi
|
||||
APPIMAGETOOL="$AT"
|
||||
fi
|
||||
|
||||
print_status "Creating AppDir structure..."
|
||||
|
||||
@@ -256,8 +256,8 @@ HEADER_START
|
||||
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
||||
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
||||
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
|
||||
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
|
||||
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
|
||||
#
|
||||
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
|
||||
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
|
||||
# is published. 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.
|
||||
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
|
||||
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
|
||||
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
|
||||
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
|
||||
# checksum table in the release body, so `release` prints that table for you to paste in.
|
||||
#
|
||||
# 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
|
||||
# (the same flow the wallet's unit tests verify for the miner updater).
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
|
||||
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
|
||||
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
|
||||
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
|
||||
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
|
||||
# # binary staged are skipped.
|
||||
#
|
||||
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
|
||||
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||
|
||||
@@ -27,6 +33,26 @@ command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25
|
||||
# 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; }
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
|
||||
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
||||
}
|
||||
|
||||
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
|
||||
sign_file() {
|
||||
local key="$1" f="$2" raw
|
||||
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"
|
||||
}
|
||||
|
||||
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
|
||||
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
|
||||
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
|
||||
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
keygen)
|
||||
@@ -42,26 +68,90 @@ case "$cmd" in
|
||||
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||
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"
|
||||
sign_file "$key" "$f"
|
||||
echo "signed: $f -> $f.sig"
|
||||
done
|
||||
echo "Upload each .sig as a release asset next to its archive."
|
||||
;;
|
||||
|
||||
release)
|
||||
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
|
||||
key="$1"; version="$2"; shift 2
|
||||
src="$PROJECT_ROOT/prebuilt-binaries"
|
||||
out="$PROJECT_ROOT/release/daemon"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
|
||||
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
[ -d "$src" ] || die "no such source dir: $src"
|
||||
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
|
||||
mkdir -p "$out"
|
||||
|
||||
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
|
||||
# then reject every signature made with it — only expected when deliberately rotating the key).
|
||||
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
|
||||
mine="$(pubkey_b64 "$key")"
|
||||
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
|
||||
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
|
||||
echo " signing key -> $mine" >&2
|
||||
echo " pinned key -> $pinned" >&2
|
||||
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
|
||||
echo >&2
|
||||
fi
|
||||
|
||||
made=0
|
||||
table=""
|
||||
for plat in linux mac win; do
|
||||
d="$src/$(plat_dir "$plat")"
|
||||
daemon="$d/$(plat_daemon "$plat")"
|
||||
if [ ! -f "$daemon" ]; then
|
||||
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
|
||||
continue
|
||||
fi
|
||||
archive="dragonx-$version-$(plat_token "$plat").zip"
|
||||
apath="$out/$archive"
|
||||
rm -f "$apath"
|
||||
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
|
||||
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
|
||||
files=()
|
||||
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
|
||||
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
|
||||
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
|
||||
sign_file "$key" "$apath"
|
||||
sum="$(sha256_of "$apath")"
|
||||
table+="| $archive | \`$sum\` |"$'\n'
|
||||
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
|
||||
made=$((made + 1))
|
||||
done
|
||||
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
|
||||
|
||||
echo
|
||||
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
|
||||
echo "| Archive | SHA-256 |"
|
||||
echo "|---|---|"
|
||||
printf '%s' "$table"
|
||||
echo
|
||||
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
|
||||
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
|
||||
;;
|
||||
|
||||
*)
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
|
||||
;;
|
||||
esac
|
||||
|
||||
90
setup.sh
90
setup.sh
@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
|
||||
libxkbcommon wayland libsodium curl
|
||||
autoconf automake libtool wget python xxd"
|
||||
|
||||
pkgs_core_macos="cmake python xxd"
|
||||
pkgs_core_macos="bash cmake python xxd"
|
||||
|
||||
# Windows cross-compile (from Linux)
|
||||
pkgs_win_debian="mingw-w64 zip"
|
||||
@@ -284,18 +284,27 @@ fi
|
||||
header "Windows Cross-Compile"
|
||||
|
||||
if $SETUP_WIN; then
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
fi
|
||||
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
|
||||
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
|
||||
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
|
||||
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
|
||||
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
|
||||
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
||||
ok "Windows cross-compile toolchain already present — skipping apt install"
|
||||
else
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
fi
|
||||
|
||||
# Set posix thread model if available
|
||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
# Set posix thread model if available
|
||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||
if ! $CHECK_ONLY; then
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -391,11 +400,26 @@ elif $SETUP_SAPLING; then
|
||||
|
||||
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
||||
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
|
||||
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
|
||||
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
|
||||
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
|
||||
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
|
||||
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
|
||||
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
|
||||
ok "Downloaded sapling-spend.params"
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
||||
ok "Downloaded sapling-output.params"
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
|
||||
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-spend.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-spend.params"
|
||||
err "sapling-spend.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
|
||||
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-output.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-output.params"
|
||||
err "sapling-output.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
||||
@@ -684,11 +708,13 @@ if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||
fi
|
||||
|
||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||
header "xmrig-hac Mining Binary"
|
||||
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
|
||||
header "drg-xmrig Mining Binary"
|
||||
|
||||
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
|
||||
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
|
||||
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
|
||||
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||
# Only clean the binary for the platform(s) we are actually building,
|
||||
@@ -700,14 +726,14 @@ if ! $CHECK_ONLY; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper: clone xmrig-hac if not present
|
||||
# Helper: clone drg-xmrig if not present
|
||||
clone_xmrig_if_needed() {
|
||||
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||
info "Cloning xmrig-hac..."
|
||||
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
||||
info "Cloning drg-xmrig..."
|
||||
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
|
||||
else
|
||||
ok "xmrig-hac source already present"
|
||||
info "Pulling latest xmrig-hac..."
|
||||
ok "drg-xmrig source already present"
|
||||
info "Pulling latest drg-xmrig..."
|
||||
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||
fi
|
||||
}
|
||||
@@ -728,15 +754,15 @@ else
|
||||
rm -rf "$XMRIG_SRC/build"
|
||||
|
||||
# Build dependencies (libuv, hwloc, openssl)
|
||||
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
|
||||
info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
sh build_deps.sh
|
||||
)
|
||||
ok "xmrig-hac dependencies built"
|
||||
ok "drg-xmrig dependencies built"
|
||||
|
||||
# Build xmrig
|
||||
info "Building xmrig-hac (Linux)..."
|
||||
info "Building drg-xmrig (Linux)..."
|
||||
mkdir -p "$XMRIG_SRC/build"
|
||||
(
|
||||
cd "$XMRIG_SRC/build"
|
||||
@@ -753,7 +779,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig (Linux) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -777,7 +803,7 @@ else
|
||||
# Clean previous Windows build
|
||||
rm -rf "$XMRIG_SRC/build-windows"
|
||||
|
||||
info "Building xmrig-hac (Windows cross-compile)..."
|
||||
info "Building drg-xmrig (Windows cross-compile)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
bash build_windows.sh
|
||||
@@ -787,7 +813,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig.exe (Windows) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -797,7 +823,7 @@ fi
|
||||
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||
header "Binary Directories"
|
||||
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
|
||||
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||
if [[ -d "$dir" ]]; then
|
||||
# Count actual files (not .gitkeep)
|
||||
|
||||
1689
src/app.cpp
1689
src/app.cpp
File diff suppressed because it is too large
Load Diff
344
src/app.h
344
src/app.h
@@ -13,9 +13,11 @@
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <deque>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "data/address_book.h"
|
||||
#include "data/wallet_index.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "services/network_refresh_service.h"
|
||||
@@ -138,6 +140,9 @@ public:
|
||||
* @brief Whether we are in the shutdown phase
|
||||
*/
|
||||
bool isShuttingDown() const { return shutting_down_; }
|
||||
// True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live
|
||||
// phase/spinner update in real time even when the app is otherwise idle.
|
||||
bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); }
|
||||
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
|
||||
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
|
||||
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||||
@@ -167,6 +172,9 @@ public:
|
||||
daemon::EmbeddedDaemon* consoleDaemon();
|
||||
daemon::XmrigManager* consoleXmrig();
|
||||
config::Settings* settings() { return settings_.get(); }
|
||||
// Request a font-atlas rebuild before the next frame (e.g. after toggling color emoji). Handled in
|
||||
// preFrame() via Typography::reload — safe to call from UI code mid-frame.
|
||||
void requestFontRebuild() { font_rebuild_requested_ = true; }
|
||||
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||||
@@ -175,6 +183,9 @@ public:
|
||||
// message (to a conversation whose peer key we know) / a new-conversation contact request.
|
||||
void sendChatMessage(const std::string& conversationId, const std::string& text);
|
||||
void startChatConversation(const std::string& peerZaddr, const std::string& text);
|
||||
// Send a contact request into an existing conversation (used to retry a failed request in place).
|
||||
void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
|
||||
const std::string& text);
|
||||
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
||||
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
||||
// the chat feature is off.
|
||||
@@ -197,6 +208,19 @@ public:
|
||||
data::AddressBook& addressBook() { return address_book_; }
|
||||
const data::AddressBook& addressBook() const { return address_book_; }
|
||||
|
||||
// Hash of the active wallet's identity (derived from its address list). This is the tx-history
|
||||
// cache key — it changes when the address set changes, so DON'T scope persistent user data on it.
|
||||
std::string activeWalletIdentityHash() const;
|
||||
|
||||
// Stable per-wallet id ("w:"+hex) for scoping persistent per-wallet data (address-book contacts).
|
||||
// Generated once and persisted in the wallet index (keyed by wallet file), never recomputed from
|
||||
// the mutable address set — so creating addresses / locking / disconnecting never changes it.
|
||||
// Empty only when there is no active wallet file. Establishes + persists the id on first call.
|
||||
std::string activeWalletScopeId();
|
||||
|
||||
data::WalletIndex& walletIndex() { return wallet_index_; }
|
||||
const data::WalletIndex& walletIndex() const { return wallet_index_; }
|
||||
|
||||
// Connection state (convenience wrappers)
|
||||
bool isConnected() const { return state_.connected; }
|
||||
int getBlockHeight() const { return state_.sync.blocks; }
|
||||
@@ -292,8 +316,18 @@ public:
|
||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
|
||||
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
|
||||
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
|
||||
|
||||
// callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
|
||||
// returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
|
||||
// startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF).
|
||||
void importPrivateKey(const std::string& key, int startHeight,
|
||||
std::function<void(bool, const std::string&, const std::string&)> callback);
|
||||
|
||||
// Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance − fee) to a
|
||||
// destination you own — a freshly generated shielded address when destMode == 0, else destExisting.
|
||||
// Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker.
|
||||
void sweepPrivateKey(const std::string& key, int startHeight, int destMode,
|
||||
const std::string& destExisting);
|
||||
|
||||
// Wallet backup
|
||||
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
|
||||
// Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
|
||||
@@ -327,7 +361,14 @@ public:
|
||||
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
|
||||
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
|
||||
void refreshMarketChart();
|
||||
|
||||
// Fetch the SELECTED pair's candles from that exchange's own API (data/exchange_candles.h) so the
|
||||
// Market chart shows the real per-exchange price. Re-fetches on pair change; falls back to the
|
||||
// CoinGecko aggregate for unmapped venues / failed fetches. Safe to call every frame.
|
||||
void refreshExchangeChart();
|
||||
// True while a market price-history fetch is in flight (CoinGecko aggregate OR per-exchange). The
|
||||
// Market chart shows a loading indicator instead of the empty state during pair switches.
|
||||
bool isMarketChartLoading() const { return chart_fetch_in_flight_ || exchange_chart_fetch_in_flight_; }
|
||||
|
||||
/// @brief Per-category refresh intervals, adjusted by active tab
|
||||
using RefreshIntervals = services::NetworkRefreshService::Intervals;
|
||||
|
||||
@@ -343,17 +384,35 @@ public:
|
||||
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
||||
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
||||
void startScreenshotSweep();
|
||||
// Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
|
||||
// state overlay into view (with injected demo data, offline, firing no live ops) and captures
|
||||
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||||
void startFullUiSweep();
|
||||
std::string screenshotFullDir() const;
|
||||
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||
void onScreenshotCaptured();
|
||||
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
|
||||
|
||||
// Dialog triggers (used by settings page to open modal dialogs)
|
||||
void showImportKeyDialog() { show_import_key_ = true; }
|
||||
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
|
||||
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
|
||||
void showExportKeyDialog() { show_export_key_ = true; }
|
||||
void showBackupDialog() { show_backup_ = true; }
|
||||
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
||||
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
|
||||
// True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
|
||||
// that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
|
||||
bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
|
||||
// Authoritative BIP39-mnemonic status of the ACTIVE wallet, decided at runtime by z_exportmnemonic
|
||||
// (not the offline file probe, which can't tell an HD-but-no-mnemonic wallet from a seed-phrase one).
|
||||
// 0 = unknown/undecidable, 1 = has a seed phrase, 2 = legacy (no mnemonic).
|
||||
int activeWalletSeedBadge() const {
|
||||
if (wallet_seed_status_ == WalletSeedStatus::HasMnemonic) return 1;
|
||||
if (wallet_seed_status_ == WalletSeedStatus::NoMnemonic) return 2;
|
||||
return 0;
|
||||
}
|
||||
void showAboutDialog() { show_about_ = true; }
|
||||
|
||||
// Legacy tab compat — maps int to NavPage
|
||||
@@ -372,6 +431,12 @@ public:
|
||||
// Embedded daemon control
|
||||
bool startEmbeddedDaemon();
|
||||
void stopEmbeddedDaemon();
|
||||
// Stop the node specifically for a wallet switch, which MUST free the RPC port to relaunch on
|
||||
// -wallet=<name>. Unlike stopEmbeddedDaemon() (whose DisconnectOnly policy leaves an adopted/external
|
||||
// daemon running), this sends a graceful RPC "stop" — using our own connection creds, so only our
|
||||
// daemon obeys it — even to an adopted daemon, then waits (bounded) for the port to actually release.
|
||||
// Returns true once the RPC port is free (or we're shutting down).
|
||||
bool stopDaemonForWalletSwitch();
|
||||
bool isEmbeddedDaemonRunning() const;
|
||||
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||||
@@ -414,6 +479,10 @@ public:
|
||||
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
||||
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
||||
|
||||
// DragonX custom chat emoji (the ":drgx:" shortcode) — the mark recolored to the theme accent (like
|
||||
// the logo), re-rasterized on theme change. Used by the emoji picker tile + inline in chat bubbles.
|
||||
ImTextureID getDrgxEmojiTexture() const { return drgx_emoji_tex_; }
|
||||
|
||||
/**
|
||||
* @brief Reload theme images (background gradient + logo) from new paths
|
||||
* @param bgPath Path to background image override (empty = use default)
|
||||
@@ -421,6 +490,10 @@ public:
|
||||
*/
|
||||
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
||||
|
||||
// Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
|
||||
// the top of render() so the wizard, lock screen, and main header all show it.
|
||||
void ensureLogoTexture();
|
||||
|
||||
// Wizard / first-run
|
||||
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
||||
bool isFirstRun() const;
|
||||
@@ -436,6 +509,14 @@ public:
|
||||
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
|
||||
*/
|
||||
void restartDaemon();
|
||||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||
// stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from
|
||||
// that dialog). salvage: start the target node with -salvagewallet (repair a corrupt wallet).
|
||||
void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false, bool salvage = false);
|
||||
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
|
||||
void processWalletSwitchRevert();
|
||||
|
||||
// Wallet encryption helpers
|
||||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
||||
@@ -474,6 +555,12 @@ public:
|
||||
void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); }
|
||||
bool hasPinVault() const;
|
||||
|
||||
// Debug-options gate: does revealing the debug dropdown need re-authentication (a PIN vault or
|
||||
// an encrypted wallet)? And verify the entered PIN/passphrase — cb(ok) is invoked on the main
|
||||
// thread (PIN via the vault, else the wallet passphrase via RPC).
|
||||
bool debugGateRequiresAuth() const;
|
||||
void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);
|
||||
|
||||
/// @brief Check if RPC worker has queued results waiting to be processed
|
||||
bool hasPendingRPCResults() const;
|
||||
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||
@@ -486,6 +573,9 @@ public:
|
||||
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||
void copySecretToClipboard(const std::string& secret);
|
||||
void pumpSecretClipboardClear();
|
||||
// Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
|
||||
// Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
|
||||
void clearSecretClipboardIfArmed();
|
||||
bool isTransactionRefreshInProgress() const {
|
||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||
}
|
||||
@@ -527,10 +617,13 @@ private:
|
||||
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
|
||||
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
|
||||
// retry of a retry is reported as a real error.
|
||||
// background=true (autonomous chat sends / note-buffer splits): keep the single-flight + opid
|
||||
// accounting but DON'T raise the global "transaction in progress" UI (status is on the chat message).
|
||||
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||
const std::string& memo, const nlohmann::json& recipients,
|
||||
const char* traceLabel, bool markFeeGapRetry,
|
||||
std::function<void(bool, const std::string&)> callback);
|
||||
std::function<void(bool, const std::string&)> callback,
|
||||
bool background = false);
|
||||
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||
const std::string& txid);
|
||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||
@@ -549,6 +642,10 @@ private:
|
||||
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
||||
std::string transactionHistoryCacheWalletIdentity() const;
|
||||
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
|
||||
// wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
|
||||
// stamps last-opened + syncedHere (call once per connect).
|
||||
void updateWalletIndexForActiveWallet(bool markOpened);
|
||||
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
||||
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
|
||||
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
|
||||
@@ -604,11 +701,96 @@ private:
|
||||
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
||||
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
||||
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
||||
// Bumped by resetChatSession() on every wallet change; an in-flight identity fetch captures the
|
||||
// value at post time and its completion callback discards its (previous-wallet) secret if the id
|
||||
// no longer matches — so wallet A's seed can't be provisioned under wallet B via a stale job.
|
||||
int chat_session_generation_ = 0;
|
||||
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
|
||||
// Per-conversation "last seen" watermark (message timestamp) for unread tracking (Q1). Updated when a
|
||||
// thread is viewed (markChatConversationSeen); wiped in resetChatSession so unread doesn't leak across
|
||||
// wallets. In-memory only (resets on app restart).
|
||||
std::map<std::string, std::int64_t> chat_seen_watermark_;
|
||||
|
||||
// ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
|
||||
// Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
|
||||
// it's spendable again (lite: backend ANCHOR_OFFSET+1 = 5; full node: z_sendmany minconf = 1), so
|
||||
// rapid sends run out of verified funds. We keep a buffer of ~kChatBufferTarget small self-notes so a
|
||||
// burst of messages each spends a separate verified note, refilled from change + background self-
|
||||
// splits. Chat sends, self-splits and user sends share the wallet's single send channel; inflight_op_
|
||||
// + (lite) lite_send_callback_ / (full node) send_submissions_in_flight_+pending_opids_ serialize them
|
||||
// so exactly one send is ever outstanding. Implemented in app_network.cpp; pumped from update().
|
||||
enum class LiteOpKind { None, ChatSend, ContactRequest, Split, UserSend };
|
||||
struct LiteInflightOp {
|
||||
LiteOpKind kind = LiteOpKind::None;
|
||||
std::string echoLocalId; // ChatSend/ContactRequest: the echo to resolve when it completes
|
||||
int sessionGen = 0; // chat_session_generation_ snapshot at submit (stale-guard)
|
||||
double submittedAt = 0.0;
|
||||
};
|
||||
struct QueuedChatOp {
|
||||
LiteOpKind kind = LiteOpKind::ChatSend;
|
||||
chat::OutgoingChatMemos memos; // kept so a transient-funds retry re-broadcasts, never recomposes
|
||||
std::string echoLocalId;
|
||||
int sessionGen = 0;
|
||||
int retries = 0;
|
||||
};
|
||||
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
|
||||
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
|
||||
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
|
||||
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
|
||||
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
|
||||
int chat_verified_note_budget_ = 0;
|
||||
int chat_pipeline_note_count_ = 0; // verified + maturing self-notes (drives shouldSplit)
|
||||
std::uint64_t chat_verified_shielded_zat_ = 0; // verified shielded balance (split affordability)
|
||||
bool chat_note_model_seen_ = false; // saw a refresh/scan carrying per-note visibility
|
||||
// Single-split-in-flight guard: a self-split's OUTPUT notes are invisible until mined (~1 block), far
|
||||
// longer than any wall-clock cooldown — so we permit only ONE outstanding split and clear the flag when
|
||||
// the pipeline recovers (outputs mined) or a watchdog expires (a split that never mines mustn't wedge
|
||||
// refill forever). Prevents runaway splitting that would drain balance into fees.
|
||||
bool chat_split_outstanding_ = false;
|
||||
double chat_split_submitted_at_ = 0.0; // ImGui time the outstanding split was submitted (watchdog)
|
||||
// Full-node only: a coordinator-owned z_listunspent worker scan feeds the per-note counts (the shared
|
||||
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
|
||||
bool chat_note_scan_in_flight_ = false;
|
||||
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
|
||||
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
|
||||
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
|
||||
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
|
||||
std::int64_t chat_last_chain_height_ = 0;
|
||||
int chat_fast_scan_last_seen_ = -1; // dedup: last memo-note count logged by the 0-conf scan
|
||||
|
||||
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
||||
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
||||
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
|
||||
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
||||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||||
bool shouldSplitChatBuffer();
|
||||
bool broadcastSelfSplitLite(int noteCount); // lite: self-send minting noteCount reply-address notes
|
||||
bool broadcastSelfSplitNode(int noteCount); // full node: z_sendmany self-send minting noteCount notes
|
||||
void enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);
|
||||
void onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error);
|
||||
static bool isTransientVerifiedFundsError(const std::string& error);
|
||||
int chatConfsRequired() const; // verified-note confs threshold: 5 (lite) / 1 (full node)
|
||||
public:
|
||||
// Status-bar summary of the chat note buffer (empty when not applicable). Shown while the Chat tab is
|
||||
// active so the buffer's state (ready / building / sending) is visible.
|
||||
std::string chatBufferStatusText();
|
||||
private:
|
||||
public:
|
||||
// Total unread incoming chat messages across all conversations (for the sidebar badge). 0 when the
|
||||
// feature is off / no identity.
|
||||
int chatUnreadCount() const;
|
||||
// Mark a conversation read up to latestTs (called by the Chat tab while a thread is displayed).
|
||||
void markChatConversationSeen(const std::string& cid, std::int64_t latestTs);
|
||||
private:
|
||||
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
|
||||
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
|
||||
// feature is off, already provisioned, in flight, or unavailable.
|
||||
void maybeProvisionChatIdentity();
|
||||
// Drop the in-memory chat identity + decrypted message store, lock the chat DB, and re-arm
|
||||
// provisioning. MUST be called whenever the loaded wallet changes (switch / seed-migration adopt)
|
||||
// so wallet A's private chat can't surface — or be signed with A's keys — under wallet B.
|
||||
void resetChatSession();
|
||||
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
|
||||
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
||||
void maybeRemindSeedBackup();
|
||||
@@ -622,11 +804,23 @@ private:
|
||||
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
|
||||
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
|
||||
void provisionChatIdentityFromSecret(std::string secret);
|
||||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
|
||||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
|
||||
// A spendable z-address that can actually PAY the fee (balance >= fee), preferring the identity
|
||||
// reply address. The reply-to in the memo stays the identity address, so paying from a different
|
||||
// funded note is transport-transparent. Empty if no z-address can cover the fee.
|
||||
std::string chatPayFromZaddr(double fee) const;
|
||||
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
|
||||
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos); // returns true if submitted
|
||||
// Broadcast the memos and, when the async op resolves, flip the echo (echoLocalId) to Sent/Failed.
|
||||
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId); // true if submitted
|
||||
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
||||
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
||||
// Full-node 0-conf fast path: re-scan just the chat reply address at minconf=0 every refresh cycle
|
||||
// so incoming messages surface at mempool speed (before a block). Hidden conversations are skipped
|
||||
// (they still un-hide via the normal confirmed harvest). Self-gated; no-op without a chat identity.
|
||||
void fastScanChatMemos();
|
||||
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
|
||||
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
|
||||
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
|
||||
// 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.
|
||||
@@ -644,6 +838,19 @@ private:
|
||||
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
|
||||
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
|
||||
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
|
||||
// Per-exchange candle chart (refreshExchangeChart): which pair the loaded series is for, an
|
||||
// in-flight guard, and a slow refresh timer (candles move slowly, like the aggregate chart).
|
||||
std::string exchange_chart_key_; // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none)
|
||||
bool exchange_chart_fetch_in_flight_ = false;
|
||||
std::chrono::steady_clock::time_point exchange_chart_last_fetch_{};
|
||||
// Per-pair candle cache: switching back to a recently-viewed venue loads instantly (no re-fetch)
|
||||
// instead of overwriting the single active buffer. Keyed like exchange_chart_key_.
|
||||
struct ExchangeChartCache {
|
||||
std::vector<std::pair<std::time_t, double>> closeIntraday, closeDaily;
|
||||
std::vector<data::Candle> ohlcIntraday, ohlcDaily;
|
||||
std::chrono::steady_clock::time_point fetchedAt{};
|
||||
};
|
||||
std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
|
||||
util::AsyncTaskManager async_tasks_;
|
||||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||||
|
||||
@@ -663,6 +870,37 @@ private:
|
||||
// Daemon restart (e.g. after changing debug log categories)
|
||||
std::atomic<bool> daemon_restarting_{false};
|
||||
|
||||
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
|
||||
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
|
||||
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
|
||||
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
|
||||
std::atomic<bool> wallet_switch_failed_{false};
|
||||
// Distinguishes the failure reason for the revert message: true when the switch failed because the
|
||||
// running node wouldn't release the RPC port in time (not because the target wallet is bad).
|
||||
std::atomic<bool> switch_stop_failed_{false};
|
||||
// True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
|
||||
// instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
|
||||
// loop flags a revert so a broken wallet still can't stick.
|
||||
std::atomic<bool> wallet_switch_pending_confirm_{false};
|
||||
std::string wallet_switch_prev_file_;
|
||||
// Confirm-before-stop for a switch that would stop an ADOPTED (externally-running) node. switchToWallet
|
||||
// sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
|
||||
bool show_switch_stop_daemon_confirm_ = false;
|
||||
std::string pending_switch_wallet_file_;
|
||||
// Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
|
||||
// reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
|
||||
// dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
|
||||
// error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI.
|
||||
enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed };
|
||||
std::atomic<int> wallet_switch_phase_{0};
|
||||
std::atomic<bool> wallet_switch_dialog_open_{false};
|
||||
std::string wallet_switch_error_;
|
||||
double wallet_switch_started_time_ = 0.0; // ImGui::GetTime() captured on first progress frame (elapsed)
|
||||
// Set when a failed switch's node output indicates the target wallet is CORRUPT — the Failed modal then
|
||||
// offers a one-click "-salvagewallet" repair, retrying the switch to wallet_switch_target_file_.
|
||||
std::atomic<bool> switch_wallet_corrupt_{false};
|
||||
std::string wallet_switch_target_file_; // the wallet we were switching TO (for a salvage retry)
|
||||
|
||||
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||
std::atomic<int> pending_delete_result_{-1};
|
||||
@@ -689,10 +927,33 @@ private:
|
||||
bool seed_backup_no_mnemonic_ = false;
|
||||
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
||||
|
||||
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
||||
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
||||
// capable daemon can migrate; Incapable = the daemon lacks z_exportmnemonic (can't tell / can't
|
||||
// migrate). Reset to Unknown on wallet switch so it re-probes the new wallet.
|
||||
enum class WalletSeedStatus { Unknown, HasMnemonic, NoMnemonic, Incapable };
|
||||
WalletSeedStatus wallet_seed_status_ = WalletSeedStatus::Unknown;
|
||||
bool wallet_seed_status_in_flight_ = false;
|
||||
int wallet_seed_status_attempts_ = 0; // give up (Incapable) after a few transient probe failures
|
||||
void probeWalletSeedStatus(); // one-shot per connect; classifies the wallet's mnemonic status
|
||||
|
||||
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
|
||||
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
|
||||
bool show_seed_migration_ = false;
|
||||
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
|
||||
|
||||
// Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
|
||||
// skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
|
||||
// (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
|
||||
enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
|
||||
SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||||
bool seed_migration_precheck_started_ = false;
|
||||
bool seed_migration_balance_loaded_ = false; // Sweep step: distinguishes "0 funds" from "not loaded yet"
|
||||
bool seed_migration_nofunds_confirmed_ = false; // "replace my wallet" gate for the no-funds adopt
|
||||
|
||||
// "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
|
||||
bool show_daemon_update_prompt_ = false;
|
||||
unsigned long long daemon_update_bundled_size_ = 0; // bundled daemon size the prompt offers
|
||||
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
|
||||
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
|
||||
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
|
||||
@@ -723,11 +984,28 @@ private:
|
||||
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||||
|
||||
// Export/Import state
|
||||
std::string export_result_;
|
||||
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
|
||||
bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
|
||||
bool export_error_ = false; // last export returned no key (locked wallet / failure)
|
||||
char import_key_input_[512] = {0};
|
||||
std::string export_address_;
|
||||
std::string import_status_;
|
||||
bool import_success_ = false;
|
||||
bool import_key_reveal_ = false; // show the key in plaintext (default masked)
|
||||
bool import_in_progress_ = false; // an import + rescan is running (disable/spinner)
|
||||
std::string import_result_address_; // address imported on success (shown as a copy field)
|
||||
bool import_view_mode_ = false; // dialog mode: false = spending key, true = viewing key
|
||||
char import_key_scan_height_[16] = {0}; // optional rescan start height (shielded spend / viewing-key imports)
|
||||
// --- Sweep: import a spending key then move all its funds to one of your own addresses, instead
|
||||
// of keeping the key in the wallet (spending-key / non-view mode only). ---
|
||||
bool import_sweep_mode_ = false; // sweep instead of a plain import
|
||||
int sweep_dest_mode_ = 0; // destination: 0 = fresh shielded address, 1 = an existing one
|
||||
char sweep_dest_pick_[128] = {0}; // chosen existing destination (sweep_dest_mode_ == 1)
|
||||
enum class SweepStep { Idle, Running, Done, Error };
|
||||
SweepStep sweep_step_ = SweepStep::Idle;
|
||||
std::string sweep_status_; // progress / error text
|
||||
std::string sweep_txid_; // sweep transaction id (on success)
|
||||
std::string sweep_dest_shown_; // the destination address the funds were swept to
|
||||
std::string backup_status_;
|
||||
bool backup_success_ = false;
|
||||
|
||||
@@ -756,16 +1034,50 @@ private:
|
||||
bool screenshot_sweep_active_ = false;
|
||||
bool sweep_capture_this_frame_ = false;
|
||||
int sweep_skin_idx_ = 0;
|
||||
int sweep_page_idx_ = 0;
|
||||
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
|
||||
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||||
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
||||
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
|
||||
std::string sweep_dir_; // output folder for this sweep
|
||||
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
||||
std::string sweep_saved_skin_; // restore on completion
|
||||
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
||||
void updateScreenshotSweep(); // called at the top of render() while active
|
||||
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
|
||||
void applySweepTarget(); // apply current (skin,page/surface), path, arm settle
|
||||
|
||||
// --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
|
||||
// state forced on top). A tab is a surface with a null setup. ---
|
||||
struct SweepTarget {
|
||||
std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview"
|
||||
ui::NavPage page = ui::NavPage::Overview; // base tab under the surface
|
||||
std::function<void(App&)> setup; // reveal the surface (null = plain tab)
|
||||
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
|
||||
int settle = 4;// blur overlays override to 8
|
||||
};
|
||||
std::vector<SweepTarget> sweep_targets_;
|
||||
int sweep_target_idx_ = 0;
|
||||
bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
|
||||
// capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
|
||||
// RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
|
||||
bool capture_mode_ = false;
|
||||
void startSweepImpl(bool full);
|
||||
void buildSweepCatalog();
|
||||
void installDemoWalletData();
|
||||
void clearDemoWalletData();
|
||||
void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values
|
||||
void writeSweepManifest() const;
|
||||
// Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
|
||||
// plain struct because WalletState has reference-alias members and isn't copy-assignable.
|
||||
struct SweepStateSnapshot {
|
||||
bool valid = false;
|
||||
bool connected=false, warming_up=false, daemon_initializing=false;
|
||||
bool encrypted=false, locked=false, encryption_state_known=false;
|
||||
std::string warmup_status, warmup_description;
|
||||
SyncInfo sync;
|
||||
double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
|
||||
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
||||
std::vector<TransactionInfo> transactions;
|
||||
double market_price_usd=0;
|
||||
double market_change_24h=0;
|
||||
} sweep_state_snapshot_;
|
||||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||||
@@ -779,6 +1091,9 @@ private:
|
||||
int logo_h_ = 0;
|
||||
bool logo_loaded_ = false;
|
||||
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
||||
ImU32 logo_accent_ = 0; // theme accent the SVG logo was last rasterized with (re-render on change)
|
||||
ImTextureID drgx_emoji_tex_ = 0; // ":drgx:" custom chat emoji (themed to the accent, like the logo)
|
||||
int drgx_emoji_w_ = 0, drgx_emoji_h_ = 0;
|
||||
|
||||
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
||||
ImTextureID coin_logo_tex_ = 0;
|
||||
@@ -913,6 +1228,7 @@ private:
|
||||
std::unique_ptr<util::SecureVault> vault_;
|
||||
data::TransactionHistoryCache transaction_history_cache_;
|
||||
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
|
||||
data::WalletIndex wallet_index_; // per-wallet metadata cache (wallets.json); populated after each load
|
||||
std::string pending_transaction_history_cache_passphrase_;
|
||||
bool transaction_history_cache_loaded_ = false;
|
||||
|
||||
@@ -979,12 +1295,16 @@ private:
|
||||
void renderBackupDialog();
|
||||
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
||||
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
|
||||
void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
|
||||
void maybeOfferDaemonUpdate(); // at startup, flag the prompt if a newer daemon is bundled
|
||||
void renderDaemonUpdatePrompt(); // "a newer node is bundled — update the installed daemon?"
|
||||
void renderFirstRunWizard();
|
||||
void renderLockScreen();
|
||||
void renderEncryptWalletDialog();
|
||||
void renderDecryptWalletDialog();
|
||||
void renderPinDialogs();
|
||||
void renderAntivirusHelpDialog();
|
||||
void renderSwitchStopDaemonDialog(); // confirm before stopping an adopted node to switch wallets
|
||||
void processDeferredEncryption();
|
||||
|
||||
// Private methods - connection
|
||||
|
||||
1568
src/app_network.cpp
1568
src/app_network.cpp
File diff suppressed because it is too large
Load Diff
@@ -241,16 +241,23 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
|
||||
// the daemon is restarting.
|
||||
connection_status_ = TR("restarting_after_encryption");
|
||||
}
|
||||
// Gate the main-loop reconnect while the daemon is down (so tryConnect can't hit the stopped
|
||||
// node or start a duplicate) and block a concurrent wallet switch/rescan, which check this flag.
|
||||
daemon_restarting_ = true;
|
||||
// Give daemon a moment to shut down, then restart
|
||||
// (do this off the main thread to avoid stalling the UI)
|
||||
async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) {
|
||||
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (token.cancelled() || shutting_down_) return;
|
||||
stopEmbeddedDaemon();
|
||||
if (token.cancelled() || shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
// tryConnect will be called by the update loop
|
||||
// daemon_restarting_ MUST be cleared on every exit (incl. an early-out or a throw), else it
|
||||
// stays stuck true and wedges reconnect + all future switch/rescan/encryption operations.
|
||||
try {
|
||||
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (!token.cancelled() && !shutting_down_) {
|
||||
stopEmbeddedDaemon();
|
||||
if (!token.cancelled() && !shutting_down_) startEmbeddedDaemon();
|
||||
}
|
||||
} catch (...) {}
|
||||
daemon_restarting_ = false; // re-arm reconnect (tryConnect runs from the update loop)
|
||||
});
|
||||
} else {
|
||||
ui::Notifications::instance().warning(
|
||||
@@ -593,6 +600,7 @@ void App::refreshWalletEncryptionState() {
|
||||
// ===========================================================================
|
||||
|
||||
void App::checkAutoLock() {
|
||||
if (capture_mode_) return; // don't auto-lock while a UI sweep forces the encrypted/locked state
|
||||
if (!state_.isEncrypted() || state_.isLocked()) return;
|
||||
|
||||
// Don't auto-lock while mining — mining is a long-running intentional
|
||||
@@ -823,12 +831,19 @@ void App::renderLockScreen() {
|
||||
MarkBlurOverlayDrawn(nullptr); // suppress foreground theme-effect bleed + re-capture on unlock
|
||||
}
|
||||
|
||||
// Card
|
||||
// Card. The lock screen is fully hand-drawn (dl->AddText at manual y offsets), so — unlike
|
||||
// BeginOverlayDialog cards — nothing scales automatically. Multiply every geometry literal
|
||||
// (card size, logo, input/button widths, vertical gaps) by dpiScale() so the card grows with
|
||||
// OS DPI / font scale instead of stranding a native-size card in a large window. The font
|
||||
// metrics (LegacySize / CalcTextSize) are already DPI-scaled via the atlas, so leave those.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
const auto& S = ui::schema::UI();
|
||||
float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f);
|
||||
float cardH = S.drawElement("screens.lock-screen", "card").height;
|
||||
if (cardW <= 0) cardW = 400.0f;
|
||||
if (cardH <= 0) cardH = 320.0f;
|
||||
cardW *= dp;
|
||||
cardH *= dp;
|
||||
|
||||
float cardX = winPos.x + (winSize.x - cardW) * 0.5f;
|
||||
float cardY = winPos.y + (winSize.y - cardH) * 0.5f;
|
||||
@@ -844,17 +859,17 @@ void App::renderLockScreen() {
|
||||
cardGlass.borderWidth = 1.0f;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||
|
||||
float cy = cardY + 24.0f;
|
||||
float cy = cardY + 24.0f * dp;
|
||||
|
||||
// Logo
|
||||
float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f);
|
||||
float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f) * dp;
|
||||
if (logo_tex_ != 0) {
|
||||
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
||||
float logoW = logoSize * aspect;
|
||||
float logoX = cardX + (cardW - logoW) * 0.5f;
|
||||
dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize));
|
||||
}
|
||||
cy += logoSize + 16.0f;
|
||||
cy += logoSize + 16.0f * dp;
|
||||
|
||||
// Title
|
||||
ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font);
|
||||
@@ -868,7 +883,7 @@ void App::renderLockScreen() {
|
||||
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
|
||||
dl->AddText(titleFont, titleFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
|
||||
cy += ts.y + 20.0f;
|
||||
cy += ts.y + 20.0f * dp;
|
||||
}
|
||||
|
||||
// Lockout timer
|
||||
@@ -881,11 +896,11 @@ void App::renderLockScreen() {
|
||||
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
|
||||
cy += captionFont->LegacySize + 12.0f;
|
||||
cy += captionFont->LegacySize + 12.0f * dp;
|
||||
}
|
||||
|
||||
// Check if PIN vault is available
|
||||
bool hasPinVault = vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled();
|
||||
// Check if PIN vault is available (per-wallet vault presence; not the global getPinEnabled flag).
|
||||
bool hasPinVault = vault_ && vault_->hasVault();
|
||||
|
||||
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
|
||||
if (hasPinVault) {
|
||||
@@ -906,7 +921,7 @@ void App::renderLockScreen() {
|
||||
ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon);
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText);
|
||||
cy += std::max(iconSize.y, textSize.y) + 8.0f;
|
||||
cy += std::max(iconSize.y, textSize.y) + 8.0f * dp;
|
||||
|
||||
// Switch link
|
||||
ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel);
|
||||
@@ -923,7 +938,7 @@ void App::renderLockScreen() {
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(switchX, cy), ui::material::Primary(), switchLabel);
|
||||
ImGui::PopStyleColor();
|
||||
cy += captionFont->LegacySize + 12.0f;
|
||||
cy += captionFont->LegacySize + 12.0f * dp;
|
||||
} else {
|
||||
// No PIN vault — don't show toggle, force passphrase mode
|
||||
lock_use_pin_ = false;
|
||||
@@ -932,6 +947,7 @@ void App::renderLockScreen() {
|
||||
// Input field
|
||||
float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f);
|
||||
if (inputW <= 0) inputW = 320.0f;
|
||||
inputW *= dp;
|
||||
float inputX = cardX + (cardW - inputW) * 0.5f;
|
||||
|
||||
bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_;
|
||||
@@ -941,7 +957,7 @@ void App::renderLockScreen() {
|
||||
// PIN input
|
||||
ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
|
||||
ImGui::PushItemWidth(inputW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
|
||||
ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal;
|
||||
if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
submitted = ImGui::InputText("##lock_pin", lock_pin_buf_,
|
||||
@@ -952,7 +968,7 @@ void App::renderLockScreen() {
|
||||
// Passphrase input (original)
|
||||
ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
|
||||
ImGui::PushItemWidth(inputW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
|
||||
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password;
|
||||
if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_,
|
||||
@@ -964,9 +980,9 @@ void App::renderLockScreen() {
|
||||
// default frame is subtle on the glass card, so this shows the field is active and ready to type.
|
||||
if (ImGui::IsItemActive() || ImGui::IsItemFocused()) {
|
||||
dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
|
||||
ui::material::Primary(), 6.0f, 0, 2.0f);
|
||||
ui::material::Primary(), 6.0f * dp, 0, 2.0f * dp);
|
||||
}
|
||||
cy += 40.0f + 12.0f;
|
||||
cy += (40.0f + 12.0f) * dp;
|
||||
|
||||
// Focus the input when the lock screen first appears.
|
||||
// IsWindowAppearing() does not work here because the lock screen is
|
||||
@@ -984,13 +1000,13 @@ void App::renderLockScreen() {
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
||||
lock_error_msg_.c_str());
|
||||
cy += captionFont->LegacySize + 8.0f;
|
||||
cy += captionFont->LegacySize + 8.0f * dp;
|
||||
}
|
||||
|
||||
// "Unlocking..." feedback while worker thread is running
|
||||
// Always reserve the vertical space so the button doesn't shift.
|
||||
{
|
||||
float rowH = captionFont->LegacySize + 8.0f;
|
||||
float rowH = captionFont->LegacySize + 8.0f * dp;
|
||||
if (lock_unlock_in_progress_) {
|
||||
// Animated spinner dots
|
||||
char msg[64];
|
||||
@@ -1008,13 +1024,15 @@ void App::renderLockScreen() {
|
||||
float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height;
|
||||
if (unlockW <= 0) unlockW = 320.0f;
|
||||
if (unlockH <= 0) unlockH = 44.0f;
|
||||
unlockW *= dp;
|
||||
unlockH *= dp;
|
||||
float unlockX = cardX + (cardW - unlockW) * 0.5f;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(unlockX, cy));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!canSubmit);
|
||||
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
|
||||
ImGui::EndDisabled();
|
||||
@@ -1114,35 +1132,33 @@ void App::renderEncryptWalletDialog() {
|
||||
// Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
|
||||
if (show_encrypt_dialog_) {
|
||||
const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
|
||||
? "Quick-Unlock PIN" : "Encrypt Wallet";
|
||||
? TR("wiz_pin_title") : TR("settings_encrypt_wallet");
|
||||
|
||||
// Prevent closing via X button while encrypting
|
||||
bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
|
||||
bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;
|
||||
|
||||
if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = dlgTitle; ov.p_open = pOpen;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 480.0f; ov.idSuffix = "encrypt";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
// ---- Phase 1: Passphrase entry ----
|
||||
if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
||||
ImGui::TextWrapped(ICON_MD_WARNING
|
||||
" If you lose your passphrase, you lose access to your funds.");
|
||||
ImGui::PopStyleColor();
|
||||
DialogWarningHeader(TR("wiz_encrypt_warning"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped("Encrypting your wallet protects your private keys "
|
||||
"with a passphrase. After encryption, the daemon will restart.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("enc_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Passphrase:");
|
||||
ImGui::TextUnformatted(TR("wiz_passphrase"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm:");
|
||||
ImGui::TextUnformatted(TR("enc_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
@@ -1163,7 +1179,7 @@ void App::renderEncryptWalletDialog() {
|
||||
}
|
||||
int classes = (int)hasDigit + (int)hasLower + (int)hasUpper + (int)hasSymbol;
|
||||
|
||||
const char* strengthLabel = "Weak";
|
||||
const char* strengthLabel = TR("wiz_strength_weak");
|
||||
ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
|
||||
float strengthPct = 0.25f;
|
||||
int tier = 0; // 0=Weak, 1=Fair, 2=Good, 3=Strong
|
||||
@@ -1172,9 +1188,9 @@ void App::renderEncryptWalletDialog() {
|
||||
else if (len >= 8) tier = 1;
|
||||
// Downgrade one tier when only a single character class is used.
|
||||
if (classes <= 1 && tier > 0) tier -= 1;
|
||||
if (tier == 3) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
||||
else if (tier == 2) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
||||
else if (tier == 1) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||
if (tier == 3) { strengthLabel = TR("wiz_strength_strong"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
|
||||
else if (tier == 2) { strengthLabel = TR("wiz_strength_good"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
|
||||
else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
|
||||
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
float barH = 4.0f;
|
||||
@@ -1186,7 +1202,7 @@ void App::renderEncryptWalletDialog() {
|
||||
dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH),
|
||||
ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f);
|
||||
ImGui::Dummy(ImVec2(barW, barH));
|
||||
ImGui::Text("Strength: %s", strengthLabel);
|
||||
ImGui::Text(TR("wiz_strength"), strengthLabel);
|
||||
}
|
||||
|
||||
if (!encrypt_status_.empty()) {
|
||||
@@ -1199,7 +1215,7 @@ void App::renderEncryptWalletDialog() {
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||
if (ui::material::TactileButton("Encrypt Wallet", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_encrypt_wallet"), ImVec2(btnW, 40))) {
|
||||
std::string pass(encrypt_pass_buf_);
|
||||
enc_dlg_saved_passphrase_ = pass;
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
@@ -1211,7 +1227,7 @@ void App::renderEncryptWalletDialog() {
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||
show_encrypt_dialog_ = false;
|
||||
@@ -1220,7 +1236,7 @@ void App::renderEncryptWalletDialog() {
|
||||
// ---- Phase 2: Encrypting in progress ----
|
||||
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
||||
const char* statusTitle = encrypt_in_progress_
|
||||
? "Encrypting wallet..." : encrypt_status_.c_str();
|
||||
? TR("enc_encrypting") : encrypt_status_.c_str();
|
||||
ImGui::Text("%s", statusTitle);
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -1247,7 +1263,7 @@ void App::renderEncryptWalletDialog() {
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "Please wait, do not close the application.");
|
||||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", TR("enc_wait"));
|
||||
|
||||
// Transition to PIN phase when encryption finishes successfully
|
||||
if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
|
||||
@@ -1257,23 +1273,20 @@ void App::renderEncryptWalletDialog() {
|
||||
// ---- Phase 3: PIN setup (after successful encryption) ----
|
||||
} else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1));
|
||||
ImGui::Text(ICON_MD_CHECK_CIRCLE " Wallet encrypted successfully!");
|
||||
ImGui::Text(ICON_MD_CHECK_CIRCLE " %s", TR("enc_success"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped("A 4-8 digit PIN lets you unlock your wallet "
|
||||
"without typing the full passphrase every time.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("enc_pin_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("PIN (4-8 digits):");
|
||||
ImGui::TextUnformatted(TR("wiz_pin_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm PIN:");
|
||||
ImGui::TextUnformatted(TR("wiz_pin_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_,
|
||||
sizeof(enc_dlg_pin_confirm_buf_),
|
||||
@@ -1293,7 +1306,7 @@ void App::renderEncryptWalletDialog() {
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
|
||||
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Set PIN", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
enc_dlg_pin_status_.clear();
|
||||
std::string savedPass = enc_dlg_saved_passphrase_;
|
||||
@@ -1306,7 +1319,7 @@ void App::renderEncryptWalletDialog() {
|
||||
settings_->setPinEnabled(true);
|
||||
settings_->save();
|
||||
pin_in_progress_ = false;
|
||||
ui::Notifications::instance().info("PIN set successfully");
|
||||
ui::Notifications::instance().info(TR("enc_pin_set_ok"));
|
||||
// Clean up
|
||||
if (!enc_dlg_saved_passphrase_.empty()) {
|
||||
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
||||
@@ -1317,20 +1330,20 @@ void App::renderEncryptWalletDialog() {
|
||||
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
||||
show_encrypt_dialog_ = false;
|
||||
} else {
|
||||
enc_dlg_pin_status_ = "Failed to create PIN vault";
|
||||
enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
enc_dlg_pin_status_ = "Failed to create PIN vault";
|
||||
enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
|
||||
pin_in_progress_ = false;
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Skip", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW, 40))) {
|
||||
if (!enc_dlg_saved_passphrase_.empty()) {
|
||||
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
||||
enc_dlg_saved_passphrase_.size());
|
||||
@@ -1339,8 +1352,7 @@ void App::renderEncryptWalletDialog() {
|
||||
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
|
||||
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
||||
show_encrypt_dialog_ = false;
|
||||
ui::Notifications::instance().info(
|
||||
"PIN skipped. You can set one later in Settings.");
|
||||
ui::Notifications::instance().info(TR("enc_pin_skipped"));
|
||||
}
|
||||
}
|
||||
EndOverlayDialog();
|
||||
@@ -1356,21 +1368,25 @@ void App::renderEncryptWalletDialog() {
|
||||
|
||||
// Change passphrase dialog
|
||||
if (show_change_passphrase_) {
|
||||
if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) {
|
||||
|
||||
ImGui::Text("Current Passphrase:");
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("change_pass_title"); ov.p_open = &show_change_passphrase_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("New Passphrase:");
|
||||
ImGui::TextUnformatted(TR("change_pass_new"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm New:");
|
||||
ImGui::TextUnformatted(TR("change_pass_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
@@ -1385,13 +1401,22 @@ void App::renderEncryptWalletDialog() {
|
||||
strlen(change_new_pass_buf_) >= 8 &&
|
||||
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;
|
||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||
if (ui::material::TactileButton("Change Passphrase", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(-1, 40))) {
|
||||
changePassphrase(std::string(change_old_pass_buf_),
|
||||
std::string(change_new_pass_buf_));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Wipe the passphrase buffers if the dialog was dismissed (X / Esc /
|
||||
// outside-click) without submitting. The success path already zeroes
|
||||
// them in changePassphrase(); the failure path keeps them for retry.
|
||||
if (!show_change_passphrase_) {
|
||||
memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_));
|
||||
memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_));
|
||||
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1416,26 +1441,21 @@ void App::renderDecryptWalletDialog() {
|
||||
bool canClose = wallet_security_workflow_.canClose();
|
||||
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
|
||||
|
||||
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("decrypt_title"); ov.p_open = pOpen;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 480.0f; ov.idSuffix = "decrypt";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
// ---- Phase 0: Passphrase entry ----
|
||||
if (decryptState.phase == DecryptPhase::PassphraseEntry) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
||||
ImGui::TextWrapped(ICON_MD_WARNING
|
||||
" This will remove encryption from your wallet. "
|
||||
"Your private keys will be stored unprotected on disk.");
|
||||
ImGui::PopStyleColor();
|
||||
DialogWarningHeader(TR("decrypt_warning"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"The wallet will be exported, the daemon restarted with a fresh "
|
||||
"unencrypted wallet, and all keys re-imported. This may take "
|
||||
"several minutes depending on wallet size.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", TR("decrypt_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current Passphrase:");
|
||||
ImGui::TextUnformatted(TR("change_pass_current"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
|
||||
sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
|
||||
@@ -1451,7 +1471,7 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
||||
if (ui::material::TactileButton("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
std::string passphrase(decrypt_pass_buf_);
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
||||
@@ -1642,7 +1662,7 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
@@ -1651,11 +1671,11 @@ void App::renderDecryptWalletDialog() {
|
||||
} else if (decryptState.phase == DecryptPhase::Working) {
|
||||
// Step checklist
|
||||
const char* stepLabels[] = {
|
||||
"Unlocking wallet",
|
||||
"Exporting wallet keys",
|
||||
"Stopping daemon",
|
||||
"Backing up encrypted wallet",
|
||||
"Restarting daemon"
|
||||
TR("decrypt_step_unlock"),
|
||||
TR("decrypt_step_export"),
|
||||
TR("decrypt_step_stop"),
|
||||
TR("decrypt_step_backup"),
|
||||
TR("decrypt_step_restart")
|
||||
};
|
||||
const int numSteps = 5;
|
||||
|
||||
@@ -1732,10 +1752,9 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
// Step-specific hints
|
||||
if (decryptState.step == DecryptStep::RestartDaemon) {
|
||||
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_wait_restart"));
|
||||
} else {
|
||||
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
||||
"and re-importing. This may take several minutes.");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_wait_general"));
|
||||
}
|
||||
|
||||
// Total elapsed
|
||||
@@ -1752,15 +1771,13 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!");
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "%s", TR("decrypt_success_title"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped(
|
||||
"Your wallet is now unencrypted. A backup of the encrypted wallet "
|
||||
"was saved as wallet.dat.encrypted.bak in your data directory.");
|
||||
ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
|
||||
@@ -1770,23 +1787,31 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed");
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", TR("decrypt_error_title"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", decryptState.status.c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ui::material::TactileButton("Try Again", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
|
||||
wallet_security_workflow_.reset();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) {
|
||||
if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
|
||||
show_decrypt_dialog_ = false;
|
||||
}
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
|
||||
// Wipe the passphrase buffer if the dialog was dismissed (X / Esc / outside-
|
||||
// click) without submitting. The submit and Cancel paths already memset it;
|
||||
// this covers the BlurFloat dismiss paths. (canClose is false during Working,
|
||||
// so an in-flight decrypt cannot be dismissed here.)
|
||||
if (!show_decrypt_dialog_) {
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -1798,29 +1823,28 @@ void App::renderPinDialogs() {
|
||||
|
||||
// ---- Set PIN dialog ----
|
||||
if (show_pin_setup_) {
|
||||
if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) {
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Set a 4-8 digit PIN for quick wallet unlock. "
|
||||
"Your wallet passphrase will be encrypted with this PIN "
|
||||
"and stored locally.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("settings_set_pin"); ov.p_open = &show_pin_setup_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 420.0f; ov.idSuffix = "pinsetup";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextWrapped("%s", TR("pin_setup_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Wallet Passphrase:");
|
||||
ImGui::TextUnformatted(TR("pin_wallet_passphrase"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_),
|
||||
ImGuiInputTextFlags_Password);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("New PIN (4-8 digits):");
|
||||
ImGui::TextUnformatted(TR("pin_new_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm PIN:");
|
||||
ImGui::TextUnformatted(TR("wiz_pin_confirm"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
@@ -1837,7 +1861,7 @@ void App::renderPinDialogs() {
|
||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Set PIN", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying passphrase...";
|
||||
|
||||
@@ -1893,30 +1917,39 @@ void App::renderPinDialogs() {
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
|
||||
// outside-click) without submitting. The submit path already memsets them.
|
||||
if (!show_pin_setup_) {
|
||||
memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_));
|
||||
memset(pin_buf_, 0, sizeof(pin_buf_));
|
||||
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Change PIN dialog ----
|
||||
if (show_pin_change_) {
|
||||
if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) {
|
||||
|
||||
ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("settings_change_pin"); ov.p_open = &show_pin_change_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 420.0f; ov.idSuffix = "pinchange";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextWrapped("%s", TR("pin_change_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current PIN:");
|
||||
ImGui::TextUnformatted(TR("pin_current_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("New PIN (4-8 digits):");
|
||||
ImGui::TextUnformatted(TR("pin_new_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::Text("Confirm New PIN:");
|
||||
ImGui::TextUnformatted(TR("pin_confirm_new_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
@@ -1933,7 +1966,7 @@ void App::renderPinDialogs() {
|
||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Change PIN", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Changing PIN...";
|
||||
std::string oldPin(pin_old_buf_);
|
||||
@@ -1966,20 +1999,26 @@ void App::renderPinDialogs() {
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the PIN buffers if the dialog was dismissed without submitting.
|
||||
if (!show_pin_change_) {
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
memset(pin_buf_, 0, sizeof(pin_buf_));
|
||||
memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Remove PIN dialog ----
|
||||
if (show_pin_remove_) {
|
||||
if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) {
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Enter your current PIN to confirm removal. "
|
||||
"You will need to use your full passphrase to unlock.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("settings_remove_pin"); ov.p_open = &show_pin_remove_;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 400.0f; ov.idSuffix = "pinremove";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
|
||||
ImGui::TextWrapped("%s", TR("pin_remove_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Current PIN:");
|
||||
ImGui::TextUnformatted(TR("pin_current_label"));
|
||||
ImGui::PushItemWidth(-1);
|
||||
ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_),
|
||||
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
|
||||
@@ -1992,7 +2031,7 @@ void App::renderPinDialogs() {
|
||||
ImGui::Spacing();
|
||||
bool valid = strlen(pin_old_buf_) >= 4;
|
||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||
if (ui::material::TactileButton("Remove PIN", ImVec2(-1, 40))) {
|
||||
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
||||
pin_in_progress_ = true;
|
||||
pin_status_ = "Verifying PIN...";
|
||||
std::string oldPin(pin_old_buf_);
|
||||
@@ -2029,6 +2068,10 @@ void App::renderPinDialogs() {
|
||||
ImGui::EndDisabled();
|
||||
EndOverlayDialog();
|
||||
}
|
||||
// Wipe the PIN buffer if the dialog was dismissed without submitting.
|
||||
if (!show_pin_remove_) {
|
||||
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
810
src/app_sweep.cpp
Normal file
810
src/app_sweep.cpp
Normal file
@@ -0,0 +1,810 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Screenshot sweep: capture the UI under every skin. Two modes share one state machine:
|
||||
// - startScreenshotSweep() — the legacy tab-only sweep (<config>/screenshots).
|
||||
// - startFullUiSweep() — ALSO drives every modal / dialog / multi-step flow / state overlay
|
||||
// into view (offline, with injected demo data, firing no live ops) and captures each under
|
||||
// every skin (<config>/screenshots-full/<surface>/<skin>.png + index.md).
|
||||
// The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces
|
||||
// a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the
|
||||
// settled frame and calls onScreenshotCaptured() to advance.
|
||||
|
||||
#include "app.h"
|
||||
|
||||
#include "config/settings.h"
|
||||
#include "data/address_book.h"
|
||||
#include "ui/schema/skin_manager.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/send_tab.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/export_transactions_dialog.h"
|
||||
#include "ui/windows/export_all_keys_dialog.h"
|
||||
#include "ui/windows/bootstrap_download_dialog.h"
|
||||
#include "ui/windows/daemon_download_dialog.h"
|
||||
#include "ui/windows/xmrig_download_dialog.h"
|
||||
#include "ui/windows/qr_popup_dialog.h"
|
||||
#include "ui/windows/request_payment_dialog.h"
|
||||
#include "ui/windows/validate_address_dialog.h"
|
||||
#include "ui/windows/address_label_dialog.h"
|
||||
#include "ui/windows/shield_dialog.h"
|
||||
#include "ui/windows/address_transfer_dialog.h"
|
||||
#include "ui/windows/key_export_dialog.h"
|
||||
#include "ui/windows/contacts_tab.h"
|
||||
#include "ui/pages/settings_page.h"
|
||||
#include "util/platform.h"
|
||||
#include "wallet/wallet_capabilities.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h" // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
|
||||
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
|
||||
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
|
||||
int g_pfStyleSnapshot = 0;
|
||||
bool g_pfSnapshotValid = false;
|
||||
std::vector<double> g_marketHistorySnapshot; // real price history, restored at sweep end
|
||||
|
||||
// Filesystem-safe one-word tab name.
|
||||
const char* sweepPageName(ui::NavPage page)
|
||||
{
|
||||
switch (page) {
|
||||
case ui::NavPage::Overview: return "overview";
|
||||
case ui::NavPage::Send: return "send";
|
||||
case ui::NavPage::Receive: return "receive";
|
||||
case ui::NavPage::History: return "history";
|
||||
case ui::NavPage::Contacts: return "contacts";
|
||||
case ui::NavPage::Chat: return "chat";
|
||||
case ui::NavPage::Mining: return "mining";
|
||||
case ui::NavPage::Market: return "market";
|
||||
case ui::NavPage::Console: return "console";
|
||||
case ui::NavPage::LiteConsole: return "console";
|
||||
case ui::NavPage::Peers: return "network";
|
||||
case ui::NavPage::LiteNetwork: return "network";
|
||||
case ui::NavPage::Explorer: return "explorer";
|
||||
case ui::NavPage::Settings: return "settings";
|
||||
default: return "page";
|
||||
}
|
||||
}
|
||||
|
||||
bool sweepPageEnabled(ui::NavPage page)
|
||||
{
|
||||
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
|
||||
}
|
||||
|
||||
// Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display).
|
||||
const char* kDemoMnemonic =
|
||||
"select milk exit banana type alcohol comic moral drama federal just green "
|
||||
"elevator render stumble lesson convince organ category caution panther misery pelican immune";
|
||||
const char* kDemoZAddr =
|
||||
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
|
||||
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
|
||||
|
||||
// Contacts-view sweep: seed a few demo contacts (Z + T types, some global) so the Cards/List/Table
|
||||
// modes render with data, and restore the real book afterward. Uses sweepSetEntries (no disk write),
|
||||
// so the user's persisted address book is never touched even if the sweep is interrupted.
|
||||
std::vector<data::AddressBookEntry> s_contactsSweepBackup;
|
||||
void seedSweepContacts(App& a)
|
||||
{
|
||||
s_contactsSweepBackup = a.addressBook().entries();
|
||||
data::AddressBookEntry e1("drgx pool payout address", kDemoZAddr, "mining pool payouts"); // Z, global
|
||||
e1.avatar = "icon:account_balance"; // icon avatar (verifies the icon-badge render path)
|
||||
data::AddressBookEntry e2("exchange deposit", "t1DemoTransparentAddressForUiSweep00000", ""); // T
|
||||
// Scope to the active wallet so it's visible (the tab hides contacts not in the active wallet);
|
||||
// non-empty hash also keeps it non-global -> no globe badge, so the list shows a global/non-global mix.
|
||||
e2.scope = a.activeWalletIdentityHash();
|
||||
data::AddressBookEntry e3("cold savings",
|
||||
"zs1sweepdemocoldsavingsaddressforuicapture0000000000000000000000000000", "long-term storage"); // Z, global
|
||||
a.addressBook().sweepSetEntries({ e1, e2, e3 });
|
||||
}
|
||||
void restoreSweepContacts(App& a)
|
||||
{
|
||||
a.addressBook().sweepSetEntries(s_contactsSweepBackup);
|
||||
s_contactsSweepBackup.clear();
|
||||
if (a.settings()) a.settings()->setContactsViewMode(0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string App::screenshotDir() const
|
||||
{
|
||||
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
|
||||
}
|
||||
|
||||
std::string App::screenshotFullDir() const
|
||||
{
|
||||
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string();
|
||||
}
|
||||
|
||||
// ── Demo data (full sweep only): make every data-dependent screen render offline ─────────────
|
||||
void App::installDemoWalletData()
|
||||
{
|
||||
// Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases).
|
||||
auto& s = sweep_state_snapshot_;
|
||||
s.connected = state_.connected; s.warming_up = state_.warming_up;
|
||||
s.daemon_initializing = state_.daemon_initializing;
|
||||
s.encrypted = state_.encrypted; s.locked = state_.locked;
|
||||
s.encryption_state_known = state_.encryption_state_known;
|
||||
s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description;
|
||||
s.sync = state_.sync;
|
||||
s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance;
|
||||
s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance;
|
||||
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
|
||||
s.transactions = state_.transactions;
|
||||
s.market_price_usd = state_.market.price_usd;
|
||||
s.market_change_24h = state_.market.change_24h;
|
||||
s.valid = true;
|
||||
|
||||
applyHealthyDemoState();
|
||||
state_.sync.blocks = state_.sync.headers = 3124322;
|
||||
state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
|
||||
// Legacy (pre-seed) wallet so the Settings "Migrate to seed" button glows in the sweep.
|
||||
wallet_seed_status_ = WalletSeedStatus::NoMnemonic;
|
||||
|
||||
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
||||
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
||||
state_.market.price_usd = 0.01336200;
|
||||
state_.market.change_24h = 5.24000000;
|
||||
// Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
|
||||
g_marketHistorySnapshot = state_.market.price_history;
|
||||
{
|
||||
std::vector<double> hist;
|
||||
for (int i = 0; i < 48; i++) {
|
||||
double t = static_cast<double>(i);
|
||||
hist.push_back(0.01250 + 0.0000180 * t
|
||||
+ 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
|
||||
}
|
||||
state_.market.price_history = hist;
|
||||
}
|
||||
|
||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
};
|
||||
auto taddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
};
|
||||
state_.z_addresses = {
|
||||
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
||||
zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""),
|
||||
};
|
||||
state_.t_addresses = {
|
||||
taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"),
|
||||
taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""),
|
||||
};
|
||||
state_.rebuildAddressList();
|
||||
|
||||
auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf,
|
||||
const char* addr, const char* memo) {
|
||||
TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts;
|
||||
t.confirmations = conf; t.address = addr; t.memo = memo; return t;
|
||||
};
|
||||
state_.transactions = {
|
||||
tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42,
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"),
|
||||
tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000,
|
||||
1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""),
|
||||
tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000,
|
||||
1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""),
|
||||
tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000,
|
||||
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
|
||||
};
|
||||
|
||||
// Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
|
||||
// the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
|
||||
// here), and clearDemoWalletData() restores them at sweep end.
|
||||
if (settings_) {
|
||||
g_pfSnapshot = settings_->getPortfolioEntries();
|
||||
g_pfStyleSnapshot = settings_->getPortfolioStyle();
|
||||
g_pfSnapshotValid = true;
|
||||
auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
|
||||
bool drgx, bool value, bool ch, bool spark) {
|
||||
config::Settings::PortfolioEntry e;
|
||||
e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
|
||||
e.addresses = { addr }; e.priceBasis = 0;
|
||||
e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
|
||||
return e;
|
||||
};
|
||||
settings_->setPortfolioEntries({
|
||||
grp("Savings", "savings", 0xFFFF9D4Fu,
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
|
||||
grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
|
||||
"t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
|
||||
grp("Cold storage", "diamond", 0xFFFF7BB0u,
|
||||
"zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
|
||||
});
|
||||
settings_->setPortfolioStyle(0);
|
||||
}
|
||||
|
||||
seedChatDemoData();
|
||||
}
|
||||
|
||||
void App::applyHealthyDemoState()
|
||||
{
|
||||
state_.connected = true;
|
||||
state_.warming_up = false;
|
||||
state_.daemon_initializing = false;
|
||||
state_.encryption_state_known = true;
|
||||
state_.encrypted = false;
|
||||
state_.locked = false;
|
||||
state_.warmup_status.clear();
|
||||
state_.warmup_description.clear();
|
||||
// Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set
|
||||
// wizard_phase_ themselves and restore None on teardown).
|
||||
wizard_phase_ = WizardPhase::None;
|
||||
}
|
||||
|
||||
void App::clearDemoWalletData()
|
||||
{
|
||||
auto& s = sweep_state_snapshot_;
|
||||
if (!s.valid) return;
|
||||
wallet_seed_status_ = WalletSeedStatus::Unknown; // re-probe on the next real connect
|
||||
state_.connected = s.connected; state_.warming_up = s.warming_up;
|
||||
state_.daemon_initializing = s.daemon_initializing;
|
||||
state_.encrypted = s.encrypted; state_.locked = s.locked;
|
||||
state_.encryption_state_known = s.encryption_state_known;
|
||||
state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description;
|
||||
state_.sync = s.sync;
|
||||
state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance;
|
||||
state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance;
|
||||
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
|
||||
state_.transactions = s.transactions;
|
||||
state_.market.price_usd = s.market_price_usd;
|
||||
state_.market.change_24h = s.market_change_24h;
|
||||
state_.market.price_history = g_marketHistorySnapshot;
|
||||
g_marketHistorySnapshot.clear();
|
||||
s = SweepStateSnapshot{}; // invalidate
|
||||
|
||||
// Restore the user's real portfolio (demo groups were in-memory only).
|
||||
if (g_pfSnapshotValid && settings_) {
|
||||
settings_->setPortfolioEntries(g_pfSnapshot);
|
||||
settings_->setPortfolioStyle(g_pfStyleSnapshot);
|
||||
}
|
||||
g_pfSnapshot.clear();
|
||||
g_pfSnapshotValid = false;
|
||||
}
|
||||
|
||||
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
|
||||
// Lambdas defined in this member function may touch App's private members through the App& arg.
|
||||
void App::buildSweepCatalog()
|
||||
{
|
||||
sweep_targets_.clear();
|
||||
|
||||
// Tabs (both sweeps).
|
||||
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
|
||||
ui::NavPage pg = static_cast<ui::NavPage>(p);
|
||||
if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 });
|
||||
}
|
||||
if (!sweep_full_) return;
|
||||
|
||||
// Full sweep: modals / flows / states. Blur overlays need more settle frames.
|
||||
const int kOverlaySettle = 8;
|
||||
auto add = [&](const char* name, ui::NavPage pg, std::function<void(App&)> setup,
|
||||
std::function<void(App&)> teardown) {
|
||||
sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle });
|
||||
};
|
||||
|
||||
// Simple bool-flag modals.
|
||||
add("modal-import-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.import_view_mode_ = false; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
|
||||
add("modal-import-viewkey", ui::NavPage::Overview,
|
||||
[](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
|
||||
add("modal-export-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
||||
add("modal-export-transactions", ui::NavPage::Settings,
|
||||
[](App&) { ui::ExportTransactionsDialog::show(); }, [](App&) { ui::ExportTransactionsDialog::hide(); });
|
||||
add("modal-export-all-keys", ui::NavPage::Settings,
|
||||
[](App&) { ui::ExportAllKeysDialog::show(); }, [](App&) { ui::ExportAllKeysDialog::hide(); });
|
||||
add("modal-bootstrap", ui::NavPage::Settings,
|
||||
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
|
||||
add("modal-encrypt", ui::NavPage::Settings,
|
||||
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
|
||||
[](App& a) {
|
||||
a.show_encrypt_dialog_ = false; a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
|
||||
a.encrypt_status_.clear();
|
||||
memset(a.encrypt_pass_buf_, 0, sizeof(a.encrypt_pass_buf_));
|
||||
memset(a.encrypt_confirm_buf_, 0, sizeof(a.encrypt_confirm_buf_));
|
||||
});
|
||||
add("modal-change-passphrase", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_change_passphrase_ = true; },
|
||||
[](App& a) {
|
||||
a.show_change_passphrase_ = false; a.encrypt_status_.clear();
|
||||
memset(a.change_old_pass_buf_, 0, sizeof(a.change_old_pass_buf_));
|
||||
memset(a.change_new_pass_buf_, 0, sizeof(a.change_new_pass_buf_));
|
||||
memset(a.change_confirm_buf_, 0, sizeof(a.change_confirm_buf_));
|
||||
});
|
||||
// Remove-encryption dialog — the redesigned passphrase-entry phase (reset() keeps the
|
||||
// workflow in PassphraseEntry; nothing fires the async unlock/export/restart pyramid).
|
||||
add("modal-decrypt", ui::NavPage::Settings,
|
||||
[](App& a) { a.wallet_security_workflow_.reset(); a.show_decrypt_dialog_ = true; },
|
||||
[](App& a) {
|
||||
a.show_decrypt_dialog_ = false; a.wallet_security_workflow_.reset();
|
||||
memset(a.decrypt_pass_buf_, 0, sizeof(a.decrypt_pass_buf_));
|
||||
});
|
||||
// PIN setup / change / remove dialogs (never fire the async vault store/verify).
|
||||
add("modal-pin-setup", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_pin_setup_ = true; },
|
||||
[](App& a) {
|
||||
a.show_pin_setup_ = false; a.pin_status_.clear();
|
||||
memset(a.pin_passphrase_buf_, 0, sizeof(a.pin_passphrase_buf_));
|
||||
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
|
||||
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
|
||||
});
|
||||
add("modal-pin-change", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_pin_change_ = true; },
|
||||
[](App& a) {
|
||||
a.show_pin_change_ = false; a.pin_status_.clear();
|
||||
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
|
||||
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
|
||||
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
|
||||
});
|
||||
add("modal-pin-remove", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_pin_remove_ = true; },
|
||||
[](App& a) {
|
||||
a.show_pin_remove_ = false; a.pin_status_.clear();
|
||||
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
|
||||
});
|
||||
// Wave-1 standalone dialogs (rendered globally from App::render, so any page works).
|
||||
add("modal-qr-popup", ui::NavPage::Receive,
|
||||
[](App&) { ui::QRPopupDialog::show(kDemoZAddr, "Savings"); },
|
||||
[](App&) { ui::QRPopupDialog::close(); });
|
||||
add("modal-request-payment", ui::NavPage::Receive,
|
||||
[](App&) { ui::RequestPaymentDialog::show(kDemoZAddr); },
|
||||
[](App&) { ui::RequestPaymentDialog::hide(); });
|
||||
add("modal-validate-address", ui::NavPage::Receive,
|
||||
[](App&) { ui::ValidateAddressDialog::show(); },
|
||||
[](App&) { ui::ValidateAddressDialog::hide(); });
|
||||
add("modal-address-label", ui::NavPage::Overview,
|
||||
[](App& a) { ui::AddressLabelDialog::show(&a, kDemoZAddr, true); },
|
||||
[](App&) { ui::AddressLabelDialog::hide(); });
|
||||
// (No modal-daemon-prompt surface: renderDaemonUpdatePrompt is gated behind !capture_mode_,
|
||||
// so it can't render during a sweep. Its migration is verified by build + the shared overlay pattern.)
|
||||
add("modal-antivirus", ui::NavPage::Mining,
|
||||
[](App& a) { a.pending_antivirus_dialog_ = true; },
|
||||
[](App& a) { a.pending_antivirus_dialog_ = false; });
|
||||
add("modal-switch-stopnode", ui::NavPage::Settings,
|
||||
[](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
|
||||
[](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
|
||||
add("modal-switch-progress", ui::NavPage::Settings,
|
||||
[](App& a) { a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::Stopping));
|
||||
a.wallet_switch_dialog_open_.store(true); },
|
||||
[](App& a) { a.wallet_switch_dialog_open_.store(false);
|
||||
a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::None)); });
|
||||
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
|
||||
add("modal-shield", ui::NavPage::Send,
|
||||
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
|
||||
[](App&) { ui::ShieldDialog::hide(); });
|
||||
add("modal-merge", ui::NavPage::Send,
|
||||
[](App&) { ui::ShieldDialog::showMerge(); },
|
||||
[](App&) { ui::ShieldDialog::hide(); });
|
||||
// z->t transfer so the (converted) deshielding DialogWarningHeader renders.
|
||||
add("modal-transfer", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
ui::AddressTransferInfo info;
|
||||
info.fromAddr = kDemoZAddr;
|
||||
info.toAddr = "t1DemoTransparentReceiveAddress00000";
|
||||
info.fromBalance = 12.5; info.toBalance = 3.0;
|
||||
info.fromIsZ = true; info.toIsZ = false;
|
||||
ui::AddressTransferDialog::show(&a, info);
|
||||
},
|
||||
[](App&) { ui::AddressTransferDialog::close(); });
|
||||
// Distinct from the settings "modal-export-key" (App::renderExportKeyDialog) — this is the
|
||||
// per-address KeyExportDialog opened from Overview address rows.
|
||||
add("modal-key-export", ui::NavPage::Overview,
|
||||
[](App&) { ui::KeyExportDialog::show(kDemoZAddr, ui::KeyExportDialog::KeyType::Private); },
|
||||
[](App&) { ui::KeyExportDialog::hide(); });
|
||||
// Contacts address-list view modes, each seeded with demo contacts (restored after; no disk write).
|
||||
add("contacts-cards", ui::NavPage::Contacts,
|
||||
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(0); },
|
||||
[](App& a) { restoreSweepContacts(a); });
|
||||
add("contacts-list", ui::NavPage::Contacts,
|
||||
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1); },
|
||||
[](App& a) { restoreSweepContacts(a); });
|
||||
add("contacts-table", ui::NavPage::Contacts,
|
||||
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(2); },
|
||||
[](App& a) { restoreSweepContacts(a); });
|
||||
// Revamped edit dialog: live preview + avatar picker, one surface per avatar mode.
|
||||
add("contacts-edit-icon", ui::NavPage::Contacts,
|
||||
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
|
||||
ui::ContactsSweepOpenEditDialog(1); },
|
||||
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
||||
add("contacts-edit-badge", ui::NavPage::Contacts,
|
||||
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
|
||||
ui::ContactsSweepOpenEditDialog(0); },
|
||||
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
||||
add("contacts-edit-image", ui::NavPage::Contacts,
|
||||
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
|
||||
ui::ContactsSweepOpenEditDialog(2); },
|
||||
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
||||
add("modal-about", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
||||
add("modal-settings", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });
|
||||
|
||||
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
|
||||
add("modal-seed-backup", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true;
|
||||
a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false;
|
||||
a.seed_backup_status_.clear();
|
||||
if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic;
|
||||
},
|
||||
[](App& a) {
|
||||
a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false;
|
||||
if (!a.seed_backup_phrase_.empty())
|
||||
sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size());
|
||||
a.seed_backup_phrase_.clear();
|
||||
});
|
||||
|
||||
// Migrate-to-seed — one surface per step (full-node only). Setting the step directly never
|
||||
// fires the async create/sweep/adopt (those are button-triggered).
|
||||
if (supportsFullNodeLifecycleActions()) {
|
||||
auto mig = [&](const char* name, SeedMigrationStep step, std::function<void(App&)> extra) {
|
||||
add(name, ui::NavPage::Settings,
|
||||
[step, extra](App& a) {
|
||||
a.show_seed_migration_ = true; a.seed_migration_step_ = step;
|
||||
a.seed_migration_dest_ = kDemoZAddr;
|
||||
if (extra) extra(a);
|
||||
},
|
||||
[](App& a) {
|
||||
a.show_seed_migration_ = false;
|
||||
if (!a.seed_migration_seed_.empty())
|
||||
sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size());
|
||||
a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
|
||||
});
|
||||
};
|
||||
// Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
|
||||
// (the probe itself is guarded off during capture_mode_).
|
||||
mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
|
||||
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
|
||||
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
|
||||
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
|
||||
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
|
||||
[](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
|
||||
mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
|
||||
a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
|
||||
a.seed_migration_legacy_remaining_ = 0.0;
|
||||
});
|
||||
mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
|
||||
mig("modal-migrate-error", SeedMigrationStep::Error,
|
||||
[](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });
|
||||
|
||||
// Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
|
||||
// datadir + cache their metadata so the list renders populated.
|
||||
add("modal-wallets", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(dd, ec);
|
||||
if (!fs::exists(dd + "/wallet.dat", ec))
|
||||
std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
|
||||
if (!fs::exists(dd + "/wallet-savings.dat", ec))
|
||||
std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
|
||||
data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
|
||||
e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
|
||||
e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
|
||||
data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
|
||||
e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
|
||||
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
|
||||
a.walletIndex().upsert(e1);
|
||||
a.walletIndex().upsert(e2);
|
||||
// An out-of-datadir wallet (in an extra folder) so the Import action shows too.
|
||||
const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
|
||||
fs::create_directories(extra, ec);
|
||||
if (!fs::exists(extra + "/my-wallet.dat", ec))
|
||||
std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
|
||||
a.walletIndex().addExtraFolder(extra);
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
[](App&) { ui::WalletsDialog::hide(); });
|
||||
|
||||
// Many wallets — exercises the viewport-cap path: the card can't fit all rows, so the list
|
||||
// must scroll internally while the create/scan/footer controls stay pinned (esp. at 150%).
|
||||
// Teardown removes the extra files it dropped so the plain modal-wallets surface stays a
|
||||
// clean 3-wallet reference (both surfaces share the one throwaway datadir).
|
||||
static const char* kManyExtra[] = {"wallet-cold.dat", "wallet-trading.dat", "wallet-mining.dat",
|
||||
"wallet-donations.dat", "wallet-2023.dat", "wallet-payroll.dat"};
|
||||
add("modal-wallets-many", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(dd, ec);
|
||||
const char* names[] = {"wallet.dat", "wallet-savings.dat", "wallet-cold.dat",
|
||||
"wallet-trading.dat", "wallet-mining.dat", "wallet-donations.dat",
|
||||
"wallet-2023.dat", "wallet-payroll.dat"};
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (!fs::exists(dd + "/" + names[i], ec))
|
||||
std::ofstream(dd + "/" + names[i], std::ios::binary) << std::string(64000 + i * 4096, '\0');
|
||||
data::WalletIndexEntry e; e.fileName = names[i]; e.displayName = names[i];
|
||||
e.cachedBalance = 5.0 * (i + 1); e.cachedAddressCount = 2 + i;
|
||||
e.lastOpenedEpoch = 1720000000 - i * 86400; e.syncedHere = true;
|
||||
a.walletIndex().upsert(e);
|
||||
}
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
[](App& a) {
|
||||
ui::WalletsDialog::hide();
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
for (const char* n : kManyExtra) fs::remove(dd + "/" + n, ec);
|
||||
});
|
||||
}
|
||||
|
||||
// First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).
|
||||
if (!isLiteBuild()) {
|
||||
auto wiz = [&](const char* name, WizardPhase phase) {
|
||||
add(name, ui::NavPage::Overview,
|
||||
[phase](App& a) { a.wizard_phase_ = phase; },
|
||||
[](App& a) { a.wizard_phase_ = WizardPhase::None; });
|
||||
};
|
||||
wiz("wizard-appearance", WizardPhase::Appearance);
|
||||
wiz("wizard-bootstrap", WizardPhase::BootstrapOffer);
|
||||
wiz("wizard-encrypt", WizardPhase::EncryptOffer);
|
||||
wiz("wizard-pin", WizardPhase::PinSetup);
|
||||
}
|
||||
|
||||
// App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end).
|
||||
add("overlay-lock", ui::NavPage::Overview,
|
||||
[](App& a) { a.state_.encrypted = true; a.state_.locked = true; },
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
add("overlay-warmup", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
a.state_.warming_up = true;
|
||||
a.state_.warmup_status = "Processing blocks…";
|
||||
a.state_.warmup_description = "The node is loading the block index.";
|
||||
},
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
add("overlay-not-ready", ui::NavPage::Overview,
|
||||
[](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; },
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
|
||||
// Send-confirm popup (state lives in send_tab statics → driven via a debug hook).
|
||||
add("popup-send-confirm", ui::NavPage::Send,
|
||||
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
|
||||
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
|
||||
|
||||
// Market portfolio row styles — capture all three so the redesign is reviewable per skin.
|
||||
add("market-rows-compact", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
add("market-rows-detailed", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
add("market-rows-featured", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
|
||||
// Console RPC command-reference popup (fixed-height dialog with a fill-height command list).
|
||||
add("modal-console-commands", ui::NavPage::Console,
|
||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
|
||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
|
||||
|
||||
// Debug-options gate: confirmation + warning, with the passphrase re-auth field (encrypted).
|
||||
add("modal-debug-gate", ui::NavPage::Settings,
|
||||
[](App& a) { a.state_.encrypted = true; ui::SweepOpenDebugGate(true); },
|
||||
[](App& a) { ui::SweepOpenDebugGate(false); a.applyHealthyDemoState(); });
|
||||
|
||||
// Daemon updater — the two-pane version picker (versions left, selected-version detail right).
|
||||
// Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
|
||||
add("modal-daemon-update", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||
util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||
util::DaemonReleaseAsset as;
|
||||
as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
|
||||
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
|
||||
r.assets.push_back(as);
|
||||
return r;
|
||||
};
|
||||
std::vector<util::DaemonRelease> rels;
|
||||
rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
|
||||
"## What is DragonX?\n\n"
|
||||
"DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. "
|
||||
"It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n"
|
||||
"## Bug Fixes\n"
|
||||
"* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n"
|
||||
"* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n"
|
||||
"## Key Features\n"
|
||||
"* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n"
|
||||
"* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n"
|
||||
"* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n"
|
||||
"## Checksums\n| File | SHA-256 |\n"
|
||||
"|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
|
||||
rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
|
||||
"## Notes\n- Stability improvements\n- RPC fixes\n", false));
|
||||
rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
|
||||
"- First tagged mainnet build\n", false));
|
||||
rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
|
||||
"Release candidate for the 1.1.0 series. Testing only.\n", true));
|
||||
ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
|
||||
"v1.0.3-dc45e7d90");
|
||||
},
|
||||
[](App&) { ui::DaemonUpdateDialog::sweepClose(); });
|
||||
|
||||
// Miner (xmrig) updater — same two-pane version picker, seeded with fake releases for the sweep.
|
||||
add("modal-xmrig-update", ui::NavPage::Mining,
|
||||
[](App& a) {
|
||||
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||
util::XmrigRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||
util::XmrigReleaseAsset as;
|
||||
as.name = std::string("drg-xmrig-") + tag + "-linux-x64.zip";
|
||||
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 8000000;
|
||||
r.assets.push_back(as);
|
||||
return r;
|
||||
};
|
||||
std::vector<util::XmrigRelease> rels;
|
||||
rels.push_back(mk("v6.25.3", "DRG-XMRig v6.25.3", "2026-06-20",
|
||||
"## What's new\n- Rebased on upstream XMRig 6.25.3\n- **RandomX** JIT speedups on modern CPUs\n"
|
||||
"- Fix `--cpu-priority` parsing on Windows\n\n## Checksums\n| File | SHA-256 |\n"
|
||||
"|---|---|\n| drg-xmrig-v6.25.3-linux-x64.zip | `ab12cd34` |\n", false));
|
||||
rels.push_back(mk("v6.24.0", "DRG-XMRig v6.24.0", "2026-04-30",
|
||||
"## Notes\n- Pool TLS fixes\n- Lower idle CPU\n", false));
|
||||
rels.push_back(mk("v6.23.0", "DRG-XMRig v6.23.0", "2026-03-11",
|
||||
"- First DRG-XMRig build\n", false));
|
||||
rels.push_back(mk("v6.26.0-rc1", "DRG-XMRig v6.26.0-rc1", "2026-07-02",
|
||||
"Release candidate. **Testing only.**\n", true));
|
||||
ui::XmrigDownloadDialog::sweepSeed(&a, rels, util::XmrigUpdater::State::ReleaseList, "v6.24.0");
|
||||
},
|
||||
[](App&) { ui::XmrigDownloadDialog::sweepClose(); });
|
||||
|
||||
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
|
||||
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
|
||||
add("modal-folder-picker", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string demo = util::Platform::getConfigDir() + "picker-demo";
|
||||
for (const char* sub : {"Documents", "Downloads", "Backups", "wallet-archive"})
|
||||
fs::create_directories(demo + "/" + sub, ec);
|
||||
for (const char* f : {"wallet-cold.dat", "wallet-2023.dat"})
|
||||
if (!fs::exists(demo + "/" + f, ec))
|
||||
std::ofstream(demo + "/" + f, std::ios::binary) << std::string(66000, '\0');
|
||||
ui::WalletsDialog::show(&a);
|
||||
ui::FolderPicker::open(demo, [](const std::string&) {});
|
||||
},
|
||||
[](App&) { ui::FolderPicker::close(); ui::WalletsDialog::hide(); });
|
||||
}
|
||||
|
||||
// ── State machine ───────────────────────────────────────────────────────────────────────────
|
||||
void App::startSweepImpl(bool full)
|
||||
{
|
||||
if (screenshot_sweep_active_) return;
|
||||
|
||||
sweep_skins_.clear();
|
||||
for (const auto& sk : ui::schema::SkinManager::instance().available())
|
||||
if (sk.valid) sweep_skins_.push_back(sk.id);
|
||||
if (sweep_skins_.empty()) return;
|
||||
|
||||
sweep_full_ = full;
|
||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||
buildSweepCatalog();
|
||||
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
||||
|
||||
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
||||
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
||||
|
||||
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
|
||||
sweep_saved_page_ = current_page_;
|
||||
sweep_skin_idx_ = 0; sweep_target_idx_ = 0;
|
||||
screenshot_sweep_active_ = true;
|
||||
sweep_capture_this_frame_ = false;
|
||||
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
|
||||
applySweepTarget();
|
||||
ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
|
||||
DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
|
||||
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
|
||||
}
|
||||
|
||||
void App::startScreenshotSweep() { startSweepImpl(false); }
|
||||
void App::startFullUiSweep() { startSweepImpl(true); }
|
||||
|
||||
void App::applySweepTarget()
|
||||
{
|
||||
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
||||
current_page_ = t.page;
|
||||
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
|
||||
if (t.setup) t.setup(*this);
|
||||
// <dir>/<surface>/<skin>.png — one subfolder per surface, one PNG per theme, overwritten in
|
||||
// place next sweep. (writePng in main.cpp creates the parent subfolder.)
|
||||
sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string();
|
||||
sweep_settle_frames_ = t.settle;
|
||||
sweep_capture_this_frame_ = false;
|
||||
}
|
||||
|
||||
void App::updateScreenshotSweep()
|
||||
{
|
||||
if (!screenshot_sweep_active_) return;
|
||||
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
||||
current_page_ = t.page; // keep the surface pinned each frame
|
||||
page_alpha_ = 1.0f;
|
||||
// Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups
|
||||
// (must re-fire while open) + to keep the surface state fixed against any refresh.
|
||||
if (t.setup) t.setup(*this);
|
||||
if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
|
||||
else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame
|
||||
}
|
||||
|
||||
void App::onScreenshotCaptured()
|
||||
{
|
||||
sweep_capture_this_frame_ = false;
|
||||
if (!screenshot_sweep_active_) return;
|
||||
|
||||
{ const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); }
|
||||
|
||||
// Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is
|
||||
// torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal
|
||||
// click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps
|
||||
// IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on
|
||||
// Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so
|
||||
// it can't bleed into the next surface's capture or survive past the sweep.
|
||||
if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
|
||||
ImGui::ClosePopupToLevel(0, false);
|
||||
|
||||
if (++sweep_target_idx_ >= static_cast<int>(sweep_targets_.size())) {
|
||||
sweep_target_idx_ = 0;
|
||||
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
|
||||
// Done — restore the original skin + page, and (full) the real state.
|
||||
const bool wasFull = sweep_full_;
|
||||
if (wasFull) writeSweepManifest();
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
|
||||
current_page_ = sweep_saved_page_;
|
||||
page_alpha_ = 1.0f;
|
||||
if (wasFull) { clearDemoWalletData(); capture_mode_ = false; }
|
||||
sweep_full_ = false;
|
||||
screenshot_sweep_active_ = false;
|
||||
ui::Notifications::instance().success(
|
||||
(wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_);
|
||||
DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
|
||||
return;
|
||||
}
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
|
||||
}
|
||||
applySweepTarget();
|
||||
}
|
||||
|
||||
void App::writeSweepManifest() const
|
||||
{
|
||||
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
||||
std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc);
|
||||
if (!out) return;
|
||||
out << "# Full UI sweep\n\n";
|
||||
out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n";
|
||||
for (const auto& t : sweep_targets_) {
|
||||
out << "## " << t.name << " (base: " << sweepPageName(t.page) << ")\n\n";
|
||||
for (const auto& skin : sweep_skins_)
|
||||
out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n";
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dragonx
|
||||
@@ -121,16 +121,21 @@ void App::renderFirstRunWizard() {
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // reveal the skin backdrop behind
|
||||
ImGui::Begin("##FirstRunWizard", nullptr, flags);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 winPos = ImGui::GetWindowPos();
|
||||
ImVec2 winSize = ImGui::GetWindowSize();
|
||||
|
||||
// Background fill
|
||||
ImU32 bgCol = ui::material::Surface();
|
||||
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
|
||||
// The app's skin backdrop (marble / gradient / acrylic) is already painted behind every window by
|
||||
// drawWindowBackdrop(); reveal it here instead of a flat Surface() slab, under a gentle theme-tinted
|
||||
// scrim so the wizard cards and text keep their contrast on busy skins.
|
||||
ImU32 bgCol = ui::material::Surface(); // still used by the completed / not-reached card overlays
|
||||
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y),
|
||||
ui::material::WithAlpha(ui::material::Background(), 120));
|
||||
|
||||
// --- Determine which of the 3 masonry sections is focused ---
|
||||
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
|
||||
@@ -184,11 +189,18 @@ void App::renderFirstRunWizard() {
|
||||
headerCy += logoSize + 8.0f * dp;
|
||||
|
||||
{
|
||||
const char* welcomeTitle = "Welcome to ObsidianDragon!";
|
||||
const char* welcomeTitle = TR("wiz_welcome_title");
|
||||
ImVec2 wts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, welcomeTitle);
|
||||
dl->AddText(titleFont, titleFont->LegacySize,
|
||||
ImVec2(winPos.x + (winSize.x - wts.x) * 0.5f, headerCy), textCol, welcomeTitle);
|
||||
headerCy += wts.y + 16.0f * dp;
|
||||
headerCy += wts.y + 6.0f * dp;
|
||||
|
||||
// Warmer, less-sparse header: a one-line subtitle under the welcome (dimmed body).
|
||||
const char* welcomeSub = TR("wiz_welcome_sub");
|
||||
ImVec2 sts = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, 0, welcomeSub);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(winPos.x + (winSize.x - sts.x) * 0.5f, headerCy), dimCol, welcomeSub);
|
||||
headerCy += sts.y + 16.0f * dp;
|
||||
}
|
||||
|
||||
// --- Masonry: 2 columns ---
|
||||
@@ -223,11 +235,8 @@ void App::renderFirstRunWizard() {
|
||||
// Background (channel 0)
|
||||
dl->ChannelsSetCurrent(0);
|
||||
if (state == 1) {
|
||||
// Focused card: subtle drop shadow
|
||||
float shadowOff = 3.0f * dp;
|
||||
dl->AddRectFilled(
|
||||
ImVec2(cMin.x + shadowOff, cMin.y + shadowOff), ImVec2(cMax.x + shadowOff, cMax.y + shadowOff),
|
||||
IM_COL32(0, 0, 0, 35), cardRound);
|
||||
// Focused card lifts off the backdrop with the app's uniform card shadow (not a hard offset).
|
||||
ui::material::DrawCardDropShadow(dl, cMin, cMax, cardRound);
|
||||
}
|
||||
// Use DrawGlassPanel for proper acrylic/opacity/noise/theme effects
|
||||
ui::material::GlassPanelSpec glass;
|
||||
@@ -237,14 +246,14 @@ void App::renderFirstRunWizard() {
|
||||
// Overlays & borders (channel 2)
|
||||
dl->ChannelsSetCurrent(2);
|
||||
if (state == 1) {
|
||||
// Focused: accent border
|
||||
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 2.0f * dp);
|
||||
// Focused: soft accent ring
|
||||
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 1.5f * dp);
|
||||
} else if (state == 2) {
|
||||
// Completed: dim overlay (preserves color)
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 110), cardRound);
|
||||
// Completed: a light veil — reads as "done", not disabled.
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 70), cardRound);
|
||||
} else {
|
||||
// Not reached: heavy overlay (creates greyscale look)
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 165), cardRound);
|
||||
// Upcoming: a gentle veil — reads as "waiting", not greyed-out.
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 115), cardRound);
|
||||
}
|
||||
|
||||
dl->ChannelsSetCurrent(1);
|
||||
@@ -265,13 +274,13 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 1");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
// Title
|
||||
{
|
||||
const char* t = "Appearance";
|
||||
const char* t = TR("wiz_appearance");
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
@@ -327,14 +336,14 @@ void App::renderFirstRunWizard() {
|
||||
for (const auto& skin : skins) {
|
||||
if (skin.id == skinMgr.activeSkinId()) { activePreview = skin.name; break; }
|
||||
}
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Theme");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("theme"));
|
||||
float comboX = cx + 110.0f * dp;
|
||||
float comboW = contentW - 110.0f * dp;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
ImGui::SetNextItemWidth(comboW);
|
||||
if (ImGui::BeginCombo("##wiz_theme", activePreview.c_str())) {
|
||||
ImGui::TextDisabled("Built-in");
|
||||
ImGui::TextDisabled(TR("wiz_theme_builtin"));
|
||||
ImGui::Separator();
|
||||
for (const auto& skin : skins) {
|
||||
if (!skin.bundled) continue;
|
||||
@@ -350,7 +359,7 @@ void App::renderFirstRunWizard() {
|
||||
for (const auto& skin : skins) { if (!skin.bundled) { hasCustom = true; break; } }
|
||||
if (hasCustom) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Custom");
|
||||
ImGui::TextDisabled(TR("wiz_theme_custom"));
|
||||
ImGui::Separator();
|
||||
for (const auto& skin : skins) {
|
||||
if (skin.bundled) continue;
|
||||
@@ -358,7 +367,7 @@ void App::renderFirstRunWizard() {
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1));
|
||||
ImGui::BeginDisabled(true);
|
||||
ImGui::Selectable((skin.name + " (invalid)").c_str(), false);
|
||||
ImGui::Selectable((skin.name + TR("wiz_theme_invalid")).c_str(), false);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
@@ -386,7 +395,7 @@ void App::renderFirstRunWizard() {
|
||||
for (const auto& l : layouts) {
|
||||
if (l.id == wiz_balance_layout) { balPreview = l.name; break; }
|
||||
}
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Balance Layout");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("balance_layout"));
|
||||
float comboX = cx + 110.0f * dp;
|
||||
float comboW = contentW - 110.0f * dp;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
|
||||
@@ -417,7 +426,7 @@ void App::renderFirstRunWizard() {
|
||||
langNames.reserve(languages.size());
|
||||
for (const auto& lang : languages) langNames.push_back(lang.second.c_str());
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Language");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("language"));
|
||||
float comboX = cx + 110.0f * dp;
|
||||
float comboW = contentW - 110.0f * dp;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
|
||||
@@ -488,10 +497,10 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::SameLine();
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
|
||||
"Low-spec mode");
|
||||
TR("low_spec_mode"));
|
||||
cy += bodyFont->LegacySize + 6.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, "Disable all heavy visual effects");
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_lowspec_desc"));
|
||||
cy += captionFont->LegacySize + 16.0f * dp;
|
||||
|
||||
ImGui::BeginDisabled(wiz_low_spec);
|
||||
@@ -499,15 +508,15 @@ void App::renderFirstRunWizard() {
|
||||
// Acrylic blur slider
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(cx, cy + 2.0f * dp), textCol,
|
||||
"Acrylic glass effects");
|
||||
TR("wiz_acrylic"));
|
||||
cy += bodyFont->LegacySize + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx, cy), dimCol, "Translucent blur on panels (Off disables)");
|
||||
ImVec2(cx, cy), dimCol, TR("wiz_acrylic_desc"));
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
|
||||
{
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 4.0f * dp, cy), textCol, "Level:");
|
||||
ImVec2(cx + 4.0f * dp, cy), textCol, TR("wiz_level"));
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx + 72.0f * dp, cy - 2.0f * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
float sliderW = contentW - 72.0f * dp;
|
||||
@@ -515,7 +524,7 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
char blur_fmt[16];
|
||||
if (wiz_blur_amount < 0.01f)
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), TR("wiz_off"));
|
||||
else
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", wiz_blur_amount * 25.0f);
|
||||
if (ImGui::SliderFloat("##wiz_blur", &wiz_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||||
@@ -549,19 +558,19 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::SameLine();
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
|
||||
"Theme visual effects");
|
||||
TR("wiz_theme_effects"));
|
||||
cy += bodyFont->LegacySize + 6.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, "Animated borders, color wash");
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_theme_effects_desc"));
|
||||
cy += captionFont->LegacySize + 16.0f * dp;
|
||||
|
||||
// UI Opacity slider
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(cx, cy + 2.0f * dp), textCol,
|
||||
"UI Opacity");
|
||||
TR("ui_opacity"));
|
||||
cy += bodyFont->LegacySize + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx, cy), dimCol, "Card & sidebar transparency (1.0 = solid)");
|
||||
ImVec2(cx, cy), dimCol, TR("wiz_ui_opacity_desc"));
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy - 2.0f * dp));
|
||||
@@ -590,10 +599,10 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::SameLine();
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
|
||||
"Console scanline");
|
||||
TR("console_scanline"));
|
||||
cy += bodyFont->LegacySize + 6.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, "CRT scanline effect in console");
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_scanline_desc"));
|
||||
cy += captionFont->LegacySize + 24.0f * dp;
|
||||
|
||||
ImGui::EndDisabled(); // low-spec
|
||||
@@ -611,7 +620,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
|
||||
if (ui::material::TactileButton(TR("wiz_continue"), ImVec2(btnW, btnH))) {
|
||||
// Save appearance choices, advance to Bootstrap
|
||||
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
|
||||
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
|
||||
@@ -658,23 +667,23 @@ void App::renderFirstRunWizard() {
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
float labelX = cx + iconW + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, "Step 2");
|
||||
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, "Step 2").x;
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step2"));
|
||||
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step2")).x;
|
||||
float titleX = labelX + step2W + 12.0f * dp;
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, "Bootstrap");
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_bootstrap"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
} else {
|
||||
// Step indicator
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 2");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step2"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
}
|
||||
|
||||
// Title
|
||||
{
|
||||
const char* t = "Bootstrap";
|
||||
const char* t = TR("wiz_bootstrap");
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
@@ -697,11 +706,11 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
const char* statusTitle;
|
||||
if (prog.state == util::Bootstrap::State::Downloading)
|
||||
statusTitle = "Downloading bootstrap...";
|
||||
statusTitle = TR("bootstrap_downloading");
|
||||
else if (prog.state == util::Bootstrap::State::Verifying)
|
||||
statusTitle = "Verifying checksums...";
|
||||
statusTitle = TR("bootstrap_verifying");
|
||||
else
|
||||
statusTitle = "Extracting blockchain data...";
|
||||
statusTitle = TR("bootstrap_extracting");
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
|
||||
cy += bodyFont->LegacySize + 12.0f * dp;
|
||||
|
||||
@@ -730,7 +739,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
if (prog.state == util::Bootstrap::State::Extracting) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
dimCol, "(wallet.dat is protected)");
|
||||
dimCol, TR("bootstrap_wallet_protected"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
@@ -746,9 +755,9 @@ void App::renderFirstRunWizard() {
|
||||
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||
dotR, dotCol);
|
||||
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
||||
? "Daemon stopping..."
|
||||
: "Daemon running")
|
||||
: "Daemon stopped";
|
||||
? TR("bootstrap_daemon_stopping")
|
||||
: TR("bootstrap_daemon_running"))
|
||||
: TR("bootstrap_daemon_stopped");
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
||||
@@ -763,7 +772,7 @@ void App::renderFirstRunWizard() {
|
||||
float cancelBX = rightX + (colW - cancelW) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(cancelW, cancelH))) {
|
||||
bootstrap_->cancel();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -790,10 +799,10 @@ void App::renderFirstRunWizard() {
|
||||
errMsg = bootstrap_->getProgress().error;
|
||||
bootstrap_.reset();
|
||||
}
|
||||
if (errMsg.empty()) errMsg = "Bootstrap failed";
|
||||
if (errMsg.empty()) errMsg = TR("wiz_bootstrap_failed");
|
||||
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), "Download Failed");
|
||||
ui::material::Error(), TR("wiz_download_failed"));
|
||||
cy += bodyFont->LegacySize + 8.0f * dp;
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
|
||||
@@ -812,7 +821,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("retry"), ImVec2(btnW2, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
stopDaemonForBootstrap();
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
@@ -826,7 +835,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -869,11 +878,11 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, "External daemon running");
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, TR("wiz_ext_daemon_running"));
|
||||
}
|
||||
cy += bodyFont->LegacySize + 4.0f * dp;
|
||||
{
|
||||
const char* warnBody = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
|
||||
const char* warnBody = TR("wiz_ext_daemon_warning");
|
||||
ImVec2 ws = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, warnBody);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, warnBody, nullptr, contentW);
|
||||
cy += ws.y + 12.0f * dp;
|
||||
@@ -896,9 +905,9 @@ void App::renderFirstRunWizard() {
|
||||
IM_COL32(220, 60, 60, 255)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_stop_daemon"), ImVec2(stopW, btnH2))) {
|
||||
wizard_stopping_external_ = true;
|
||||
wizard_stop_status_ = "Sending stop command...";
|
||||
wizard_stop_status_ = TR("wiz_daemon_sending_stop");
|
||||
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
auto config = rpc::Connection::autoDetectConfig();
|
||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||
@@ -910,17 +919,17 @@ void App::renderFirstRunWizard() {
|
||||
tmp_rpc->disconnect();
|
||||
}
|
||||
}
|
||||
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
||||
wizard_stop_status_ = TR("wiz_daemon_waiting_stop");
|
||||
for (int i = 0; i < 60 && !token.cancelled(); i++) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||
wizard_stop_status_ = "Daemon stopped.";
|
||||
wizard_stop_status_ = TR("wiz_daemon_stopped_ok");
|
||||
wizard_stopping_external_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (token.cancelled()) return;
|
||||
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
||||
wizard_stop_status_ = TR("wiz_daemon_stop_failed");
|
||||
wizard_stopping_external_ = false;
|
||||
});
|
||||
}
|
||||
@@ -929,7 +938,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -938,7 +947,7 @@ void App::renderFirstRunWizard() {
|
||||
} else {
|
||||
// --- Normal bootstrap offer ---
|
||||
{
|
||||
const char* bsText = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
|
||||
const char* bsText = TR("wiz_bootstrap_desc");
|
||||
ImVec2 bsSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, bsText);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, bsText, nullptr, contentW);
|
||||
cy += bsSize.y + 8.0f * dp;
|
||||
@@ -951,7 +960,7 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
||||
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
|
||||
const char* twText = TR("bootstrap_trust_warning");
|
||||
float twWrap = contentW - iw - 4.0f * dp;
|
||||
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
|
||||
@@ -970,9 +979,9 @@ void App::renderFirstRunWizard() {
|
||||
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||
dotR, dotCol);
|
||||
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
||||
? "Daemon stopping..."
|
||||
: "Daemon running")
|
||||
: "Daemon stopped";
|
||||
? TR("bootstrap_daemon_stopping")
|
||||
: TR("bootstrap_daemon_running"))
|
||||
: TR("bootstrap_daemon_stopped");
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
||||
@@ -995,7 +1004,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("download"), ImVec2(dlBtnW, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
stopDaemonForBootstrap();
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
@@ -1014,7 +1023,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2))) {
|
||||
stopDaemonForBootstrap();
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
@@ -1024,7 +1033,7 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||
ui::material::Tooltip(TR("bootstrap_mirror_tooltip"));
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
@@ -1032,7 +1041,7 @@ void App::renderFirstRunWizard() {
|
||||
// --- Skip button ---
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1084,14 +1093,14 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 3");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step3"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
}
|
||||
|
||||
// Title (changes for PinSetup sub-state)
|
||||
{
|
||||
const char* t = (isFocused && wizard_phase_ == WizardPhase::PinSetup)
|
||||
? "Quick-Unlock PIN" : "Encryption";
|
||||
? TR("wiz_pin_title") : TR("wiz_encryption");
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
@@ -1108,11 +1117,11 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 okCol = ui::material::Secondary();
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_VERIFIED_USER).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), okCol, ICON_MD_VERIFIED_USER);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, "Wallet is already encrypted");
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, TR("wiz_already_encrypted"));
|
||||
cy += bodyFont->LegacySize + 12.0f * dp;
|
||||
}
|
||||
{
|
||||
const char* desc = "Your wallet is protected with a passphrase. No further action is needed.";
|
||||
const char* desc = TR("wiz_already_encrypted_desc");
|
||||
ImVec2 ds = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, desc);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, desc, nullptr, contentW);
|
||||
cy += ds.y + 20.0f * dp;
|
||||
@@ -1127,7 +1136,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_continue"), ImVec2(btnW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
@@ -1139,7 +1148,7 @@ void App::renderFirstRunWizard() {
|
||||
} else if (isFocused) {
|
||||
// ---- Encryption offer + optional PIN (combined) ----
|
||||
{
|
||||
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
|
||||
const char* encDesc = TR("wiz_encrypt_desc");
|
||||
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, encDesc, nullptr, contentW);
|
||||
cy += edSize.y + 6.0f * dp;
|
||||
@@ -1148,7 +1157,7 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 warnCol2 = ui::material::Warning();
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol2, ICON_MD_WARNING);
|
||||
const char* warnLoss = "If you lose your passphrase, you lose access to your funds.";
|
||||
const char* warnLoss = TR("wiz_encrypt_warning");
|
||||
float wlWrap = contentW - iw - 4.0f * dp;
|
||||
ImVec2 wlSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, wlWrap, warnLoss);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol2, warnLoss, nullptr, wlWrap);
|
||||
@@ -1156,7 +1165,7 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
|
||||
// Passphrase input
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Passphrase:");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_passphrase"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1168,7 +1177,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PopItemWidth();
|
||||
cy += 36.0f * dp + 6.0f * dp;
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm:");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_confirm"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1183,16 +1192,16 @@ void App::renderFirstRunWizard() {
|
||||
// Strength meter
|
||||
{
|
||||
size_t len = strlen(encrypt_pass_buf_);
|
||||
const char* strengthLabel = "Weak";
|
||||
const char* strengthLabel = TR("wiz_strength_weak");
|
||||
ImU32 strengthCol = ui::material::Error();
|
||||
float strengthPct = 0.25f;
|
||||
|
||||
if (len >= 16) {
|
||||
strengthLabel = "Strong"; strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
|
||||
strengthLabel = TR("wiz_strength_strong"); strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
|
||||
} else if (len >= 12) {
|
||||
strengthLabel = "Good"; strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
|
||||
strengthLabel = TR("wiz_strength_good"); strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
|
||||
} else if (len >= 8) {
|
||||
strengthLabel = "Fair"; strengthCol = ui::material::Warning(); strengthPct = 0.5f;
|
||||
strengthLabel = TR("wiz_strength_fair"); strengthCol = ui::material::Warning(); strengthPct = 0.5f;
|
||||
}
|
||||
|
||||
float sBarH = 4.0f * dp, sBarR = 2.0f * dp;
|
||||
@@ -1205,7 +1214,7 @@ void App::renderFirstRunWizard() {
|
||||
cy += sBarH + 4.0f * dp;
|
||||
|
||||
char slabel[64];
|
||||
snprintf(slabel, sizeof(slabel), "Strength: %s", strengthLabel);
|
||||
snprintf(slabel, sizeof(slabel), TR("wiz_strength"), strengthLabel);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, slabel);
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
@@ -1215,14 +1224,14 @@ void App::renderFirstRunWizard() {
|
||||
size_t pLen = strlen(encrypt_pass_buf_);
|
||||
if (pLen > 0 && pLen < 8) {
|
||||
char fb[80];
|
||||
snprintf(fb, sizeof(fb), "Passphrase must be at least 8 characters (%zu/8)", pLen);
|
||||
snprintf(fb, sizeof(fb), TR("wiz_pass_too_short"), pLen);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), fb);
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
} else if (pLen >= 8 && strlen(encrypt_confirm_buf_) > 0 &&
|
||||
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) != 0) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), "Passphrases do not match");
|
||||
ui::material::Error(), TR("wiz_pass_mismatch"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
}
|
||||
@@ -1234,12 +1243,12 @@ void App::renderFirstRunWizard() {
|
||||
cy += 8.0f * dp;
|
||||
|
||||
{
|
||||
const char* pinTitle = "Quick-Unlock PIN (optional)";
|
||||
const char* pinTitle = TR("wiz_pin_optional");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, pinTitle);
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
}
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "PIN (4-8 digits):");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_pin_label"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1251,7 +1260,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PopItemWidth();
|
||||
cy += 36.0f * dp + 6.0f * dp;
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm PIN:");
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_pin_confirm"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1268,12 +1277,12 @@ void App::renderFirstRunWizard() {
|
||||
std::string pinStr(wizard_pin_buf_);
|
||||
if (!pinStr.empty() && !util::SecureVault::isValidPin(pinStr)) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), "PIN must be 4-8 digits");
|
||||
ui::material::Error(), TR("wiz_pin_invalid"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
} else if (!pinStr.empty() && strlen(wizard_pin_confirm_buf_) > 0 &&
|
||||
pinStr != std::string(wizard_pin_confirm_buf_)) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), "PINs do not match");
|
||||
ui::material::Error(), TR("wiz_pin_mismatch"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
}
|
||||
@@ -1294,7 +1303,7 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
if (passEdgeSpace) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), "Passphrase has leading/trailing spaces — remove them");
|
||||
ui::material::Error(), TR("wiz_pass_spaces"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
@@ -1324,7 +1333,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!canEncrypt);
|
||||
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_encrypt_continue"), ImVec2(encBtnW, btnH2))) {
|
||||
// Save passphrase + optional PIN for background processing
|
||||
wallet_security_.beginDeferredEncryption(
|
||||
std::string(encrypt_pass_buf_),
|
||||
@@ -1347,7 +1356,7 @@ void App::renderFirstRunWizard() {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
ui::Notifications::instance().info("Encryption will complete in the background");
|
||||
ui::Notifications::instance().info(TR("wiz_encrypt_bg"));
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1355,12 +1364,12 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
|
||||
static bool s_skipEncConfirm = false;
|
||||
if (!s_skipEncConfirm) {
|
||||
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
|
||||
s_skipEncConfirm = true;
|
||||
encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm.";
|
||||
encrypt_status_ = TR("wiz_skip_confirm");
|
||||
} else {
|
||||
s_skipEncConfirm = false;
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
@@ -1380,7 +1389,7 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
} else {
|
||||
// ---- Not focused: show static description ----
|
||||
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
|
||||
const char* encDesc = TR("wiz_encrypt_desc");
|
||||
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), dimCol, encDesc, nullptr, contentW);
|
||||
cy += edSize.y + 6.0f * dp;
|
||||
|
||||
@@ -129,6 +129,35 @@ bool ChatDatabase::append(const ChatMessage& message)
|
||||
return sqlite3_changes(db_) > 0;
|
||||
}
|
||||
|
||||
bool ChatDatabase::upsert(const ChatMessage& message)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return false;
|
||||
|
||||
std::vector<unsigned char> nonce;
|
||||
std::vector<unsigned char> cipher;
|
||||
std::string plain = serialize(message);
|
||||
const bool encrypted = encrypt(plain, nonce, cipher);
|
||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||
if (!encrypted) return false;
|
||||
|
||||
const std::string dedup = dedupHash(message.txid, message.payload_position);
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"INSERT INTO chat_messages (wallet_tag, dedup_hash, nonce, payload) VALUES (?, ?, ?, ?) "
|
||||
"ON CONFLICT(wallet_tag, dedup_hash) DO UPDATE SET nonce=excluded.nonce, payload=excluded.payload",
|
||||
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_blob(stmt, 3, nonce.data(), static_cast<int>(nonce.size()), SQLITE_TRANSIENT);
|
||||
sqlite3_bind_blob(stmt, 4, cipher.data(), static_cast<int>(cipher.size()), SQLITE_TRANSIENT);
|
||||
const bool done = sqlite3_step(stmt) == SQLITE_DONE;
|
||||
sqlite3_finalize(stmt);
|
||||
return done;
|
||||
}
|
||||
|
||||
std::vector<ChatMessage> ChatDatabase::load()
|
||||
{
|
||||
std::vector<ChatMessage> out;
|
||||
@@ -199,6 +228,17 @@ bool ChatDatabase::ensureOpen()
|
||||
exec("PRAGMA journal_mode=WAL");
|
||||
exec("PRAGMA synchronous=NORMAL");
|
||||
|
||||
// C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with
|
||||
// umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD
|
||||
// ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows).
|
||||
{
|
||||
std::error_code perr;
|
||||
const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write;
|
||||
std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
}
|
||||
|
||||
if (!createSchema()) {
|
||||
close();
|
||||
return false;
|
||||
@@ -267,7 +307,10 @@ bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
||||
out.body = parsed.value("b", std::string());
|
||||
out.timestamp = parsed.value("ts", static_cast<std::int64_t>(0));
|
||||
out.payload_position = static_cast<std::size_t>(parsed.value("pos", static_cast<std::uint64_t>(0)));
|
||||
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent
|
||||
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent (0)
|
||||
// A persisted "Sending" means we crashed mid-broadcast; the outcome is unknown. Resolve it
|
||||
// optimistically to Sent on load so it can't show a stuck spinner forever.
|
||||
if (out.delivery == ChatDelivery::Sending) out.delivery = ChatDelivery::Sent;
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
|
||||
@@ -43,6 +43,11 @@ public:
|
||||
// Returns true if newly inserted; false on duplicate or while locked.
|
||||
bool append(const ChatMessage& message);
|
||||
|
||||
// Persist-or-overwrite one message by its (txid+position) dedup key. Unlike append(), this
|
||||
// updates an existing row's payload — used for outgoing echoes whose delivery status changes
|
||||
// (Sending → Sent/Failed). Returns true on success; false while locked / on error.
|
||||
bool upsert(const ChatMessage& message);
|
||||
|
||||
// Decrypt and return every stored message for the unlocked wallet, in insertion order. Empty
|
||||
// while locked or if none. Rows that fail to decrypt/parse are skipped.
|
||||
std::vector<ChatMessage> load();
|
||||
|
||||
@@ -10,9 +10,11 @@ namespace dragonx::chat {
|
||||
|
||||
enum class ChatDirection { Incoming, Outgoing };
|
||||
enum class ChatMessageKind { Message, ContactRequest };
|
||||
// Outgoing delivery: Sent = the broadcast was submitted; Failed = it wasn't (not connected, no
|
||||
// spendable address, a send already in progress). Always Sent for incoming.
|
||||
enum class ChatDelivery { Sent, Failed };
|
||||
// Outgoing delivery status. Sending = broadcast in flight (async op not yet resolved); Sent = the
|
||||
// daemon accepted + broadcast the tx; Failed = it didn't (not connected, no funded address, rejected).
|
||||
// Always Sent for incoming. NB: the values are persisted (chat DB serializes the int), so Sent MUST
|
||||
// stay 0 and new states are APPENDED — never reordered.
|
||||
enum class ChatDelivery { Sent, Failed, Sending };
|
||||
|
||||
struct ChatMessage {
|
||||
ChatDirection direction = ChatDirection::Incoming;
|
||||
|
||||
@@ -16,7 +16,8 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
const std::string& conversationId,
|
||||
const char* type,
|
||||
const std::string& streamHeaderHex,
|
||||
const std::string& publicKeyHex)
|
||||
const std::string& publicKeyHex,
|
||||
std::int64_t sentAt)
|
||||
{
|
||||
nlohmann::json header;
|
||||
header["h"] = 1; // header number (>= 1)
|
||||
@@ -26,6 +27,7 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
header["t"] = type; // "Memo" or "Cont"
|
||||
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
||||
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
||||
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
|
||||
return header.dump();
|
||||
}
|
||||
|
||||
@@ -67,7 +69,8 @@ ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex);
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemo = ciphertextHex;
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||
@@ -95,7 +98,8 @@ ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex);
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemo = requestText;
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||
|
||||
@@ -124,6 +124,10 @@ HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
||||
if (!readRequiredString(object, "t", type, error)) return fail(error);
|
||||
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
||||
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
|
||||
// Optional sender compose time (Unix seconds). Absent on older senders — leave sent_at = 0 so the
|
||||
// receiver falls back to the tx/receive time. Read leniently; never fail the header on a bad value.
|
||||
if (auto it = object.find("ts"); it != object.end() && it->is_number_integer())
|
||||
header.sent_at = it->get<std::int64_t>();
|
||||
|
||||
if (header.header_number < 1) return fail("header number must be positive");
|
||||
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
|
||||
@@ -303,6 +307,7 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||
metadata.sender_public_key_hex = pair.header.public_key_hex;
|
||||
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
||||
metadata.payload_memo = pair.payload_memo;
|
||||
metadata.sent_at = pair.header.sent_at; // carry the sender's compose time (0 if absent)
|
||||
result.metadata.push_back(std::move(metadata));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ struct HushChatHeader {
|
||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||
std::string secretstream_header_hex;
|
||||
std::string public_key_hex;
|
||||
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
||||
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
|
||||
std::int64_t sent_at = 0;
|
||||
};
|
||||
|
||||
struct HushChatHeaderParseResult {
|
||||
@@ -80,6 +83,7 @@ struct HushChatTransactionMetadata {
|
||||
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
|
||||
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
||||
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
|
||||
};
|
||||
|
||||
struct HushChatTransactionExtractionResult {
|
||||
|
||||
@@ -25,19 +25,41 @@ void ChatService::clearIdentity() {
|
||||
|
||||
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||
std::int64_t fallbackTimestamp) {
|
||||
std::int64_t fallbackTimestamp,
|
||||
std::vector<std::string>* newIncomingCids) {
|
||||
if (!has_identity_) return 0;
|
||||
|
||||
const std::string myPubKey = chatIdentityPublicKeyHex(identity_);
|
||||
|
||||
int added = 0;
|
||||
for (const auto& meta : metadata) {
|
||||
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
|
||||
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
|
||||
// "from peer" message. (This also collapses same-seed self-chat, where the "peer" wallet shares
|
||||
// our identity, so every message would otherwise loop back.)
|
||||
if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;
|
||||
|
||||
ChatMessage message;
|
||||
message.direction = ChatDirection::Incoming;
|
||||
message.txid = meta.txid;
|
||||
message.conversation_id = meta.conversation_id;
|
||||
message.peer_zaddr = meta.reply_zaddr;
|
||||
message.peer_public_key_hex = meta.sender_public_key_hex;
|
||||
// Reference time: the tx/receive time (block time if confirmed, else the receiver's wall clock for
|
||||
// a mempool receive).
|
||||
const auto timeIt = txTimestamps.find(meta.txid);
|
||||
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
const std::int64_t refTime = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
// Prefer the sender's stamped compose time (header "ts") — the true send time, shown identically on
|
||||
// both ends. But REJECT a value implausibly in the FUTURE vs the reference: a wrong/ahead peer clock
|
||||
// would otherwise pin their messages to the bottom of the thread forever. A compose time in the
|
||||
// PAST is fine — the note buffer can broadcast a queued message long after it was composed, and a
|
||||
// confirmed tx's block time is always >= the compose time.
|
||||
constexpr std::int64_t kSenderTsFutureToleranceSec = 3600; // 1 hour of clock skew tolerated
|
||||
if (meta.sent_at > 0 && (refTime <= 0 || meta.sent_at <= refTime + kSenderTsFutureToleranceSec)) {
|
||||
message.timestamp = meta.sent_at;
|
||||
} else {
|
||||
message.timestamp = refTime;
|
||||
}
|
||||
message.payload_position = meta.payload_position;
|
||||
|
||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||
@@ -59,6 +81,9 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
if (store_.append(message)) {
|
||||
if (db_) db_->append(message);
|
||||
++added;
|
||||
// Every ingested message is incoming — report its cid so the caller can notify without
|
||||
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
|
||||
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
|
||||
}
|
||||
}
|
||||
return added;
|
||||
@@ -105,4 +130,18 @@ bool ChatService::recordOutgoing(const ChatMessage& message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ChatService::recordOutgoingPending(const ChatMessage& message) {
|
||||
// Persist immediately (as Sending) so a send survives an app quit before the broadcast resolves;
|
||||
// resolveOutgoing() then UPSERTS the row to the final status. A stray persisted Sending (crash mid-
|
||||
// broadcast) loads as Sent (see ChatDatabase::deserialize).
|
||||
const bool appended = store_.append(message);
|
||||
if (appended && db_) db_->upsert(message);
|
||||
return appended;
|
||||
}
|
||||
|
||||
void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery) {
|
||||
const ChatMessage* updated = store_.updateDelivery(txid, delivery);
|
||||
if (updated && db_) db_->upsert(*updated); // overwrite the Sending row with the final status
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
|
||||
@@ -40,9 +40,13 @@ public:
|
||||
// when the txid isn't present. Newly-added messages are also persisted (if a database is
|
||||
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
|
||||
// Messages are dropped silently (no logging of memo/plaintext).
|
||||
// When `newIncomingCids` is non-null it is filled with the conversation ids of the genuinely-new
|
||||
// incoming (non-request) messages appended this call — a reliable "a new message arrived here"
|
||||
// signal for notifications that doesn't depend on any timestamp/seen-watermark comparison.
|
||||
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||
std::int64_t fallbackTimestamp = 0);
|
||||
std::int64_t fallbackTimestamp = 0,
|
||||
std::vector<std::string>* newIncomingCids = nullptr);
|
||||
|
||||
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
||||
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
||||
@@ -72,6 +76,12 @@ public:
|
||||
// only local record of what we sent.)
|
||||
bool recordOutgoing(const ChatMessage& message);
|
||||
|
||||
// Two-phase echo for delivery tracking: record + persist immediately as Sending (survives an app
|
||||
// quit), then resolveOutgoing() upserts the final status once the broadcast completes. A stray
|
||||
// persisted Sending (crash mid-broadcast) loads back as Sent.
|
||||
bool recordOutgoingPending(const ChatMessage& message);
|
||||
void resolveOutgoing(const std::string& txid, ChatDelivery delivery);
|
||||
|
||||
const ChatStore& store() const { return store_; }
|
||||
ChatStore& store() { return store_; }
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "chat_store.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
||||
@@ -19,9 +21,29 @@ std::vector<ChatMessage> ChatStore::conversation(const std::string& conversation
|
||||
for (const auto& message : messages_) {
|
||||
if (message.conversation_id == conversationId) out.push_back(message);
|
||||
}
|
||||
// Return chronological (oldest→newest). messages_ is in scan/insertion order, which is NOT
|
||||
// time-ordered (a full scan harvests txids in std::set/unordered_map order) — that would mis-order the
|
||||
// rendered thread AND let the reply-target pin (first-seen peer address) latch onto a non-establishing
|
||||
// message (B2). timestamp is the block/tx time (peer can't set it); tie-break txid + payload_position
|
||||
// for determinism.
|
||||
std::stable_sort(out.begin(), out.end(), [](const ChatMessage& a, const ChatMessage& b) {
|
||||
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
|
||||
if (a.txid != b.txid) return a.txid < b.txid;
|
||||
return a.payload_position < b.payload_position;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery) {
|
||||
for (auto& message : messages_) {
|
||||
if (message.txid == txid) {
|
||||
message.delivery = delivery;
|
||||
return &message;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> ChatStore::conversationIds() const {
|
||||
std::vector<std::string> ids;
|
||||
std::unordered_set<std::string> seenIds;
|
||||
|
||||
@@ -21,9 +21,23 @@ public:
|
||||
// Messages in a conversation, in insertion order.
|
||||
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
||||
|
||||
// Update an outgoing echo's delivery status by its local txid. Returns a pointer to the updated
|
||||
// message (for the caller to persist), or nullptr if no message has that txid.
|
||||
const ChatMessage* updateDelivery(const std::string& txid, ChatDelivery delivery);
|
||||
|
||||
// Distinct conversation ids, in first-seen order.
|
||||
std::vector<std::string> conversationIds() const;
|
||||
|
||||
// The set of on-chain txids that carried a chat message (sent or received, messages + contact
|
||||
// requests) — used by the History tab to badge / filter chat transactions. O(messages), no copies.
|
||||
std::unordered_set<std::string> chatTxids() const {
|
||||
std::unordered_set<std::string> out;
|
||||
out.reserve(messages_.size());
|
||||
for (const auto& m : messages_)
|
||||
if (!m.txid.empty()) out.insert(m.txid);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::size_t size() const { return messages_.size(); }
|
||||
bool empty() const { return messages_.empty(); }
|
||||
void clear();
|
||||
|
||||
@@ -147,6 +147,26 @@ bool Settings::load(const std::string& path)
|
||||
loadScalar(j, "language", language_);
|
||||
loadScalar(j, "skin_id", skin_id_);
|
||||
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
||||
if (j.contains("muted_chat_cids") && j["muted_chat_cids"].is_array()) {
|
||||
muted_chat_cids_.clear();
|
||||
for (const auto& c : j["muted_chat_cids"])
|
||||
if (c.is_string()) muted_chat_cids_.push_back(c.get<std::string>());
|
||||
}
|
||||
if (j.contains("hidden_chat_cids") && j["hidden_chat_cids"].is_array()) {
|
||||
hidden_chat_cids_.clear();
|
||||
for (const auto& c : j["hidden_chat_cids"])
|
||||
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
|
||||
}
|
||||
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
||||
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
||||
loadScalar(j, "chat_bubble_style", chat_bubble_style_); setChatBubbleStyle(chat_bubble_style_);
|
||||
loadScalar(j, "chat_bubble_accent", chat_bubble_accent_); setChatBubbleAccent(chat_bubble_accent_);
|
||||
loadScalar(j, "chat_density", chat_density_); setChatDensity(chat_density_);
|
||||
loadScalar(j, "chat_font_scale", chat_font_scale_); setChatFontScale(chat_font_scale_);
|
||||
loadScalar(j, "chat_time_format", chat_time_format_); setChatTimeFormat(chat_time_format_);
|
||||
loadScalar(j, "chat_enter_sends", chat_enter_sends_);
|
||||
loadScalar(j, "time_format", time_format_); setTimeFormat(time_format_);
|
||||
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||
@@ -172,7 +192,18 @@ bool Settings::load(const std::string& path)
|
||||
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
||||
}
|
||||
}
|
||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
|
||||
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
|
||||
loadScalar(j, "animate_avatars", animate_avatars_);
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||
loadScalar(j, "console_text_color", console_text_color_);
|
||||
loadScalar(j, "console_zoom", console_zoom_);
|
||||
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
|
||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||
hidden_addresses_.clear();
|
||||
for (const auto& a : j["hidden_addresses"])
|
||||
@@ -200,6 +231,8 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
|
||||
loadScalar(j, "active_wallet_file", active_wallet_file_);
|
||||
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
|
||||
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
|
||||
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
|
||||
@@ -281,6 +314,8 @@ bool Settings::load(const std::string& path)
|
||||
loadScalar(j, "reduce_motion", reduce_motion_);
|
||||
loadScalar(j, "selected_exchange", selected_exchange_);
|
||||
loadScalar(j, "selected_pair", selected_pair_);
|
||||
loadScalar(j, "chart_interval", chart_interval_);
|
||||
loadScalar(j, "chart_style", chart_style_);
|
||||
loadScalar(j, "pool_url", pool_url_);
|
||||
// Migrate old default pool URL that was missing the stratum port
|
||||
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
||||
@@ -342,6 +377,7 @@ bool Settings::load(const std::string& path)
|
||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||
entry.scope = e.value("scope", "");
|
||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||
entry.gridCol = e["grid_col"].get<int>();
|
||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||
@@ -408,6 +444,21 @@ bool Settings::save(const std::string& path)
|
||||
j["language"] = language_;
|
||||
j["skin_id"] = skin_id_;
|
||||
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
||||
j["muted_chat_cids"] = json::array();
|
||||
for (const auto& c : muted_chat_cids_)
|
||||
j["muted_chat_cids"].push_back(c);
|
||||
j["hidden_chat_cids"] = json::array();
|
||||
for (const auto& c : hidden_chat_cids_)
|
||||
j["hidden_chat_cids"].push_back(c);
|
||||
j["chat_emoji_color"] = chat_emoji_color_;
|
||||
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||
j["chat_bubble_style"] = chat_bubble_style_;
|
||||
j["chat_bubble_accent"] = chat_bubble_accent_;
|
||||
j["chat_density"] = chat_density_;
|
||||
j["chat_font_scale"] = chat_font_scale_;
|
||||
j["chat_time_format"] = chat_time_format_;
|
||||
j["chat_enter_sends"] = chat_enter_sends_;
|
||||
j["time_format"] = time_format_;
|
||||
j["acrylic_enabled"] = acrylic_enabled_;
|
||||
j["acrylic_quality"] = acrylic_quality_;
|
||||
j["blur_multiplier"] = blur_multiplier_;
|
||||
@@ -416,7 +467,15 @@ bool Settings::save(const std::string& path)
|
||||
j["ui_opacity"] = ui_opacity_;
|
||||
j["window_opacity"] = window_opacity_;
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["contacts_view_mode"] = contacts_view_mode_;
|
||||
j["contacts_avatar_shape"] = contacts_avatar_shape_;
|
||||
j["contacts_list_scale"] = contacts_list_scale_;
|
||||
j["animate_avatars"] = animate_avatars_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
j["console_text_color"] = console_text_color_;
|
||||
j["console_zoom"] = console_zoom_;
|
||||
j["hidden_addresses"] = json::array();
|
||||
for (const auto& addr : hidden_addresses_)
|
||||
j["hidden_addresses"].push_back(addr);
|
||||
@@ -438,6 +497,8 @@ bool Settings::save(const std::string& path)
|
||||
}
|
||||
j["wizard_completed"] = wizard_completed_;
|
||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
|
||||
j["active_wallet_file"] = active_wallet_file_;
|
||||
j["seed_migration_pending"] = seed_migration_pending_;
|
||||
j["seed_migration_dest"] = seed_migration_dest_;
|
||||
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
|
||||
@@ -478,6 +539,8 @@ bool Settings::save(const std::string& path)
|
||||
j["reduce_motion"] = reduce_motion_;
|
||||
j["selected_exchange"] = selected_exchange_;
|
||||
j["selected_pair"] = selected_pair_;
|
||||
j["chart_interval"] = chart_interval_;
|
||||
j["chart_style"] = chart_style_;
|
||||
j["pool_url"] = pool_url_;
|
||||
j["pool_algo"] = pool_algo_;
|
||||
j["pool_worker"] = pool_worker_;
|
||||
@@ -516,6 +579,7 @@ bool Settings::save(const std::string& path)
|
||||
entry["show_24h"] = e.show24h;
|
||||
entry["show_sparkline"] = e.showSparkline;
|
||||
entry["sparkline_interval"] = e.sparklineInterval;
|
||||
entry["scope"] = e.scope;
|
||||
entry["grid_col"] = e.gridCol;
|
||||
entry["grid_row"] = e.gridRow;
|
||||
entry["grid_w"] = e.gridW;
|
||||
|
||||
@@ -91,13 +91,18 @@ public:
|
||||
bool showValue = true; // show the converted/fiat value on the card
|
||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
||||
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
|
||||
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
|
||||
int sparklineInterval = 4; // 0=minute 1=hour 2=day 3=week 4=month (default month: a real curve
|
||||
// from the daily series, vs the young in-session minute buffer)
|
||||
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
|
||||
// hash (shown only when that wallet is active). New entries are tagged with the current
|
||||
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||
std::string scope;
|
||||
// Legacy dashboard-grid placement (deprecated by the row layout; kept for back-compat so an
|
||||
// older build's saved positions aren't dropped, but no longer used by the renderer).
|
||||
int gridCol = -1;
|
||||
int gridRow = -1;
|
||||
int gridW = 8; // default group width (cells); minimum is 8
|
||||
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
|
||||
int gridW = 8;
|
||||
int gridH = 3;
|
||||
};
|
||||
|
||||
// Theme
|
||||
@@ -114,6 +119,52 @@ public:
|
||||
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
||||
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
||||
|
||||
// Muted chat conversations (by cid) — muted conversations don't badge or raise a toast (Q10).
|
||||
bool isChatMuted(const std::string& cid) const {
|
||||
return std::find(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid) != muted_chat_cids_.end();
|
||||
}
|
||||
void setChatMuted(const std::string& cid, bool muted) {
|
||||
const bool already = isChatMuted(cid);
|
||||
if (muted && !already) muted_chat_cids_.push_back(cid);
|
||||
else if (!muted && already)
|
||||
muted_chat_cids_.erase(std::remove(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid),
|
||||
muted_chat_cids_.end());
|
||||
}
|
||||
|
||||
// Hidden chat conversations (by cid) — hidden ones are filtered out of the list; a new incoming
|
||||
// message un-hides them (you can't un-receive) so nothing is silently lost.
|
||||
bool isChatHidden(const std::string& cid) const {
|
||||
return std::find(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid) != hidden_chat_cids_.end();
|
||||
}
|
||||
void setChatHidden(const std::string& cid, bool hidden) {
|
||||
const bool already = isChatHidden(cid);
|
||||
if (hidden && !already) hidden_chat_cids_.push_back(cid);
|
||||
else if (!hidden && already)
|
||||
hidden_chat_cids_.erase(std::remove(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid),
|
||||
hidden_chat_cids_.end());
|
||||
}
|
||||
|
||||
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||
float getChatPollRateSec() const { return chat_poll_rate_sec_; }
|
||||
void setChatPollRateSec(float v) { chat_poll_rate_sec_ = std::max(0.5f, std::min(15.0f, v)); }
|
||||
int getChatBubbleStyle() const { return chat_bubble_style_; }
|
||||
void setChatBubbleStyle(int v) { chat_bubble_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
int getChatBubbleAccent() const { return chat_bubble_accent_; }
|
||||
void setChatBubbleAccent(int v) { chat_bubble_accent_ = (v < 0 || v > 5) ? 0 : v; }
|
||||
int getChatDensity() const { return chat_density_; }
|
||||
void setChatDensity(int v) { chat_density_ = (v < 0 || v > 1) ? 0 : v; }
|
||||
float getChatFontScale() const { return chat_font_scale_; }
|
||||
void setChatFontScale(float v) { chat_font_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
int getChatTimeFormat() const { return chat_time_format_; } // 0=follow global, 1=24h, 2=12h
|
||||
void setChatTimeFormat(int v) { chat_time_format_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
bool getChatEnterSends() const { return chat_enter_sends_; }
|
||||
void setChatEnterSends(bool v) { chat_enter_sends_ = v; }
|
||||
// Global clock format (0=24h, 1=12h) — chat can override it for the Chat tab only.
|
||||
int getTimeFormat() const { return time_format_; }
|
||||
void setTimeFormat(int v) { time_format_ = (v < 0 || v > 1) ? 0 : v; }
|
||||
|
||||
// Privacy
|
||||
bool getSaveZtxs() const { return save_ztxs_; }
|
||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||
@@ -176,10 +227,36 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
|
||||
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts customization (gear modal): avatar shape + card/list row scale.
|
||||
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
|
||||
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
|
||||
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
float getContactsListScale() const { return contacts_list_scale_; }
|
||||
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
|
||||
bool getAnimateAvatars() const { return animate_avatars_; }
|
||||
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
||||
|
||||
// Console scanline effect
|
||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
||||
|
||||
// Console output appearance: per-line left color accent bars, and per-channel text coloring.
|
||||
// (Defaults match the ConsoleTab statics so an upgrade re-save doesn't flip visible behavior.)
|
||||
bool getConsoleLineAccents() const { return console_line_accents_; }
|
||||
void setConsoleLineAccents(bool v) { console_line_accents_ = v; }
|
||||
bool getConsoleTextColor() const { return console_text_color_; }
|
||||
void setConsoleTextColor(bool v) { console_text_color_ = v; }
|
||||
float getConsoleZoom() const { return console_zoom_; }
|
||||
void setConsoleZoom(float v) { console_zoom_ = v; }
|
||||
|
||||
// Hidden addresses (addresses hidden from the UI by the user)
|
||||
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
|
||||
bool isAddressHidden(const std::string& addr) const { return hidden_addresses_.count(addr) > 0; }
|
||||
@@ -250,6 +327,17 @@ public:
|
||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||
|
||||
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
|
||||
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
|
||||
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
|
||||
void setDaemonUpdatePromptedSize(long long v) { daemon_update_prompted_size_ = v; }
|
||||
|
||||
// Active wallet file the daemon loads via -wallet=<name> (multi-wallet). A plain filename in
|
||||
// the datadir; defaults to the daemon's own default. Used at launch to know which wallet we're
|
||||
// on before connect + to scope per-wallet data.
|
||||
std::string getActiveWalletFile() const { return active_wallet_file_; }
|
||||
void setActiveWalletFile(const std::string& v) { active_wallet_file_ = v; }
|
||||
|
||||
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
|
||||
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
|
||||
bool getSeedMigrationPending() const { return seed_migration_pending_; }
|
||||
@@ -350,6 +438,10 @@ public:
|
||||
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
||||
std::string getSelectedPair() const { return selected_pair_; }
|
||||
void setSelectedPair(const std::string& v) { selected_pair_ = v; }
|
||||
int getChartInterval() const { return chart_interval_; } // Market chart range 0=Live..4=1M
|
||||
void setChartInterval(int v) { chart_interval_ = v; }
|
||||
int getChartStyle() const { return chart_style_; } // 0 = line, 1 = candlestick
|
||||
void setChartStyle(int v) { chart_style_ = v; }
|
||||
|
||||
// Pool mining
|
||||
std::string getPoolUrl() const { return pool_url_; }
|
||||
@@ -434,6 +526,18 @@ private:
|
||||
std::string theme_ = "dragonx";
|
||||
std::string skin_id_ = "dragonx";
|
||||
std::string chat_reply_zaddr_;
|
||||
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
||||
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
||||
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
||||
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
||||
int chat_bubble_style_ = 0; // 0 = rounded, 1 = square, 2 = minimal
|
||||
int chat_bubble_accent_ = 0; // outgoing-bubble accent preset (0 = theme primary)
|
||||
int chat_density_ = 0; // 0 = comfortable, 1 = compact
|
||||
float chat_font_scale_ = 1.0f; // message text scale
|
||||
int chat_time_format_ = 0; // 0 = follow global, 1 = 24h, 2 = 12h (Chat tab only)
|
||||
bool chat_enter_sends_ = true; // Enter sends (vs. inserts newline; Ctrl+Enter sends)
|
||||
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
|
||||
bool save_ztxs_ = true;
|
||||
bool auto_shield_ = true;
|
||||
bool use_tor_ = false;
|
||||
@@ -450,18 +554,28 @@ private:
|
||||
bool gradient_background_ = false;
|
||||
#ifdef _WIN32
|
||||
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.3–1.0, 1.0 = opaque)
|
||||
float window_opacity_ = 0.75f; // Background alpha (0.3–1.0, <1 = desktop visible)
|
||||
float window_opacity_ = 0.90f; // Background alpha (0.3–1.0, <1 = desktop visible)
|
||||
#else
|
||||
float ui_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
|
||||
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
|
||||
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
|
||||
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
|
||||
bool scanline_enabled_ = true;
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||
float console_zoom_ = 1.0f; // console output font zoom factor
|
||||
std::set<std::string> hidden_addresses_;
|
||||
std::set<std::string> favorite_addresses_;
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
bool wizard_completed_ = false;
|
||||
bool seed_backup_reminded_ = false;
|
||||
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
||||
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
|
||||
bool seed_migration_pending_ = false;
|
||||
std::string seed_migration_dest_;
|
||||
std::string seed_migration_temp_dir_;
|
||||
@@ -501,6 +615,8 @@ private:
|
||||
bool reduce_motion_ = false;
|
||||
std::string selected_exchange_ = "Nonkyc.io";
|
||||
std::string selected_pair_ = "DRGX/USDT";
|
||||
int chart_interval_ = 4; // Market chart range (0=Live 1=1H 2=1D 3=1W 4=1M)
|
||||
int chart_style_ = 1; // Market chart style (0=line, 1=candlestick)
|
||||
|
||||
// Pool mining
|
||||
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "daemon_controller.h"
|
||||
#include "../config/settings.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
@@ -23,6 +26,18 @@ void DaemonController::syncSettings(const config::Settings* settings)
|
||||
if (!settings) return;
|
||||
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||
|
||||
std::string walletFile = settings->getActiveWalletFile();
|
||||
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
|
||||
// "wallet-ip-<hash>.dat" name. If that link went dangling (the external file was moved / a USB was
|
||||
// unplugged between sessions), don't let the daemon create a fresh EMPTY wallet at that name — fall
|
||||
// back to the default this launch. fs::exists follows the link, so it's false for a dangling one.
|
||||
if (walletFile.rfind("wallet-ip-", 0) == 0) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + walletFile, ec))
|
||||
walletFile = "wallet.dat";
|
||||
}
|
||||
daemon_->setWalletFile(walletFile);
|
||||
}
|
||||
|
||||
bool DaemonController::start(const config::Settings* settings)
|
||||
@@ -46,6 +61,11 @@ bool DaemonController::externalDaemonDetected() const
|
||||
return daemon_->externalDaemonDetected();
|
||||
}
|
||||
|
||||
void DaemonController::clearExternalDaemonDetected()
|
||||
{
|
||||
daemon_->clearExternalDaemonDetected();
|
||||
}
|
||||
|
||||
DaemonController::State DaemonController::state() const
|
||||
{
|
||||
return daemon_->getState();
|
||||
@@ -101,6 +121,11 @@ void DaemonController::setZapOnNextStart(bool enabled)
|
||||
daemon_->setZapOnNextStart(enabled);
|
||||
}
|
||||
|
||||
void DaemonController::setSalvageOnNextStart(bool enabled)
|
||||
{
|
||||
daemon_->setSalvageOnNextStart(enabled);
|
||||
}
|
||||
|
||||
bool DaemonController::zapOnNextStart() const
|
||||
{
|
||||
return daemon_->zapOnNextStart();
|
||||
|
||||
@@ -93,6 +93,7 @@ public:
|
||||
|
||||
bool isRunning() const;
|
||||
bool externalDaemonDetected() const;
|
||||
void clearExternalDaemonDetected();
|
||||
State state() const;
|
||||
const std::string& lastError() const;
|
||||
int crashCount() const;
|
||||
@@ -106,6 +107,7 @@ public:
|
||||
bool rescanOnNextStart() const;
|
||||
void setZapOnNextStart(bool enabled);
|
||||
bool zapOnNextStart() const;
|
||||
void setSalvageOnNextStart(bool enabled);
|
||||
|
||||
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
||||
bool externalDaemonDetected,
|
||||
|
||||
@@ -386,52 +386,80 @@ static std::string getPortOwnerInfo(int port)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Check if a TCP port is already in use (something is LISTENING)
|
||||
// Check if a TCP port is already in use (something is LISTENING). The daemon binds BOTH 127.0.0.1 (IPv4)
|
||||
// and ::1 (IPv6); during shutdown one can linger after the other releases (and a "Binding RPC on ::1 …
|
||||
// failed" is fatal to a fresh start), so we treat the port as in use if EITHER localhost family has it.
|
||||
static bool isPortInUse(int port)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == INVALID_SOCKET) { WSACleanup(); return false; }
|
||||
struct sockaddr_in addr;
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<u_short>(port));
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
||||
closesocket(sock);
|
||||
bool inUse = false;
|
||||
{ // IPv4 127.0.0.1
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock != INVALID_SOCKET) {
|
||||
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<u_short>(port));
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
|
||||
closesocket(sock);
|
||||
}
|
||||
}
|
||||
if (!inUse) { // IPv6 ::1
|
||||
SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock != INVALID_SOCKET) {
|
||||
struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr));
|
||||
addr.sin6_family = AF_INET6;
|
||||
addr.sin6_port = htons(static_cast<u_short>(port));
|
||||
addr.sin6_addr = in6addr_loopback; // ::1 — avoids inet_pton's _WIN32_WINNT gating on mingw
|
||||
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
|
||||
closesocket(sock);
|
||||
}
|
||||
}
|
||||
WSACleanup();
|
||||
return (result == 0);
|
||||
return inUse;
|
||||
#else
|
||||
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid
|
||||
// creating sockets. Fall back to connect() if /proc is unavailable.
|
||||
FILE* fp = fopen("/proc/net/tcp", "r");
|
||||
if (fp) {
|
||||
char line[256];
|
||||
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The
|
||||
// parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port.
|
||||
auto scanProc = [port](const char* path) -> bool {
|
||||
FILE* fp = fopen(path, "r");
|
||||
if (!fp) return false;
|
||||
char line[512];
|
||||
unsigned int localPort, state;
|
||||
bool found = false;
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
|
||||
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { found = true; break; }
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
return found;
|
||||
};
|
||||
if (FILE* probe = fopen("/proc/net/tcp", "r")) { // /proc available → authoritative LISTEN check
|
||||
fclose(probe);
|
||||
return scanProc("/proc/net/tcp") || scanProc("/proc/net/tcp6");
|
||||
}
|
||||
// Fallback (macOS): try to connect
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) return false;
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(port));
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
||||
close(sock);
|
||||
return (result == 0);
|
||||
// Fallback (macOS): connect() probe on both loopback families.
|
||||
auto connProbe = [port](int family, const char* addr) -> bool {
|
||||
int sock = socket(family, SOCK_STREAM, 0);
|
||||
if (sock < 0) return false;
|
||||
bool ok = false;
|
||||
if (family == AF_INET) {
|
||||
struct sockaddr_in a; memset(&a, 0, sizeof(a));
|
||||
a.sin_family = AF_INET; a.sin_port = htons(static_cast<uint16_t>(port));
|
||||
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
|
||||
} else {
|
||||
struct sockaddr_in6 a; memset(&a, 0, sizeof(a));
|
||||
a.sin6_family = AF_INET6; a.sin6_port = htons(static_cast<uint16_t>(port));
|
||||
inet_pton(AF_INET6, addr, &a.sin6_addr);
|
||||
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
|
||||
}
|
||||
close(sock);
|
||||
return ok;
|
||||
};
|
||||
return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -488,9 +516,24 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||
}
|
||||
|
||||
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
|
||||
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
|
||||
if (zap_on_next_start_.exchange(false)) {
|
||||
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
|
||||
// non-default name so the common case's command line is unchanged; skip during an isolated
|
||||
// start (seed migration manages its own throwaway wallet).
|
||||
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
|
||||
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
|
||||
args.push_back("-wallet=" + wallet_file_);
|
||||
}
|
||||
|
||||
// Add wallet-repair flag if requested (one-shot). Precedence: salvage > zap > rescan; each implies a
|
||||
// rescan in the daemon, so we don't stack them.
|
||||
if (salvage_on_next_start_.exchange(false)) {
|
||||
// -salvagewallet recovers readable keypairs from a corrupt wallet.dat; the daemon then implies -rescan.
|
||||
DEBUG_LOGF("[INFO] Adding -salvagewallet flag to recover a corrupt wallet\n");
|
||||
args.push_back("-salvagewallet");
|
||||
zap_on_next_start_.store(false);
|
||||
rescan_on_next_start_.store(false);
|
||||
} else if (zap_on_next_start_.exchange(false)) {
|
||||
// -zapwallettxes=2 wipes all wallet tx/note records and rebuilds them from the chain (implies -rescan).
|
||||
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
|
||||
args.push_back("-zapwallettxes=2");
|
||||
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
|
||||
@@ -647,18 +690,25 @@ static DWORD findProcessByName(const char* name)
|
||||
{
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return 0;
|
||||
|
||||
PROCESSENTRY32 entry;
|
||||
|
||||
// Use the explicit WIDE Toolhelp API + a wide compare so this is correct regardless of the UNICODE
|
||||
// macro. (The non-suffixed PROCESSENTRY32/Process32First map to the wide variants when UNICODE is
|
||||
// defined, in which case szExeFile is WCHAR[] and an ANSI _stricmp would compare garbage and NEVER
|
||||
// match — silently making findProcessByName a no-op that returns 0 for a running process.)
|
||||
wchar_t wname[MAX_PATH];
|
||||
if (MultiByteToWideChar(CP_ACP, 0, name, -1, wname, MAX_PATH) == 0) { CloseHandle(snap); return 0; }
|
||||
|
||||
PROCESSENTRY32W entry;
|
||||
entry.dwSize = sizeof(entry);
|
||||
|
||||
|
||||
DWORD pid = 0;
|
||||
if (Process32First(snap, &entry)) {
|
||||
if (Process32FirstW(snap, &entry)) {
|
||||
do {
|
||||
if (_stricmp(entry.szExeFile, name) == 0) {
|
||||
if (lstrcmpiW(entry.szExeFile, wname) == 0) { // Win32 case-insensitive wide compare
|
||||
pid = entry.th32ProcessID;
|
||||
break;
|
||||
}
|
||||
} while (Process32Next(snap, &entry));
|
||||
} while (Process32NextW(snap, &entry));
|
||||
}
|
||||
CloseHandle(snap);
|
||||
return pid;
|
||||
@@ -1239,5 +1289,28 @@ bool EmbeddedDaemon::tcpPortInUse(int port)
|
||||
return isPortInUse(port);
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::isDaemonProcessRunning()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return findProcessByName("dragonxd.exe") != 0;
|
||||
#elif defined(__linux__)
|
||||
// Scan /proc for a process whose comm is exactly "dragonxd". Iterate with an error_code so a proc
|
||||
// entry vanishing mid-scan (a process exiting) can't throw.
|
||||
std::error_code ec;
|
||||
fs::directory_iterator it("/proc", ec), end;
|
||||
for (; !ec && it != end; it.increment(ec)) {
|
||||
const std::string pid = it->path().filename().string();
|
||||
if (pid.empty() || pid[0] < '0' || pid[0] > '9') continue; // numeric pid dirs only
|
||||
std::ifstream f((it->path() / "comm").string());
|
||||
std::string comm;
|
||||
if (f && std::getline(f, comm) && comm == "dragonxd") return true;
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
// macOS has no /proc; fall back to the RPC-port probe (best-effort).
|
||||
return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -142,6 +142,10 @@ public:
|
||||
* When true the wallet should connect to it instead of showing an error.
|
||||
*/
|
||||
bool externalDaemonDetected() const { return external_daemon_detected_; }
|
||||
// Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that
|
||||
// stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also
|
||||
// clears it on the port-free fall-through, but only if not short-circuited by an early guard.
|
||||
void clearExternalDaemonDetected() { external_daemon_detected_ = false; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for state changes
|
||||
@@ -183,6 +187,12 @@ public:
|
||||
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
||||
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
||||
|
||||
// Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
|
||||
// <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
|
||||
// filename in the datadir — the daemon rejects paths.
|
||||
void setWalletFile(const std::string& v) { wallet_file_ = v; }
|
||||
std::string walletFile() const { return wallet_file_; }
|
||||
|
||||
/**
|
||||
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
|
||||
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
|
||||
@@ -191,6 +201,11 @@ public:
|
||||
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
||||
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
||||
|
||||
// -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in
|
||||
// the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt.
|
||||
void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; }
|
||||
bool salvageOnNextStart() const { return salvage_on_next_start_.load(); }
|
||||
|
||||
/**
|
||||
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
|
||||
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
|
||||
@@ -211,6 +226,15 @@ public:
|
||||
*/
|
||||
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
|
||||
|
||||
/**
|
||||
* @brief True while ANY dragonxd process is running (by process name), regardless of who started it.
|
||||
* Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a
|
||||
* graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the
|
||||
* process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released
|
||||
* the datadir before starting a replacement. Matches the daemon binary name on all platforms.
|
||||
*/
|
||||
static bool isDaemonProcessRunning();
|
||||
|
||||
/** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
|
||||
static bool tcpPortInUse(int port);
|
||||
|
||||
@@ -251,9 +275,11 @@ private:
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::set<std::string> debug_categories_;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
std::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
|
||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||
std::atomic<bool> salvage_on_next_start_{false}; // -salvagewallet flag for next start
|
||||
std::string override_datadir_; // one-shot: -datadir for the next start
|
||||
std::vector<std::string> override_extra_args_; // one-shot: extra args for the next start
|
||||
bool skip_port_check_ = false; // isolated instance on a non-default port
|
||||
|
||||
@@ -110,14 +110,38 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
||||
|
||||
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
|
||||
try {
|
||||
auto m = cli.call("z_exportmnemonic");
|
||||
if (m.contains("mnemonic") && m["mnemonic"].is_string())
|
||||
r.seedPhrase = m["mnemonic"].get<std::string>();
|
||||
auto m = cli.callSecret("z_exportmnemonic"); // zero the raw body too (B7)
|
||||
if (m.contains("mnemonic") && m["mnemonic"].is_string()) {
|
||||
// Take our copy, then scrub the json node's own copy so it isn't freed in the clear (B7).
|
||||
auto& mn = m["mnemonic"].get_ref<std::string&>();
|
||||
r.seedPhrase = mn;
|
||||
if (!mn.empty()) sodium_memzero(&mn[0], mn.size());
|
||||
}
|
||||
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
|
||||
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
|
||||
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
|
||||
} catch (const std::exception& e) {
|
||||
r.error = std::string("Seed export failed: ") + e.what();
|
||||
const std::string what = e.what();
|
||||
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
|
||||
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
|
||||
// until the daemon is updated, so give an actionable message, not the raw RPC error.
|
||||
if (what.find("Method not found") != std::string::npos ||
|
||||
what.find("-32601") != std::string::npos) {
|
||||
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
|
||||
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
|
||||
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
|
||||
"try again.";
|
||||
} else {
|
||||
r.error = std::string("Seed export failed: ") + what;
|
||||
}
|
||||
}
|
||||
|
||||
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
|
||||
// later step failed (empty address, or z_getnewaddress threw), the caller discards this
|
||||
// result without wiping it, which would leave the seed resident. Success keeps it deliberately.
|
||||
if (!r.ok && !r.seedPhrase.empty()) {
|
||||
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
|
||||
r.seedPhrase.clear();
|
||||
}
|
||||
|
||||
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
|
||||
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
|
||||
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
@@ -208,19 +208,20 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
try {
|
||||
fs::create_directories(fs::path(outPath).parent_path());
|
||||
std::ofstream ofs(outPath);
|
||||
std::ofstream ofs(outPath, std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
|
||||
#ifndef _WIN32
|
||||
// 0600 permissions — only owner can read/write
|
||||
// Restrict to owner (0600) BEFORE writing any secret material (API token, wallet
|
||||
// address, worker name). The file is still empty here, so the config is never
|
||||
// world-readable — closing the window between creation and the previous post-write chmod.
|
||||
chmod(outPath.c_str(), 0600);
|
||||
#endif
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
last_error_ = std::string("Config write error: ") + e.what();
|
||||
|
||||
@@ -51,7 +51,11 @@ bool AddressBook::load()
|
||||
e.label = entry.value("label", "");
|
||||
e.address = entry.value("address", "");
|
||||
e.notes = entry.value("notes", "");
|
||||
|
||||
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
|
||||
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
|
||||
e.scope = entry.value("scope", "global");
|
||||
e.avatar = entry.value("avatar", "");
|
||||
|
||||
if (!e.address.empty()) {
|
||||
entries_.push_back(e);
|
||||
}
|
||||
@@ -82,6 +86,8 @@ bool AddressBook::save()
|
||||
e["label"] = entry.label;
|
||||
e["address"] = entry.address;
|
||||
e["notes"] = entry.notes;
|
||||
e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope;
|
||||
if (!entry.avatar.empty()) e["avatar"] = entry.avatar;
|
||||
j["entries"].push_back(e);
|
||||
}
|
||||
|
||||
@@ -103,11 +109,12 @@ bool AddressBook::save()
|
||||
|
||||
bool AddressBook::addEntry(const AddressBookEntry& entry)
|
||||
{
|
||||
// Check for duplicate address
|
||||
if (findByAddress(entry.address) >= 0) {
|
||||
// Reject a duplicate only within the same visible set (same wallet or global) — the same
|
||||
// address may legitimately be a contact in two different wallets.
|
||||
if (hasVisibleDuplicate(entry.address, entry.scope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
entries_.push_back(entry);
|
||||
return save();
|
||||
}
|
||||
@@ -118,12 +125,11 @@ bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate address (excluding current entry)
|
||||
int existing = findByAddress(entry.address);
|
||||
if (existing >= 0 && static_cast<size_t>(existing) != index) {
|
||||
// Check for a duplicate visible alongside this entry's scope (excluding the entry being edited)
|
||||
if (hasVisibleDuplicate(entry.address, entry.scope, static_cast<int>(index))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
entries_[index] = entry;
|
||||
return save();
|
||||
}
|
||||
@@ -138,6 +144,20 @@ bool AddressBook::removeEntry(size_t index)
|
||||
return save();
|
||||
}
|
||||
|
||||
int AddressBook::reattachLegacyScopes(const std::string& scopeId)
|
||||
{
|
||||
if (scopeId.empty()) return 0;
|
||||
int rescoped = 0;
|
||||
for (auto& e : entries_) {
|
||||
if (e.isGlobal()) continue; // global stays global
|
||||
if (e.scope.rfind("w:", 0) == 0) continue; // already a stable scope
|
||||
e.scope = scopeId;
|
||||
++rescoped;
|
||||
}
|
||||
if (rescoped > 0) save();
|
||||
return rescoped;
|
||||
}
|
||||
|
||||
int AddressBook::findByAddress(const std::string& address) const
|
||||
{
|
||||
for (size_t i = 0; i < entries_.size(); i++) {
|
||||
@@ -148,5 +168,19 @@ int AddressBook::findByAddress(const std::string& address) const
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool AddressBook::hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
||||
int excludeIndex) const
|
||||
{
|
||||
AddressBookEntry probe; probe.scope = scope; // reuse the isGlobal()/scope logic
|
||||
for (size_t i = 0; i < entries_.size(); i++) {
|
||||
if (static_cast<int>(i) == excludeIndex) continue;
|
||||
const auto& e = entries_[i];
|
||||
if (e.address != address) continue;
|
||||
// Collides if they'd ever be shown together: same wallet scope, or either is global.
|
||||
if (e.isGlobal() || probe.isGlobal() || e.scope == scope) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -17,10 +17,24 @@ struct AddressBookEntry {
|
||||
std::string label;
|
||||
std::string address;
|
||||
std::string notes;
|
||||
|
||||
// Per-wallet visibility scope: "global" (shown in every wallet) or a wallet-identity hash
|
||||
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
|
||||
// entries — written before multi-wallet scoping — keep showing everywhere.
|
||||
std::string scope = "global";
|
||||
// Contact avatar: "" = default type badge (Z/T), "icon:<name>" = a Material wallet-icon,
|
||||
// "img:<path>" = a custom image (copied into <config>/contact-avatars/).
|
||||
std::string avatar;
|
||||
|
||||
AddressBookEntry() = default;
|
||||
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "")
|
||||
: label(l), address(a), notes(n) {}
|
||||
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",
|
||||
const std::string& s = "global")
|
||||
: label(l), address(a), notes(n), scope(s) {}
|
||||
|
||||
bool isGlobal() const { return scope.empty() || scope == "global"; }
|
||||
// Visible under the wallet whose identity hash is walletHash (or globally).
|
||||
bool visibleInWallet(const std::string& walletHash) const {
|
||||
return isGlobal() || scope == walletHash;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,12 +87,30 @@ public:
|
||||
bool removeEntry(size_t index);
|
||||
|
||||
/**
|
||||
* @brief Find entry by address
|
||||
* @brief Re-attach contacts stuck on a legacy (drifting address-hash) scope to a stable wallet id.
|
||||
* Rewrites every non-global entry whose scope is NOT already a stable "w:"-prefixed id to
|
||||
* `scopeId`, and saves once if anything changed. Recovery for contacts orphaned when the
|
||||
* old address-set-hash scope shifted (e.g. after creating a new address).
|
||||
* @return number of entries re-scoped.
|
||||
*/
|
||||
int reattachLegacyScopes(const std::string& scopeId);
|
||||
|
||||
/**
|
||||
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
||||
* @param address Address to search for
|
||||
* @return Index or -1 if not found
|
||||
*/
|
||||
int findByAddress(const std::string& address) const;
|
||||
|
||||
/**
|
||||
* @brief Is there an existing entry with this address that would be visible ALONGSIDE a new
|
||||
* entry of the given scope? (Same wallet, or either side global.) Used to reject
|
||||
* duplicates within a wallet while still allowing the same address in different wallets.
|
||||
* @param excludeIndex Storage index to skip (for edit), or -1.
|
||||
*/
|
||||
bool hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
||||
int excludeIndex = -1) const;
|
||||
|
||||
/**
|
||||
* @brief Get all entries
|
||||
*/
|
||||
@@ -89,6 +121,12 @@ public:
|
||||
*/
|
||||
size_t size() const { return entries_.size(); }
|
||||
|
||||
/**
|
||||
* @brief UI-sweep ONLY: replace the in-memory entries WITHOUT persisting to disk, so the sweep can
|
||||
* seed demo contacts and restore the real book without a disk write. Do not use outside the sweep.
|
||||
*/
|
||||
void sweepSetEntries(std::vector<AddressBookEntry> e) { entries_ = std::move(e); }
|
||||
|
||||
/**
|
||||
* @brief Check if empty
|
||||
*/
|
||||
|
||||
44
src/data/candle.h
Normal file
44
src/data/candle.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
// A single OHLC candle — the shared shape for per-exchange candlestick charts. Kept in its own tiny,
|
||||
// dependency-free header so both the parser (data/exchange_candles.h, which pulls in nlohmann/json)
|
||||
// and the model (data/wallet_state.h, included widely) can use it without dragging json everywhere.
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
struct Candle {
|
||||
std::time_t time = 0; // epoch SECONDS (the candle's open/bucket time)
|
||||
double open = 0.0;
|
||||
double high = 0.0;
|
||||
double low = 0.0;
|
||||
double close = 0.0;
|
||||
};
|
||||
|
||||
// Aggregate fine candles into fixed time buckets (e.g. 5-min -> hourly for the 1D view): open = first
|
||||
// open, high = max high, low = min low, close = last close. Input must be ascending by time.
|
||||
inline std::vector<Candle> bucketOHLC(const std::vector<Candle>& src, long windowSec) {
|
||||
std::vector<Candle> out;
|
||||
if (src.empty() || windowSec <= 0) return out;
|
||||
long curBucket = -1;
|
||||
Candle cur;
|
||||
for (const auto& c : src) {
|
||||
const long b = (long)(c.time / windowSec);
|
||||
if (b != curBucket) {
|
||||
if (curBucket >= 0) out.push_back(cur);
|
||||
curBucket = b;
|
||||
cur = c;
|
||||
cur.time = (std::time_t)(b * windowSec);
|
||||
} else {
|
||||
if (c.high > cur.high) cur.high = c.high;
|
||||
if (c.low < cur.low) cur.low = c.low;
|
||||
cur.close = c.close;
|
||||
}
|
||||
}
|
||||
if (curBucket >= 0) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
117
src/data/exchange_candles.h
Normal file
117
src/data/exchange_candles.h
Normal file
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
// Per-exchange OHLC candle adapters. CoinGecko tells us WHICH exchanges list DRGX (via the ticker
|
||||
// `market.identifier`, e.g. "ourbit" / "nonkyc_io") but NOT their API — this is the hand-maintained
|
||||
// mapping from that identifier to each venue's public candle endpoint, so the market chart can show the
|
||||
// SELECTED exchange's real price history instead of CoinGecko's cross-exchange aggregate.
|
||||
//
|
||||
// Header-only + pure (no I/O — the actual HTTP fetch happens in app_network.cpp via util::httpGetString)
|
||||
// so the URL builder + parsers are unit-tested against real captured responses. An unmapped exchange
|
||||
// returns an empty URL, so the caller falls back to the CoinGecko aggregate and nothing regresses.
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "candle.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
using PricePoint = std::pair<std::time_t, double>; // (epoch SECONDS, close price)
|
||||
|
||||
enum class CandleRange {
|
||||
Intraday, // ~2 days of 5-minute candles — backs the Live / 1H / 1D views
|
||||
Daily, // ~1 year of daily candles — backs the 1W / 1M views
|
||||
};
|
||||
|
||||
// True when we have a candle adapter for this CoinGecko exchange identifier.
|
||||
inline bool hasExchangeCandleAdapter(const std::string& identifier) {
|
||||
return identifier == "ourbit" || identifier == "nonkyc_io";
|
||||
}
|
||||
|
||||
// Build the venue's candle URL for a pair + range. Returns "" when the exchange isn't mapped.
|
||||
// `now` is epoch seconds, passed in (no hidden clock reads) so URLs are deterministic in tests.
|
||||
inline std::string buildExchangeCandleUrl(const std::string& identifier, const std::string& base,
|
||||
const std::string& quote, CandleRange range, std::time_t now) {
|
||||
if (identifier == "ourbit") {
|
||||
// Ourbit = MEXC-style /api/v3/klines: symbol=BASEQUOTE, interval=5m|1d, limit.
|
||||
const char* iv = (range == CandleRange::Intraday) ? "5m" : "1d";
|
||||
const int limit = (range == CandleRange::Intraday) ? 576 : 365;
|
||||
return "https://api.ourbit.com/api/v3/klines?symbol=" + base + quote +
|
||||
"&interval=" + iv + "&limit=" + std::to_string(limit);
|
||||
}
|
||||
if (identifier == "nonkyc_io") {
|
||||
// NonKYC = TradingView-UDF candles: symbol=BASE_QUOTE, resolution in minutes, from/to seconds.
|
||||
const char* res = (range == CandleRange::Intraday) ? "5" : "1440";
|
||||
const std::time_t span = (range == CandleRange::Intraday) ? (std::time_t)2 * 24 * 3600
|
||||
: (std::time_t)365 * 24 * 3600;
|
||||
return "https://api.nonkyc.io/api/v2/market/candles?symbol=" + base + "_" + quote +
|
||||
"&resolution=" + res + "&from=" + std::to_string(now - span) + "&to=" + std::to_string(now);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
// Read a JSON value that may be a number or a numeric string (Ourbit sends prices as strings).
|
||||
inline double jnum(const nlohmann::json& v) {
|
||||
if (v.is_number()) return v.get<double>();
|
||||
if (v.is_string()) { try { return std::stod(v.get<std::string>()); } catch (...) { return 0.0; } }
|
||||
return 0.0;
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
// Parse the venue's candle response into ascending OHLC candles. Empty on failure.
|
||||
inline std::vector<Candle> parseExchangeOHLC(const std::string& identifier, const std::string& body) {
|
||||
std::vector<Candle> out;
|
||||
if (body.empty()) return out;
|
||||
try {
|
||||
const nlohmann::json j = nlohmann::json::parse(body);
|
||||
if (identifier == "ourbit") {
|
||||
// [[openTime_ms, open, high, low, close, volume, closeTime, quoteVol], ...]
|
||||
if (!j.is_array()) return out;
|
||||
out.reserve(j.size());
|
||||
for (const auto& k : j) {
|
||||
if (!k.is_array() || k.size() < 5) continue;
|
||||
Candle c;
|
||||
c.time = (std::time_t)(detail::jnum(k[0]) / 1000.0);
|
||||
c.open = detail::jnum(k[1]);
|
||||
c.high = detail::jnum(k[2]);
|
||||
c.low = detail::jnum(k[3]);
|
||||
c.close = detail::jnum(k[4]);
|
||||
if (c.time > 0 && c.close > 0) out.push_back(c);
|
||||
}
|
||||
} else if (identifier == "nonkyc_io") {
|
||||
// {"bars":[{"time":ms,"open":..,"high":..,"low":..,"close":..,"volume":..}, ...]}
|
||||
if (!j.contains("bars") || !j["bars"].is_array()) return out;
|
||||
out.reserve(j["bars"].size());
|
||||
for (const auto& b : j["bars"]) {
|
||||
if (!b.is_object() || !b.contains("time") || !b.contains("close")) continue;
|
||||
Candle c;
|
||||
c.time = (std::time_t)(detail::jnum(b["time"]) / 1000.0);
|
||||
c.close = detail::jnum(b["close"]);
|
||||
c.open = b.contains("open") ? detail::jnum(b["open"]) : c.close;
|
||||
c.high = b.contains("high") ? detail::jnum(b["high"]) : c.close;
|
||||
c.low = b.contains("low") ? detail::jnum(b["low"]) : c.close;
|
||||
if (c.time > 0 && c.close > 0) out.push_back(c);
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
std::sort(out.begin(), out.end(), [](const Candle& a, const Candle& b) { return a.time < b.time; });
|
||||
return out;
|
||||
}
|
||||
|
||||
// Close-only convenience over parseExchangeOHLC (backs the line chart + change-% fallback). bucketOHLC
|
||||
// for candlestick resampling lives in candle.h (dependency-free, reused by market_series.h).
|
||||
inline std::vector<PricePoint> parseExchangeCandles(const std::string& identifier, const std::string& body) {
|
||||
std::vector<PricePoint> out;
|
||||
for (const auto& c : parseExchangeOHLC(identifier, body)) out.emplace_back(c.time, c.close);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
@@ -22,14 +22,14 @@ const std::vector<ExchangeInfo>& getExchangeRegistry()
|
||||
"Nonkyc.io",
|
||||
"https://nonkyc.io",
|
||||
{
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT"},
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT", "nonkyc_io"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"OurBit",
|
||||
"https://www.ourbit.com",
|
||||
{
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://www.ourbit.com/exchange/DRGX_USDT"},
|
||||
{"DRGX", "USDT", "DRGX/USDT", "https://www.ourbit.com/exchange/DRGX_USDT", "ourbit"},
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -59,11 +59,19 @@ std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& body)
|
||||
if (!t.is_object()) continue;
|
||||
const std::string base = t.value("base", std::string{});
|
||||
const std::string target = t.value("target", std::string{});
|
||||
std::string market;
|
||||
if (t.contains("market") && t["market"].is_object())
|
||||
market = t["market"].value("name", std::string{});
|
||||
std::string market, identifier;
|
||||
if (t.contains("market") && t["market"].is_object()) {
|
||||
market = t["market"].value("name", std::string{});
|
||||
identifier = t["market"].value("identifier", std::string{}); // keys the per-exchange candle adapter
|
||||
}
|
||||
const std::string tradeUrl = t.value("trade_url", std::string{});
|
||||
if (base.empty() || target.empty() || market.empty()) continue;
|
||||
double lastUsd = 0.0; // this venue's current USD price + 24h volume — differ per exchange
|
||||
if (t.contains("converted_last") && t["converted_last"].is_object())
|
||||
lastUsd = t["converted_last"].value("usd", 0.0);
|
||||
double volumeUsd = 0.0;
|
||||
if (t.contains("converted_volume") && t["converted_volume"].is_object())
|
||||
volumeUsd = t["converted_volume"].value("usd", 0.0);
|
||||
|
||||
auto it = byName.find(market);
|
||||
if (it == byName.end()) {
|
||||
@@ -71,7 +79,7 @@ std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& body)
|
||||
exchanges.push_back(ExchangeInfo{market, originOf(tradeUrl), {}});
|
||||
it = byName.find(market);
|
||||
}
|
||||
ExchangePair pair{base, target, base + "/" + target, tradeUrl};
|
||||
ExchangePair pair{base, target, base + "/" + target, tradeUrl, identifier, lastUsd, volumeUsd};
|
||||
// Skip duplicate pairs on the same exchange.
|
||||
bool dup = false;
|
||||
for (const auto& p : exchanges[it->second].pairs)
|
||||
|
||||
@@ -18,6 +18,9 @@ struct ExchangePair {
|
||||
std::string quote; ///< e.g. "BTC"
|
||||
std::string displayName; ///< e.g. "DRGX/BTC"
|
||||
std::string tradeUrl; ///< Link to the exchange pair page
|
||||
std::string identifier; ///< CoinGecko exchange id (e.g. "ourbit", "nonkyc_io") — keys the candle adapter
|
||||
double lastUsd = 0.0; ///< This venue's current price in USD (CoinGecko converted_last.usd); 0 if unknown
|
||||
double volumeUsd = 0.0; ///< This venue's 24h volume in USD (CoinGecko converted_volume.usd); 0 if unknown
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,11 +81,15 @@ inline std::vector<std::pair<std::time_t, double>> chartSeries(const MarketInfo&
|
||||
for (const auto& s : src) if (s.first >= cutoff) out.push_back(s);
|
||||
return out;
|
||||
};
|
||||
// Draw the SELECTED exchange's own candles when active (data/exchange_candles.h), else the CoinGecko
|
||||
// cross-exchange aggregate. Only the main chart switches source; portfolio sparklines stay aggregate.
|
||||
const auto& intraday = m.exchange_chart_active ? m.exchange_chart_intraday : m.price_chart_intraday;
|
||||
const auto& daily = m.exchange_chart_active ? m.exchange_chart_daily : m.price_chart_daily;
|
||||
switch (interval) {
|
||||
case 1: { auto v = lastWindow(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; } // 1H
|
||||
case 2: { auto v = lastWindow(m.price_chart_intraday, kDay); if (v.size() >= 2) return v; break; } // 1D
|
||||
case 3: { auto v = lastWindow(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; } // 1W
|
||||
case 4: { auto v = lastWindow(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; } // 1M
|
||||
case 1: { auto v = lastWindow(intraday, 3600); if (v.size() >= 2) return v; break; } // 1H
|
||||
case 2: { auto v = lastWindow(intraday, kDay); if (v.size() >= 2) return v; break; } // 1D
|
||||
case 3: { auto v = lastWindow(daily, 7 * kDay); if (v.size() >= 2) return v; break; } // 1W
|
||||
case 4: { auto v = lastWindow(daily, 30 * kDay); if (v.size() >= 2) return v; break; } // 1M
|
||||
default: break;
|
||||
}
|
||||
std::vector<std::pair<std::time_t, double>> out;
|
||||
@@ -95,5 +99,28 @@ inline std::vector<std::pair<std::time_t, double>> chartSeries(const MarketInfo&
|
||||
return out;
|
||||
}
|
||||
|
||||
// OHLC candles for the main chart at the selected RANGE — only when the per-exchange series is active
|
||||
// (the CoinGecko aggregate is close-only, so this returns empty and the chart draws a line). The 1D
|
||||
// view buckets the 5-minute intraday to hourly so it isn't ~288 hair-thin candles; other ranges use
|
||||
// the raw candles. Empty for the Live range (uses the in-session line).
|
||||
inline std::vector<Candle> chartCandles(const MarketInfo& m, int interval, std::time_t now)
|
||||
{
|
||||
if (!m.exchange_chart_active) return {};
|
||||
const long kDay = 86400;
|
||||
auto window = [now](const std::vector<Candle>& src, long rangeSec) {
|
||||
std::vector<Candle> out;
|
||||
const std::time_t cutoff = now - (std::time_t)rangeSec;
|
||||
for (const auto& c : src) if (c.time >= cutoff) out.push_back(c);
|
||||
return out;
|
||||
};
|
||||
switch (interval) {
|
||||
case 1: return window(m.exchange_ohlc_intraday, 3600); // 1H: 5-min candles (~12)
|
||||
case 2: return bucketOHLC(window(m.exchange_ohlc_intraday, kDay), 3600); // 1D: hourly candles (~24)
|
||||
case 3: return window(m.exchange_ohlc_daily, 7 * kDay); // 1W: daily (~7)
|
||||
case 4: return window(m.exchange_ohlc_daily, 30 * kDay); // 1M: daily (~30)
|
||||
default: return {}; // Live -> line
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
|
||||
162
src/data/wallet_index.cpp
Normal file
162
src/data/wallet_index.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "wallet_index.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
namespace {
|
||||
// A field-wise "did anything change" check so we only rewrite wallets.json when needed.
|
||||
bool sameEntry(const WalletIndexEntry& a, const WalletIndexEntry& b)
|
||||
{
|
||||
return a.fileName == b.fileName
|
||||
&& a.displayName == b.displayName
|
||||
&& a.walletIdentityHash == b.walletIdentityHash
|
||||
&& a.scopeId == b.scopeId
|
||||
&& a.cachedBalance == b.cachedBalance
|
||||
&& a.cachedAddressCount == b.cachedAddressCount
|
||||
&& a.lastOpenedEpoch == b.lastOpenedEpoch
|
||||
&& a.sizeBytesAtLastOpen == b.sizeBytesAtLastOpen
|
||||
&& a.syncedHere == b.syncedHere;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string WalletIndex::getDefaultPath()
|
||||
{
|
||||
// Co-located with settings.json / addressbook.json in the per-variant config dir.
|
||||
const std::string dir = util::Platform::getConfigDir();
|
||||
fs::create_directories(dir);
|
||||
return (fs::path(dir) / "wallets.json").string();
|
||||
}
|
||||
|
||||
bool WalletIndex::load()
|
||||
{
|
||||
file_path_ = getDefaultPath();
|
||||
entries_.clear();
|
||||
extra_folders_.clear();
|
||||
|
||||
std::ifstream file(file_path_);
|
||||
if (!file.is_open()) return true; // no file yet is fine
|
||||
|
||||
try {
|
||||
json j;
|
||||
file >> j;
|
||||
|
||||
if (j.contains("entries") && j["entries"].is_array()) {
|
||||
for (const auto& e : j["entries"]) {
|
||||
WalletIndexEntry w;
|
||||
w.fileName = e.value("file", "");
|
||||
if (w.fileName.empty()) continue;
|
||||
w.displayName = e.value("name", w.fileName);
|
||||
w.walletIdentityHash = e.value("identity", "");
|
||||
w.scopeId = e.value("scopeId", "");
|
||||
w.cachedBalance = e.value("balance", -1.0);
|
||||
w.cachedAddressCount = e.value("addresses", (long long)-1);
|
||||
w.lastOpenedEpoch = e.value("lastOpened", (long long)0);
|
||||
w.sizeBytesAtLastOpen = e.value("size", (long long)0);
|
||||
w.syncedHere = e.value("syncedHere", false);
|
||||
entries_.push_back(std::move(w));
|
||||
}
|
||||
}
|
||||
if (j.contains("extraFolders") && j["extraFolders"].is_array()) {
|
||||
for (const auto& d : j["extraFolders"]) {
|
||||
if (d.is_string() && !d.get<std::string>().empty())
|
||||
extra_folders_.push_back(d.get<std::string>());
|
||||
}
|
||||
}
|
||||
DEBUG_LOGF("Wallet index loaded: %zu wallets, %zu extra folders\n",
|
||||
entries_.size(), extra_folders_.size());
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error loading wallet index: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletIndex::save()
|
||||
{
|
||||
if (file_path_.empty()) file_path_ = getDefaultPath();
|
||||
|
||||
try {
|
||||
json j;
|
||||
j["entries"] = json::array();
|
||||
for (const auto& w : entries_) {
|
||||
json e;
|
||||
e["file"] = w.fileName;
|
||||
e["name"] = w.displayName;
|
||||
e["identity"] = w.walletIdentityHash;
|
||||
e["scopeId"] = w.scopeId;
|
||||
e["balance"] = w.cachedBalance;
|
||||
e["addresses"] = w.cachedAddressCount;
|
||||
e["lastOpened"] = w.lastOpenedEpoch;
|
||||
e["size"] = w.sizeBytesAtLastOpen;
|
||||
e["syncedHere"] = w.syncedHere;
|
||||
j["entries"].push_back(std::move(e));
|
||||
}
|
||||
j["extraFolders"] = extra_folders_;
|
||||
|
||||
// No secrets here (file names + coarse metadata) but keep it owner-only for consistency.
|
||||
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
|
||||
DEBUG_LOGF("Could not write wallet index: %s\n", file_path_.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error saving wallet index: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletIndex::upsert(const WalletIndexEntry& e)
|
||||
{
|
||||
for (auto& w : entries_) {
|
||||
if (w.fileName == e.fileName) {
|
||||
if (sameEntry(w, e)) return false; // nothing changed -> caller can skip save()
|
||||
w = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
entries_.push_back(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
const WalletIndexEntry* WalletIndex::find(const std::string& fileName) const
|
||||
{
|
||||
for (const auto& w : entries_) {
|
||||
if (w.fileName == fileName) return &w;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WalletIndex::addExtraFolder(const std::string& dir)
|
||||
{
|
||||
if (dir.empty()) return false;
|
||||
if (std::find(extra_folders_.begin(), extra_folders_.end(), dir) != extra_folders_.end())
|
||||
return false;
|
||||
extra_folders_.push_back(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletIndex::removeExtraFolder(const std::string& dir)
|
||||
{
|
||||
auto it = std::find(extra_folders_.begin(), extra_folders_.end(), dir);
|
||||
if (it == extra_folders_.end()) return false;
|
||||
extra_folders_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
70
src/data/wallet_index.h
Normal file
70
src/data/wallet_index.h
Normal file
@@ -0,0 +1,70 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
/**
|
||||
* @brief Cached, file-keyed metadata for one wallet file.
|
||||
*
|
||||
* A wallet.dat on disk only reveals its size + mtime; balance and address count require the daemon
|
||||
* to have loaded it. So those are cached here after each load and shown in the wallet-files list
|
||||
* (P2) even when the wallet isn't the active one. walletIdentityHash bridges file -> per-wallet data
|
||||
* (address book, tx history) so the right caches can be selected before/after a switch.
|
||||
*/
|
||||
struct WalletIndexEntry {
|
||||
std::string fileName; // plain wallet filename in the datadir (e.g. "wallet.dat")
|
||||
std::string displayName; // user-facing name (defaults to fileName)
|
||||
std::string walletIdentityHash; // address-derived identity of the last load; "" = unknown
|
||||
std::string scopeId; // stable per-wallet id ("w:"+hex) for scoping contacts etc.;
|
||||
// generated once, never recomputed from the (mutable) address set
|
||||
double cachedBalance = -1.0; // last-known total balance; < 0 = unknown (never opened)
|
||||
long long cachedAddressCount = -1;// < 0 = unknown
|
||||
long long lastOpenedEpoch = 0; // unix seconds of last open; 0 = never opened
|
||||
long long sizeBytesAtLastOpen = 0;
|
||||
bool syncedHere = false; // loaded in this datadir before -> catch-up on switch (no full rescan)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Persistent wallet-files metadata index (wallets.json, in the config dir).
|
||||
*
|
||||
* Keyed by wallet file name. Populated after each load (P1b) and read by the wallet-files list (P2).
|
||||
* Also remembers extra folders the user added to scan for wallet files. Not encrypted — it holds
|
||||
* only file names + coarse metadata (no keys/addresses).
|
||||
*/
|
||||
class WalletIndex {
|
||||
public:
|
||||
bool load();
|
||||
bool save();
|
||||
static std::string getDefaultPath();
|
||||
|
||||
const std::vector<WalletIndexEntry>& entries() const { return entries_; }
|
||||
const std::vector<std::string>& extraFolders() const { return extra_folders_; }
|
||||
|
||||
/**
|
||||
* @brief Insert or update the entry for e.fileName.
|
||||
* @return true if a new entry was added or any field changed (so the caller can skip save()).
|
||||
*/
|
||||
bool upsert(const WalletIndexEntry& e);
|
||||
|
||||
/** @brief Find the entry for a wallet file, or nullptr. */
|
||||
const WalletIndexEntry* find(const std::string& fileName) const;
|
||||
|
||||
/** @brief Add/remove an extra scan folder. Returns true if the set changed. */
|
||||
bool addExtraFolder(const std::string& dir);
|
||||
bool removeExtraFolder(const std::string& dir);
|
||||
|
||||
private:
|
||||
std::vector<WalletIndexEntry> entries_;
|
||||
std::vector<std::string> extra_folders_;
|
||||
std::string file_path_;
|
||||
};
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
@@ -3,6 +3,7 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "wallet_state.h"
|
||||
#include "../util/text_format.h" // util::formatClockDateTime (app-wide 24h/12h clock)
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
@@ -44,13 +45,7 @@ int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
||||
std::string TransactionInfo::getTimeString() const
|
||||
{
|
||||
if (timestamp == 0) return "Unknown";
|
||||
|
||||
std::time_t t = static_cast<std::time_t>(timestamp);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
|
||||
return ss.str();
|
||||
return util::formatClockDateTime(timestamp);
|
||||
}
|
||||
|
||||
std::string TransactionInfo::getTypeDisplay() const
|
||||
@@ -77,13 +72,7 @@ std::string PeerInfo::getConnectionTime() const
|
||||
std::string BannedPeer::getBannedUntilString() const
|
||||
{
|
||||
if (banned_until == 0) return "Never";
|
||||
|
||||
std::time_t t = static_cast<std::time_t>(banned_until);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
|
||||
return ss.str();
|
||||
return util::formatClockDateTime(banned_until);
|
||||
}
|
||||
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "exchange_info.h"
|
||||
#include "candle.h"
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
@@ -175,6 +176,18 @@ struct MarketInfo {
|
||||
std::chrono::steady_clock::time_point chart_last_fetch_time{};
|
||||
bool chart_loaded = false;
|
||||
|
||||
// Per-EXCHANGE candle series for the SELECTED pair, fetched from that venue's own API (see
|
||||
// data/exchange_candles.h). When exchange_chart_active is true the Market chart draws these instead
|
||||
// of the CoinGecko cross-exchange aggregate above; it's set false while switching pairs or when the
|
||||
// selected venue has no adapter / its fetch failed, so the chart gracefully falls back to aggregate.
|
||||
std::vector<std::pair<std::time_t, double>> exchange_chart_intraday;
|
||||
std::vector<std::pair<std::time_t, double>> exchange_chart_daily;
|
||||
bool exchange_chart_active = false;
|
||||
// Full OHLC for the same per-exchange candles, backing the candlestick rendering (the aggregate
|
||||
// CoinGecko series is close-only, so it stays a line). Parallel to exchange_chart_* above.
|
||||
std::vector<data::Candle> exchange_ohlc_intraday;
|
||||
std::vector<data::Candle> exchange_ohlc_daily;
|
||||
|
||||
// Exchanges/pairs fetched live from CoinGecko (empty until fetched; the Market tab
|
||||
// falls back to data::getExchangeRegistry() while empty).
|
||||
std::vector<data::ExchangeInfo> exchanges;
|
||||
|
||||
@@ -15,3 +15,5 @@ INCBIN(ubuntu_mono, "@CMAKE_SOURCE_DIR@/res/fonts/UbuntuMono-R.ttf");
|
||||
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
||||
INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
|
||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
||||
INCBIN(noto_emoji_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoEmoji-Subset.ttf");
|
||||
INCBIN(twemoji_color, "@CMAKE_SOURCE_DIR@/res/fonts/TwemojiMozilla-Color.ttf");
|
||||
|
||||
@@ -35,4 +35,11 @@ extern "C" {
|
||||
|
||||
extern const unsigned char g_noto_cjk_subset_data[];
|
||||
extern const unsigned int g_noto_cjk_subset_size;
|
||||
|
||||
extern const unsigned char g_noto_emoji_subset_data[];
|
||||
extern const unsigned int g_noto_emoji_subset_size;
|
||||
|
||||
// Twemoji COLR/CPAL color-emoji font (used only when color emoji is enabled + FreeType is available).
|
||||
extern const unsigned char g_twemoji_color_data[];
|
||||
extern const unsigned int g_twemoji_color_size;
|
||||
}
|
||||
|
||||
39
src/embedded/logo_dragonx_svg.h
Normal file
39
src/embedded/logo_dragonx_svg.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Embedded source of the DragonX mark (res/img/logos/logo_dragonx.svg). Kept as a string so the logo
|
||||
// is available in every build (dev + portable single-file) with no resource-pipeline or file dependency.
|
||||
// It is rasterized + recolored per theme at runtime (see util/svg_texture.*). Two fills: the crimson
|
||||
// body (.cls-2 #d82652) becomes the theme accent; the white detail (.cls-1 #fff) becomes the light tone.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Global `embedded` namespace to match the generated embedded resources (embedded::ui_toml_data, etc.).
|
||||
namespace embedded {
|
||||
|
||||
inline constexpr const char* kLogoDragonXSvg = R"SVG(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>)SVG";
|
||||
|
||||
} // namespace embedded
|
||||
234
src/main.cpp
234
src/main.cpp
@@ -9,6 +9,7 @@
|
||||
#include "ui/schema/ui_schema.h"
|
||||
#include "ui/effects/low_spec.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "ui/windows/contacts_tab.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/material/color_theme.h"
|
||||
#include "ui/material/typography.h"
|
||||
@@ -71,6 +72,7 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid,
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -235,6 +237,8 @@ static float g_borderlessDpi = 1.0f; // DPI scale, updated from SDL each f
|
||||
// Custom WndProc that removes the native title bar by extending the
|
||||
// client area to cover the entire window (WM_NCCALCSIZE) and provides
|
||||
// resize borders + caption drag zone (WM_NCHITTEST).
|
||||
static void winlog(const char* fmt, ...); // [WINLOG] tracer (defined below)
|
||||
|
||||
static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg,
|
||||
WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
@@ -314,6 +318,12 @@ static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg,
|
||||
// Scale min window size by DPI so it matches the SDL-side minimum.
|
||||
mmi->ptMinTrackSize.x = (LONG)(1024 * g_borderlessDpi);
|
||||
mmi->ptMinTrackSize.y = (LONG)(720 * g_borderlessDpi);
|
||||
static float s_lastBd = -1.0f; // [WINLOG] log only when g_borderlessDpi changes (this fires a lot)
|
||||
if (g_borderlessDpi != s_lastBd) {
|
||||
s_lastBd = g_borderlessDpi;
|
||||
winlog("WM_GETMINMAXINFO g_borderlessDpi=%.3f -> minTrack=%ldx%ld",
|
||||
g_borderlessDpi, (long)mmi->ptMinTrackSize.x, (long)mmi->ptMinTrackSize.y);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -429,8 +439,16 @@ static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context);
|
||||
static void Shutdown(SDL_Window* window, SDL_GLContext gl_context);
|
||||
#endif
|
||||
|
||||
// Global single instance lock
|
||||
// Global single instance lock. Keyed PER VARIANT so the full node and Lite can run side by side —
|
||||
// their config dirs are already separate (DRAGONX_APP_NAME), so only this lock kept them apart.
|
||||
// NB: DRAGONX_LITE_BUILD is ALWAYS defined (0 for the full node, 1 for Lite) via $<BOOL:...>, so this
|
||||
// must be #if (value), not #ifdef (existence) — #ifdef is true for both and made the full node grab
|
||||
// the Lite lock.
|
||||
#if DRAGONX_LITE_BUILD
|
||||
static dragonx::util::SingleInstance g_single_instance("obsidiandragonlite");
|
||||
#else
|
||||
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");
|
||||
#endif
|
||||
|
||||
// Check for payment URI in command line args
|
||||
static std::string findPaymentURI(int argc, char* argv[])
|
||||
@@ -531,6 +549,63 @@ static void drawWindowBackdrop(dragonx::App& app, ImDrawList* bgDL, ImVec2 p0, I
|
||||
}
|
||||
}
|
||||
|
||||
// ── Window-dimension diagnostic tracer ──────────────────────────────────────────────────────
|
||||
// Always-on (writes to stderr → dragonx-debug.log on Windows) so it's visible in release builds,
|
||||
// unlike DRAGONX_DEBUG-gated DEBUG_LOGF. Grep the log for "[WINLOG]" to trace how the window size
|
||||
// changes when dragging between monitors of different DPI. Low volume — only fires on window
|
||||
// resize / move / DPI-scale events. TODO: remove once the multi-monitor sizing bug is resolved.
|
||||
static void winlog(const char* fmt, ...)
|
||||
{
|
||||
char buf[1024];
|
||||
va_list a; va_start(a, fmt); vsnprintf(buf, sizeof(buf), fmt, a); va_end(a);
|
||||
fprintf(stderr, "[WINLOG] %s\n", buf);
|
||||
fflush(stderr);
|
||||
}
|
||||
// Dump the full current window state with a short tag.
|
||||
static void winlogState(SDL_Window* w, const char* tag)
|
||||
{
|
||||
int sw = 0, sh = 0, pw = 0, ph = 0, mw = 0, mh = 0;
|
||||
SDL_GetWindowSize(w, &sw, &sh);
|
||||
SDL_GetWindowSizeInPixels(w, &pw, &ph);
|
||||
SDL_GetWindowMinimumSize(w, &mw, &mh);
|
||||
float ds = SDL_GetWindowDisplayScale(w);
|
||||
float st = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
SDL_DisplayID did = SDL_GetDisplayForWindow(w);
|
||||
winlog("%-20s size=%dx%d px=%dx%d min=%dx%d dispScale=%.3f storedScale=%.3f display=%u",
|
||||
tag, sw, sh, pw, ph, mw, mh, ds, st, (unsigned)did);
|
||||
#ifdef _WIN32
|
||||
HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(w),
|
||||
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
if (hwnd) {
|
||||
RECT wr = {}, cr = {};
|
||||
GetWindowRect(hwnd, &wr);
|
||||
GetClientRect(hwnd, &cr);
|
||||
winlog("%-20s native win=%ldx%ld client=%ldx%ld pos=%ld,%ld",
|
||||
tag, (long)(wr.right - wr.left), (long)(wr.bottom - wr.top),
|
||||
(long)(cr.right - cr.left), (long)(cr.bottom - cr.top), (long)wr.left, (long)wr.top);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Resize the window to an EXACT client size. On the DX11 borderless native HWND, SDL_SetWindowSize
|
||||
// inflates the result by a phantom caption+border frame: SDL runs AdjustWindowRectEx to turn the
|
||||
// requested client size into a window rect (adding the WS_CAPTION/WS_THICKFRAME frame), but our
|
||||
// WM_NCCALCSIZE handler folds that entire window rect back into the client area — so the client ends
|
||||
// up ~frame bigger than asked (e.g. a 720 min becomes 759, unreachable). Resize the HWND directly:
|
||||
// for a borderless window the window rect equals the client, so SetWindowPos gives the exact size.
|
||||
static void resizeWindowExact(SDL_Window* window, int w, int h)
|
||||
{
|
||||
#if defined(_WIN32) && defined(DRAGONX_USE_DX11)
|
||||
HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(window),
|
||||
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
if (hwnd) {
|
||||
SetWindowPos(hwnd, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
SDL_SetWindowSize(window, w, h);
|
||||
}
|
||||
|
||||
// Handle SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: save the old-scale size,
|
||||
// compute + clamp the new physical size, arm the deferred resize retry,
|
||||
// and rebuild fonts / reset ImGui style at the new DPI scale. newScale
|
||||
@@ -539,7 +614,8 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
std::map<int,std::pair<int,int>>& savedSizeForScale,
|
||||
int& lastKnownW, int& lastKnownH,
|
||||
int& dpiTargetW, int& dpiTargetH,
|
||||
int& dpiResizeRetries, bool& dpiResizePending)
|
||||
int& dpiResizeRetries, bool& dpiResizePending,
|
||||
int& dpiSettleFrames)
|
||||
{
|
||||
auto& typo = dragonx::ui::material::Typography::instance();
|
||||
float oldScale = typo.getDpiScale();
|
||||
@@ -547,6 +623,9 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
float ratio = newScale / oldScale;
|
||||
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
|
||||
oldScale, newScale, ratio);
|
||||
winlog("======== DPI CHANGE storedScale %.3f -> newScale %.3f (ratio %.3f) ========",
|
||||
oldScale, newScale, ratio);
|
||||
winlogState(window, " entry");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
#ifdef DRAGONX_USE_DX11
|
||||
@@ -562,6 +641,7 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
|
||||
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
|
||||
lastKnownW, lastKnownH, oldPct);
|
||||
winlog(" saved lastKnown %dx%d under old scale %d%%", lastKnownW, lastKnownH, oldPct);
|
||||
}
|
||||
|
||||
// Compute new physical size: if we've been to this
|
||||
@@ -573,14 +653,24 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
if (it != savedSizeForScale.end()) {
|
||||
newW = it->second.first;
|
||||
newH = it->second.second;
|
||||
winlog(" restore saved %dx%d for scale %d%%", newW, newH, newPct);
|
||||
} else {
|
||||
// Use lastKnownW/H (pre-auto-resize) instead of
|
||||
// SDL_GetWindowSize() which already reflects the
|
||||
// WM_DPICHANGED auto-resize.
|
||||
newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
|
||||
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
|
||||
winlog(" proportional %dx%d = lastKnown %dx%d * %.3f/%.3f",
|
||||
newW, newH, lastKnownW, lastKnownH, newScale, oldScale);
|
||||
}
|
||||
|
||||
// Never restore/scale below the new scale's minimum — guarantees the window lands exactly
|
||||
// on the min in both directions even if the per-scale map was ever seeded oddly.
|
||||
const int minW = (int)(1024 * newScale), minH = (int)(720 * newScale);
|
||||
newW = std::max(newW, minW);
|
||||
newH = std::max(newH, minH);
|
||||
winlog(" after min-clamp (min %dx%d): %dx%d", minW, minH, newW, newH);
|
||||
|
||||
// Clamp to the target display's work area so the
|
||||
// window doesn't overflow a smaller/higher-density monitor.
|
||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||
@@ -589,23 +679,33 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
||||
newW = std::min(newW, usable.w);
|
||||
newH = std::min(newH, usable.h);
|
||||
winlog(" after display-clamp (usable %dx%d): %dx%d", usable.w, usable.h, newW, newH);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new minimum BEFORE resizing. SDL_SetWindowSize clamps the request UP to the
|
||||
// current minimum, so when moving to a LOWER scale the shrink (e.g. 1080 -> 720) issued
|
||||
// while the min is still the old 1080 would be clamped back to 1080 and stick there
|
||||
// (SDL_SetWindowMinimumSize only ever grows a window, never shrinks it). Min-then-size
|
||||
// makes the shrink land on the new min immediately, without relying on the deferred retry.
|
||||
SDL_SetWindowMinimumSize(window, minW, minH);
|
||||
dpiResizePending = true;
|
||||
SDL_SetWindowSize(window, newW, newH);
|
||||
resizeWindowExact(window, newW, newH);
|
||||
lastKnownW = newW;
|
||||
lastKnownH = newH;
|
||||
// Arm deferred retry — the SetWindowSize above may
|
||||
// not stick if the user is mid-drag (modal loop).
|
||||
// Arm deferred retry (SetWindowSize may not stick mid-drag) and suppress resize-recording
|
||||
// for the whole settle burst: a transient WINDOW_RESIZED (ours, the WM_DPICHANGED settle,
|
||||
// or a retry frame) that arrives after dpiResizePending was consumed and with scales already
|
||||
// matching would otherwise be misclassified as a user resize and corrupt savedSizeForScale.
|
||||
dpiTargetW = newW;
|
||||
dpiTargetH = newH;
|
||||
dpiResizeRetries = 30; // retry for ~30 frames
|
||||
SDL_SetWindowMinimumSize(window,
|
||||
(int)(1024 * newScale),
|
||||
(int)(720 * newScale));
|
||||
dpiSettleFrames = 40; // ignore resize-recording until the transition settles
|
||||
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
|
||||
newW, newH, newScale, newPct);
|
||||
winlog(" SetWindowMinimumSize(%dx%d) then SetWindowSize(%dx%d); armed retries=30 settle=40",
|
||||
minW, minH, newW, newH);
|
||||
winlogState(window, " after set");
|
||||
|
||||
// 2) Rebuild font atlas at the new DPI scale
|
||||
typo.reload(io, newScale);
|
||||
@@ -672,11 +772,12 @@ int main(int argc, char* argv[])
|
||||
|
||||
// Check for existing instance
|
||||
if (!g_single_instance.tryLock()) {
|
||||
fprintf(stderr, "Another instance of ObsidianDragon is already running.\n");
|
||||
fprintf(stderr, "Another instance of %s is already running.\n", DRAGONX_APP_NAME);
|
||||
DEBUG_LOGF("Please close the existing instance first.\n");
|
||||
#ifdef _WIN32
|
||||
MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.",
|
||||
L"ObsidianDragon", MB_OK | MB_ICONINFORMATION);
|
||||
const std::string msg = std::string("Another instance of ") + DRAGONX_APP_NAME +
|
||||
" is already running.\nPlease close it first.";
|
||||
MessageBoxA(nullptr, msg.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONINFORMATION);
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
@@ -703,6 +804,9 @@ int main(int argc, char* argv[])
|
||||
savedWinW = sw;
|
||||
savedWinH = sh;
|
||||
DEBUG_LOGF("Restored window size from settings: %dx%d\n", sw, sh);
|
||||
} else if (sw > 0 || sh > 0) {
|
||||
DEBUG_LOGF("Ignored saved window size %dx%d (below 1024x720 minimum) — using default %dx%d\n",
|
||||
sw, sh, savedWinW, savedWinH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1062,13 +1166,21 @@ int main(int argc, char* argv[])
|
||||
// (point) coordinates and the framebuffer handles pixel density, so
|
||||
// we must NOT multiply the window size by the content scale.
|
||||
#ifndef __APPLE__
|
||||
if (currentDpiScale > 1.01f) {
|
||||
{
|
||||
int curW = 0, curH = 0;
|
||||
SDL_GetWindowSize(window, &curW, &curH);
|
||||
int newW = (int)(curW * currentDpiScale);
|
||||
int newH = (int)(curH * currentDpiScale);
|
||||
int newW = curW, newH = curH;
|
||||
if (currentDpiScale > 1.01f) {
|
||||
// On a HiDPI display the saved size is logical px — scale it up to physical so the UI
|
||||
// appears the same physical size as on a 100% display, and scale the minimum too.
|
||||
newW = (int)(curW * currentDpiScale);
|
||||
newH = (int)(curH * currentDpiScale);
|
||||
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
|
||||
}
|
||||
|
||||
// Clamp to the display's work area so the window fits on screen.
|
||||
// Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
|
||||
// inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
|
||||
// window off-screen or bigger than the screen on a same-DPI cold start).
|
||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||
if (did) {
|
||||
SDL_Rect usable;
|
||||
@@ -1078,15 +1190,16 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
SDL_SetWindowSize(window, newW, newH);
|
||||
SDL_SetWindowMinimumSize(window,
|
||||
(int)(1024 * currentDpiScale),
|
||||
(int)(720 * currentDpiScale));
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
DEBUG_LOGF("HiDPI startup: window resized %dx%d -> %dx%d (scale %.2f)\n",
|
||||
curW, curH, newW, newH, currentDpiScale);
|
||||
if (newW != curW || newH != curH) {
|
||||
SDL_SetWindowSize(window, newW, newH);
|
||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
|
||||
curW, curH, newW, newH, currentDpiScale);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
|
||||
winlogState(window, " startup");
|
||||
|
||||
// Create application instance
|
||||
dragonx::App app;
|
||||
@@ -1267,6 +1380,10 @@ int main(int argc, char* argv[])
|
||||
// overrides SetWindowPos). We retry for several frames.
|
||||
int dpiTargetW = 0, dpiTargetH = 0;
|
||||
int dpiResizeRetries = 0;
|
||||
// Frames to keep ignoring resize-recording after a DPI transition (settle window) so transient
|
||||
// WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
|
||||
int dpiSettleFrames = 0;
|
||||
SDL_DisplayID lastLoggedDisplay = 0; // [WINLOG] throttle: log MOVED only when the display changes
|
||||
{
|
||||
float s = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
int w = 0, h = 0;
|
||||
@@ -1286,16 +1403,23 @@ int main(int argc, char* argv[])
|
||||
SDL_GetWindowSize(window, &curW, &curH);
|
||||
if (curW != dpiTargetW || curH != dpiTargetH) {
|
||||
dpiResizePending = true;
|
||||
SDL_SetWindowSize(window, dpiTargetW, dpiTargetH);
|
||||
resizeWindowExact(window, dpiTargetW, dpiTargetH);
|
||||
DEBUG_LOGF(" Deferred resize retry %d: %dx%d -> %dx%d\n",
|
||||
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
|
||||
winlog("RETRY %d: cur %dx%d != target %dx%d -> SetWindowSize(target)",
|
||||
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
|
||||
} else {
|
||||
// Size matches — done
|
||||
winlog("RETRY done: reached target %dx%d", dpiTargetW, dpiTargetH);
|
||||
dpiResizeRetries = 0;
|
||||
}
|
||||
if (dpiResizeRetries == 1) winlogState(window, "RETRY-last(stuck?)");
|
||||
--dpiResizeRetries;
|
||||
needsRedraw = true;
|
||||
}
|
||||
// Burn down the post-DPI-transition settle window (see handleDisplayScaleChange). The retry
|
||||
// above forces redraws while active, so this drains promptly even if the app is otherwise idle.
|
||||
if (dpiSettleFrames > 0) --dpiSettleFrames;
|
||||
|
||||
// P6: Idle rendering — sleep if nothing needs redrawing
|
||||
if (!needsRedraw) {
|
||||
@@ -1326,8 +1450,12 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
||||
#endif
|
||||
bool isDpiResize = dpiResizePending ||
|
||||
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
|
||||
std::abs(actualScale - storedScale) > 0.01f;
|
||||
winlog("RESIZED(wait) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d -> %s",
|
||||
waitEvent.window.data1, waitEvent.window.data2, actualScale, storedScale,
|
||||
(int)dpiResizePending, dpiSettleFrames, dpiResizeRetries,
|
||||
isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
|
||||
if (dpiResizePending) dpiResizePending = false;
|
||||
if (!isDpiResize) {
|
||||
int pct = (int)lroundf(storedScale * 100.0f);
|
||||
@@ -1349,9 +1477,10 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||
#endif
|
||||
winlog("SCALE_CHANGED(wait) reported newScale=%.3f", newScale);
|
||||
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||
dpiResizeRetries, dpiResizePending);
|
||||
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
|
||||
}
|
||||
needsRedraw = true; // Got an event — redraw
|
||||
}
|
||||
@@ -1408,10 +1537,22 @@ int main(int argc, char* argv[])
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
app.beginShutdown();
|
||||
}
|
||||
if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
|
||||
if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
|
||||
event.window.windowID == SDL_GetWindowID(window)) {
|
||||
app.beginShutdown();
|
||||
}
|
||||
// [WINLOG] Log a window MOVE only when it crosses to a different display (avoids drag spam).
|
||||
if (event.type == SDL_EVENT_WINDOW_MOVED &&
|
||||
event.window.windowID == SDL_GetWindowID(window)) {
|
||||
SDL_DisplayID d = SDL_GetDisplayForWindow(window);
|
||||
if (d != lastLoggedDisplay) {
|
||||
lastLoggedDisplay = d;
|
||||
winlog("MOVED to display %u pos=%d,%d dispScale=%.3f",
|
||||
(unsigned)d, event.window.data1, event.window.data2,
|
||||
SDL_GetWindowDisplayScale(window));
|
||||
winlogState(window, " on-cross");
|
||||
}
|
||||
}
|
||||
#ifdef DRAGONX_USE_DX11
|
||||
// Handle window resize for DX11 swap chain
|
||||
if (event.type == SDL_EVENT_WINDOW_RESIZED &&
|
||||
@@ -1429,8 +1570,12 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
||||
#endif
|
||||
bool isDpiResize = dpiResizePending ||
|
||||
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
|
||||
std::abs(actualScale - storedScale) > 0.01f;
|
||||
winlog("RESIZED(poll) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d fsPending=%d -> %s",
|
||||
event.window.data1, event.window.data2, actualScale, storedScale,
|
||||
(int)dpiResizePending, dpiSettleFrames, dpiResizeRetries, (int)fontScaleResizePending,
|
||||
isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
|
||||
if (dpiResizePending) dpiResizePending = false;
|
||||
bool isFSResize = fontScaleResizePending;
|
||||
if (fontScaleResizePending) fontScaleResizePending = false;
|
||||
@@ -1462,9 +1607,10 @@ int main(int argc, char* argv[])
|
||||
#ifdef __APPLE__
|
||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||
#endif
|
||||
winlog("SCALE_CHANGED(poll) reported newScale=%.3f", newScale);
|
||||
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||
dpiResizeRetries, dpiResizePending);
|
||||
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1754,6 +1900,16 @@ int main(int argc, char* argv[])
|
||||
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
||||
}
|
||||
|
||||
// Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often
|
||||
// heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends.
|
||||
{
|
||||
static bool sweepVsyncOff = false;
|
||||
const bool sweeping = app.isScreenshotSweeping();
|
||||
if (sweeping != sweepVsyncOff) {
|
||||
SDL_GL_SetSwapInterval(sweeping ? 0 : 1);
|
||||
sweepVsyncOff = sweeping;
|
||||
}
|
||||
}
|
||||
SDL_GL_SwapWindow(window);
|
||||
#endif
|
||||
PERF_END("Present", _perfPresent);
|
||||
@@ -1761,6 +1917,21 @@ int main(int argc, char* argv[])
|
||||
// --- PerfLog: end frame ---
|
||||
dragonx::util::PerfLog::instance().endFrame();
|
||||
|
||||
// Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has
|
||||
// settled, then quits when it finishes. Used to run the sweep without clicking the button.
|
||||
{
|
||||
static const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
|
||||
static int autoFrames = 0;
|
||||
static bool autoStarted = false;
|
||||
if (autoSweep) {
|
||||
if (!autoStarted) {
|
||||
if (++autoFrames >= 90) { app.startFullUiSweep(); autoStarted = true; }
|
||||
} else if (!app.isScreenshotSweeping()) {
|
||||
running = false; // sweep complete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exit when shutdown is complete
|
||||
if (app.shouldQuit()) {
|
||||
running = false;
|
||||
@@ -1804,12 +1975,14 @@ int main(int argc, char* argv[])
|
||||
bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
|
||||
&& !opaqueBackground;
|
||||
bool animating = app.isShuttingDown()
|
||||
|| app.isWalletSwitchInProgress()
|
||||
|| backdropNeedsFrames
|
||||
|| app.hasTransactionSendProgress()
|
||||
|| app.isTransactionRefreshInProgress()
|
||||
|| dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
|
||||
|| dragonx::ui::Notifications::instance().hasActive()
|
||||
|| dragonx::ui::material::SmoothScrollAnimating();
|
||||
|| dragonx::ui::material::SmoothScrollAnimating()
|
||||
|| dragonx::ui::ConsumeContactsAvatarAnimation();
|
||||
// If nothing is happening, allow the next iteration to idle
|
||||
needsRedraw = uiActive || animating;
|
||||
}
|
||||
@@ -1825,9 +1998,12 @@ int main(int argc, char* argv[])
|
||||
float s = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||
int logW = (int)lroundf((float)curW / s);
|
||||
int logH = (int)lroundf((float)curH / s);
|
||||
winlog("SHUTDOWN save: size=%dx%d storedScale=%.3f -> logical=%dx%d", curW, curH, s, logW, logH);
|
||||
if (logW >= 1024 && logH >= 720) {
|
||||
app.settings()->setWindowSize(logW, logH);
|
||||
app.settings()->save();
|
||||
} else {
|
||||
DEBUG_LOGF("Not saving window size %dx%d (logical, below 1024x720 minimum)\n", logW, logH);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,19 @@ namespace rpc {
|
||||
|
||||
namespace {
|
||||
|
||||
// Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree
|
||||
// that held a secret (B7). Operates on the underlying std::string buffers via get_ref.
|
||||
// Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter).
|
||||
template <typename J>
|
||||
void scrubJsonSecrets(J& j) {
|
||||
if (j.is_string()) {
|
||||
auto& s = j.template get_ref<std::string&>();
|
||||
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
||||
} else if (j.is_object() || j.is_array()) {
|
||||
for (auto& el : j) scrubJsonSecrets(el);
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex g_trace_mutex;
|
||||
RPCClient::TraceCallback g_trace_callback;
|
||||
std::atomic_bool g_trace_enabled{false};
|
||||
@@ -85,6 +98,10 @@ void RPCClient::setTraceSource(std::string source)
|
||||
// Callback for libcurl to write response data
|
||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
||||
size_t totalSize = size * nmemb;
|
||||
// Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded
|
||||
// response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion.
|
||||
static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024;
|
||||
if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0; // short count aborts the transfer
|
||||
userp->append((char*)contents, totalSize);
|
||||
return totalSize;
|
||||
}
|
||||
@@ -191,6 +208,10 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||
// 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);
|
||||
// Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's
|
||||
// build defaults. Harmless on the localhost http:// case; essential for a remote https daemon.
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
|
||||
// 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
|
||||
@@ -317,7 +338,7 @@ std::string RPCClient::performCall(const std::string& method, const json& params
|
||||
return response_data;
|
||||
}
|
||||
|
||||
json RPCClient::parseRpcResult(long httpCode, const std::string& body)
|
||||
json RPCClient::parseRpcResult(long httpCode, const std::string& body, bool scrubSource)
|
||||
{
|
||||
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
|
||||
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
||||
@@ -355,7 +376,9 @@ json RPCClient::parseRpcResult(long httpCode, const std::string& body)
|
||||
throw RpcError(errCode, "RPC error: " + err_msg);
|
||||
}
|
||||
|
||||
return response["result"];
|
||||
json result = response["result"]; // a COPY of the subobject (operator[] yields an lvalue ref)
|
||||
if (scrubSource) scrubJsonSecrets(response); // zero the discarded tree's secret before it frees (B7)
|
||||
return result;
|
||||
}
|
||||
|
||||
json RPCClient::call(const std::string& method, const json& params)
|
||||
@@ -370,6 +393,56 @@ json RPCClient::call(const std::string& method, const json& params)
|
||||
return parseRpcResult(http_code, response_data);
|
||||
}
|
||||
|
||||
json RPCClient::callSecret(const std::string& method, const json& params)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||
if (!impl_->curl) {
|
||||
throw std::runtime_error("Not connected");
|
||||
}
|
||||
|
||||
// Zero the raw response body whether parsing succeeds or throws — it holds the secret in the
|
||||
// clear (the curl write buffer performCall returns), and would otherwise be freed un-wiped (B7).
|
||||
long http_code = 0;
|
||||
std::string response_data = performCall(method, params, http_code);
|
||||
try {
|
||||
json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
return result;
|
||||
} catch (...) {
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
std::string RPCClient::callSecretString(const std::string& method, const json& params)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||
if (!impl_->curl) {
|
||||
throw std::runtime_error("Not connected");
|
||||
}
|
||||
|
||||
long http_code = 0;
|
||||
std::string response_data = performCall(method, params, http_code);
|
||||
try {
|
||||
json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
|
||||
std::string out;
|
||||
if (result.is_string()) {
|
||||
// Copy the secret out, then zero the json node's OWN heap buffer — parseRpcResult's
|
||||
// json::parse allocates this independently of response_data, so scrubbing the raw body
|
||||
// alone would leave it behind (B7). `out` is now the only live copy; the caller wipes it.
|
||||
auto& s = result.get_ref<std::string&>();
|
||||
out = s;
|
||||
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
||||
}
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
if (!result.is_string()) throw std::runtime_error("RPC result is not a string");
|
||||
return out;
|
||||
} catch (...) {
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||
@@ -445,14 +518,22 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
||||
}
|
||||
|
||||
auto& result = oj["result"];
|
||||
std::string out;
|
||||
if (result.is_null()) {
|
||||
return "null";
|
||||
out = "null";
|
||||
} else if (result.is_string()) {
|
||||
// Return the raw string (not JSON-encoded) — caller wraps as needed
|
||||
return result.get<std::string>();
|
||||
out = result.get<std::string>();
|
||||
} else {
|
||||
return result.dump(4);
|
||||
out = result.dump(4);
|
||||
}
|
||||
// B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey,
|
||||
// whose response carries plaintext key material. Zero the raw buffer and the parsed tree so
|
||||
// the secret does not linger in freed heap (matching callSecret). The single returned copy is
|
||||
// the caller's to manage.
|
||||
scrubJsonSecrets(oj);
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
|
||||
|
||||
@@ -133,6 +133,26 @@ public:
|
||||
*/
|
||||
json call(const std::string& method, const json& params = json::array());
|
||||
|
||||
/**
|
||||
* @brief Like call(), but zeroes the raw HTTP response body after parsing (B7).
|
||||
*
|
||||
* For RPCs whose response carries a secret (z_exportmnemonic / z_exportkey), the parsed
|
||||
* value is scrubbed by the caller — but the raw response string this method builds also
|
||||
* holds that secret and is otherwise freed without zeroing. Use this variant so the largest,
|
||||
* longest-lived plaintext copy doesn't linger in freed heap. Slightly slower (an extra wipe);
|
||||
* only worth it for secret-bearing calls.
|
||||
*/
|
||||
json callSecret(const std::string& method, const json& params = json::array());
|
||||
|
||||
/**
|
||||
* @brief callSecret() for RPCs whose result is a bare secret string (z_exportkey / dumpprivkey /
|
||||
* z_exportviewingkey). Returns the result string with BOTH the raw response body AND the
|
||||
* parsed json's own copy of the secret zeroed — so no un-scrubbed heap copy survives the
|
||||
* call. The returned std::string is the only remaining copy; the caller owns it and must
|
||||
* wipe it (sodium_memzero) when done. Throws if the result is not a JSON string.
|
||||
*/
|
||||
std::string callSecretString(const std::string& method, const json& params = json::array());
|
||||
|
||||
/**
|
||||
* @brief Make a raw RPC call with a custom timeout
|
||||
* @param method RPC method name
|
||||
@@ -250,8 +270,10 @@ private:
|
||||
// hold curl_mutex_ and have verified impl_->curl.
|
||||
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
||||
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
||||
// response["result"] on success.
|
||||
static json parseRpcResult(long httpCode, const std::string& body);
|
||||
// response["result"] on success. When scrubSource is true (secret-bearing calls), the intermediate
|
||||
// parse tree's string values are zeroed before it is discarded — parseRpcResult returns a COPY of
|
||||
// the result node, so its own tree would otherwise free the secret un-wiped (B7).
|
||||
static json parseRpcResult(long httpCode, const std::string& body, bool scrubSource = false);
|
||||
|
||||
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
|
||||
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
|
||||
|
||||
@@ -168,20 +168,6 @@ void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetada
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -448,8 +434,15 @@ std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::
|
||||
result.market.market_cap = data.value("usd_market_cap", 0.0);
|
||||
|
||||
char buf[64];
|
||||
std::tm* tm = std::localtime(&fetchedAt);
|
||||
if (tm && std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm) > 0) {
|
||||
// Runs on the RPC worker thread — std::localtime shares a process-wide static tm, so use the
|
||||
// reentrant variant into a local tm (matches the rest of the codebase).
|
||||
std::tm tmv{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tmv, &fetchedAt);
|
||||
#else
|
||||
localtime_r(&fetchedAt, &tmv);
|
||||
#endif
|
||||
if (std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmv) > 0) {
|
||||
result.market.last_updated = buf;
|
||||
}
|
||||
return result;
|
||||
@@ -920,7 +913,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
if (cached != snapshot.viewTxCache.end()) {
|
||||
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
||||
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
||||
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, cached->second);
|
||||
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
||||
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
||||
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
||||
// recorded by the local echo. (The recent-refresh variant already omits this.)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -945,7 +941,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
|
||||
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
||||
appendViewTransactionOutputs(result.transactions, txid, entry);
|
||||
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, entry);
|
||||
// (Chat metadata is harvested only from RECEIVED notes, not our own outgoing memos — see
|
||||
// the cached-view branch above.)
|
||||
|
||||
json rawTransaction;
|
||||
bool hasRawTransaction = false;
|
||||
|
||||
@@ -101,10 +101,18 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
|
||||
// (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.)
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded;
|
||||
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded;
|
||||
if (!key.empty() && key[0] == 's') return KeyKind::Shielded;
|
||||
// A "z"-prefixed key is a shielded viewing key (zxview…); "s"-prefixed is a legacy Sprout key.
|
||||
if (!key.empty() && (key[0] == 's' || key[0] == 'z')) return KeyKind::Shielded;
|
||||
return KeyKind::Transparent;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isViewingKey(const std::string& key)
|
||||
{
|
||||
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
|
||||
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
|
||||
return key.rfind("zxview", 0) == 0;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||
{
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
|
||||
@@ -115,6 +123,11 @@ bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WalletSecurityController::isRecognizedImportKey(const std::string& key)
|
||||
{
|
||||
return isRecognizedPrivateKey(key) || isViewingKey(key);
|
||||
}
|
||||
|
||||
const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
|
||||
{
|
||||
return kind == KeyKind::Shielded
|
||||
|
||||
@@ -74,9 +74,13 @@ public:
|
||||
std::size_t minLength = 4);
|
||||
static KeyKind classifyAddress(const std::string& address);
|
||||
static KeyKind classifyPrivateKey(const std::string& key);
|
||||
// True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only).
|
||||
static bool isViewingKey(const std::string& key);
|
||||
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
|
||||
// Single source of truth for the import dialog's indicator AND its submit guard.
|
||||
static bool isRecognizedPrivateKey(const std::string& key);
|
||||
// As above, but also accepts a recognized shielded viewing key — the import dialog auto-detects
|
||||
// both, so this is the single source of truth for its indicator AND its submit guard.
|
||||
static bool isRecognizedImportKey(const std::string& key);
|
||||
static const char* importSuccessMessage(KeyKind kind);
|
||||
static std::string decryptExportFileName(std::uint64_t timestampSeconds);
|
||||
static void secureClear(std::string& value);
|
||||
|
||||
@@ -388,14 +388,14 @@ void AcrylicMaterial::setQuality(AcrylicQuality quality)
|
||||
}
|
||||
}
|
||||
|
||||
void AcrylicMaterial::applyBlur(float radius)
|
||||
void AcrylicMaterial::applyBlur(float radius, bool ignoreMultiplier)
|
||||
{
|
||||
if (!blurBuffers_.isValid() || !blurShader_.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply blur radius multiplier from settings
|
||||
float scaledRadius = radius * settings_.blurRadiusMultiplier;
|
||||
|
||||
// Apply the user's blur-strength multiplier — unless the caller wants an absolute radius.
|
||||
float scaledRadius = ignoreMultiplier ? radius : radius * settings_.blurRadiusMultiplier;
|
||||
|
||||
// Skip blur entirely when multiplier is at or near zero — show sharp background
|
||||
if (scaledRadius < 0.5f) {
|
||||
@@ -566,7 +566,7 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
|
||||
}
|
||||
|
||||
// Apply blur to captured background
|
||||
applyBlur(params.blurRadius);
|
||||
applyBlur(params.blurRadius, params.absoluteBlurRadius);
|
||||
|
||||
// Calculate UV coordinates for sampling the blur FBO texture.
|
||||
// With multi-viewport enabled, ImGui draw coordinates are in
|
||||
@@ -586,12 +586,14 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
|
||||
float u1 = localX1 / viewportWidth_;
|
||||
float v1 = 1.0f - localY1 / viewportHeight_; // V at bottom-right (low)
|
||||
|
||||
// Draw the blurred background. Scale by both the glass preset
|
||||
// opacity (fallbackColor.w) AND the user's UI opacity slider so
|
||||
// that lowering card opacity lets the sharp background through.
|
||||
// Draw the blurred background at (near) full strength so the panel is actually FROSTED — the
|
||||
// blurred capture replaces the sharp background in the card. UI opacity is applied only to the
|
||||
// tint overlay (in DrawGlassPanel), NOT here: scaling the blur by uiOpacity used to let the
|
||||
// SHARP background bleed straight through at low opacity, which made the acrylic slider look
|
||||
// like a no-op. A small floor keeps it frosted even for low-alpha presets.
|
||||
ImTextureID blurTex = getBlurredTexture();
|
||||
uint8_t glassAlpha = static_cast<uint8_t>(
|
||||
std::min(255.0f, std::max(0.0f, params.fallbackColor.w * settings_.uiOpacity * 255.0f)));
|
||||
std::min(255.0f, std::max(0.0f, std::max(params.fallbackColor.w, 0.90f) * 255.0f)));
|
||||
|
||||
if (blurTex) {
|
||||
drawList->AddImageRounded(
|
||||
@@ -1272,11 +1274,11 @@ void AcrylicMaterial::captureLiveFramebuffer()
|
||||
blurCacheValid_ = false;
|
||||
}
|
||||
|
||||
void AcrylicMaterial::applyBlur(float radius)
|
||||
void AcrylicMaterial::applyBlur(float radius, bool ignoreMultiplier)
|
||||
{
|
||||
if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return;
|
||||
|
||||
float scaledRadius = radius * settings_.blurRadiusMultiplier;
|
||||
float scaledRadius = ignoreMultiplier ? radius : radius * settings_.blurRadiusMultiplier;
|
||||
if (blurCacheValid_ && std::abs(scaledRadius - lastBlurRadius_) < 0.1f) return;
|
||||
|
||||
int passes = 1;
|
||||
@@ -1538,7 +1540,7 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
|
||||
}
|
||||
|
||||
// Run DX11 blur pipeline
|
||||
applyBlur(params.blurRadius);
|
||||
applyBlur(params.blurRadius, params.absoluteBlurRadius);
|
||||
|
||||
// DX11 UV: top-left = (0,0), bottom-right = (1,1) — same as ImGui
|
||||
// With multi-viewport, pMin/pMax are in OS screen space; subtract
|
||||
@@ -1562,11 +1564,12 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I
|
||||
(void*)blurTex, u0, v0, u1, v1);
|
||||
}
|
||||
|
||||
// Draw the blurred background. Scale by both the glass preset
|
||||
// opacity (fallbackColor.w) AND the user's UI opacity slider so
|
||||
// that lowering card opacity lets the sharp background through.
|
||||
// Draw the blurred background at (near) full strength so the panel is actually FROSTED — UI
|
||||
// opacity is applied only to the tint overlay, NOT here (scaling the blur by uiOpacity let the
|
||||
// SHARP background bleed through at low opacity, making the acrylic slider a no-op). Mirrors the
|
||||
// GL drawRect fix above.
|
||||
uint8_t glassAlpha = (uint8_t)std::min(255.f,
|
||||
std::max(0.f, params.fallbackColor.w * settings_.uiOpacity * 255.f));
|
||||
std::max(0.f, std::max(params.fallbackColor.w, 0.90f) * 255.f));
|
||||
|
||||
if (blurTex) {
|
||||
drawList->AddImageRounded(
|
||||
@@ -1701,7 +1704,7 @@ void AcrylicMaterial::resize(int, int) {}
|
||||
void AcrylicMaterial::captureBackground() {}
|
||||
void AcrylicMaterial::captureBackgroundDirect() {}
|
||||
void AcrylicMaterial::captureLiveFramebuffer() {}
|
||||
void AcrylicMaterial::applyBlur(float) {}
|
||||
void AcrylicMaterial::applyBlur(float, bool) {}
|
||||
ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; }
|
||||
ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; }
|
||||
void AcrylicMaterial::refreshCapabilities() {}
|
||||
|
||||
@@ -39,7 +39,12 @@ struct AcrylicParams {
|
||||
|
||||
// Blur radius in pixels (typical: 20-60)
|
||||
float blurRadius = 30.0f;
|
||||
|
||||
|
||||
// When true, blurRadius is used AS-IS — NOT scaled by the user's blur-strength slider
|
||||
// (blurRadiusMultiplier). The modal backdrop sets this so its blur is a fixed value, independent
|
||||
// of the global acrylic slider.
|
||||
bool absoluteBlurRadius = false;
|
||||
|
||||
// Noise texture opacity (typical: 0.02-0.04)
|
||||
float noiseOpacity = 0.02f;
|
||||
|
||||
@@ -341,7 +346,7 @@ private:
|
||||
/**
|
||||
* @brief Apply blur passes to captured content
|
||||
*/
|
||||
void applyBlur(float radius);
|
||||
void applyBlur(float radius, bool ignoreMultiplier = false);
|
||||
|
||||
/**
|
||||
* @brief Composite final acrylic appearance
|
||||
|
||||
@@ -160,6 +160,10 @@ void ThemeEffects::loadFromTheme() {
|
||||
|
||||
// ---- Gradient Border Shift ----
|
||||
gradient_border_.enabled = eff("gradient-border-enabled").sizeOr(0.0f) > 0.5f;
|
||||
// Opt-in: also draw the shifting border on every glass panel (not just the
|
||||
// active nav button). Off by default so themes that only want the button
|
||||
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
|
||||
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
|
||||
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
||||
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
||||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
||||
@@ -512,11 +516,15 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
// ============================================================================
|
||||
|
||||
void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const {
|
||||
float rounding, float phaseOffset,
|
||||
float alphaMul) const {
|
||||
if (!enabled_ || !gradient_border_.enabled) return;
|
||||
|
||||
// Smooth sinusoidal oscillation between color A and color B
|
||||
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||
// Smooth sinusoidal oscillation between color A and color B.
|
||||
// phaseOffset shifts where in the cycle this element sits so a wall of
|
||||
// panels reads like veins at different depths rather than one pulse.
|
||||
float phase = std::sin((time_ * gradient_border_.speed + phaseOffset)
|
||||
* 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||||
|
||||
// Extract RGBA from both colors and lerp
|
||||
RGB ca = unpackRGB(gradient_border_.colorA);
|
||||
@@ -525,7 +533,7 @@ void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 p
|
||||
int r = ca.r + (int)((cb.r - ca.r) * phase);
|
||||
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
||||
int b = ca.b + (int)((cb.b - ca.b) * phase);
|
||||
int a = scaledAlpha(gradient_border_.alpha, bgOpacity_);
|
||||
int a = scaledAlpha(gradient_border_.alpha * alphaMul, bgOpacity_);
|
||||
|
||||
// Draw the shifting border
|
||||
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
|
||||
@@ -896,6 +904,22 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const {
|
||||
if (!enabled_ || effects::isLowSpecMode()) return;
|
||||
|
||||
// Gradient border on panels — a slow color-shifting outline that hugs the
|
||||
// panel's rounded corners (drawn via AddRect, so it follows the rounding
|
||||
// exactly — no polygonal corners). Position-based phase offset makes each
|
||||
// panel drift like a vein at a different depth; softer than the active
|
||||
// nav button (alphaMul 0.6) so a screenful of panels stays calm.
|
||||
if (gradient_border_.enabled && gradient_border_.panels) {
|
||||
float w = pMax.x - pMin.x;
|
||||
float h = pMax.y - pMin.y;
|
||||
if (w > 80 && h > 40) { // skip small panels
|
||||
float posKey = (pMin.x * 0.0073f + pMin.y * 0.0137f);
|
||||
posKey = posKey - (int)posKey; // fractional 0..1
|
||||
if (posKey < 0) posKey += 1.0f;
|
||||
drawGradientBorderShift(dl, pMin, pMax, rounding, posKey, 0.6f);
|
||||
}
|
||||
}
|
||||
|
||||
// Edge trace on panels — use position-based phase offset so each
|
||||
// panel's tracer is at a different position around the border
|
||||
if (edge_trace_.enabled) {
|
||||
|
||||
@@ -69,9 +69,12 @@ public:
|
||||
void drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const;
|
||||
|
||||
/// Draw a border that shifts between two colors over time (gem-like)
|
||||
/// Draw a border that shifts between two colors over time (gem-like).
|
||||
/// phaseOffset (0..1) shifts this element's point in the color cycle so
|
||||
/// many panels don't pulse in unison; alphaMul scales the whole effect.
|
||||
void drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding) const;
|
||||
float rounding, float phaseOffset = 0.0f,
|
||||
float alphaMul = 1.0f) const;
|
||||
|
||||
/// Draw ember particles that rise from an element (fire theme)
|
||||
void drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const;
|
||||
@@ -186,6 +189,7 @@ private:
|
||||
|
||||
struct GradientBorderConfig {
|
||||
bool enabled = false;
|
||||
bool panels = false; ///< also draw on glass panels, not just the active nav button
|
||||
float speed = 0.15f; ///< full color shift cycles per second
|
||||
float thickness = 1.5f; ///< border line thickness in pixels
|
||||
float alpha = 0.6f; ///< peak alpha
|
||||
|
||||
@@ -44,6 +44,34 @@ inline bool IsLightTheme() {
|
||||
return (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f;
|
||||
}
|
||||
|
||||
// Theme error color tuned for use as TEXT. The Material --error reds are muted, so on a dark modal
|
||||
// backdrop they read low-contrast; lift them toward a legible light red on dark themes. Light themes
|
||||
// keep the theme error (already a dark red readable on a light background).
|
||||
inline ImU32 ReadableError() {
|
||||
ImU32 e = Error();
|
||||
if (IsLightTheme()) return e;
|
||||
const float m = 0.35f; // blend toward white
|
||||
int r = (int)(((e >> IM_COL32_R_SHIFT) & 0xFF) * (1 - m) + 255 * m);
|
||||
int g = (int)(((e >> IM_COL32_G_SHIFT) & 0xFF) * (1 - m) + 255 * m);
|
||||
int b = (int)(((e >> IM_COL32_B_SHIFT) & 0xFF) * (1 - m) + 255 * m);
|
||||
return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF);
|
||||
}
|
||||
|
||||
// Middle-ellipsis truncation ("front...back", roughly equal halves) so `text` fits within
|
||||
// maxWidth pixels when drawn with `font` at `fontSize`. Returns `text` unchanged if it already
|
||||
// fits (or maxWidth is non-positive). Display-only — never mutate the underlying value with this.
|
||||
inline std::string TruncateToWidth(const std::string& text, ImFont* font, float fontSize, float maxWidth) {
|
||||
if (text.empty() || !font || maxWidth <= 0.0f) return text;
|
||||
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, text.c_str()).x <= maxWidth) return text;
|
||||
const int n = static_cast<int>(text.size());
|
||||
for (int f = n / 2; f >= 3; --f) {
|
||||
const int b = (f - 2 > 3) ? (f - 2) : 3; // keep the two halves roughly equal
|
||||
std::string t = text.substr(0, f) + "..." + text.substr(n - b);
|
||||
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, t.c_str()).x <= maxWidth) return t;
|
||||
}
|
||||
return n > 6 ? (text.substr(0, 3) + "..." + text.substr(n - 3)) : text;
|
||||
}
|
||||
|
||||
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
|
||||
inline const char* LoadingDots() {
|
||||
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
@@ -545,10 +573,75 @@ inline bool& FullWindowBlurOverlayActiveRef() { static bool v = false; return v;
|
||||
inline void SetFullWindowBlurOverlayActive(bool v) { FullWindowBlurOverlayActiveRef() = v; }
|
||||
inline bool IsFullWindowBlurOverlayActive() { return FullWindowBlurOverlayActiveRef(); }
|
||||
|
||||
// Light-surface themes (Light / Marble) want cards to lift off the pale background with a subtle
|
||||
// drop shadow. Cached per theme-generation so the string compare runs once per theme load, not per
|
||||
// panel. (Data-driven later if more themes want it.)
|
||||
inline bool CardDropShadowWanted()
|
||||
{
|
||||
static uint32_t s_gen = ~0u;
|
||||
static bool s_want = false;
|
||||
uint32_t g = schema::UI().generation();
|
||||
if (g != s_gen) {
|
||||
s_gen = g;
|
||||
const std::string& tn = schema::UI().themeName();
|
||||
s_want = (tn == "Light" || tn == "Marble");
|
||||
}
|
||||
return s_want;
|
||||
}
|
||||
|
||||
// Soft, UNIFORM shadow around a rounded rect — no directional offset, equal on all sides. Drawn as
|
||||
// fading rounded-rect STROKES (not fills), so it never paints across the card body — only the outer
|
||||
// blur remains once the card fill covers the interior. The "render then mask out the UI area" result
|
||||
// without an FBO.
|
||||
inline void DrawCardDropShadow(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, float rounding)
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
// Matches the design mockup's `box-shadow: 0 0 4px rgb(50 53 58 / 26%)`: a tight, darker, colored
|
||||
// uniform shadow. Sampled as a stack of rounded-rect strokes stepping OUTWARD from the card edge
|
||||
// with a Gaussian alpha falloff (darkest at the edge, ~0 by the tail). ~1px-spaced, near-1px-thick
|
||||
// rings keep overlap (and thus alpha accumulation) low, so the peak reads close to the target.
|
||||
const float blur = 4.0f * dp; // CSS-like blur radius
|
||||
const float reach = blur * 1.5f; // how far the shadow extends beyond the card edge (all sides)
|
||||
const float sigma = blur * 0.6f;
|
||||
const float peak = 34.0f; // edge alpha for rgb(50,53,58) (~13% after the card fill masks the inner half)
|
||||
const int steps = std::max(6, (int)(reach / dp + 0.5f));
|
||||
|
||||
// The shadow rides on the card's OWN draw list (so it keeps the right z-order in EVERY context —
|
||||
// main content, modals, foreground popups, notifications; a global background draw list would drop
|
||||
// a modal/popup card's shadow behind its scrim). But cards often live inside clipping child windows
|
||||
// whose clip rect chops the shadow wherever a card sits flush with the child's top/bottom/side edge.
|
||||
// Clip instead to the shadow's own bounding box (card ± reach), so all four sides show in full —
|
||||
// bounded on each side to at most `reach` beyond the child clip (and the viewport) so a card scrolled
|
||||
// partly out of a scroll area still can't smear its shadow more than the halo width into neighbours.
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
const ImVec2 cur0 = dl->GetClipRectMin();
|
||||
const ImVec2 cur1 = dl->GetClipRectMax();
|
||||
const float x0 = std::max(vp->Pos.x, std::max(pMin.x - reach, cur0.x - reach));
|
||||
const float y0 = std::max(vp->Pos.y, std::max(pMin.y - reach, cur0.y - reach));
|
||||
const float x1 = std::min(vp->Pos.x + vp->Size.x, std::min(pMax.x + reach, cur1.x + reach));
|
||||
const float y1 = std::min(vp->Pos.y + vp->Size.y, std::min(pMax.y + reach, cur1.y + reach));
|
||||
dl->PushClipRect(ImVec2(x0, y0), ImVec2(x1, y1), false);
|
||||
for (int i = 0; i < steps; ++i) {
|
||||
float d = reach * (float)i / (float)(steps - 1); // 0 (card edge) .. reach (outer tail)
|
||||
float g = std::exp(-(d * d) / (2.0f * sigma * sigma));
|
||||
int a = (int)(peak * g + 0.5f);
|
||||
if (a < 1) continue;
|
||||
ImVec2 smn(pMin.x - d, pMin.y - d);
|
||||
ImVec2 smx(pMax.x + d, pMax.y + d);
|
||||
dl->AddRect(smn, smx, IM_COL32(50, 53, 58, a), rounding + d, 0, 1.3f * dp);
|
||||
}
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
||||
const ImVec2& pMax,
|
||||
const GlassPanelSpec& spec = GlassPanelSpec())
|
||||
{
|
||||
// Subtle drop shadow behind the card (light themes only). Drawn first so the card fill below
|
||||
// covers the interior, leaving only the outer soft blur — the card body is never contaminated.
|
||||
if (CardDropShadowWanted())
|
||||
DrawCardDropShadow(dl, pMin, pMax, spec.rounding);
|
||||
|
||||
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && !IsFullWindowBlurOverlayActive()) {
|
||||
// --- Cached color lookups (invalidated on theme change) ---
|
||||
// These 3 resolveColor() calls do string parsing + map lookup
|
||||
@@ -664,10 +757,19 @@ inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const
|
||||
// panel skips acrylic (IsFullWindowBlurOverlayActive), so the backdrop is the SOLE applyBlur
|
||||
// caller — no other radius contends for the shared, radius-keyed blur cache.
|
||||
auto params = GetCurrentAcrylicTheme().card;
|
||||
params.blurRadius = 64.0f; // strong full-window blur
|
||||
params.blurRadius = 120.0f; // strong full-window blur (deeper than panels)
|
||||
params.absoluteBlurRadius = true; // fixed modal blur — independent of the user's acrylic slider
|
||||
params.fallbackColor.w = 1.0f; // full-strength blur so the live content reads
|
||||
effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax, params, 0.0f);
|
||||
dl->AddRectFilled(pMin, pMax, WithAlpha(Background(), 70)); // subtle theme-tinted frost so the card reads as foreground
|
||||
// Veil over the blur so the floating (card-less) modal reads as foreground. On DARK themes a
|
||||
// theme-tinted frost darkens the backdrop and the light content pops. On LIGHT themes the modal's
|
||||
// own controls carry a dropshadow for figure/ground, so the backdrop stays LIGHT — a theme-tinted
|
||||
// light frost — rather than a dark veil (which made a "light" theme read dark and sank the
|
||||
// dark-on-light chrome into it).
|
||||
if (IsLightTheme())
|
||||
dl->AddRectFilled(pMin, pMax, WithAlpha(Background(), 205)); // light frost — dropshadows give shape
|
||||
else
|
||||
dl->AddRectFilled(pMin, pMax, WithAlpha(Background(), 120)); // theme-tinted darken, stronger
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,10 +1362,17 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
||||
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
|
||||
WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f);
|
||||
ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, labels[i]);
|
||||
// Center when the label fits; otherwise left-align with a small pad (so a long translation clips
|
||||
// on the right, not on BOTH sides). Clip to the cell so no label can bleed into a neighbouring
|
||||
// segment or past the rounded track — English fits, but de/es/fr/pt/ru labels can overrun.
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= cellW - 2.0f * lpad) ? (cellW - ts.x) * 0.5f : lpad;
|
||||
dl->PushClipRect(cMin, cMax, true);
|
||||
dl->AddText(font, font->LegacySize,
|
||||
ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (height - ts.y) * 0.5f),
|
||||
ImVec2(cMin.x + tx, cMin.y + (height - ts.y) * 0.5f),
|
||||
active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||
labels[i]);
|
||||
dl->PopClipRect();
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetCursorScreenPos(cMin);
|
||||
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;
|
||||
@@ -1283,11 +1392,23 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
||||
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
|
||||
// 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;
|
||||
// Per-dialog content state (keyed by the child's id), measured at the end of each frame so the next
|
||||
// frame can position/size the (auto-height) card to its content. An auto-resize child reports a
|
||||
// too-small height on its first render and the true height a frame later, so we keep the card hidden
|
||||
// until the height holds steady (stableCount) before revealing it centered — otherwise it visibly
|
||||
// slides for a couple frames as the height converges. g_overlayCurrentKey carries the active dialog's
|
||||
// key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
|
||||
struct OverlayCardState {
|
||||
float height = 0.0f; // last measured content-child height
|
||||
int stableCount = 0; // consecutive frames the height held steady (within 1px)
|
||||
int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap
|
||||
bool shown = false; // revealed (centered) at least once this open; don't re-hide after
|
||||
};
|
||||
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
||||
inline std::string g_overlayCurrentKey;
|
||||
// Set when BeginOverlayDialog clips the card on an auto-height dialog's measuring frame (so it never
|
||||
// flashes off-center); EndOverlayDialog pops the clip. Overlays don't nest, so a single flag is fine.
|
||||
inline bool g_overlayHideFrameClip = false;
|
||||
|
||||
// The dark base color for any full-window overlay backdrop (dialog scrims + the shutdown screen),
|
||||
// so every overlay shares one backdrop tone. `opacity` is the alpha (dialogs vary it per call).
|
||||
@@ -1362,6 +1483,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
return false;
|
||||
}
|
||||
|
||||
// True only on the frame the dialog (re)opens — used to re-arm the auto-height settle so each
|
||||
// fresh open reveals already-centered (rather than flashing last-open's stale position).
|
||||
const bool scrimAppearing = ImGui::IsWindowAppearing();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Live-blur backdrop with capture-once: (re)capture the content drawn BELOW this overlay for a
|
||||
@@ -1396,10 +1521,13 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGuiButtonFlags_MouseButtonRight |
|
||||
ImGuiButtonFlags_MouseButtonMiddle);
|
||||
|
||||
// Card geometry: fixed-height (large modals) is centered; auto-height reuses last frame's
|
||||
// measured content height (short dialogs get short cards), capped at the viewport ratio.
|
||||
// Card geometry: both fixed- and auto-height cards are VERTICALLY CENTERED in the window.
|
||||
// Fixed-height centers its known height; auto-height centers last frame's measured content
|
||||
// height (cached per dialog id, so a re-opened dialog is centered from frame 1 — only the very
|
||||
// first open of a given dialog this session briefly anchors near the top before it re-centers).
|
||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
||||
float cardY, cardBottomY;
|
||||
bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame
|
||||
const bool fixedHeight = (spec.cardHeight > 0.0f);
|
||||
if (fixedHeight) {
|
||||
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
|
||||
@@ -1407,25 +1535,45 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
cardBottomY = cardY + cardH;
|
||||
g_overlayCurrentKey.clear();
|
||||
} else {
|
||||
cardY = vp_pos.y + vp_size.y * 0.15f;
|
||||
g_overlayCurrentKey = childId;
|
||||
float ratioMaxY = vp_pos.y + vp_size.y * spec.cardBottomViewportRatio;
|
||||
auto it = g_overlayCardHeights.find(childId);
|
||||
cardBottomY = (it != g_overlayCardHeights.end() && it->second > 0.0f)
|
||||
? std::min(cardY + it->second, ratioMaxY) : ratioMaxY;
|
||||
OverlayCardState& cs = g_overlayCardHeights[childId];
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; }
|
||||
if (!cs.shown) cs.appearFrames++;
|
||||
const float measuredH = cs.height;
|
||||
// Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's
|
||||
// already been shown this open (don't re-hide on a mid-dialog content change); a frame cap
|
||||
// guarantees a pathological ever-changing height can't hide the dialog forever.
|
||||
const bool ready = measuredH > 0.0f &&
|
||||
(cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8);
|
||||
if (ready) {
|
||||
cs.shown = true;
|
||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||
cardY = (measuredH < vp_size.y - 32.0f)
|
||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||
: vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + measuredH;
|
||||
} else {
|
||||
// Still settling: lay the content out (so the auto-height child gets measured) but keep
|
||||
// the card hidden (hideForMeasure below) so it never flashes off-center — it appears,
|
||||
// already centered and stable, once the height stops changing.
|
||||
hideForMeasure = true;
|
||||
cardY = vp_pos.y + vp_size.y * 0.15f;
|
||||
cardBottomY = vp_pos.y + vp_size.y * spec.cardBottomViewportRatio;
|
||||
}
|
||||
}
|
||||
ImVec2 cardMin(cardX, cardY), cardMax(cardX + cardWidth, cardBottomY);
|
||||
|
||||
// Card background — a glass panel unless the content floats directly on the backdrop.
|
||||
if (!floating) {
|
||||
// Card background — a glass panel unless the content floats directly on the backdrop. Skipped on
|
||||
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
||||
if (!floating && !hideForMeasure) {
|
||||
GlassPanelSpec cardGlass;
|
||||
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||
}
|
||||
|
||||
// Click outside the card dismisses — but not on the appearing frame (the opening click) or while
|
||||
// a popup opened from the dialog is showing (a click in an overhanging combo would look outside).
|
||||
if (spec.p_open && !overlayPopupOpen && !ImGui::IsWindowAppearing() &&
|
||||
// Click outside the card dismisses — but not on the measuring frame (placeholder geometry), the
|
||||
// appearing frame (the opening click), or while a popup opened from the dialog is showing.
|
||||
if (spec.p_open && !hideForMeasure && !overlayPopupOpen && !ImGui::IsWindowAppearing() &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
|
||||
*spec.p_open = false;
|
||||
@@ -1437,9 +1585,13 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (fixedHeight ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||
// NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift
|
||||
// the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll
|
||||
// the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on
|
||||
// their own; auto-height cards resize to content so they never overflow anyway.
|
||||
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
||||
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
|
||||
cflags, ImGuiWindowFlags_NoScrollbar);
|
||||
cflags, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// Floating (portfolio-style) cards: the padding applies to this content child only, so pop it
|
||||
// now (nested children mustn't inherit it), and center button labels. Net style-var count stays
|
||||
// at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged.
|
||||
@@ -1448,6 +1600,14 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
// Measuring frame: clip the card to nothing. Content (title + the caller's body) still lays out,
|
||||
// so the child's auto-height is measured for next frame, but nothing is drawn — no off-center
|
||||
// flash. EndOverlayDialog pops this before EndChild. Only entered when childVisible (End runs).
|
||||
if (childVisible && hideForMeasure) {
|
||||
ImGui::PushClipRect(ImVec2(0, 0), ImVec2(0, 0), false);
|
||||
g_overlayHideFrameClip = true;
|
||||
}
|
||||
|
||||
if (childVisible && spec.title) {
|
||||
if (plainHd) {
|
||||
ImGui::PushFont(Type().h6());
|
||||
@@ -1462,10 +1622,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
return childVisible;
|
||||
}
|
||||
|
||||
// Positional overload — the default look for the app's dialogs: a card (auto-height, so small
|
||||
// dialogs stay small) on the live-blur backdrop. The card renders opaque via the sole-consumer
|
||||
// fallback (glass would thrash the single-slot blur cache against the radius-64 backdrop). A dialog
|
||||
// that wants the old opaque scrim (no blur) can pass a spec with blurBackdrop = false.
|
||||
// Positional overload — the default look for the app's dialogs: content floats directly on the
|
||||
// live-blur backdrop (no glass card) under a plain h6 heading, matching the Manage-Portfolio
|
||||
// editor. Auto-height keeps short dialogs short. A dialog that needs the old contained card can
|
||||
// pass an explicit OverlayDialogSpec with style = OverlayStyle::GlassCard.
|
||||
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)
|
||||
@@ -1473,17 +1633,26 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
||||
OverlayDialogSpec s;
|
||||
s.title = title; s.p_open = p_open; s.cardWidth = cardWidth;
|
||||
s.scrimOpacity = scrimOpacity; s.cardBottomViewportRatio = cardBottomViewportRatio; s.idSuffix = idSuffix;
|
||||
s.blurBackdrop = true;
|
||||
s.blurBackdrop = true;
|
||||
s.floatingContent = true; // no glass card — content floats on the blur
|
||||
s.plainHeading = true; // h6 heading instead of the boxed title bar (drops the ✕)
|
||||
return BeginOverlayDialog(s);
|
||||
}
|
||||
|
||||
inline void EndOverlayDialog()
|
||||
{
|
||||
if (g_overlayHideFrameClip) { ImGui::PopClipRect(); g_overlayHideFrameClip = false; }
|
||||
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.
|
||||
// Remember the rendered card height (the child is the last item) + track whether it has stopped
|
||||
// changing, so next frame's BeginOverlayDialog can reveal the card only once its height (and thus
|
||||
// its centered position) is stable.
|
||||
if (!g_overlayCurrentKey.empty()) {
|
||||
g_overlayCardHeights[g_overlayCurrentKey] = ImGui::GetItemRectSize().y;
|
||||
OverlayCardState& cs = g_overlayCardHeights[g_overlayCurrentKey];
|
||||
const float h = ImGui::GetItemRectSize().y;
|
||||
const float d = (h >= cs.height) ? (h - cs.height) : (cs.height - h);
|
||||
if (h > 0.0f && d <= 1.0f) cs.stableCount++;
|
||||
else cs.stableCount = 0;
|
||||
cs.height = h;
|
||||
}
|
||||
ImGui::PopStyleColor(); // ChildBg
|
||||
ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child)
|
||||
@@ -1519,11 +1688,21 @@ inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator
|
||||
// TextColored(label). The label text is passed in (already translated).
|
||||
inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4())
|
||||
{
|
||||
ImGui::PushFont(Type().iconLarge());
|
||||
ImFont* iconF = Type().iconLarge();
|
||||
const float rowTop = ImGui::GetCursorPosY();
|
||||
const float iconH = iconF->LegacySize; // already dp-scaled
|
||||
const float textH = ImGui::GetFontSize();
|
||||
ImGui::PushFont(iconF);
|
||||
ImGui::TextColored(col, ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
// Vertically center the label with the taller warning glyph (stays within the icon's row height).
|
||||
if (iconH > textH) ImGui::SetCursorPosY(rowTop + (iconH - textH) * 0.5f);
|
||||
// Wrap so a long warning flows to a second line instead of clipping at the card edge; short
|
||||
// warnings (which fit) are unaffected.
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextColored(col, "%s", warningLabel);
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
// 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` /
|
||||
@@ -1553,6 +1732,70 @@ inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
|
||||
}
|
||||
}
|
||||
|
||||
// Reference dialog footer: a centered primary + Close (or Cancel) TactileButton pair, fixed width,
|
||||
// NO divider above (the reference dropped the separator). Sets outPrimary/outClose when the respective
|
||||
// button is clicked — the caller runs the bodies (so the primary's action can be arbitrary). The
|
||||
// primary is BeginDisabled'd when !primaryEnabled. btnW<=0 → 130·dp default.
|
||||
inline void DialogActionFooter(const char* primaryLabel, bool primaryEnabled,
|
||||
const char* closeLabel, bool& outPrimary, bool& outClose,
|
||||
float btnW = 0.0f)
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
if (btnW <= 0.0f) btnW = 130.0f * dp;
|
||||
const float total = btnW * 2.0f + ImGui::GetStyle().ItemSpacing.x;
|
||||
const float off = (ImGui::GetContentRegionAvail().x - total) * 0.5f;
|
||||
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
|
||||
ImGui::BeginDisabled(!primaryEnabled);
|
||||
if (TactileButton(primaryLabel, ImVec2(btnW, 0))) outPrimary = true;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(closeLabel, ImVec2(btnW, 0))) outClose = true;
|
||||
}
|
||||
|
||||
// RAII glass sub-section for grouping an optional/advanced block inside a dialog (the reference's
|
||||
// scan-height panel). Auto-sizes to its content and paints a subtle glass panel behind it. Uses a
|
||||
// LOCAL ImDrawListSplitter — safe with BeginCombo popups, unlike the shared ChannelsSplit that
|
||||
// GlassCardScope uses. padX/padY are the interior insets (in logical px, dp-scaled here). Render the
|
||||
// section body between construction and destruction; use interiorWidth() for full-width children.
|
||||
struct GlassSectionScope {
|
||||
ImDrawListSplitter splitter;
|
||||
ImDrawList* dl;
|
||||
ImVec2 cardMin;
|
||||
float panelW, padX, padY;
|
||||
GlassPanelSpec spec;
|
||||
|
||||
explicit GlassSectionScope(float padXLogical = 12.0f, float padYLogical = 10.0f)
|
||||
{
|
||||
const float dp = Layout::dpiScale();
|
||||
padX = padXLogical * dp;
|
||||
padY = padYLogical * dp;
|
||||
spec.rounding = 8.0f * dp;
|
||||
spec.fillAlpha = 20;
|
||||
spec.borderAlpha = 30;
|
||||
dl = ImGui::GetWindowDrawList();
|
||||
panelW = ImGui::GetContentRegionAvail().x;
|
||||
cardMin = ImGui::GetCursorScreenPos();
|
||||
splitter.Split(dl, 2);
|
||||
splitter.SetCurrentChannel(dl, 1); // content above the panel background
|
||||
ImGui::Dummy(ImVec2(0, padY));
|
||||
ImGui::Indent(padX);
|
||||
}
|
||||
~GlassSectionScope()
|
||||
{
|
||||
ImGui::Unindent(padX);
|
||||
ImGui::Dummy(ImVec2(0, padY));
|
||||
const ImVec2 cardMax(cardMin.x + panelW, ImGui::GetCursorScreenPos().y);
|
||||
splitter.SetCurrentChannel(dl, 0);
|
||||
DrawGlassPanel(dl, cardMin, cardMax, spec);
|
||||
splitter.Merge(dl);
|
||||
}
|
||||
// Interior content width (panel width minus both side insets) for full-width children.
|
||||
float interiorWidth() const { return panelW - 2.0f * padX; }
|
||||
|
||||
GlassSectionScope(const GlassSectionScope&) = delete;
|
||||
GlassSectionScope& operator=(const GlassSectionScope&) = delete;
|
||||
};
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
154
src/ui/material/markdown.h
Normal file
154
src/ui/material/markdown.h
Normal file
@@ -0,0 +1,154 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Minimal in-app markdown renderer for release notes (the daemon / miner updater bodies are GitHub-
|
||||
// flavoured markdown). Supports the subset those notes actually use: # / ## / ### headings,
|
||||
// **bold**, `inline code`, - / * / + bullets, and blank-line paragraph breaks. It lays text out with
|
||||
// the ImGui draw list (word-wrapped, mixed fonts) and reserves the block height via a Dummy so a
|
||||
// scrollable parent scrolls correctly. Not a full CommonMark implementation — just enough to make
|
||||
// release notes readable.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "../layout.h"
|
||||
#include "colors.h"
|
||||
#include "type.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
|
||||
// Render `md` into the current window, wrapping to `wrapW` (defaults to the content region width).
|
||||
// Advances the ImGui cursor past the rendered block.
|
||||
inline void RenderMarkdown(const std::string& md, float wrapW = 0.0f) {
|
||||
const float dp = Layout::dpiScale();
|
||||
if (wrapW <= 0.0f) wrapW = ImGui::GetContentRegionAvail().x;
|
||||
if (wrapW <= 1.0f) return;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
ImFont* fBody = Type().body2(); // 14 Regular
|
||||
ImFont* fBold = Type().subtitle2(); // 14 Medium (bold at body size)
|
||||
ImFont* fCode = Type().mono();
|
||||
ImFont* fH1 = Type().h6(); // 20 Medium
|
||||
ImFont* fH2 = Type().subtitle2(); // heading level 2+ = medium + top spacing
|
||||
|
||||
const ImU32 cText = OnSurface();
|
||||
const ImU32 cCode = Primary();
|
||||
const ImU32 cBullet = OnSurfaceMedium();
|
||||
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float xLeft = origin.x;
|
||||
const float maxX = xLeft + wrapW;
|
||||
float y = origin.y;
|
||||
|
||||
struct Run { std::string text; ImFont* font; ImU32 col; bool code; };
|
||||
|
||||
// Split a line into styled runs on **bold** and `code`.
|
||||
auto parseInline = [&](const std::string& line, ImFont* base, ImU32 col) {
|
||||
std::vector<Run> runs;
|
||||
std::string cur;
|
||||
auto flush = [&]() { if (!cur.empty()) { runs.push_back({cur, base, col, false}); cur.clear(); } };
|
||||
for (size_t i = 0; i < line.size();) {
|
||||
if (line[i] == '*' && i + 1 < line.size() && line[i + 1] == '*') {
|
||||
size_t e = line.find("**", i + 2);
|
||||
if (e == std::string::npos) { cur += line[i]; ++i; continue; }
|
||||
flush();
|
||||
runs.push_back({line.substr(i + 2, e - (i + 2)), fBold, col, false});
|
||||
i = e + 2;
|
||||
} else if (line[i] == '`') {
|
||||
size_t e = line.find('`', i + 1);
|
||||
if (e == std::string::npos) { cur += line[i]; ++i; continue; }
|
||||
flush();
|
||||
runs.push_back({line.substr(i + 1, e - (i + 1)), fCode, cCode, true});
|
||||
i = e + 1;
|
||||
} else {
|
||||
cur += line[i];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return runs;
|
||||
};
|
||||
|
||||
// Lay runs out as words, wrapping to [xStart, maxX] and hanging at xHang on wrap. Advances y.
|
||||
auto layout = [&](const std::vector<Run>& runs, float xStart, float xHang) {
|
||||
float x = xStart;
|
||||
float lineH = 0.0f;
|
||||
for (const Run& r : runs) {
|
||||
size_t i = 0;
|
||||
while (i < r.text.size()) {
|
||||
size_t ws = r.text.find(' ', i);
|
||||
std::string word = (ws == std::string::npos) ? r.text.substr(i) : r.text.substr(i, ws - i + 1);
|
||||
i = (ws == std::string::npos) ? r.text.size() : ws + 1;
|
||||
if (word.empty()) continue;
|
||||
float wW = r.font->CalcTextSizeA(r.font->LegacySize, FLT_MAX, 0, word.c_str()).x;
|
||||
if (x + wW > maxX && x > xHang + 0.5f) { // wrap
|
||||
y += (lineH > 0.0f ? lineH : r.font->LegacySize * 1.4f);
|
||||
x = xHang;
|
||||
lineH = 0.0f;
|
||||
}
|
||||
lineH = std::max(lineH, r.font->LegacySize * 1.4f);
|
||||
if (r.code) {
|
||||
std::string trimmed = word;
|
||||
while (!trimmed.empty() && trimmed.back() == ' ') trimmed.pop_back();
|
||||
float tw = r.font->CalcTextSizeA(r.font->LegacySize, FLT_MAX, 0, trimmed.c_str()).x;
|
||||
dl->AddRectFilled(ImVec2(x - 2.0f * dp, y), ImVec2(x + tw + 2.0f * dp, y + r.font->LegacySize + 3.0f * dp),
|
||||
WithAlpha(OnSurface(), 26), 3.0f * dp);
|
||||
}
|
||||
dl->AddText(r.font, r.font->LegacySize, ImVec2(x, y), r.col, word.c_str());
|
||||
x += wW;
|
||||
}
|
||||
}
|
||||
y += (lineH > 0.0f ? lineH : fBody->LegacySize * 1.4f);
|
||||
};
|
||||
|
||||
std::istringstream ss(md);
|
||||
std::string raw;
|
||||
bool prevBlank = true;
|
||||
while (std::getline(ss, raw)) {
|
||||
if (!raw.empty() && raw.back() == '\r') raw.pop_back();
|
||||
size_t a = raw.find_first_not_of(" \t");
|
||||
std::string t = (a == std::string::npos) ? std::string() : raw.substr(a);
|
||||
|
||||
if (t.empty()) { y += fBody->LegacySize * 0.55f; prevBlank = true; continue; }
|
||||
|
||||
if (t[0] == '#') { // heading
|
||||
size_t h = t.find_first_not_of('#');
|
||||
std::string txt;
|
||||
if (h != std::string::npos) {
|
||||
size_t ts = t.find_first_not_of(" \t", h);
|
||||
if (ts != std::string::npos) txt = t.substr(ts);
|
||||
}
|
||||
if (!prevBlank) y += fBody->LegacySize * 0.5f; // breathing room above a heading
|
||||
ImFont* hf = (h <= 1) ? fH1 : fH2;
|
||||
layout(parseInline(txt, hf, cText), xLeft, xLeft);
|
||||
prevBlank = false;
|
||||
continue;
|
||||
}
|
||||
if ((t[0] == '*' || t[0] == '-' || t[0] == '+') && t.size() > 1 && t[1] == ' ') { // bullet
|
||||
const float indent = fBody->LegacySize * 1.15f;
|
||||
dl->AddText(fBody, fBody->LegacySize, ImVec2(xLeft + fBody->LegacySize * 0.35f, y),
|
||||
cBullet, "\xE2\x80\xA2"); // •
|
||||
layout(parseInline(t.substr(2), fBody, cText), xLeft + indent, xLeft + indent);
|
||||
prevBlank = false;
|
||||
continue;
|
||||
}
|
||||
layout(parseInline(t, fBody, cText), xLeft, xLeft); // paragraph
|
||||
prevBlank = false;
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::Dummy(ImVec2(wrapW, std::max(0.0f, y - origin.y)));
|
||||
}
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
193
src/ui/material/settings_controls.h
Normal file
193
src/ui/material/settings_controls.h
Normal file
@@ -0,0 +1,193 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Settings design-system controls — the polished chat-settings look (accent subsection headers,
|
||||
// labeled rows with right-aligned controls, iOS-style segmented controls) promoted to reusable
|
||||
// components, plus a tiered ActionButton (Primary/Secondary/Tertiary/Destructive) with optional
|
||||
// leading Material icon and a ButtonFlow that wraps rows of buttons instead of shrinking them.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "draw_helpers.h" // colors, type, layout, icons, WithAlpha, ScaleAlpha, DrawButtonGlassOverlay
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
|
||||
// Accent small-caps subsection header ("KEYS & BACKUP", "APPEARANCE"…) — the chat-settings section() look.
|
||||
inline void SettingsSubheader(const char* text) {
|
||||
const float dp = Layout::dpiScale();
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
ImGui::PushFont(Type().caption());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(Primary(), 235));
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0.0f, 2.0f * dp));
|
||||
}
|
||||
|
||||
// A labeled settings row: label left, control right-aligned in a fixed column. Construct once per card
|
||||
// section with the content width (0 = auto), then call .label(text) before drawing each control (leaves
|
||||
// the cursor at the control origin and sets the next item width to the control column).
|
||||
struct SettingsRow {
|
||||
float leftX, rowW, ctrlW, rowGap;
|
||||
explicit SettingsRow(float contentWidth, float controlWidth = 250.0f) {
|
||||
const float dp = Layout::dpiScale();
|
||||
leftX = ImGui::GetCursorPosX();
|
||||
rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x;
|
||||
ctrlW = controlWidth * dp;
|
||||
rowGap = 5.0f * dp;
|
||||
}
|
||||
void label(const char* text) {
|
||||
ImGui::Dummy(ImVec2(0.0f, rowGap));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), leftX + std::max(0.0f, rowW - ctrlW)));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
}
|
||||
};
|
||||
|
||||
// iOS-style segmented control (rounded track + inset pill on the selection). Draws its own labeled row
|
||||
// and returns the (possibly changed) index.
|
||||
inline int SegmentedControl(SettingsRow& row, const char* label, const char* const* items, int count, int value) {
|
||||
const float dp = Layout::dpiScale();
|
||||
row.label(label);
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const float seg = row.ctrlW / static_cast<float>(count);
|
||||
const float round = 7.0f * dp;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(origin, ImVec2(origin.x + row.ctrlW, origin.y + h), WithAlpha(OnSurface(), 20), round);
|
||||
int result = value;
|
||||
ImGui::PushID(label);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ImGui::PushID(i);
|
||||
const ImVec2 mn(origin.x + i * seg, origin.y), mx(origin.x + (i + 1) * seg, origin.y + h);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
if (ImGui::InvisibleButton("##s", ImVec2(seg, h))) result = i;
|
||||
const bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const bool sel = (value == i);
|
||||
if (sel) {
|
||||
const float in = 2.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(mn.x + in, mn.y + in), ImVec2(mx.x - in, mx.y - in),
|
||||
WithAlpha(Primary(), 210), std::max(1.0f, round - in));
|
||||
} else if (hov) {
|
||||
dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 26), round);
|
||||
}
|
||||
const ImVec2 ts = ImGui::CalcTextSize(items[i]);
|
||||
const float lpad = 4.0f * dp;
|
||||
const float tx = (ts.x <= seg - 2.0f * lpad) ? (seg - ts.x) * 0.5f : lpad;
|
||||
ImGui::PushClipRect(mn, mx, true);
|
||||
dl->AddText(ImVec2(mn.x + tx, mn.y + (h - ts.y) * 0.5f),
|
||||
sel ? IM_COL32(255, 255, 255, 236) : OnSurfaceMedium(), items[i]);
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::Dummy(ImVec2(row.ctrlW, h)); // reserve the control's rect for layout flow
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Tiered action buttons ───────────────────────────────────────────────────
|
||||
// Primary = filled accent (the one main action of a group)
|
||||
// Secondary = glass (default — common actions)
|
||||
// Tertiary = ghost / low-emphasis (rarely used)
|
||||
// Destructive = error-tinted outline (delete / reset)
|
||||
enum class ActionTier { Primary, Secondary, Tertiary, Destructive };
|
||||
|
||||
// The width an ActionButton will occupy (for ButtonFlow / manual layout).
|
||||
inline float ActionButtonWidth(const char* label, const char* icon, float minWidth = 0.0f) {
|
||||
ImFont* lf = Type().button();
|
||||
ImFont* icf = Type().iconSmall();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float padX = 12.0f * dp, gap = 6.0f * dp;
|
||||
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
|
||||
const float iconW = (icon && icon[0] && icf) ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
|
||||
const float w = padX * 2.0f + iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
|
||||
return std::max(w, minWidth);
|
||||
}
|
||||
|
||||
// A tiered action button with an optional leading Material icon (ICON_MD_* or nullptr). Auto-sizes to
|
||||
// its content (>= minWidth). Respects BeginDisabled() (dims via the style alpha, not clickable).
|
||||
inline bool ActionButton(const char* id, const char* label, const char* icon, ActionTier tier, float minWidth = 0.0f) {
|
||||
ImFont* lf = Type().button();
|
||||
ImFont* icf = Type().iconSmall();
|
||||
const float dp = Layout::dpiScale();
|
||||
const float gap = 6.0f * dp;
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
const bool hasIcon = icon && icon[0] && icf;
|
||||
const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x;
|
||||
const float iconW = hasIcon ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f;
|
||||
const float w = ActionButtonWidth(label, icon, minWidth);
|
||||
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
const bool pressed = ImGui::InvisibleButton(id, ImVec2(w, h));
|
||||
const bool hov = ImGui::IsItemHovered(), act = ImGui::IsItemActive();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 pMax(pos.x + w, pos.y + h);
|
||||
const float round = ImGui::GetStyle().FrameRounding;
|
||||
const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
|
||||
|
||||
ImU32 bg = 0, border = 0, fg = OnSurface();
|
||||
bool glass = false;
|
||||
switch (tier) {
|
||||
case ActionTier::Primary:
|
||||
bg = WithAlpha(Primary(), act ? 255 : (hov ? 245 : 220));
|
||||
fg = IM_COL32(255, 255, 255, 240);
|
||||
break;
|
||||
case ActionTier::Secondary:
|
||||
bg = WithAlpha(OnSurface(), hov ? 26 : 16);
|
||||
border = WithAlpha(OnSurface(), 40);
|
||||
glass = true;
|
||||
fg = OnSurface();
|
||||
break;
|
||||
case ActionTier::Tertiary:
|
||||
bg = hov ? WithAlpha(OnSurface(), 18) : 0;
|
||||
fg = OnSurfaceMedium();
|
||||
break;
|
||||
case ActionTier::Destructive:
|
||||
bg = hov ? WithAlpha(Error(), 32) : WithAlpha(Error(), 12);
|
||||
border = WithAlpha(Error(), 90);
|
||||
fg = Error();
|
||||
break;
|
||||
}
|
||||
if (bg) dl->AddRectFilled(pos, pMax, ScaleAlpha(bg, a), round);
|
||||
if (border) dl->AddRect(pos, pMax, ScaleAlpha(border, a), round, 0, 1.0f);
|
||||
if (glass) DrawButtonGlassOverlay(dl, pos, pMax, round, act, hov);
|
||||
fg = ScaleAlpha(fg, a);
|
||||
|
||||
const float contentW = iconW + (iconW > 0.0f ? gap : 0.0f) + labelW;
|
||||
float cx = pos.x + (w - contentW) * 0.5f;
|
||||
if (hasIcon) {
|
||||
dl->AddText(icf, icf->LegacySize, ImVec2(cx, pos.y + (h - icf->LegacySize) * 0.5f), fg, icon);
|
||||
cx += iconW + gap;
|
||||
}
|
||||
dl->AddText(lf, lf->LegacySize, ImVec2(cx, pos.y + (h - lf->LegacySize) * 0.5f), fg, label);
|
||||
return pressed;
|
||||
}
|
||||
|
||||
// Places ActionButtons left→right, wrapping to a new row when the next one won't fit (instead of the
|
||||
// old font-scale-to-fit). Call next(width) before each ActionButton.
|
||||
struct ButtonFlow {
|
||||
float availW, gap; float x = 0.0f; bool firstOnRow = true;
|
||||
explicit ButtonFlow(float availWidth, float gapPx = 8.0f) : availW(availWidth) {
|
||||
gap = gapPx * Layout::dpiScale();
|
||||
}
|
||||
void next(float w) {
|
||||
if (firstOnRow) { firstOnRow = false; x = w; return; }
|
||||
if (x + gap + w <= availW) { ImGui::SameLine(0, gap); x += gap + w; }
|
||||
else { x = w; } // natural newline wraps to the next row
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace material
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -15,6 +15,11 @@
|
||||
#include "../embedded/IconsMaterialDesign.h" // Icon codepoint defines
|
||||
#include "../../util/logger.h"
|
||||
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
#include "imgui_internal.h" // ImFontAtlasGetFontLoaderForStbTruetype
|
||||
#include "misc/freetype/imgui_freetype.h" // ImGuiFreeType::GetFontLoader + LoadColor flag
|
||||
#endif
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace material {
|
||||
@@ -125,7 +130,16 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
||||
float scale = dpiScale * Layout::kFontScale() * Layout::userFontScale();
|
||||
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, userFontScale: %.2f, combined: %.2f)\n",
|
||||
dpiScale, Layout::kFontScale(), Layout::userFontScale(), scale);
|
||||
|
||||
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
// Choose the atlas font loader BEFORE any font is added: FreeType (required to rasterize COLR
|
||||
// color-emoji glyphs) when color emoji is enabled, else the default stb_truetype loader. Toggling
|
||||
// the setting + reload() flips this cleanly.
|
||||
io.Fonts->SetFontLoader(color_emoji_ ? ImGuiFreeType::GetFontLoader()
|
||||
: ImFontAtlasGetFontLoaderForStbTruetype());
|
||||
DEBUG_LOGF("Typography: font loader = %s\n", color_emoji_ ? "FreeType (color emoji)" : "stb_truetype");
|
||||
#endif
|
||||
|
||||
// For ImGui, we need to load fonts at specific pixel sizes.
|
||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||
|
||||
@@ -325,11 +339,68 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
name, g_noto_cjk_subset_size);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge monochrome emoji for chat + user text (Q12). Only into the small text fonts — baking
|
||||
// ~1400 emoji at heading sizes would bloat the atlas for glyphs no heading needs. ImGui does no
|
||||
// shaping, so single-codepoint emoji render but ZWJ sequences / flags won't compose. The
|
||||
// >0xFFFF ranges require IMGUI_USE_WCHAR32 (imconfig.h).
|
||||
static const char* const kEmojiFonts[] = {
|
||||
"Body1", "Body2", "Subtitle1", "Subtitle2", "Caption", "Overline", "Button", "ButtonSm"
|
||||
};
|
||||
bool wantEmoji = false;
|
||||
for (const char* n : kEmojiFonts) if (strcmp(name, n) == 0) { wantEmoji = true; break; }
|
||||
|
||||
// Emoji blob: the COLR/CPAL color font (FreeType-rendered) when color emoji is enabled and this
|
||||
// is a FreeType build, else the monochrome subset (default / non-FreeType path).
|
||||
const unsigned char* emojiData = g_noto_emoji_subset_data;
|
||||
unsigned int emojiSize = g_noto_emoji_subset_size;
|
||||
bool colorGlyphs = false;
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
if (color_emoji_ && g_twemoji_color_size > 0) {
|
||||
emojiData = g_twemoji_color_data;
|
||||
emojiSize = g_twemoji_color_size;
|
||||
colorGlyphs = true;
|
||||
}
|
||||
#endif
|
||||
if (wantEmoji && emojiSize > 0) {
|
||||
void* emojiCopy = IM_ALLOC(emojiSize);
|
||||
memcpy(emojiCopy, emojiData, emojiSize);
|
||||
|
||||
ImFontConfig emojiCfg;
|
||||
emojiCfg.FontDataOwnedByAtlas = true;
|
||||
emojiCfg.MergeMode = true; // merge into the text font just loaded
|
||||
emojiCfg.OversampleH = 1;
|
||||
emojiCfg.OversampleV = 1;
|
||||
emojiCfg.PixelSnapH = true;
|
||||
emojiCfg.GlyphMinAdvanceX = 0;
|
||||
#ifdef DRAGONX_HAVE_FREETYPE
|
||||
if (colorGlyphs) emojiCfg.FontLoaderFlags |= ImGuiFreeTypeLoaderFlags_LoadColor; // render COLR in color
|
||||
#endif
|
||||
// The base Ubuntu font already owns U+2600–26FF etc.; MergeMode keeps the first-loaded glyph,
|
||||
// so its text-style symbols win and only the codepoints it lacks fall through to emoji.
|
||||
static const ImWchar emojiRanges[] = {
|
||||
0x2600, 0x27BF, // Misc Symbols + Dingbats
|
||||
0x2B00, 0x2BFF, // stars (⭐) + arrows
|
||||
0x1F000, 0x1FAFF, // emoji planes (emoticons, pictographs, transport, supplement, extended)
|
||||
0,
|
||||
};
|
||||
emojiCfg.GlyphRanges = emojiRanges;
|
||||
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)",
|
||||
colorGlyphs ? "Twemoji" : "NotoEmoji", size);
|
||||
|
||||
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, emojiSize, size, &emojiCfg);
|
||||
if (emojiMerge) {
|
||||
DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n",
|
||||
colorGlyphs ? "color" : "mono", emojiSize, name);
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
||||
IM_FREE(fontDataCopy);
|
||||
}
|
||||
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,14 @@ public:
|
||||
* @brief Get the current DPI scale
|
||||
*/
|
||||
float getDpiScale() const { return dpiScale_; }
|
||||
|
||||
/**
|
||||
* @brief Select color vs monochrome emoji for the next (re)load. Color needs a FreeType-enabled
|
||||
* build (DRAGONX_HAVE_FREETYPE); otherwise this is inert and monochrome is always used.
|
||||
* Set before load()/reload() (App does this from the chat_emoji_color setting).
|
||||
*/
|
||||
void setColorEmoji(bool enabled) { color_emoji_ = enabled; }
|
||||
bool colorEmoji() const { return color_emoji_; }
|
||||
|
||||
/**
|
||||
* @brief Get font for a type style
|
||||
@@ -261,7 +269,8 @@ private:
|
||||
|
||||
bool loaded_ = false;
|
||||
float dpiScale_ = 1.0f;
|
||||
|
||||
bool color_emoji_ = false; // when true + FreeType present, merge the COLR color-emoji font
|
||||
|
||||
// Fonts for each type style
|
||||
ImFont* fonts_[15] = {};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,5 +15,9 @@ namespace ui {
|
||||
*/
|
||||
void RenderSettingsPage(App* app);
|
||||
|
||||
// Sweep-only: force the debug-options confirmation gate open/closed so the offline UI sweep can
|
||||
// capture it (it is otherwise opened only by clicking the DEBUG OPTIONS header).
|
||||
void SweepOpenDebugGate(bool open);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -149,6 +149,7 @@ struct SidebarStatus {
|
||||
int unconfirmedTxCount = 0; // badge on History
|
||||
bool miningActive = false; // green dot on Mining
|
||||
int peerCount = 0; // badge on Peers
|
||||
int chatUnreadCount = 0; // badge on Chat (unread incoming messages)
|
||||
// Exit
|
||||
bool exitClicked = false;
|
||||
// Branding logo (optional — loaded at startup)
|
||||
@@ -715,6 +716,8 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
||||
dotOnly = true; badgeCol = Success();
|
||||
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
|
||||
badgeCount = status.peerCount;
|
||||
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
|
||||
badgeCount = status.chatUnreadCount;
|
||||
}
|
||||
|
||||
if (badgeCount > 0 || dotOnly) {
|
||||
|
||||
@@ -24,7 +24,16 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
auto closeBtn = S.button("dialogs.about", "close-button");
|
||||
auto versionLbl = S.label("dialogs.about", "version-label");
|
||||
auto editionLbl = S.label("dialogs.about", "edition-label");
|
||||
|
||||
|
||||
// Schema positions/widths are logical px. BeginOverlayDialog scales the card by dpiScale(), so
|
||||
// scale the value-column x, the edition inset, and the button widths to match — raw values left
|
||||
// the value column jammed against the labels and the buttons left-packed at higher DPI.
|
||||
const float dp = Layout::dpiScale();
|
||||
const float valX = versionLbl.position * dp;
|
||||
const float edX = editionLbl.position * dp;
|
||||
const float linkW = linkBtn.width * dp;
|
||||
const float closeW = closeBtn.width * dp;
|
||||
|
||||
if (!material::BeginOverlayDialog(TR("about_title"), p_open, win.width, 0.94f)) {
|
||||
return;
|
||||
}
|
||||
@@ -39,7 +48,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position);
|
||||
ImGui::SameLine(ImGui::GetWindowWidth() - edX);
|
||||
ImGui::TextDisabled("%s", TR("about_edition"));
|
||||
|
||||
ImGui::Spacing();
|
||||
@@ -48,24 +57,24 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
|
||||
// Version info
|
||||
ImGui::Text("%s", TR("about_version"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s", DRAGONX_VERSION);
|
||||
|
||||
ImGui::Text("%s", TR("about_imgui"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s", IMGUI_VERSION);
|
||||
|
||||
ImGui::Text("%s", TR("about_build_date"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s %s", __DATE__, __TIME__);
|
||||
|
||||
#ifdef DRAGONX_DEBUG
|
||||
ImGui::Text("%s", TR("about_build_type"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug"));
|
||||
#else
|
||||
ImGui::Text("%s", TR("about_build_type"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%s", TR("about_release"));
|
||||
#endif
|
||||
|
||||
@@ -76,20 +85,20 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("%s", TR("about_daemon"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected"));
|
||||
|
||||
const auto& state = app->getWalletState();
|
||||
ImGui::Text("%s", TR("about_chain"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("ObsidianDragon");
|
||||
|
||||
ImGui::Text("%s", TR("about_block_height"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text("%d", state.sync.blocks);
|
||||
|
||||
ImGui::Text("%s", TR("about_connections"));
|
||||
ImGui::SameLine(versionLbl.position);
|
||||
ImGui::SameLine(valX);
|
||||
ImGui::Text(TR("about_peers_count"), state.peers.size());
|
||||
}
|
||||
|
||||
@@ -121,22 +130,22 @@ void RenderAboutDialog(App* app, bool* p_open)
|
||||
ImGui::Spacing();
|
||||
|
||||
// Links
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://dragonx.is");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
|
||||
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
|
||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Close button
|
||||
float button_width = closeBtn.width;
|
||||
float button_width = closeW;
|
||||
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
|
||||
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
|
||||
*p_open = false;
|
||||
|
||||
@@ -61,7 +61,12 @@ public:
|
||||
|
||||
constexpr float cardTopViewportRatio = 0.15f;
|
||||
constexpr float cardBottomViewportRatio = 0.80f;
|
||||
if (BeginOverlayDialog(TR("set_label"), &s_open, 660.0f, 0.92f, cardBottomViewportRatio)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("set_label"); ov.p_open = &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 660.0f; ov.idSuffix = "addrlabel";
|
||||
ov.cardBottomViewportRatio = cardBottomViewportRatio;
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
float dp = Layout::dpiScale();
|
||||
|
||||
// Address preview
|
||||
@@ -108,7 +113,7 @@ public:
|
||||
const bool showClearIcon = (s_selectedIcon >= 0);
|
||||
const float clearRowH = showClearIcon ? ImGui::GetTextLineHeight() : 0.0f;
|
||||
const float buttonRowH = ImGui::GetFrameHeight();
|
||||
const float separatorH = 1.0f;
|
||||
const float separatorH = 0.0f; // footer divider removed (reference design)
|
||||
const float preButtonReserve =
|
||||
(showClearIcon ? (style.ItemSpacing.y + clearRowH) : 0.0f) +
|
||||
style.ItemSpacing.y * 3.0f + separatorH;
|
||||
@@ -196,7 +201,6 @@ public:
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::GetCursorPosY() < buttonY) {
|
||||
@@ -247,6 +251,9 @@ public:
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
|
||||
// Close the dialog (used by the UI sweep teardown).
|
||||
static void hide() { s_open = false; }
|
||||
|
||||
private:
|
||||
static inline bool s_open = false;
|
||||
static inline App* s_app = nullptr;
|
||||
|
||||
@@ -59,7 +59,11 @@ public:
|
||||
|
||||
using namespace material;
|
||||
|
||||
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 620.0f, 0.94f)) {
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("transfer_funds"); ov.p_open = &s_open;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 620.0f; ov.idSuffix = "addrtransfer";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
float dp = Layout::dpiScale();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
@@ -72,9 +76,7 @@ public:
|
||||
|
||||
// Shielding / deshielding warning
|
||||
if (s_info.fromIsZ && !s_info.toIsZ) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
ImGui::TextWrapped("%s", TR("deshielding_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
DialogWarningHeader(TR("deshielding_warning"));
|
||||
ImGui::Spacing();
|
||||
} else if (!s_info.fromIsZ && s_info.toIsZ) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Success()));
|
||||
@@ -188,7 +190,7 @@ public:
|
||||
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
|
||||
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
|
||||
float totalW = cancelW + confirmW + Layout::spacingMd();
|
||||
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f + 1.0f;
|
||||
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
|
||||
float footerTopY = cardBottomY - 24.0f * dp - footerH;
|
||||
@@ -199,7 +201,6 @@ public:
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
float rowStartX = ImGui::GetCursorPosX();
|
||||
@@ -300,7 +301,7 @@ private:
|
||||
ImGui::Spacing();
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Operation ID: %s",
|
||||
snprintf(buf, sizeof(buf), TR("shield_operation_id"),
|
||||
s_resultMsg.size() > 40
|
||||
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str()
|
||||
: s_resultMsg.c_str());
|
||||
|
||||
@@ -70,6 +70,7 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
float spacingSm,
|
||||
float spacingXs)
|
||||
{
|
||||
(void)spacingXs; // trailing button now insets by rowPadLeft (mirrors the left margin)
|
||||
AddressRowLayout layout;
|
||||
layout.contentStartX = rowX + rowPadLeft;
|
||||
layout.contentStartY = rowY + spacingMd;
|
||||
@@ -77,7 +78,9 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
|
||||
|
||||
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
|
||||
const float rightEdge = rowX + rowWidth;
|
||||
const float favoriteX = rightEdge - layout.buttonSize - spacingXs;
|
||||
// Inset the trailing (favorite/star) button by the card's inner padding — the same margin the
|
||||
// left content uses (rowPadLeft) — so it mirrors the left edge instead of hugging the card edge.
|
||||
const float favoriteX = rightEdge - layout.buttonSize - rowPadLeft;
|
||||
const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
|
||||
|
||||
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};
|
||||
|
||||
@@ -680,19 +680,31 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu(TR("portfolio_add_to"))) {
|
||||
const auto& pf = app->settings()->getPortfolioEntries();
|
||||
if (pf.empty()) {
|
||||
ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false);
|
||||
}
|
||||
const std::string activeHash = app->activeWalletIdentityHash();
|
||||
int shown = 0;
|
||||
for (int pi = 0; pi < (int)pf.size(); pi++) {
|
||||
// Only this wallet's groups (or legacy/unscoped), matching the Market summary —
|
||||
// don't offer to add this address to another wallet's group.
|
||||
if (!(pf[pi].scope.empty() || activeHash.empty() || pf[pi].scope == activeHash))
|
||||
continue;
|
||||
// PushID keeps the item ID unique even when two groups share a label (a
|
||||
// label-derived MenuItem ID would collide); a blank label shows a placeholder
|
||||
// instead of rendering as an invisible row.
|
||||
ImGui::PushID(pi);
|
||||
bool inSet = dragonx::data::PortfolioEntryContains(pf[pi].addresses, addr.address);
|
||||
if (ImGui::MenuItem(pf[pi].label.c_str(), nullptr, inSet)) {
|
||||
const char* lbl = pf[pi].label.empty() ? TR("portfolio_new_entry") : pf[pi].label.c_str();
|
||||
if (ImGui::MenuItem(lbl, nullptr, inSet)) {
|
||||
auto entries = pf; // copy, mutate, persist
|
||||
if (inSet) dragonx::data::PortfolioEntryRemove(entries[pi].addresses, addr.address);
|
||||
else dragonx::data::PortfolioEntryAdd(entries[pi].addresses, addr.address);
|
||||
app->settings()->setPortfolioEntries(entries);
|
||||
app->settings()->save();
|
||||
}
|
||||
ImGui::PopID();
|
||||
shown++;
|
||||
}
|
||||
if (shown == 0)
|
||||
ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
|
||||
@@ -203,10 +203,7 @@ void BlockInfoDialog::render(App* app)
|
||||
ImGui::Text("%s", TR("block_timestamp"));
|
||||
ImGui::SameLine(lbl.position);
|
||||
if (s_block_time > 0) {
|
||||
std::time_t t = static_cast<std::time_t>(s_block_time);
|
||||
char time_buf[64];
|
||||
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
ImGui::Text("%s", time_buf);
|
||||
ImGui::Text("%s", dragonx::util::formatClockDateTime(s_block_time, /*withSeconds=*/true).c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", TR("unknown"));
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ public:
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
|
||||
// Close the dialog (used by the UI sweep teardown).
|
||||
static void hide() { s_open = false; s_state = State::Confirm; }
|
||||
|
||||
static void render() {
|
||||
if (!s_app) return;
|
||||
if (!s_open) {
|
||||
@@ -91,21 +94,8 @@ private:
|
||||
ImGui::TextWrapped("%s", TR("bootstrap_desc"));
|
||||
ImGui::Spacing();
|
||||
|
||||
// Warning card
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.9f, 0.6f, 0.0f, 0.08f));
|
||||
ImGui::BeginChild("##bsWarn", ImVec2(0, 0),
|
||||
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
{
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
ImGui::PushFont(iconFont);
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Warning()), "%s", ICON_MD_WARNING);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextWrapped("%s", TR("bootstrap_warning"));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
// Care header (reference style: ⚠ + Warning color, wraps if long).
|
||||
DialogWarningHeader(TR("bootstrap_warning"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -124,9 +114,12 @@ private:
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons: Download | Mirror | Cancel
|
||||
// Buttons: Download | Mirror | Cancel — centered as a group.
|
||||
float btnW = 140.0f * dp;
|
||||
float btnSm = 90.0f * dp;
|
||||
const float rowTotal = btnW * 2.0f + btnSm + ImGui::GetStyle().ItemSpacing.x * 2.0f;
|
||||
const float rowOff = (ImGui::GetContentRegionAvail().x - rowTotal) * 0.5f;
|
||||
if (rowOff > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rowOff);
|
||||
|
||||
if (TactileButton(TR("download"), ImVec2(btnW, 0))) {
|
||||
startDownload("");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user