103 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 ee6903ae56 fix(theme): mop-up per-skin nits — ratio bar + light-skin muted text
Lower-severity polish from the per-skin review:
- Market portfolio ratio bar: was full-saturation Success/Warning at a208 over a
  white a10 track — the green read as "neon" on dark skins and the track was
  invisible on light skins. Make it theme-aware: muted fills (a165) on dark, a
  dark track on light so the bar is defined on white/pastel surfaces.
- Light-skin muted text: bump --on-surface-disabled 0.52 -> 0.62 on light/
  color-pop-light/dune/marble/iridescent so secondary text (addresses, category
  labels) clears the pastel/gradient backgrounds (analog of the dark-skin bump).

Remaining review nits are the "white-overlay-assumes-dark" track/hover pattern
(a broader separate sweep) and per-skin aesthetic trade-offs; deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:46:53 -05:00
DanSandClaude Opus 4.8 1c5bc0d4d6 fix(theme): per-skin legibility polish from the sweep review
Address the high-severity, systemic legibility issues found in the per-skin
screenshot review (verified across all 9 skins):

- Status pills (history): "Confirmed" text was 55%-alpha (dim); make it full
  opacity, and give status/shield pills a visible tinted fill (a48) + 1px border
  (a90) so they stop vanishing on dark-red / near-white / gradient skins.
- Console light theming: light skins now use a near-white terminal surface, and
  the log text/JsonBrace colors switch to the dark defaults in light themes (the
  schema colors are dark-terminal-authored — honor them only in dark themes) so
  text isn't pale-on-white.
- Dark-skin muted text: bump --on-surface-medium 0.85 / --on-surface-disabled
  0.58 on dragonx/dark/color-pop-dark/obsidian (obsidian addresses were ~1.1:1).
- Disabled Send button: the disabled fill was hardcoded white (invisible on
  light skins) — use a dark overlay in light themes.
- Light-skin inputs: stronger frame fill + border color and a 1px frame border
  for light themes so fields (and buttons) have defined boundaries on white/
  pastel surfaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:33:48 -05:00
DanSandClaude Opus 4.8 dfca9bbd92 refactor(ui): add LabeledInputRow + LabeledInputMultiline; complete inputs layer
Extend the inputs layer with the two remaining form-field idioms, pixel-identical:
- material::LabeledInputRow(label, labelPos, ...) — horizontal label-beside-input
  (Text + SameLine + SetNextItemWidth + InputText). Adopt at the settings RPC
  host/port/user/pass rows.
- material::LabeledInputMultiline(label, id, buf, size, flags) — label above a
  sized multiline input. Adopt at the address-book notes and request-payment
  memo (drops the redundant SetNextItemWidth that InputTextMultiline's explicit
  size already governs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 21:41:42 -05:00
DanSandClaude Opus 4.8 840ead3645 refactor(ui): add material::LabeledInput helper + adopt at 8 form fields
UI-standardization audit: the labeled single-line form field (a plain
ImGui::Text label above a fixed-width InputText/InputTextWithHint) was
open-coded across dialogs. Add material::LabeledInput(label, id, buf, size,
width=-1, hint=nullptr, flags=0) mirroring that idiom exactly — pixel-identical,
but a single chokepoint so input styling (label typography, frame, spacing) can
later evolve in one place. Returns the input's edited bool.

Adopt at the clean vertical (label-above) single-line sites: address-book
add/edit (label + address), shield from-address, export-all-keys + export-
transactions filename, validate-address input, request-payment label + URI.
Left as-is: horizontal label-beside-input rows (settings RPC host/port/user/
pass use Text + SameLine), multiline (InputTextMultiline), numeric (InputDouble),
combos, and hint-only inputs — none match the vertical single-line idiom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 21:11:02 -05:00
DanSandClaude Opus 4.8 1c8e97c0a4 refactor(ui): adopt material::SegmentedControl for the receive All/Z/T filter
UI-standardization audit: the receive address-type filter (All/Z/T) was three
hand-rolled gapped ImGui::Button pills with per-segment PushStyleColor. Replace
with the shared material::SegmentedControl (rounded track + Primary inset-pill
on the active cell + centered caption labels), the same control the market
portfolio editor uses. Keeps the same right-aligned footprint (toggleTotalW)
and drives s_addr_type_filter from the returned clicked index. Verified across
all 9 skins via the screenshot sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:58:58 -05:00
DanSandClaude Opus 4.8 f1e66c08d1 refactor(ui): adopt material::IconButton for the idle/scale/gpu mining toggles
Follow-up to c83348a: convert the three mining solo-mode header toggles
(idle-mining, thread-scaling, gpu-aware) from the hand-rolled "draw active
pill + glyph, then hit-test" idiom to material::IconButton using the restBg
(active-pill) path. Each keeps its exact behavior pixel-for-pixel: a circular
Primary-tinted pill when active (alpha 60 idle / 40 scale+gpu), a state-colored
glyph (Primary when on, muted when off), hand cursor + tooltip on hover, and
the idle button's cursor save/restore (savedCur, restored at block end) is
preserved. Completes the clean icon-button adoption; the raw-rect IsRectHovered
sites remain intentionally separate (different hit mechanism).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:12:15 -05:00
DanSandClaude Opus 4.8 c83348ab63 refactor(ui): add material::IconButton helper + adopt at 8 frameless icon buttons
UI-standardization audit (icon-button discovery pass): the app had ~22 clean
hand-rolled icon buttons repeating the same "InvisibleButton hit-target +
centered glyph + hover feedback + tooltip" boilerplate. Add a shared
material::IconButton(id, glyph, font, size, IconButtonStyle) — centered glyph,
optional resting + hover backgrounds (pill/rounded-rect), hover recolor, hand
cursor + tooltip; caller resolves state-dependent glyph/colors and returns
clicked. Adopt at the homogeneous, self-contained sites, pixel-faithful:
- mining_mode_toggle: pool/worker dropdown + bookmark + reset (×5)
- network: server hide/unhide eye (×1)
- mining_controls: pool benchmark start (×1)
- mining_stats: pool-balance refresh (×1)

Deferred (own follow-up + sweep): the idle/scale/gpu active-pill toggles use a
"draw pill+glyph earlier, hit-test later" idiom needing restructuring; the
raw-rect (IsRectHovered, no InvisibleButton) sites in market/mining_stats/
balance are a different hit-test mechanism left intentionally. Framed icon
pagers already ride TactileButton after the prior commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:25:20 -05:00
DanSandClaude Opus 4.8 da56bb73a0 refactor(ui): consolidate raw ImGui buttons onto material::TactileButton
UI-standardization audit: ~66 call sites still used raw ImGui::Button /
ImGui::SmallButton instead of the app's canonical TactileButton, so those
buttons missed the standard glass fill + rim + tactile overlay (and the
light-theme flat treatment). Convert 59 plain action-button sites across the
wizard, security dialogs, settings, market/console/explorer tabs, and the
address dialogs to material::TactileButton / TactileSmallButton (drop-in:
same signature/return, args preserved verbatim). Danger buttons keep their
PushStyleColor wrappers — Tactile renders through them then overlays.

Deliberately left raw (not plain buttons): InvisibleButton hit-targets, the
frameless transparent-bg collapsible-header toggles (RPC/Debug), the icon-only
pagination chevrons, and the receive All/Z/T segmented filter — a glass rim
would clash with those intentionally borderless/segmented looks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:00:14 -05:00
DanSandClaude Opus 4.8 ad59d62b82 refactor(ui): extract shared ProgressBar / CopyableTextOverlay / DrawPill helpers
UI-standardization audit: three draw patterns were copy-pasted across tabs and
dialogs. Promote each to a material:: helper and adopt at every verbatim site.
Output is pixel-identical (same colors/geometry threaded through as params).

- material::DrawProgressBar / ProgressBar — replaces 3 byte-identical rounded
  fill bars in the daemon/xmrig/bootstrap download dialogs.
- material::CopyableTextOverlay — the click-to-copy affordance (hit button +
  hand cursor + tooltip + hover underline + clipboard) shared by the peers
  best-block hash and the 3 mining stats (difficulty/block/address). Returns
  "copied this frame" so callers keep their own toast (no ui-layer dependency).
- material::DrawPill / PillSize — rounded bg + optional border + inset text
  badge, shared by the explorer status pill, the market Z/T chip, and the
  network official/custom tag.

Divergent one-offs left in place (transactions status pills use schema rounding
around pre-positioned multi-line text; the mining filter-pill is a button bg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:48:08 -05:00
DanSandClaude Opus 4.8 07f6d2efc9 refactor(ui): delete dead button code (buttons.h + ApplyTactile)
UI-standardization audit found two dead, contradictory "how to draw a button"
surfaces with zero call sites:
- src/ui/material/components/buttons.h (353 lines: TextButton/Outlined/Contained/
  Button(spec)/IconButton/FAB) — included by 3 mining files but never used.
- ApplyTactile (draw_helpers.h) — a retrofit-tactile-onto-plain-button wrapper,
  never called.
Remove the file, the 3 dead includes, and ApplyTactile. TactileButton/StyledButton
remain the canonical button helpers. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:34:12 -05:00
DanSandClaude Opus 4.8 e84cea27d9 feat(theme): overarching light-theme contrast (buttons, dividers, accent bars)
From the theme sweep review, three overarching light-theme fixes:
- Buttons: bump ImGuiCol_Button light overlays 13/20/28% -> 16/23/30% so the
  small/secondary buttons read as clearly as the main ones.
- Dividers + outlines: raise --divider (0.12-0.14 -> 0.20) and --outline
  (-> 0.24) across all light skins so section/list dividers are visible on white.
- Ratio/balance bars: deepen the muted --success/--warning greens/ambers in the
  light skins that were pale (light, marble, dune) so the shielded/transparent
  bars + success text are vivid (color-pop-light + iridescent were already vivid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:19:31 -05:00
DanSandClaude Opus 4.8 a457f0262e fix(theme): define light-theme input fields (darker FrameBg)
From the theme screenshot sweep: input fields, dropdowns and amount boxes were
nearly invisible on light themes (Send form especially) — ImGuiCol_FrameBg was
black@4% vs the dark theme's white@7%, so boxes blended into the white surface.
Bump the light-theme FrameBg / hovered / active to 9/14/18% so inputs read as
defined boxes. Affects all light skins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:13:51 -05:00
DanSandClaude Opus 4.8 4c466d78d1 feat(debug): per-tab screenshot subfolders + overwrite + Open location button
- Organize sweep output into per-tab subfolders: <config>/screenshots/<tab>/<skin>.png
  (was one timestamped folder with idx_skin_tab.png names).
- Fixed folder (no timestamp): subsequent sweeps overwrite the existing PNGs in
  place instead of creating a new directory each run.
- Add an "Open location" TactileButton next to "Run screenshot sweep" that opens
  the screenshots folder via Platform::openFolder(app->screenshotDir()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:58:47 -05:00
DanSandClaude Opus 4.8 0d4eaa02e7 fix(debug): put screenshot sweep in the renamed DEBUG OPTIONS section
Correct the placement: instead of a new "Debug options" button + dialog (and
moving the Verbose logging checkbox), rename the existing collapsible "DEBUG
LOGGING" section to "DEBUG OPTIONS" and put the "Run screenshot sweep" button at
the top of it, above the dragonxd daemon debug= categories. Restore the Verbose
logging checkbox to the wallet row (unchanged). Drop the now-unused
debug_options / debug_options_title i18n keys + the debug_options_open flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:50:58 -05:00
DanSandClaude Opus 4.8 dddf1b6ec7 feat(debug): theme x tab screenshot sweep + Debug options dialog
Add a debug tool that cycles every skin across every build-enabled tab and saves a
PNG of each to a timestamped folder under the config dir (screenshots/sweep_<ts>/)
— for gathering visual context on theme-specific UI work.

- App state machine (app_network.cpp): startScreenshotSweep() builds the
  skin+enabled-page lists, saves the current skin/page, and steps through them;
  updateScreenshotSweep() (top of App::render) pins the page + settles ~4 frames
  after each skin/page change (skin switches reload TOML + reset the acrylic
  capture, so they need to settle); wantsScreenshotThisFrame()/screenshotSweepPath()
  /onScreenshotCaptured() coordinate with main.cpp. Restores the original skin/page
  when done; nothing persisted.
- Framebuffer capture (main.cpp): read the finished frame and encode PNG via the
  bundled miniz (tdefl_write_image_to_png_file_in_memory_ex). GL reads FBO 0 (RGBA,
  bottom-up -> flip); DX11 copies the backbuffer to a staging texture + maps it
  (BGRA, top-down -> channel-swap). Alpha forced opaque.
- Settings UI: move the Verbose logging checkbox off the wallet row into a new
  "Debug options" button that opens a dialog housing the verbose toggle + a "Run
  screenshot sweep" button. New i18n keys.

Both platforms build; no-crash smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:41:49 -05:00
DanSandClaude Opus 4.8 a523611779 feat(theme): flat light-theme buttons (match the portfolio modal)
Per the request for a flatter look like the Manage-portfolio modal's plain
buttons, render TactileButton FLAT on light themes: DrawButtonGlassOverlay now
early-returns in light themes with just a subtle defining edge (--rim-light),
skipping the white glass sheen (--glass-fill) and the tactile 3D depth (bright top
edge + the hardcoded bottom shadow that couldn't be removed via TOML).
ImGui::Button still draws the fill/hover/active (the darkened ImGuiCol_Button
overlays), so the button reads as a flat, defined element. Dark themes keep the
raised glass/tactile look. Re-adds the IsLightTheme() luminance helper (early in
the header so the overlay can use it). Applies to all TactileButtons app-wide in
light themes, not just Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:16:22 -05:00
DanSandClaude Opus 4.8 7c90d72ded feat(theme): define light-theme buttons (TactileButton glass)
The settings-page buttons (TactileButton = ImGui::Button + a glass overlay) looked
faint in light themes: --glass-fill was a 55-62% WHITE wash that paled the button
body, and --rim-light was a 6-10% (sometimes light-colored) edge that was invisible
on light surfaces, so buttons had no defined border. Across all light skins:
- --glass-fill white wash 0.55-0.62 -> 0.20 (button body no longer washed out;
  panels use the acrylic blur on capable systems, so only low-spec panel fills are
  affected).
- --rim-light -> each skin's on-surface tint @ 0.22 (a defined dark button edge
  instead of an invisible/light rim).
- --hover-overlay alpha -> 0.10 (visible hover).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:07:09 -05:00
DanSandClaude Opus 4.8 bb2337b552 feat(theme): darker text + buttons in light themes (revert modal backing)
Replace the light-theme modal backing (19e3138, now reverted) with a root-cause
fix: darken the light themes' UI elements for contrast.
- Buttons: the light-theme ImGuiCol_Button overlays were barely-there grey
  (5/8/12% black); bump to 13/20/28% so buttons read as buttons on light
  surfaces (one edit in ApplyColorThemeToImGui covers all light skins).
- Text: raise --on-surface-medium (60-72% -> 86%) and --on-surface-disabled
  (38-40% -> 52%) across the light skins (light, color-pop-light, dune, marble,
  iridescent) — the dominant secondary/label text was too faint. Base
  --on-surface was already near-black, left as-is.
The active theme is TOML-skin-driven, so the code GetMaterialLightTheme() is not
the live source; edits are in the skin TOMLs + the ImGui-style mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:57:38 -05:00
DanSandClaude Opus 4.8 19e31388d9 feat(ui): light-theme contrast for floating modals
Floating (BlurFloat) modals draw no card, so in a light theme the light content
sits on a light frosted backdrop with almost no separation (washed out — the
Manage-portfolio modal being the clearest case). Add a faint elevated sheet
(translucent Surface) + a defining edge (OnSurface) behind floating content in
LIGHT themes only, gated on a new IsLightTheme() (background luminance) helper.
Dark themes are unchanged (they already read fine). Kept translucent to preserve
the airy floating feel; respects "no dark backdrop in light theme".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:44:17 -05:00
DanSandClaude Opus 4.8 30139a5698 feat(security): focus ring on the lock-screen input
The PIN/passphrase input uses default ImGui framing, which is subtle on the glass
card — when focused there was no clear indication the field is active. Draw an
accent (Primary) ring around the input while it's focused/being edited so it's
obvious the field is ready for typing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:32:28 -05:00
DanSandClaude Opus 4.8 94b57e91e3 fix(security): restore lock-screen interactivity (AllowOverlap on input blocker)
Regression from the full-window lock overlay: the full-window ##LockInputBlocker
InvisibleButton (drawn first) shadowed the lock card's widgets (PIN/passphrase
input, Unlock button, mode toggle) that overlap it, so clicks never reached them
(dead) and the app's global "hand cursor on clickable hover" fired for the whole
screen. Mark the blocker SetNextItemAllowOverlap() so the later widgets take
hit-test priority — the exact use case in ImGui's docs (invisible button covering
an area where subsequent items are added). The portfolio avoids this by putting
its content in a child window; the lock screen draws direct, so it needs the flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:12:01 -05:00
DanSandClaude Opus 4.8 10289b33b5 fix(ui): theme-aware modal backdrop + full-window lock overlay
Address two GUI-review points:
1. Light themes no longer get a dark modal backdrop: DrawFullWindowBlurBackdrop
   now tints toward the theme app background (WithAlpha(Background(), ...)) for
   both the opaque base and the frost, so a light theme gets a light frosted
   backdrop and a dark theme a dark one. Fixes all blur overlays, not just the
   lock screen.
2. The lock screen now blurs the WHOLE window (sidebar included), not just the
   content area: renderLockScreen opens a full-viewport borderless overlay window
   (##LockOverlay, same pattern as the modal overlays / portfolio, opened from
   within ##ContentArea) with an input blocker, instead of drawing in the content
   child. Removed the theme-independent dark scrim (contradicted point 1).

Auth logic untouched. Needs GUI re-verify of all auth paths + that the whole
screen (incl. sidebar) obscures on lock and unlock returns cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:51:57 -05:00
DanSandClaude Opus 4.8 42df658275 fix(security): dim the lock-screen backdrop so the card pops in all themes
GUI review (light theme) showed the blurred backdrop is light there, washing out
the (now glass) card. Add a theme-independent dark scrim over the blur so the lock
screen reads as a clearly-dimmed "locked" state and the card stands out in both
light and dark themes — standard lock-screen dimming. Alpha tunable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:36:21 -05:00
DanSandClaude Opus 4.8 9d2c4a8ad3 feat(security): match the lock-screen card to the dialog cards
GUI review showed the lock card was a flat light SurfaceVariant panel that stood
out against the dark glass cards of the other modals. Draw it with the same
GlassPanelSpec (rounding 16, fill 35, border 50) that BeginOverlayDialog uses, so
it renders as the same opaque-dark glass card on the blur backdrop. Visual only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:49:42 -05:00
DanSandClaude Opus 4.8 65c06d0e6b chore(theme): drop orphaned lock-screen backdrop-alpha key
The lock screen no longer reads screens.lock-screen.backdrop-alpha (replaced by
the live-blur backdrop in c7f89e5). Remove the now-dead schema key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:20:01 -05:00
DanSandClaude Opus 4.8 c7f89e5cd3 feat(security): live-blur backdrop for the lock screen (Phase 4)
The security DIALOGS (encrypt/decrypt/change-passphrase/remove-encryption, PIN
set/change/remove) already adopted the blur backdrop via the Phase 2 flip (they
use BeginOverlayDialog). This finishes Phase 4 by giving the bespoke lock screen
the same live-blur backdrop over the wallet content — restyle only, auth logic
(PIN/passphrase input, attempt counting, lockout timer) untouched.

The lock screen previously defaulted to backdrop-alpha=0 (wallet fully visible
behind the card); the blur now obscures it — a privacy improvement as well as a
consistency one. Capture-once on lock-open + resize (frame-gap detected via the
shared BlurCaptureStateFor), MarkBlurOverlayDrawn drives the theme-effect
suppression + acrylic re-capture on unlock. The opaque base in
DrawFullWindowBlurBackdrop guarantees the wallet is obscured even if blur is
unavailable (low-spec / acrylic-off / capture failure) — no content leak. UV
mapping is screen-space, so the content-area sub-rect blurs the correct region.
Needs GUI verification of every auth path (PIN/passphrase unlock, lockout).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:14:27 -05:00
DanSandClaude Opus 4.8 34ded6b441 feat(ui): shutdown-screen consistency polish (Phase 3)
The two Phase 3 "bespoke overlays" aren't content modals: the send-error is an
in-tab glass banner (already consistent — left as-is), and the shutdown screen is
a teardown screen where a blur backdrop would be semantically wrong (should read
as "closing") + risky on the teardown path. Per that finding, apply minor
consistency polish to the shutdown screen only:
- Share the overlay-scrim tone via a new material::OverlayScrimColor(opacity),
  used by both the dialog scrim and the shutdown scrim (was a slightly different
  hardcoded 0.06/0.06/0.08 → now the 0.04/0.04/0.06 dialog base).
- "Shutting Down" title → theme Warning() amber (was hardcoded gold).
- Force-Quit confirm destructive button → theme Error() (alpha-varied) instead of
  hardcoded reds.
No behavior change; the shutdown scrim stays opaque (no blur).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:07:36 -05:00
DanSandClaude Opus 4.8 0ea423dcea feat(ui): float the remaining large dialogs (Phase 2 float rollout)
Per the user's selection, convert the large multi-section dialogs to the BlurFloat
style (floating content on the blur, plain heading, no glass card), each KEEPING
its authored width so width-coupled content is untouched:
- Export all keys, Console RPC reference, Explorer block detail
- Updater dialogs: daemon update, xmrig update, bootstrap download (multi-state)

Settings and Address book intentionally kept as cards (the modal Settings *window*
is dead code — show_settings_ is never set; the live Settings surface is the nav
page, not a modal; Address book has a nested edit that reads better contained).
All other confirm/input dialogs stay card-on-blur ("card for small"). Updater
dialogs are multi-state — GUI-check each state (confirm/progress/done/failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:20:13 -05:00
DanSandClaude Opus 4.8 61738ef24a feat(ui): float Transaction Details + Block Info dialogs (Phase 2 large-dialog example)
Convert two clean detail-view dialogs to the BlurFloat style (floating content on
the blur, plain h6 heading, no glass card) — the "float for large" half of the
plan, applied conservatively: keep each dialog's authored card width so its
width-coupled content (absolute SameLine positions, GetWindowWidth math) is
unchanged, and rely on the footer Close + outside-click for dismissal (the plain
heading drops the title-bar close X, matching the portfolio modal). Auto-height
preserved. A representative pair to GUI-review before rolling the treatment out to
the other large dialogs (or reverting if the card-on-blur reads better).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:34:13 -05:00
DanSandClaude Opus 4.8 ccbb0783b5 chore(ui): remove dead dialog duplicates (Phase 2 cleanup)
Grep-verified dead, zero live callers (the live paths all go through
BeginOverlayDialog and already got the blur backdrop):
- App::renderAboutDialog (app.cpp/app.h) — dispatch uses ui::RenderAboutDialog.
- src/ui/windows/import_key_dialog.{h,cpp} (ImportKeyDialog class) — live path is
  App::renderImportKeyDialog.
- src/ui/windows/backup_wallet_dialog.{h,cpp} (BackupWalletDialog class) — live
  path is App::renderBackupDialog.
Drop the two dead files from CMakeLists. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:29:38 -05:00
DanSandClaude Opus 4.8 c6b32e86c0 feat(ui): all overlay dialogs adopt the blur backdrop (Phase 2 core)
Flip the positional BeginOverlayDialog overload's default to blurBackdrop=true, so
every app dialog (~43 call sites) now renders its card on the live-blur backdrop
instead of the opaque scrim — matching the Manage-Portfolio look. Cards stay
auto-height (small dialogs stay small = "card for small") and render opaque via
the existing sole-consumer fallback (a translucent glass card would thrash the
single-slot blur cache against the radius-64 backdrop). Nested dialogs (address
book edit) already pass a distinct idSuffix, so their blur windows/capture keys
don't collide. A dialog that wants the old scrim can pass a spec with
blurBackdrop=false. Big visual change — needs a GUI sweep; trivially reverted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:55:40 -05:00
DanSandClaude Opus 4.8 5caffc84b0 fix(material): make blur-overlay effect guard truly close-equivalent
The adversarial review found the ModalRenderGuard lagged the old
PortfolioEditorActive() signal by one extra frame on CLOSE (a possible 1-frame
"opaque panels" flash). Latch the overlay's open flag (already available via
spec.p_open) — "drew this frame AND still open after Esc/Close/outside handling" —
instead of just "drew last frame", so the guard drops and the acrylic re-capture
fire on the same frame the overlay closes, matching the old inline behavior on
both edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:46:11 -05:00
DanSandClaude Opus 4.8 ebf01bd858 feat(market): migrate portfolio modal onto the shared blur-overlay framework (Phase 1)
Dogfood the Phase 0 framework: RenderPortfolioEditor now opens via
material::BeginOverlayDialog(BlurFloat) + EndOverlayDialog instead of its own
inline overlay scaffold, and uses the promoted material:: SegmentedControl /
RightAlignX / BeginFadeScrollChild helpers (overlay_scroll.h). Delete the
portfolio's inline backdrop/capture-once state machine + its PfEditState capture
statics + the four local helper copies. App::render's ModalRenderGuard is now
driven by the modal-agnostic AnyBlurOverlayActiveLastFrame() (timing-equivalent),
and LatchBlurOverlayActive() at frame end handles the acrylic re-capture on close.

Framework tweak: floating cards pop the card WindowPadding after BeginChild (so
nested children don't inherit it) and center button labels, keeping the net
style-var count at 2 so EndOverlayDialog is unchanged and GlassCard stays
byte-identical. RenderPortfolioEditor: 357 -> 282 lines. Needs GUI verification
that the portfolio modal is visually/behaviourally unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:33:15 -05:00
DanSandClaude Opus 4.8 d91be3b34e feat(material): spec-driven BeginOverlayDialog (Phase 0b)
Refactor BeginOverlayDialog to run through an OverlayDialogSpec + OverlayStyle
(GlassCard vs BlurFloat) with orthogonal flags (blurBackdrop / floatingContent /
plainHeading, fixed vs auto card height). The classic look is now GlassCard with
all flags off; the existing positional overload forwards to it → all ~43 current
dialogs are byte-identical (verified line-by-line). The BlurFloat path — live-blur
backdrop with per-overlay capture-once (frame-gap arming), floating content, plain
h6 heading — is implemented but unused until Phase 1 dogfoods it on the portfolio.
Also replace the unusable open-refcount with a per-frame drawn flag + last-frame
latch that invalidates the acrylic capture on the overlay's close transition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:13:47 -05:00
DanSandClaude Opus 4.8 4b46d4a20c feat(material): promote blur-overlay modal primitives (Phase 0a)
First step toward unifying all app modals on the Manage-Portfolio visual style.
Add, as shared material:: helpers (additive — no caller yet, no visible change):
- blur-overlay open refcount + 1-frame latch (Push/Pop/AnyBlurOverlayActive[LastFrame],
  LatchBlurOverlayActive) so App::render's effect guard can be modal-agnostic;
- capture-frame query (IsCapturingBlurBackdrop) for scroll-fade suppression;
- RightAlignX and SegmentedControl (promoted verbatim from market_tab);
- BeginFadeScrollChild/EndFadeScrollChild in a dedicated overlay_scroll.h (kept
  out of the widely-included draw_helpers.h because effects::ScrollFadeShader's GL
  headers clash with SDL's in some TUs).
Also fix IsCurrentWindowOverlayDialog to prefix-match "##OverlayScrim" so suffixed
overlay windows are recognized by the modal-aware hover check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:05:03 -05:00
DanSandClaude Opus 4.8 69cd8ba048 i18n(market): route chart interval labels + "Updated" through TR()
Add market_iv_1h/1d/1w/1m and market_updated ("· Updated %s") keys and use them
in the chart interval strip (previously hardcoded "1H"/"1D"/"1W"/"1M") and the
pair-selector attribution line (previously a literal "· Updated %s"). Closes the
audit's i18n gaps in the market tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:19:30 -05:00
DanSandClaude Opus 4.8 4543cdc495 refactor(market): extract portfolio section into mktDrawPortfolio
Move the balance summary (fiat/DRGX/BTC + shielded ratio bar) and the
draggable/resizable custom-group card grid (drag/resize gestures, dot grid,
click-to-edit) out of RenderMarketTab into mktDrawPortfolio(MktCtx); the local
x-coord `cx` renamed `cx0` (ctx param is `cx`). Drop the now-unused
gap/dl/glassSpec/buf locals. RenderMarketTab is now ~150 lines (from 962) — a
thin orchestrator: precompute -> build MktCtx -> hero/chart/pair/portfolio.
Verbatim bodies. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 01:02:11 -05:00
DanSandClaude Opus 4.8 71fb81670f refactor(market): extract price chart into mktDrawPriceChart
Move the ~250-line chart block (interval strip + 24h stats + refresh, plotted
curve with grid/area-fill/hi-lo+time labels/hover crosshair+tooltip, empty
state) out of RenderMarketTab into mktDrawPriceChart(MktCtx), recomputing cheap
locals and reading the precomputed series via the ctx. Drop the now-unused
hs/pad locals. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:57:59 -05:00
DanSandClaude Opus 4.8 a092097141 refactor(market): extract MktCtx + price hero into mktDrawPriceHero
Introduce a MktCtx struct holding RenderMarketTab's precomputed geometry/series,
built once after the precompute, and extract the combined hero card (glass panel
spanning header + chart, price/ticker/period-badge/trade button) into
mktDrawPriceHero(MktCtx). Cheap locals (dl/fonts/dp/market) recomputed in the
helper; the hero's local x-coord `cx` renamed `cx0` (the ctx param is `cx`).
Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:53:27 -05:00
DanSandClaude Opus 4.8 50873d1112 refactor(market): extract pair selector into mktDrawPairSelector
Move the exchange/pair chip grid (wrapping flat chips + click-to-switch +
attribution line) out of RenderMarketTab into mktDrawPairSelector(app, registry,
availWidth), recomputing its own schema/fonts/market. Drop the now-unused ovFont
local. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:43:43 -05:00
DanSandClaude Opus 4.8 7e8628f94c refactor(market): extract drawCard lambda to mktDrawCard function
Lift the ~90-line [&]-capturing drawCard closure out of RenderMarketTab's grid
code into a file-static mktDrawCard(MktCardCtx, min, max, entry, hov). The
captured locals (dl/state/market/mktDp/pfInset/pfCardPad/sub1/capFont) are
bundled into a MktCardCtx bound once per frame. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:40:56 -05:00
DanSandClaude Opus 4.8 0ae52ee095 refactor(market): extract pure portfolio grid-layout into pfComputeGridLayout
Pull the ~30-line row-major cell placement (honor stored placement else
auto-place) out of RenderMarketTab into a pure, deterministic helper taking
(entries, cols, minW, minH). Also bump the grid resize-readout buffer 16->32 to
silence a worst-case -Wformat-truncation the extraction surfaced. No behavior
change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:38:08 -05:00
DanSandClaude Opus 4.8 529a74f6a7 refactor(market): extract portfolio detail sections into helpers
Pull the three detail-pane section blocks out of RenderPortfolioEditor into
pfDrawAppearanceSection / pfDrawPriceSection / pfDrawAddressSection (each
recomputes its own dp/fonts/state; the section dispatch is a 3-line switch).
Verbatim move — same widgets, IDs and drawing. RenderPortfolioEditor drops from
~760 to ~357 lines. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:20:25 -05:00
DanSandClaude Opus 4.8 88f1415bcc refactor(market): lift portfolio-editor lambdas to file-static functions
Convert the four [&]-capturing lambdas (persist / buildWorking / workingMatches /
commitIfNeeded) into file-static pfPersist / pfBuildWorking / pfWorkingMatches /
pfCommitIfNeeded that operate on the s_pfEdit struct + take Settings*. Removes
the per-frame closure setup and lets the upcoming section helpers reuse them.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:13:42 -05:00
DanSandClaude Opus 4.8 b8cfc1c79c refactor(market): group ~35 file-scope statics into three structs
Collapse the loose market_tab.cpp statics into MarketViewState (s_mkt),
PfEditState (s_pfEdit) and PfGridDrag (s_grid). Pure mechanical rename
(compile-verified, no behavior change) that shrinks the global surface and gives
the upcoming function decompositions a single state handle per concern instead
of ~35 free names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:10:59 -05:00
DanSandClaude Opus 4.8 a0fd9aff82 refactor(market): extract pure price-series math to data/market_series.h + tests
Move the no-I/O, no-ImGui series helpers (resampleHistory, bucketBySeconds,
sparklineSeries, chartSeries) out of market_tab.cpp into an inline header under
data/ (mirroring data/portfolio.h) and cover them with a testMarketSeries()
group in test_phase4.cpp (resample block averaging + partial blocks, window
bucketing + guards, sparkline fallback, chart time-window filtering + live
timestamp synthesis). market_tab now calls data::sparklineSeries/chartSeries.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:03:19 -05:00
DanSandClaude Opus 4.8 14b08d1358 refactor(market): extract pfSegmentedControl helper
Collapse the two identical equal-width segmented controls (the detail section
switcher Appearance/Price/Addresses and the address type filter
All/Shielded/Transparent) into one pfSegmentedControl(dl, origin, w, h, labels,
count, selected, font, idBase, dp) that returns the clicked index. Same track /
active-pill / centered-label drawing and per-cell hit-test as before; the
variable-width chart-interval strip is left as-is. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:56:51 -05:00
DanSandClaude Opus 4.8 d689b02de9 refactor(market): extract pfProjectSeries + pfRightAlignX helpers
Dedup the identical min/max→rect projection shared by pfDrawSparkline and
pfDrawSparklineFilled into pfProjectSeries, and the repeated right-align cursor
math (footer Close, detail Revert/Save, summary Manage button) into
pfRightAlignX. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:54:10 -05:00
DanSandClaude Opus 4.8 84498b1312 refactor(market): drop dead statics, fix BTC preview/card precision mismatch
Audit cleanup of market_tab.cpp:
- Remove the write-only statics s_history_initialized and s_last_refresh_time
  (assigned in several places, never read) and their now-dead assignments.
- Align the portfolio grid card's BTC value to %.8f to match the editor preview
  (pfBuildDisplay) and the summary rows; it was the lone %.6f, so the same
  balance rendered with different precision in the preview vs the card.
- Fix the stale s_pf_sel comment (no "-2 = unsaved draft" state exists).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:49:09 -05:00
DanSandClaude Opus 4.8 8c2b0521a6 feat(market): combine portfolio address filter + selection onto one row
Merge the two address-section rows into one: the All/Shielded/Transparent
segmented type filter sits on the left and Select all / Clear / Funded are
right-aligned on the same line, both vertically centered. Select-all is deferred
to a flag applied after the filtered set is built, so it still targets the
current filter (and stays idempotent via PortfolioEntryAdd's dedup).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:37:10 -05:00
DanSandClaude Opus 4.8 4411cb45e2 feat(market): Esc / outside-click discard portfolio edits; only Close saves
Complete the explicit-save model: leaving the Manage-portfolio modal via Esc or
an outside-click now discards the current group's uncommitted edits (matching
the discard-on-switch behavior), while the Close button still commits. Only the
Save and Close buttons persist; every other exit path is a cancel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:27:16 -05:00
DanSandClaude Opus 4.8 7db6564a0e feat(market): discard uncommitted group edits when switching away
Switching to another group in the Manage-portfolio list (or adding/deleting a
group) now discards the current group's uncommitted working edits instead of
auto-saving them — only the Save button (and closing the modal) persists. This
matches the explicit-save model signalled by the Revert/Save buttons and the
unsaved-changes dot: navigating between drafts is ephemeral until saved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:24:52 -05:00
DanSandClaude Opus 4.8 faf8e4e430 feat(market): rework portfolio-modal actions, addresses row, price & unsaved state
- Move the group-scoped Revert/Save buttons into the detail container (sized to
  the Add-entry height) so they read as lower-hierarchy than the modal-level
  Close, which now stands alone in the footer. The detail body is wrapped in a
  child that reserves the bottom row.
- Add an amber "unsaved changes" dot on the selected group row while its working
  copy differs from the stored entry; also gate the detail Revert/Save on that
  dirty state.
- Addresses: narrow the All/Shielded/Transparent segmented control (360px) and
  right-align Select all / Clear / Funded on one line; give the Select all /
  Clear pills more text padding.
- Appearance: shrink the icon search field and right-align it on the heading
  line, with a "Search icons…" hint.
- Price: move the sparkline toggle onto the 24h line; the interval radios stay
  below, enabled only when the sparkline is shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:14:40 -05:00
DanSandClaude Opus 4.8 1affea0273 feat(market): icon-picker search + Settings-style scroll for portfolio modal
Rework the Manage-portfolio detail pane's Appearance and Addresses sections:

- Icon picker: search field inline with the "Icon" heading (filters by name,
  hides the "None" slot while searching); grid now always fits 12 icons per
  row, scaling cell size to width and centering the block.
- Both the icon grid and the address list scroll via a shared helper
  (pfBeginScrollChild/pfEndScrollChild): smooth scrolling + the Settings-tab
  edge-fade shader + a thicker, rounded scrollbar inset from the container edge
  (bordered outer frame wrapping an inner scrolling child). Smaller container
  radius. Fade gates off in low-spec and while the acrylic backdrop is being
  captured — latched per-frame so the last capture frame no longer clashes with
  the render-state reset.
- Addresses: All/Shielded/Transparent segmented control moved to its own row,
  capped at 580px; Select all / Clear pills moved left of the Funded checkbox
  on one left-aligned row (Funded vertically centered against the pills).
- Custom colour "+" swatch inset a few px so its selection outline no longer
  clips at the modal's right edge.
- Remove the divider above the footer Revert/Save/Close buttons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:50:26 -05:00
DanSandClaude Opus 4.8 32d8d5663a feat(market): portfolio detail polish — bigger icons, inline filter+funded, pill buttons, single-row colors
- Appearance: icon-picker cells doubled (56px, iconXL glyphs); color swatches back to a
  single row that hides overflow but always keeps the custom "+" cell visible at the right.
- Price: basis radios are now horizontal, matching the sparkline-interval radios.
- Addresses: All/Shielded/Transparent segmented control shrinks to sit on one line with
  the Funded toggle to its right; Select all / Clear are rounded (pill) buttons with
  padding, right-aligned on their own row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:15:50 -05:00
DanSandClaude Opus 4.8 3568b12dbf feat(market): portfolio detail — radio price options, segmented address filter, +17 colors
- Price: the basis and sparkline-interval dropdowns become radio groups (all options
  visible), now that the segmented layout gives the section full height.
- Addresses: the All/Shielded/Transparent chips become a rounded segmented control
  matching the top switcher. "Select shown" renamed to "Select all"; Select all + Clear
  moved onto the Funded row, right-aligned. Address list gets inner WindowPadding.
- Appearance: 17 more preset colors (25 total), and the swatches now wrap into a grid
  (with the custom-color cell) instead of a single overflowing row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:42:08 -05:00
DanSandClaude Opus 4.8 e74989ec2c feat(market): replace portfolio detail accordion with a segmented control
Swap the collapsible Appearance/Price/Addresses dropdown sections for a single
segmented pill control (one section shown at a time) — no dropdown headers. The
active section now owns the full detail height, so the icon grid and address list
fill the available space instead of fixed heights. State: s_pf_section (0/1/2)
replaces the three accordion open-flags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:24:17 -05:00
DanSandClaude Opus 4.8 fe18628216 feat(market): bump the portfolio backdrop blur radius (64) via sole-consumer isolation
Raising the backdrop radius alone would thrash the shared radius-keyed blur cache (the
glass panels blur at ~30). Instead, while a full-window blur overlay is active, glass
panels use their opaque fallback (IsFullWindowBlurOverlayActive) — they're covered by the
backdrop anyway — so the backdrop is the SOLE applyBlur caller and can use a strong custom
radius (64) with the cache staying coherent and frozen. The flag is toggled alongside the
theme-effect suppression by a RAII guard in App::render().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:15:12 -05:00
DanSandClaude Opus 4.8 4dc6b2ef8f perf(market): capture the portfolio backdrop once on open (+resize), not every frame
Switch the live-blur behind the modal from a per-frame capture (which flickered from
per-frame re-blur non-determinism) to capture-once-on-open, re-captured only on resize.

- Insert the acrylic live-capture callback into the overlay draw list ONLY for a few
  frames on open and whenever the viewport size changes; other frames reuse the frozen
  blurred snapshot (blurCacheValid_), so there's no per-frame capture/re-blur cost. State:
  s_pf_was_open (reset on close), s_pf_capture_frames, s_pf_capture_w/h.

Two fixes from an adversarial review of the timing/pipeline interaction:
- Do NOT override the backdrop blurRadius to 40. applyBlur caches one blurred buffer
  keyed on radius, shared with every glass panel (market cards + the modal preview card,
  radius ~30). A different radius thrashed that cache every frame and made the backdrop
  sample the wrong blur, defeating the frozen-blur guarantee. Using the theme card's
  radius keeps one blur serving all surfaces, frozen.
- Skip the blur on the overlay's first frame (allowBlur=false) — the live content isn't
  captured until that frame's render, so blurring then would show stale/background pixels
  for one frame; draw the plain opaque scrim instead.

Compiles on OpenGL (Linux) + DX11 (Windows). Needs on-device visual verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:05:16 -05:00
DanSandClaude Opus 4.8 5afb339ba7 fix(market): portfolio modal — hover assert, cut-off footer, plain heading, FX bleed, bigger delete icon
- Fix the ImGui error on hovering a group row: the per-row delete was an
  overlapping InvisibleButton + SetCursorScreenPos over the row Selectable. Replace
  it with a manual IsMouseHoveringRect / IsMouseClicked hit-test (no overlapping
  ImGui item, no cursor manipulation).
- Delete icon is larger (iconMed) and inset from the row edge (spacingMd).
- Footer no longer clipped: reserve 56px for the 40px buttons + separator.
- Manage-portfolio heading is now plain text (h6) with no title-bar background or
  divider — content floats on the backdrop.
- Sidebar theme-effect borders no longer bleed through the overlay: suppress panel
  theme-effects frame-wide (RAII guard in App::render via ui::PortfolioEditorActive)
  while the modal is open, instead of only during the market render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:47:33 -05:00
DanSandClaude Opus 4.8 0e8816e220 feat(effects): overlay-time live-framebuffer blur behind the portfolio modal
The acrylic system only blurred a frozen snapshot of the static app background. Add
an overlay-time re-capture so the Manage-portfolio modal blurs the LIVE tab content
behind it.

- AcrylicMaterial::captureLiveFramebuffer() (GL blit of FB 0 / DX11 CopyResource of the
  backbuffer + no-backend stub) mirrors captureBackgroundDirect but bypasses the
  dirtyFrames_ gate so it refreshes every frame the overlay is open, forcing a re-blur.
- ImGuiAcrylic::GetLiveCaptureCallback() returns a draw callback for it.
- market_tab: the modal inserts that callback (+ ImDrawCallback_ResetRenderState) at the
  START of its overlay draw list, before the backdrop, so the capture holds the app UI
  drawn below and the backdrop blurs it. On close, InvalidateCapture() re-captures the
  background so other glass panels don't keep blurring the stale live capture.
- Panel theme-effects are suppressed during the market render while the modal is open
  (their foreground-draw-list borders draw above all windows and would otherwise bleed
  over the overlay). Market tab body is rendered again (no longer skipped).
- Backdrop draws the acrylic at full strength (fallbackColor.w=1) over an opaque base so
  the blurred content reads clearly; lighter dim.

Design mapped via an Explore investigation of the capture/blur pipeline; compiles on
both OpenGL (Linux) and DX11 (Windows cross-build). Needs on-device visual verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:26:11 -05:00
DanSandClaude Opus 4.8 2035f71c65 fix(market): stop portfolio group rows overlapping; center modal button text
- The per-row delete icon's SetCursorScreenPos left the ImGui cursor at the icon,
  so the next group row started too high and rows overlapped. Capture the row's
  end cursor after the Selectable and restore it after drawing the delete icon.
- Force centered button-label alignment for the modal (ButtonTextAlign 0.5,0.5)
  so the footer/Add buttons read consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:12:24 -05:00
DanSandClaude Opus 4.8 d007f3de39 feat(market): portfolio modal — per-row delete, footer Close, instant Add, no bleed-through
- Delete moved from the footer to a per-row trash icon on each group (shown on
  hover/selected), with deferred deletion + selection fix-up.
- Removed the title-bar X; the footer is now right-aligned Revert / Save / Close
  (larger buttons), Close commits + dismisses. Esc and outside-click still close.
- Add entry now immediately creates a persisted "Untitled" group and selects it
  (no more invisible draft state); the -2 draft path is gone.
- Backdrop bleed-through fixed: when the modal is open the Market tab body is not
  rendered, so its content and foreground theme-effect borders no longer show over
  the overlay — the modal floats on the app's blurred/opaque backdrop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:02:25 -05:00
DanSandClaude Opus 4.8 1fdb6656aa feat(market): fix portfolio Add button clipping; add accordion summaries
- Add button can no longer be cut off: it's now a normal widget below the group
  list (in a group with the scroll child) instead of nested inside the list child.
- Stop the modal's 28x20 WindowPadding from cascading into the nested master/detail/
  list children (pop it right after the card child begins) — that double-padding
  was squeezing the master column and the Add button.
- Right pane navigability: collapsed accordion sections now show a compact summary
  on the right (Appearance -> icon, Price -> basis, Addresses -> N selected), so a
  group's state is readable without expanding. Section titles are consistent
  title-case ("Appearance"/"Price"/"Addresses").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:13:35 -05:00
DanSandClaude Opus 4.8 3305be6318 feat(market): drop the portfolio modal card panel — float content on the backdrop
Remove the centered glass card behind the Manage-portfolio content so it floats
directly on the blurred/opaque full-window backdrop. The little live-preview card
keeps its panel (it previews the actual group card).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:57:28 -05:00
DanSandClaude Opus 4.8 19482214f1 fix(market): draw the portfolio overlay above the tab content
Drop ImGuiWindowFlags_NoBringToFrontOnFocus on the overlay window — it stopped
the per-frame SetNextWindowFocus() from raising the modal, so it rendered behind
the market chart/cards. Now matches the proven BeginOverlayDialog behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:52:06 -05:00
DanSandClaude Opus 4.8 4aed72c73b feat(market): rework Manage-Portfolio as a full-window master-detail modal
Combine the old list/edit modes into one master-detail view rendered as a
full-window overlay instead of a floating dialog.

- Backdrop: new material::DrawFullWindowBlurBackdrop draws an opaque base first
  (so the sheet is opaque regardless of the UI-opacity slider — the acrylic path
  multiplies its alpha by GetUIOpacity), then a strong full-window blur of the
  backdrop, gated exactly like DrawGlassPanel so low-performance mode skips the
  blur entirely (just the opaque scrim).
- Frame: bespoke full-window overlay (not BeginOverlayDialog) that mirrors the
  scrim plumbing and preserves both overlay popup gotchas verbatim (no focus-steal
  and no outside-click dismiss while a combo / color picker is open). Title-bar X,
  Esc, and outside-click all dismiss.
- Left (master): selectable group list with accent bar + icon + name + value line,
  and a pinned "Add group" button.
- Right (detail): live preview + label header, then a single-open accordion
  (Appearance = color + outline opacity + icon; Price; Addresses) so only one
  control group shows at a time — far less cluttered than the old two-column form.
- Footer: Delete / Revert / Save. Editing auto-commits named groups on navigation
  and on close (preserving grid geometry); unnamed drafts are dropped.
- s_pf_editing tri-state replaced by s_pf_sel (+ accordion open-state); the Manage
  button and grid-card click open with a valid selection.

Design + adversarial review done via multi-agent workflows (understand/design, then
a 3-lens ImGui-lifecycle / state-safety / backdrop review; zero confirmed defects).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:46:46 -05:00
DanSandClaude Opus 4.8 3e6987d72f fix(market): remove the chart divider between interval buttons and the plot
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:01:44 -05:00
DanSandClaude Opus 4.8 42c511b13a fix(market): tighten blank space above interval buttons; pad below the divider
- Drop the extra gap the hero header reserved below the price row, so the interval
  buttons sit closer to the top of the chart area.
- Place the divider just below the buttons with a comfortable gap (spacingMd)
  between it and the plot, instead of centering it in the strip gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:59:16 -05:00
DanSandClaude Opus 4.8 ef9141c248 feat(market): default chart to 1M, divider below interval buttons, drop chart dots
- Default the price chart to the 1M range on open (was 1D).
- Move the hero divider from below the price row to below the interval buttons
  (centered in the gap between the buttons and the plot).
- Remove the dot markers on the chart: no per-point dots, no current-price dot,
  and no high/low dot anchors — just the clean line + area fill (the high/low
  price labels and the hover crosshair remain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:25:26 -05:00
DanSandClaude Opus 4.8 8c88afe16e feat(market): card/chart refinements — 8-cell min width, larger labels, flat fill, range-matched change
Portfolio group cards:
- Always show the secondary DRGX amount, including on 2-cell cards.
- Larger group icon + label (subtitle1); value stays subtitle1 for consistency.
- Minimum group width is now 8 cells (default width 8).

Price chart:
- Area fill is now a flat translucent fill matching the group-card sparklines
  (dropped the vertical gradient + its primitive-API helper).
- Interval buttons sit near the top of the chart area (removed the empty gap
  above them) with a comfortable gap below before the plot.
- The change badge's suffix now matches the SELECTED range (1H->1h, 1D->24h,
  1W->7d, 1M->30d) instead of the raw data span (which read 23h / 6d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:09:40 -05:00
DanSandClaude Opus 4.8 d48c6db21c feat(market): range-based chart intervals; redesign group cards (value top-right + body sparkline)
- Chart interval buttons are now time RANGES, not bucket sizes: 1H = last hour,
  1D = last 24h (5-min intraday), 1W = last 7 days, 1M = last 30 days (daily).
  pfChartSeries filters the timestamped series to the window; X-axis "ago" labels
  and the period-change span now derive from the real timestamps.
- Portfolio group cards redesigned: name (+ secondary DRGX) on the left; the value
  with its % change stacked in the top-right corner; the sparkline fills the card
  body. The sparkline now renders on 2-cell cards too (down to ~8px of body), with
  tighter padding on short cards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:52:09 -05:00
DanSandClaude Opus 4.8 ced1773e70 feat(market): chart gradient fill, period change %, timestamped hover, high/low markers
Builds on the timestamped historical series (pfChartSeries returns [time,price]):

- Gradient area fill: replace the flat translucent fill with a true vertical
  gradient (solid at the line, fading to transparent) via the draw-list primitive
  API (pfFillGradientArea).
- Period-aware change: the price badge now reports the change over the DISPLAYED
  chart period with a span label derived from real timestamps (e.g. "+42% 1.0y"),
  falling back to 24h for Live. Line/fill/marker colors follow that direction.
- Timestamped hover: the crosshair tooltip shows the real date/time at the hovered
  point (time for Live/1H, date for day+ intervals) alongside the price.
- High/low markers: hollow dot + price label at the range's extremes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:53:41 -05:00
DanSandClaude Opus 4.8 d76c109063 feat(market): 2-cell min group height; declutter + polish the price chart
- Portfolio group minimum height is now 2 cells (was 3). Added a 2-cell card tier
  that also drops the sparkline (not just the footer) so a ~58px card keeps the
  header + hero value legible.
- Price chart: stop drawing a dot on every point — for a dense historical series
  (>40 points) just the clean line + fill, and always mark the latest point
  (current price) with a dot + ring.
- Y-axis labels use the shared adaptive price formatter (e.g. $0.0143) instead of
  a fixed 6 decimals, so they're cleaner and narrower.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:45:10 -05:00
DanSandClaude Opus 4.8 5aa3cc4d51 feat(market): historical chart intervals, stats overlay, configurable outline, easier resize
Chart:
- The price chart now plots the historical CoinGecko series with a Live/1H/1D/1W/1M
  interval selector (top-left of the chart). Live = in-session buffer; 1H uses the
  intraday 5-min series, 1D/1W/1M the daily series (via pfSparklineSeries). X-axis
  labels are interval-aware ("~5h"/"~3d"/"~2w"/"~4mo"). Market-tab open kicks the
  self-throttled market_chart fetch so history populates promptly.
- 24H volume + market cap moved from their own row onto the chart's top strip
  (right side, next to the refresh button), reclaiming the empty band above the
  chart; the hero header now holds just the price row + separator.
- More bottom padding under the plot so the time labels and the DRGX/USDT pair
  buttons aren't crowded against the card edges.

Portfolio groups:
- Accent-outline opacity is now configurable per group (0-100%, default 25%) via a
  slider in the editor's Appearance column; applied to the card + preview outline.
- Resize grip enlarged (14->20px) and a live "W x H" size readout is drawn while
  resizing, so shrinking a group to the 4x3 minimum is reliable and visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:37:31 -05:00
DanSandClaude Opus 4.8 4e15a2b1ba feat(market): 3-cell min groups, full accent outline, tighter chart, no scrollbar
- Portfolio groups now default to the 3-cell minimum height (was 4). The card
  design adapts: below ~112px it drops the redundant DRGX footer and uses the
  compact hero font so a 3-cell card stays legible.
- Group accent color is now a colored outline around the whole card (and the
  editor's live preview) instead of a stripe on the left edge; the text no longer
  reserves the old left-stripe offset.
- Price chart: trim the empty band between the 24H VOLUME stats and the chart
  (smaller hero-header bottom pad), and keep the Y-axis price labels inside the
  card on narrow windows (clamp to the card's left edge + bump min y-axis padding
  40->54) so they no longer spill onto the tab background.
- Market tab no longer draws a scrollbar (NoScrollbar); the portfolio grid still
  scrolls via the mouse wheel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:15:17 -05:00
DanSandClaude Opus 4.8 021ba88522 fix(market): extend portfolio drag dot-grid to the bottom of the viewport
While rearranging, the dot grid only reached two rows below the lowest card, so
the empty space beneath the cards showed no dots even though a card can be
dropped there. Fill the grid down to the bottom of the visible scroll viewport
(dl clip rect), keeping the two-row buffer as a minimum. The drop-target row is
already unclamped, so dropping into that lower area places the card and grows the
scrollable content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:00:07 -05:00
DanSandClaude Opus 4.8 c1d5f79502 feat(market): real historical sparklines via CoinGecko market_chart
Portfolio-group sparklines previously resampled the ~24-minute in-session price
buffer, so the hour/day/week/month intervals could never fill. Fetch real
historical USD series from CoinGecko /market_chart and back each interval with
appropriately-grained data.

- MarketInfo gains two timestamped series: price_chart_intraday (days=1, 5-min)
  and price_chart_daily (days=365, daily), plus a fetch timestamp.
- App::refreshMarketChart() fetches both on the RPC worker via the TLS-verifying
  util::httpGetString helper, self-throttled to ~30 min (historical data moves
  slowly). Gated by getFetchPrices(); triggered on connect and each price tick.
- Pure NetworkRefreshService::parseCoinGeckoMarketChart() parses {"prices":
  [[ms,price],...]} into (unix-seconds, price); malformed rows skipped. Unit-tested.
- market_tab pfSparklineSeries() maps interval -> series+bucket: minute = live
  buffer; hour = intraday bucketed to 1h; day/week/month = daily bucketed to
  1d/7d/30d. Falls back to the in-session buffer until the fetch populates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:50:17 -05:00
DanSandClaude Opus 4.8 db212a81c6 feat(market): fine 32px square portfolio grid that scales to width and scrolls
Replace the fill-to-height portfolio dashboard with a fine gapless grid of
~32px square cells whose column count scales to fill the tab width. The grid
extends past the window and the Market tab scrolls when cards are placed lower.

- Cells are a fixed square edge (>= ~32px), gapless; cards inset by 3dp so
  neighbours don't touch. Column count = availWidth / 32dp (min pfMinW cols).
- Card minimum span is pfMinW x pfMinH (~128x96); layout/resize clamps to it.
  New groups default to 6x4 cells.
- Content height reserves two extra rows below the lowest card so a card can be
  dragged past the current bottom; the scroll child grows to match.
- Dot grid (shown while rearranging) softened and extended into the buffer rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:30:04 -05:00
DanSandClaude Opus 4.8 67f6156030 feat(market): stat-tile group cards + grid fills available width and height
Recommendation #1 + fill-to-fit:
- Group cards are now stat tiles instead of top-heavy rows: a header (icon +
  name, with a 24h badge right), the value as a large hero centered in the
  middle band, a footer DRGX amount, and a dedicated sparkline strip along the
  bottom (soft fill under the line). Uses the taller square cells properly.
- The grid now fills the available height too: cell height is computed at render
  time so the rows reach the bottom of the tab (no dead space), and the dot grid
  extends to the bottom edge while rearranging. Cell height = availableHeight /
  rows, so adding a row shrinks cells to keep filling. The hero font drops from
  h4 to subtitle1 on short cells so it always fits.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Interactive/visual —
needs a screenshot + drag/resize check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:13:16 -05:00
DanSandClaude Opus 4.8 76ed09fad0 feat(market): square grid cells (2x1 min), preserve grid on edit, sparkline interval
- Grid cells are now square (cellH = cellW); column count is responsive with a
  2-column minimum, and a group's minimum size is 2x1 cells. New groups default
  to 2x1; drag/resize/auto-place all enforce the 2-wide minimum.
- Fix: editing a group no longer resets its grid placement/size. The editor's
  Save now starts from the existing entry and only overwrites the form fields,
  so grid_col/row/w/h (and anything else off-form) are preserved.
- Add a per-group sparkline interval (Minute/Hour/Day/Week/Month) next to the
  Sparkline toggle. The price history (~1 sample/min) is resampled by averaging
  each interval's worth of minute-samples into one point.

PortfolioEntry gains sparklineInterval and defaults gridW=2. Persisted in
settings.json. Full-node + Lite build clean; ctest 1/1; hygiene clean.

Note: the app only accumulates in-session minute samples, so Hour/Day/Month
show points only once enough data has been collected; true long-range history
(instant day/month charts) would need a CoinGecko market_chart fetch, which can
be added separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 04:15:38 -05:00
DanSandClaude Opus 4.8 77219c5ce7 feat(market): portfolio groups as a draggable/resizable dashboard grid
Turn the portfolio group area into a dashboard grid:
- Responsive column count (1-4 by width); each 1x1 cell is one card.
- Drag a card to move it to another cell; drag its bottom-right corner to
  resize (span multiple cells). Both snap to the grid.
- A subtle dot grid appears while rearranging (dragging or resizing), with a
  highlighted drop-target / resize footprint.
- Click (no drag past a 4px threshold) still opens the group editor.
- Grid placement (col/row) and span (w/h) persist per group in settings.json;
  positions are resolved each frame — stored placement when it fits, else
  auto-placed row-major into free cells. Dropping onto an occupied cell swaps.

PortfolioEntry gains grid_col/grid_row/grid_w/grid_h. The card render was
refactored into a drawCard() lambda reused for the in-grid, dragged, and
resizing states.

Full-node + Lite build clean; ctest 1/1; hygiene clean. This is an interactive
feature I can't verify without the GUI — needs drag/resize testing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:54:20 -05:00
DanSandClaude Opus 4.8 b26aff392b feat(market): optional price-trend sparkline per portfolio group
Add a per-group "Sparkline" toggle (live-market bases only, like 24h). When on,
the group card draws a faint price-trend line in its lower band — the group's
value tracks the DRGX price, so it plots the market price_history (normalized),
colored green/red by overall direction. Behind the text so amounts stay
readable. The editor's live preview reflects it too.

Persisted as show_sparkline in settings.json; wired through PortfolioEntry +
the editor working state + the pfDrawSparkline() helper shared by card+preview.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change —
needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:15:23 -05:00
DanSandClaude Opus 4.8 b0ded528b1 fix(dialogs): let Combo dropdowns / popups work inside overlay dialogs
BeginOverlayDialog called ImGui::SetNextWindowFocus() every frame to keep the
dialog on top. That forced the scrim back to the front each frame and closed
any popup opened from inside the dialog — so the portfolio editor's price-basis
Combo and the custom color-picker popup flashed open then vanished, and clicking
a dropdown item that overhung the card could trip the outside-click-dismiss and
close the whole dialog.

Make the overlay popup-aware: compute overlayPopupOpen (IsPopupOpen with
AnyPopupId|AnyPopupLevel) and (1) skip SetNextWindowFocus while a popup is
showing so it stays focused/on-top, and (2) skip the outside-click-dismiss while
a popup is open so clicks inside it don't close the dialog. No change when no
popup is open, so existing dialogs are unaffected.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:02:18 -05:00
DanSandClaude Opus 4.8 301b3df77e feat(market): per-group configurable price data
Each portfolio group can now configure how it is valued and which price
fields its card shows:

- Price basis: Market · USD, Market · BTC, DRGX only, or Manual (a custom
  price-per-DRGX + currency label). Manual mode values the group independently
  of the live DRGX market price.
- Field toggles: show DRGX amount / show value / show 24h change (24h enabled
  only for the live-market bases; value disabled for DRGX-only).
- The editor's left "Appearance" column gains a Price section (basis combo,
  manual price + currency inputs, field checkboxes); the live preview and the
  group cards both render through one pfBuildDisplay() helper so they stay in
  sync. Defaults preserve the prior behavior (DRGX + USD).

Persisted in settings.json (price_basis / manual_price / manual_currency /
show_drgx / show_value / show_24h).

Note: "price source per exchange/pair" is limited to USD/BTC/Manual — the app
only fetches the aggregate DRGX price (USD + BTC); ExchangePair carries no
price, so valuing against an arbitrary pair's live price would need a new
market fetch (can be added separately).

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change —
needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:10:42 -05:00
DanSandClaude Opus 4.8 ae4e6486c6 feat(market): portfolio editor — taller icon grid + custom color picker
- Reorder the Appearance column so Color sits above the icon grid, and let the
  icon grid fill the remaining height of the left column (via
  GetContentRegionAvail().y). Fixes the large empty space that sat below the
  color row and gives the icon picker much more room.
- Add a custom-color circle after the preset swatches: it shows the current
  color when it isn't a preset (else a "+"), and clicking it opens a
  ColorPicker3 popup. The picked RGB packs straight into the group's color.

New i18n key portfolio_custom_color. Full-node + Lite build clean; ctest 1/1;
hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:53:18 -05:00
DanSandClaude Opus 4.8 d838243caa feat(market): redesign the Manage-portfolio editor as a two-column layout
Full redesign of the group edit view (list mode unchanged):

- Two columns in a wider (780px) dialog:
  * LEFT "Appearance": a live preview of the group card (icon + color + name +
    summed DRGX/USD), then Label, the icon picker, and the color picker.
  * RIGHT "Addresses": a search box, Shielded/Transparent/All type filters, a
    "Funded" toggle, Select-shown / Clear, and the address list.
- Reworked address rows: a custom checkbox, a Z/T type chip, the address's own
  label + icon (from the address book), a middle-truncated address, and the
  balance right-aligned. Selected rows sort to the top (then by balance); the
  whole row toggles selection. Replaces the old wall of raw address strings.
- Search filters by address or label; empty-state hint when nothing matches.

New i18n keys (portfolio_appearance/addresses_hdr/search/funded/select_all/
select_shown/group_name/no_addr_match). Full-node + Lite build clean; ctest 1/1;
hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:44:16 -05:00
DanSandClaude Opus 4.8 75f33e41eb feat(market): portfolio groups — PORTFOLIO heading, clickable cards, icon + color
- Rename the portfolio heading "MY DRGX" -> "PORTFOLIO".
- Group cards are now clickable: clicking one opens the editor directly on that
  group (edit mode), with a hover highlight + hand cursor.
- PortfolioEntry gains an `icon` (project_icons wallet-icon name) and a `color`
  (packed IM_COL32 accent); both are persisted in settings.json.
- Editor edit-mode gains an icon picker (the same wallet icon set as the overview
  tab's address-label dialog, via material::project_icons) with a "None" option,
  and a color picker (preset accent palette + a default/no-color swatch).
- Group cards render the chosen icon to the left of the name and, when a color
  is set, a left accent bar + the icon tinted in that color.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change — needs
a screenshot check of the Market portfolio + the editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:20:21 -05:00
DanSandClaude Opus 4.8 1263058ba7 fix(market): remove empty band between the stats row and the chart
The hero-card header height was derived from an inflated schema value
(hero-card-height * vs), reserving far more vertical space than the price row +
separator + stats row actually use. Since the chart is drawn at the bottom of
that header, it started well below the "24H VOLUME"/"Market Cap" row, leaving an
empty band. Size heroHeaderH to the actual content so the chart begins right
after the stats.

Full-node build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:49:09 -05:00
DanSandClaude Opus 4.8 9bcdf51895 feat(market): compact chart w/ time axis; floating portfolio + redesigned group cards
Follow-up to the Market tab rework, per feedback:

- Chart is more compact: a modest responsive height (~22% of the tab, floored
  at 110px) instead of stretching to fill.
- Chart now has a time axis. The price series is one sample per ~60s market
  refresh (RefreshScheduler::kPrice), so the x-axis is labelled in approximate
  minutes-ago ("~45m … ~15m … now") instead of only "Now" — you can now read
  the interval/span of each plot point.
- Portfolio: removed the top-level card background — the summary (fiat hero +
  24h change + BTC, DRGX balance + Z/T, ratio bar) now floats directly under the
  MY DRGX header, and the "GROUPS" heading is gone.
- Group cards redesigned: a max width (280px, shrinks on narrow tabs) so they
  float rather than stretch; the group name is larger (subtitle font) top-left;
  the DRGX + fiat amounts moved to the card's top-right corner; and more inner
  padding around the content.

Removed the now-unused portfolio_groups i18n key. Full-node + Lite build clean;
ctest 1/1; hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:32:19 -05:00
DanSandClaude Opus 4.8 67feaea767 feat(market): rework tab layout — responsive chart + refined portfolio + group cards
The Market tab is non-scrolling, but an oversized fixed-height chart ate ~65%
of the height and squeezed the portfolio into a cramped strip, with custom
groups rendered as tiny afterthought rows at the very bottom.

- Precompute section geometry (hero header + portfolio card height) up front and
  let the price chart ABSORB the remaining vertical space, capped at its schema
  height and floored at 130px*vs so it stays useful. The chart now shrinks to
  make room instead of dominating. Replaces the barely-used SectionBudget.
- Portfolio summary refined: fiat value stays the hero with the 24h change beside
  it and the BTC value right-aligned; DRGX balance + Z/T breakdown on the next
  row; a full-width shielded/transparent ratio bar + % label.
- Custom groups now render as a wrapping grid of mini glass cards (label + DRGX +
  fiat each), sized to the card width, instead of a cramped one-line list. A
  "GROUPS" subheader separates them from the all-funds summary. The card grows to
  fit the grid.

New i18n key portfolio_groups ("GROUPS"). Full-node + Lite build clean; ctest
1/1; hygiene clean. Rendering change — needs a screenshot check of the Market tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:12:27 -05:00
DanSandClaude Opus 4.8 3e1cdd15f3 fix(market): portfolio editor address checklist was collapsing to zero height
The "Manage portfolio" editor renders its address checklist inside the
overlay dialog's content child, which is ImGuiChildFlags_AutoResizeY (the
dialog sizes to its content). The checklist used a negative "fill remaining
space" height (-GetFrameHeightWithSpacing()*1.5f), which is meaningless in an
auto-resizing parent — there is no fixed height to subtract from — so ImGui
collapsed the child to ~0 and no addresses were visible, even with
state.addresses fully populated.

Size the list to its rows instead, capped at 40% of the viewport height so a
large wallet scrolls rather than running off-screen. Pre-existing bug from
when the editor was added; distinct from the earlier state.addresses
population fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:53:08 -05:00
DanSandClaude Opus 4.8 af2db4366c refactor(audit): de-duplicate the main.cpp backdrop draw + DPI transition
Two render-loop de-duplications, zero behavior change (verified byte-equivalent
on both the GL and DX11 backends via the mingw Windows cross-build):

- drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active, lowSpec): the ~80-line
  gradient/texture/acrylic-capture/WindowBg-alpha block was copy-pasted in the
  main loop (with low-spec guards) and the resize-watcher lambda (without them).
  Extracted once; the main loop calls it with lowSpec=isLowSpecMode() and the
  resize watcher with lowSpec=false — each reproducing its CURRENT behavior
  exactly (pure dedup, no change). The intentional differences around it
  (resize's immediate present(0) vs the main loop's vsync, the main loop's
  try/catch, the BeginFrame low-spec guard) are left untouched.

- handleDisplayScaleChange(window, newScale, savedSizeForScale, lastKnownW/H,
  dpiTargetW/H, dpiResizeRetries, dpiResizePending): the SDL display-scale-change
  handler was duplicated ~verbatim in the idle-wait and poll event paths
  (differing only in debug-log text). Moved the body into one helper called from
  both; the #ifdef DRAGONX_USE_DX11 g_borderlessDpi and #ifdef __APPLE__ scale
  normalization are preserved.

Deliberately did NOT unify the full frame/present sequence — the resize path's
immediate present and lack of try/catch are intentional.

Full-node (GL) + Lite build clean; Windows/DX11 cross-build compiles main.cpp
clean; ctest 1/1; hygiene clean. NEEDS RUNTIME VERIFICATION: a live window
drag-resize (backdrop draws, no flicker) and a monitor DPI-scale change (window
resizes + fonts reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:00:40 -05:00
DanSandClaude Opus 4.8 cb74fbbff0 refactor(audit): unify config-dir resolution through Platform::getConfigDir
Settings::getDefaultPath and AddressBook::getDefaultPath each hand-rolled the
same per-platform #ifdef (Win SHGetFolderPath / macOS Application Support /
XDG ~/.config) with the DRAGONX_APP_NAME variant suffix. Route both through
util::Platform::getConfigDir() instead — one place owns the split — and add
the missing __APPLE__ branch to getConfigDir so it too returns the
macOS-native ~/Library/Application Support/<AppName>.

This also fixes a latent macOS inconsistency: transaction_history_cache
already resolves via getConfigDir(), which lacked the __APPLE__ branch, so on
macOS the tx cache landed in ~/.config while settings/addressbook went to
~/Library/Application Support. Now all three agree. No migration needed:
Linux/Windows paths are unchanged, and macOS packaging is not yet shipped.
Removes ~50 lines of duplicated #ifdef.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:49:24 -05:00
DanSandClaude Opus 4.8 121a79e768 refactor(audit): batch 9 — de-duplicate balance/peers/transactions UI cards
Three UI de-duplications (rendering changes; the test suite skips the GUI, so
these need a manual screenshot check of the Balance/Peers/Transactions tabs).

1. Classic balance layout — the DEFAULT layout — hand-rolled its own ~550-line
   address-list + recent-tx renderers that had DIVERGED from the shared
   RenderSharedAddressList/RenderSharedRecentTx used by the other 9 layouts.
   Deleted the inline copies and called the shared renderers (as the minimal
   layout already does), so the default layout now gains set-label +
   add-to-portfolio context items, custom address icons, drag-to-reorder,
   keyboard nav, copy-flash, and fully TR()'d strings. Hero row and Classic
   sizing (tabs.balance.classic address-table-height=340) preserved; addrH is
   computed before the call exactly as before. This is a deliberate
   feature-parity behavior change for the default layout.

2. peers_tab info cards — extracted drawStatCell() (label + value/em-dash with
   the shared offset math) and one drawCardDivider() replacing two duplicate
   divider lambdas; plain cells drive off a per-card loop. Bespoke cells
   (Blocks "(X left)", copy-on-click Best Block, TLS check-icon) kept inline.

3. transactions_tab — one drawSummaryCard() replaces the three near-identical
   Received/Sent/Mined blocks (same glass panel, icon, hover outline,
   click-to-filter). Byte-equivalent.

Full-node + Lite build clean (net -651 lines, no new warnings); ctest 1/1;
hygiene clean. NEEDS SCREENSHOT VERIFICATION.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:18:02 -05:00
DanSandClaude Opus 4.8 262891229a refactor(audit): batch 8 — share the post-encrypt daemon-restart block
Extract App::restartDaemonAfterEncryption(taskName, announceRestartStatus)
from the two verbatim copies in encryptWalletWithPassphrase and
processDeferredEncryption. The two differed only in the async task name and
whether they set connection_status_ = "restarting_after_encryption" first
(the immediate encrypt path does; the deferred path does not) — both
parameterized. The 20x100ms settle loop, cancel/shutdown guards, and
stop/startEmbeddedDaemon sequence are preserved exactly.

Deliberately NOT done: the audit's "decrypt 5-deep callback pyramid" finding.
On inspection the nesting is load-bearing, not gratuitous — WalletSecurityWorkflow
is unsynchronized main-thread-only state, and each step's worker->MainCb bounce
is what keeps its mutations on the main thread (the UI reads snapshot()/importActive()
every frame); collapsing the pipeline onto one worker/async task would race the
UI, and the final restart+import stage requires an AsyncTaskManager::Token that
worker_->post can't supply. Left byte-for-byte intact.

Full-node + Lite build clean; ctest 1/1 (incl. WalletSecurity controller tests);
hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:03:40 -05:00
DanSandClaude Opus 4.8 dcd09dc542 refactor(audit): batch 7 — RPC/settings/lite/effects mechanical dedups
Six behavior-preserving consolidations from the audit:

- RPCClient::call() overloads share a private performCall() (payload dump +
  curl_easy_perform + http code) and a static parseRpcResult() (error->RpcError
  extraction). The timeout overload restores the prior timeout in both the
  success and catch paths, exactly as before.
- The six UnifiedCallback methods delegate to one splitUnified() that builds
  the (Callback, ErrorCallback) pair once (null-check preserved).
- One liteTrimCopy() in lite_connection_service replaces 5 file-local
  trim-copy helpers across the lite slice.
- lite_wallet_controller's inline height JSON parse now routes through the
  tested parseLiteHeightResponse().
- theme_effects.cpp: unpackRGB()/scaledAlpha() replace 14 RGB-unpack + 5
  alpha-scale copy-paste blocks.
- settings.cpp load() uses loadScalar()/loadClamped() helpers for ~44 scalar
  fields. Besides removing boilerplate this HARDENS loading: ~44 fields that
  previously only checked contains() now also verify the JSON type, so a
  malformed value in settings.json falls back to the default instead of
  throwing/misreading. Custom cases (enum parses, legacy migrations, arrays)
  stay inline; save() is unchanged.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:56:54 -05:00
DanSandClaude Opus 4.8 4635a56e89 refactor(audit): batch 6 — de-duplicate the app_network send/balance paths
Three behavior-preserving consolidations in src/app_network.cpp:

1. Fold the three near-identical pending-send balance-delta blocks
   (upsertPendingSendTransaction debit, removePendingSendTransactions
   restore, applyPendingSendBalanceDeltas debit) into one
   App::applyPendingSendDelta(from, signedAmount, includeAggregates). The
   restore path's unclamped `+amt` is provably identical to the clamped
   signed form (balance/amount are >=0 invariants), so a single clamped
   helper reproduces all three exactly.

2. Collapse createNewZAddress/createNewTAddress into one
   App::createNewAddress(bool shielded, cb) with thin public forwarders,
   preserving the lite early-return, the dual push into the type list AND
   the combined state_.addresses view, and the dirty-flag/refresh bookkeeping.

3. Extract App::submitZSendMany() shared by sendTransaction and
   resendWithFeeGapWorkaround — the only differences (the TraceScope label
   and the retry-only send_feegap_retried_opids_.insert) become parameters.
   The in-flight counter, opid tracking, upsertPendingSendTransaction, and
   pending_send_callbacks_ bookkeeping are byte-equivalent.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:36:27 -05:00
DanSandClaude Opus 4.8 9c533fa485 refactor(audit): batch 5 — share the updater HTTP/download layer
XmrigUpdater and DaemonUpdater carried byte-identical libcurl code —
httpGet, downloadToFile, and the write/progress callbacks — including the
security-critical TLS-verify (SSL_VERIFYPEER=1 / SSL_VERIFYHOST=2) and
max-body-size options, duplicated across two signature-verified
download-and-execute files that had to be hand-kept in sync.

Hoist that machinery into src/util/http_download.{h,cpp}:
- util::httpGetString(url, logTag)
- util::httpDownloadToFile(url, dest, logTag, maxBytes, onProgress)
threading progress + cancellation through a std::function (returning false
aborts the transfer) instead of the class `this` pointer. Each updater's
httpGet/downloadToFile become one-line forwarders that bind their own log
tag and archive-size cap (64 MiB miner / 256 MiB node, both preserved).

Every curl option is preserved byte-identically (verified by diffing the
setopt lines against the pre-change code — the only deltas are the
now-parameterized MAXFILESIZE cap and the XFERINFODATA payload). No behavior
change; the TLS/size-guard options now live in exactly one place.

Deliberately scoped DOWN per the audit's own guidance: the divergent
per-updater logic (release DTOs/parsers, checksum-table parsing, version
compare, and installResolved's per-member-SHA vs transitive-trust verify,
install strategy, and chmod scope) stays in each class — NOT merged behind a
base-class/virtual-hook layer, which would raise review cost on
signature-verified execute-code and risk a subtle security regression. The
shared-DTO/parser consolidation (~77 test refs of churn on stable code) is
left as a separate, lower-value follow-up.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:17:44 -05:00
DanSandClaude Opus 4.8 de11850c74 refactor(audit): batch 4 — UI design-system helpers + i18n
Mechanical, behavior-preserving consolidations from the audit. Rendering
extractions were kept byte-equivalent; sites that couldn't be made identical
were left as-is (noted below).

Shared helpers:
- util::truncateMiddle(s,maxLen) / (s,front,back) in text_format.h — replaces
  6 file-local middle-ellipsis truncators + several inline substr sites
  (send/receive/transactions/balance_recent_tx/explorer + app.cpp + 3 dialogs).
  Carries the maxLen<=3 guard, fixing the latent unsigned-underflow copies.
- material::LoadingDots() in draw_helpers.h — one animated-ellipsis source for
  8 copy-pasted spinner sites (identical GetTime()*3 phase preserved).
- Reuse the existing FormatHashrate() in explorer_tab + peers_tab (dropped two
  inline hashrate ladders).
- material::DrawButtonGlassOverlay() — the glass-fill/rim/tactile overlay block
  shared by TactileButton / TactileSmallButton / schema TactileButton.
- material::CollapsibleHeader() — the invisible-button + label + chevron idiom
  (3 of 5 settings_page sites; RPC/Debug headers skipped — non-identical).
- material::GlassCardScope (RAII) — the ChannelsSplit/Indent/DrawGlassPanel/
  ChannelsMerge card scaffold (5 settings_page cards; About/send/receive skipped
  — different padding / logo interleaving).
- material::DialogWarningHeader()/DialogConfirmFooter() — warning header +
  50/50 Cancel/danger footer across the confirm dialogs; button height moved
  from a hardcoded 40px into ui.toml (components.overlay-dialog.confirm-btn-height).
- File-local enterLowSpec()/exitLowSpec() collapse the 3 low-spec snapshot copies.

i18n: wrapped hardcoded English in the shared balance render paths and the
whole Lite lifecycle/security section in TR(), with English defaults added to
loadBuiltinEnglish() (res/lang/*.json left for the translation tooling — TR
falls back to English, so output is unchanged).

Full-node + Lite build clean; ctest 1/1; hygiene clean. These are rendering
changes — the Settings page (cards/headers/confirm dialogs), all tactile
buttons, and the Lite settings section warrant a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:29:12 -05:00
DanSandClaude Opus 4.8 280a71e973 refactor(audit): batch 3 — decompose App::update() + collapse acrylic fork
Three P1 structural refactors from the audit; no behavior change.

1. Extract the ~180-line daemon-stdout rescan/witness parser out of the
   ~790-line App::update() into a pure, static, unit-testable
   NetworkRefreshService::parseDaemonRescanOutput() returning a
   DaemonRescanScan result struct (sits next to the existing static parse*
   siblings). The scanning half moved verbatim; App::update() keeps the
   state-application half, now reading scan.* fields. Adds
   testParseDaemonRescanOutput() covering all six daemon signals + edge
   cases against fixture log snippets — previously untestable without a
   live daemon.

2. Extract the ~95-line global keyboard-shortcut block (Ctrl+, / theme
   cycle / F5 / low-spec / effects / gradient / wizard) out of
   App::update() into App::handleGlobalShortcuts().

3. Collapse the ImGuiAcrylic frontend fork: the GLAD and DX11 namespaces
   were ~375 lines of semantically-identical code (the frontend makes zero
   direct GL/DX calls — all backend work delegates to AcrylicMaterial, which
   has a backend per API). Extended the single frontend's guard to
   #if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11) and deleted the
   DX11 duplicate, leaving the no-backend stub. Kills the Windows/DX11 drift
   hazard (acrylic frontend changes now made once).

Verified: full-node + Lite build clean; ctest 1/1 (incl. the new parser
tests); source-hygiene clean; and the collapsed acrylic path was
compile-verified under DX11 via the mingw-w64 Windows cross-build
(ObsidianDragon.exe links).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:40:40 -05:00
DanSandClaude Opus 4.8 7891c689fb refactor(audit): batch 2 — remove dead scaffolding from the shipping binary
Two dead-code removals surfaced by the audit; no runtime behavior change.

1. Delete the lite-lifecycle "readiness" scaffold (~1,586 lines). The pure
   dry-run evaluateLiteWalletServerLifecycleReadiness (whose own status text
   says "wallet lifecycle execution is still disabled in this scaffold") had
   zero callers; executeLiteWalletServerSelectionUi had zero callers; and
   executeLiteWalletLifecycleUiRequest's only caller was a fallback in
   settings_page reached only when app->liteWallet() is null. Deleted
   lite_wallet_server_lifecycle_readiness.{h,cpp} and the readiness /
   UI-execution machinery + enums + translation tables from both adapters,
   keeping the genuinely-live helpers (liteConnectionSettingsFromAppSettings,
   applyLiteConnectionSettingsToAppSettings, redactLiteServerSelectionValue,
   and the LiteWalletLifecycleUiExecutionInput DTO the live create/open/
   restore path still uses). The null-backend fallback now sets a plain
   "Lite wallet backend unavailable" status. This is exactly the
   readiness/scaffold pattern CLAUDE.md forbids regrowing.

2. Split the HushChat fixture/capture-manifest/seed-projection tooling
   (~2,050 lines, incl. the libsodium seed-projection) out of
   chat_protocol.cpp (1854 -> 284) and chat_protocol.h (586 -> 105) into a
   new chat_fixture_tooling.{h,cpp} compiled ONLY into the HushChatFixtureCheck
   dev tool — never into the app, lite, or test binaries (verified via nm).
   The app keeps only the runtime slice (memo parsing + tx metadata
   extraction) that network_refresh_service actually calls. Also moved the
   decrypt-preflight/hex helpers, which likewise had no runtime caller.

Note: the HushChat split is on gated-off experimental code and should be
coordinated with the pending dormant-HushChat-content handling (commit
af06b8b) before the lite PR — done here as a pure mechanical split, no redesign.

Full-node + Lite + HushChatFixtureCheck build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:20:53 -05:00
DanSandClaude Opus 4.8 f8150434d4 fix(audit): batch 1 — latent unlock/path/rescan correctness bugs
Four correctness fixes surfaced by the codebase audit:

1. Lite address-book path bug: AddressBook::getDefaultPath() hardcoded
   "ObsidianDragon" while Settings::getDefaultPath() uses DRAGONX_APP_NAME,
   so the Lite build wrote addressbook.json into the full-node app's config
   dir instead of ObsidianDragonLite/. Now uses DRAGONX_APP_NAME on all
   platforms (no macOS path change — the getConfigDir consolidation, which
   would move the macOS dir, is deferred).

2. PIN-unlock lockout bypass: the PIN path duplicated unlockWallet's
   success/lockout logic and its RPC-error branch bumped the attempt counter
   but skipped the escalating-lockout math entirely — a PIN user hitting an
   RPC error escaped the lockout curve. Extracted App::applyUnlockSuccess()
   and App::applyUnlockFailure() and routed all unlock paths (passphrase,
   PIN vault-fail, PIN RPC-fail) through them, so the lockout curve now
   applies uniformly. (noVault stays a mode-switch, not a failed attempt.)

3. Witness/rescan reset drift: the ~11-line rescan+witness completion reset
   was copy-pasted at four sites (app.cpp x2, app_network.cpp x2); adding a
   witness field and missing a copy would leave stale progress. Folded into
   App::resetWitnessRescanProgress(). Left the distinct phase-transition
   reset in App::update() untouched (it sets witness_phase, not 0).

4. Truncation underflow: send_tab/receive_tab's size_t TruncateAddress
   copies computed (maxLen - 3) without guarding maxLen <= 3, which wraps
   and throws std::out_of_range on short inputs. Added the guard.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:54:36 -05:00
97 changed files with 7323 additions and 8922 deletions
+3 -7
View File
@@ -423,7 +423,6 @@ set(APP_SOURCES
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_server_lifecycle_readiness.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
@@ -475,10 +474,8 @@ set(APP_SOURCES
src/ui/windows/shield_dialog.cpp
src/ui/windows/request_payment_dialog.cpp
src/ui/windows/block_info_dialog.cpp
src/ui/windows/import_key_dialog.cpp
src/ui/windows/export_all_keys_dialog.cpp
src/ui/windows/export_transactions_dialog.cpp
src/ui/windows/backup_wallet_dialog.cpp
src/ui/widgets/qr_code.cpp
src/rpc/rpc_client.cpp
src/rpc/rpc_worker.cpp
@@ -506,6 +503,7 @@ set(APP_SOURCES
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/pool_stats_service.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
@@ -557,7 +555,6 @@ set(APP_HEADERS
src/wallet/lite_wallet_state_mapper.h
src/wallet/lite_wallet_lifecycle_ui_adapter.h
src/wallet/lite_wallet_server_selection_adapter.h
src/wallet/lite_wallet_server_lifecycle_readiness.h
src/wallet/lite_wallet_lifecycle_service.h
src/chat/chat_protocol.h
src/config/version.h
@@ -601,10 +598,8 @@ set(APP_HEADERS
src/ui/windows/shield_dialog.h
src/ui/windows/request_payment_dialog.h
src/ui/windows/block_info_dialog.h
src/ui/windows/import_key_dialog.h
src/ui/windows/export_all_keys_dialog.h
src/ui/windows/export_transactions_dialog.h
src/ui/windows/backup_wallet_dialog.h
src/ui/widgets/qr_code.h
src/rpc/rpc_client.h
src/rpc/rpc_worker.h
@@ -825,6 +820,7 @@ endif()
add_executable(HushChatFixtureCheck
tools/hushchat_fixture_check.cpp
src/chat/chat_protocol.cpp
src/chat/chat_fixture_tooling.cpp
)
target_include_directories(HushChatFixtureCheck PRIVATE
@@ -1020,7 +1016,6 @@ if(BUILD_TESTING)
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_server_lifecycle_readiness.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/windows/balance_address_list.cpp
@@ -1051,6 +1046,7 @@ if(BUILD_TESTING)
src/util/logger.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
+3 -3
View File
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--on-secondary = "#FFFFFF"
--on-background = "#E8E6F0"
--on-surface = "#E8E6F0"
--on-surface-medium = "rgba(232,230,240,0.72)"
--on-surface-disabled = "rgba(232,230,240,0.40)"
--on-surface-medium = "rgba(232,230,240,0.85)"
--on-surface-disabled = "rgba(232,230,240,0.58)"
--error = "#FF5C72"
--on-error = "#000000"
--success = "#3DE8A0"
@@ -61,7 +61,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--sidebar-badge = "rgba(232,230,240,1.0)"
--sidebar-divider = "rgba(200,190,240,0.05)"
--chart-line = "rgba(124,108,255,0.12)"
--window-control = "rgba(232,230,240,0.72)"
--window-control = "rgba(232,230,240,0.85)"
--window-control-hover = "rgba(124,108,255,0.12)"
--window-close-hover = "rgba(255,92,114,0.75)"
--spinner-track = "rgba(200,190,240,0.08)"
+7 -7
View File
@@ -19,16 +19,16 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
--on-secondary = "#FFFFFF"
--on-background = "#1E1E2A"
--on-surface = "#1E1E2A"
--on-surface-medium = "rgba(30,30,42,0.72)"
--on-surface-disabled = "rgba(30,30,42,0.38)"
--on-surface-medium = "rgba(30,30,42,0.86)"
--on-surface-disabled = "rgba(30,30,42,0.62)"
--error = "#E0304A"
--on-error = "#FFFFFF"
--success = "#18A860"
--on-success = "#FFFFFF"
--warning = "#E09020"
--on-warning = "#000000"
--divider = "rgba(30,30,60,0.12)"
--outline = "rgba(30,30,60,0.15)"
--divider = "rgba(30,30,60,0.20)"
--outline = "rgba(30,30,60,0.24)"
--scrim = "rgba(0,0,0,0.45)"
--surface-hover = "rgba(96,64,224,0.05)"
--surface-alt = "rgba(96,64,224,0.02)"
@@ -47,14 +47,14 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
--chart-hover-ring = "rgba(96,64,224,0.30)"
--tooltip-bg = "rgba(36,34,52,0.94)"
--tooltip-border = "rgba(96,64,224,0.16)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-border = "rgba(30,30,60,0.10)"
--glass-noise-tint = "rgba(96,64,224,0.02)"
--tactile-top = "rgba(255,255,255,0.40)"
--tactile-bottom = "rgba(255,255,255,0.05)"
--hover-overlay = "rgba(96,64,224,0.04)"
--hover-overlay = "rgba(96,64,224,0.10)"
--active-overlay = "rgba(96,64,224,0.08)"
--rim-light = "rgba(96,64,224,0.06)"
--rim-light = "rgba(30,30,42,0.22)"
--status-divider = "rgba(30,30,60,0.08)"
--sidebar-hover = "rgba(96,64,224,0.07)"
--sidebar-icon = "rgba(30,30,42,0.50)"
+3 -3
View File
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--on-secondary = "#000000"
--on-background = "#D0D0D4"
--on-surface = "#D0D0D4"
--on-surface-medium = "rgba(208,208,212,0.75)"
--on-surface-disabled = "rgba(208,208,212,0.45)"
--on-surface-medium = "rgba(208,208,212,0.85)"
--on-surface-disabled = "rgba(208,208,212,0.58)"
--error = "#B07080"
--on-error = "#000000"
--success = "#7AAE7C"
@@ -61,7 +61,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--sidebar-badge = "rgba(208,208,212,1.0)"
--sidebar-divider = "rgba(220,220,225,0.05)"
--chart-line = "rgba(220,220,225,0.08)"
--window-control = "rgba(208,208,212,0.75)"
--window-control = "rgba(208,208,212,0.85)"
--window-control-hover = "rgba(220,220,225,0.10)"
--window-close-hover = "rgba(200,50,60,0.70)"
--spinner-track = "rgba(220,220,225,0.08)"
+8 -8
View File
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#3A2E22"
--on-surface = "#3A2E22"
--on-surface-medium = "rgba(58,46,34,0.68)"
--on-surface-disabled = "rgba(58,46,34,0.38)"
--on-surface-medium = "rgba(58,46,34,0.86)"
--on-surface-disabled = "rgba(58,46,34,0.62)"
--error = "#A0524A"
--on-error = "#FFFFFF"
--success = "#6A8A5C"
--success = "#4E8A42"
--on-success = "#FFFFFF"
--warning = "#C08840"
--on-warning = "#000000"
--divider = "rgba(140,110,70,0.14)"
--outline = "rgba(140,110,70,0.16)"
--divider = "rgba(140,110,70,0.20)"
--outline = "rgba(140,110,70,0.24)"
--scrim = "rgba(30,20,10,0.45)"
--surface-hover = "rgba(176,120,64,0.06)"
--surface-alt = "rgba(176,120,64,0.03)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
--chart-hover-ring = "rgba(176,120,64,0.28)"
--tooltip-bg = "rgba(50,38,24,0.94)"
--tooltip-border = "rgba(176,120,64,0.12)"
--glass-fill = "rgba(255,252,245,0.58)"
--glass-fill = "rgba(255,252,245,0.20)"
--glass-border = "rgba(176,120,64,0.14)"
--glass-noise-tint = "rgba(180,140,80,0.03)"
--tactile-top = "rgba(255,255,248,0.50)"
--tactile-bottom = "rgba(255,255,248,0.08)"
--hover-overlay = "rgba(176,120,64,0.05)"
--hover-overlay = "rgba(176,120,64,0.10)"
--active-overlay = "rgba(176,120,64,0.10)"
--rim-light = "rgba(212,160,108,0.10)"
--rim-light = "rgba(58,46,34,0.22)"
--status-divider = "rgba(176,120,64,0.10)"
--sidebar-hover = "rgba(176,120,64,0.08)"
--sidebar-icon = "rgba(58,46,34,0.50)"
+7 -7
View File
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#1C1525"
--on-surface = "#1C1525"
--on-surface-medium = "rgba(28,21,37,0.72)"
--on-surface-disabled = "rgba(28,21,37,0.40)"
--on-surface-medium = "rgba(28,21,37,0.86)"
--on-surface-disabled = "rgba(28,21,37,0.62)"
--error = "#C62828"
--on-error = "#FFFFFF"
--success = "#2E7D32"
--on-success = "#FFFFFF"
--warning = "#E65100"
--on-warning = "#000000"
--divider = "rgba(120,80,160,0.12)"
--outline = "rgba(120,80,160,0.14)"
--divider = "rgba(120,80,160,0.20)"
--outline = "rgba(120,80,160,0.24)"
--scrim = "rgba(20,10,30,0.45)"
--surface-hover = "rgba(140,107,175,0.06)"
--surface-alt = "rgba(140,107,175,0.03)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
--chart-hover-ring = "rgba(140,107,175,0.28)"
--tooltip-bg = "rgba(32,24,48,0.94)"
--tooltip-border = "rgba(140,107,175,0.12)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-border = "rgba(140,107,175,0.14)"
--glass-noise-tint = "rgba(180,140,220,0.03)"
--tactile-top = "rgba(255,255,255,0.50)"
--tactile-bottom = "rgba(255,255,255,0.08)"
--hover-overlay = "rgba(140,107,175,0.05)"
--hover-overlay = "rgba(140,107,175,0.10)"
--active-overlay = "rgba(140,107,175,0.10)"
--rim-light = "rgba(180,140,255,0.10)"
--rim-light = "rgba(28,21,37,0.22)"
--status-divider = "rgba(140,107,175,0.10)"
--sidebar-hover = "rgba(140,107,175,0.08)"
--sidebar-icon = "rgba(28,21,37,0.50)"
+9 -9
View File
@@ -19,16 +19,16 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#2A2C30"
--on-surface = "#2A2C30"
--on-surface-medium = "rgba(42,44,48,0.68)"
--on-surface-disabled = "rgba(42,44,48,0.38)"
--on-surface-medium = "rgba(42,44,48,0.86)"
--on-surface-disabled = "rgba(42,44,48,0.62)"
--error = "#8C5A62"
--on-error = "#FFFFFF"
--success = "#5A7E5C"
--success = "#3D7A42"
--on-success = "#FFFFFF"
--warning = "#8A7A52"
--warning = "#9A7A2E"
--on-warning = "#000000"
--divider = "rgba(42,44,48,0.12)"
--outline = "rgba(42,44,48,0.14)"
--divider = "rgba(42,44,48,0.20)"
--outline = "rgba(42,44,48,0.24)"
--scrim = "rgba(0,0,0,0.42)"
--surface-hover = "rgba(42,44,48,0.04)"
--surface-alt = "rgba(42,44,48,0.02)"
@@ -47,14 +47,14 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
--chart-hover-ring = "rgba(42,44,48,0.24)"
--tooltip-bg = "rgba(50,52,58,0.92)"
--tooltip-border = "rgba(42,44,48,0.10)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-border = "rgba(42,44,48,0.10)"
--glass-noise-tint = "rgba(42,44,48,0.015)"
--tactile-top = "rgba(255,255,255,0.35)"
--tactile-bottom = "rgba(255,255,255,0.04)"
--hover-overlay = "rgba(42,44,48,0.04)"
--hover-overlay = "rgba(42,44,48,0.10)"
--active-overlay = "rgba(42,44,48,0.08)"
--rim-light = "rgba(42,44,48,0.06)"
--rim-light = "rgba(42,44,48,0.22)"
--status-divider = "rgba(42,44,48,0.08)"
--sidebar-hover = "rgba(42,44,48,0.05)"
--sidebar-icon = "rgba(42,44,48,0.45)"
+9 -9
View File
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#2C2A28"
--on-surface = "#2C2A28"
--on-surface-medium = "rgba(44,42,40,0.68)"
--on-surface-disabled = "rgba(44,42,40,0.38)"
--on-surface-medium = "rgba(44,42,40,0.86)"
--on-surface-disabled = "rgba(44,42,40,0.62)"
--error = "#8C5250"
--on-error = "#FFFFFF"
--success = "#5C7A62"
--success = "#3F7A48"
--on-success = "#FFFFFF"
--warning = "#8A7A4C"
--warning = "#9A7A2E"
--on-warning = "#000000"
--divider = "rgba(80,75,68,0.12)"
--outline = "rgba(80,75,68,0.14)"
--divider = "rgba(80,75,68,0.20)"
--outline = "rgba(80,75,68,0.24)"
--scrim = "rgba(20,18,16,0.42)"
--surface-hover = "rgba(110,117,128,0.05)"
--surface-alt = "rgba(110,117,128,0.025)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
--chart-hover-ring = "rgba(110,117,128,0.24)"
--tooltip-bg = "rgba(44,42,40,0.94)"
--tooltip-border = "rgba(110,117,128,0.10)"
--glass-fill = "rgba(255,255,254,0.62)"
--glass-fill = "rgba(255,255,254,0.20)"
--glass-border = "rgba(110,117,128,0.10)"
--glass-noise-tint = "rgba(80,75,68,0.02)"
--tactile-top = "rgba(255,255,255,0.45)"
--tactile-bottom = "rgba(255,255,255,0.06)"
--hover-overlay = "rgba(110,117,128,0.04)"
--hover-overlay = "rgba(110,117,128,0.10)"
--active-overlay = "rgba(110,117,128,0.08)"
--rim-light = "rgba(180,175,168,0.10)"
--rim-light = "rgba(44,42,40,0.22)"
--status-divider = "rgba(110,117,128,0.08)"
--sidebar-hover = "rgba(110,117,128,0.06)"
--sidebar-icon = "rgba(44,42,40,0.48)"
+2 -2
View File
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--on-secondary = "#000000"
--on-background = "#E8E0F0"
--on-surface = "#E8E0F0"
--on-surface-medium = "rgba(232,224,240,0.75)"
--on-surface-disabled = "rgba(232,224,240,0.45)"
--on-surface-medium = "rgba(232,224,240,0.85)"
--on-surface-disabled = "rgba(232,224,240,0.58)"
--error = "#CF6679"
--on-error = "#000000"
--success = "#81C784"
+6 -4
View File
@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--on-secondary = "#000000"
--on-background = "#F0E0D8"
--on-surface = "#F0E0D8"
--on-surface-medium = "rgba(240,224,216,0.7)"
--on-surface-disabled = "rgba(240,224,216,0.44)"
--on-surface-medium = "rgba(240,224,216,0.85)"
--on-surface-disabled = "rgba(240,224,216,0.58)"
--error = "#FF5252"
--on-error = "#000000"
--success = "#81C784"
@@ -874,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
accent-stripe-left-offset = { size = 0.0 }
accent-stripe-width = { size = 4.0 }
accent-stripe-rounding = { size = 1.5 }
chart-y-axis-min-padding = { size = 40.0 }
chart-y-axis-min-padding = { size = 54.0 }
chart-y-axis-padding = { size = 70.0 }
chart-dot-min-radius = { size = 1.5 }
chart-dot-radius = { size = 2.0 }
@@ -1219,6 +1219,9 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
fill-alpha = { size = 12.0 }
noise-alpha = { size = 14.0 }
[components.overlay-dialog]
confirm-btn-height = { size = 40.0 }
[components.qr-code]
module-scale = { size = 4 }
border-modules = { size = 2 }
@@ -1525,7 +1528,6 @@ title = { font = "h5" }
input = { width = 320.0, height = 40.0 }
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
error-text = { font = "caption" }
backdrop-alpha = { opacity = 0.0 }
mode-toggle = { font = "caption" }
# ---------------------------------------------------------------------------
+87 -277
View File
@@ -66,6 +66,7 @@
#include "ui/notifications.h"
#include "util/i18n.h"
#include "util/platform.h"
#include "util/text_format.h"
#include "util/payment_uri.h"
#include "util/texture_loader.h"
#include "util/bootstrap.h"
@@ -777,17 +778,8 @@ void App::update()
// poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked.
ui::Notifications::instance().success("Blockchain rescan complete");
state_.sync.rescanning = false;
rescan_confirmed_active_ = false;
resetWitnessRescanProgress();
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
state_.sync.building_witnesses = false;
state_.sync.witness_phase = 0;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 0;
}
// else: rescanning=false but not yet confirmed → pre-restart daemon; keep waiting.
};
@@ -863,204 +855,18 @@ void App::update()
// be blocked for minutes on a getrescaninfo call (it waits on cs_main while the daemon
// holds it for a witness rebuild). On the worker, the parse queued behind that and the
// rescan/witness progress froze. Inline, progress updates every frame regardless.
const std::string output = std::move(newOutput);
bool foundRescan = false;
bool finished = false;
float rescanPct = 0.0f;
std::string lastStatus;
// Sapling witness rebuild progress. Two daemon signals:
// "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" (the common one —
// one per wallet tx; <hash> is the stable per-tx key, <N> the total tx count)
// "Building Witnesses for block <h> <frac> complete, <n> remaining" (rarer)
bool foundWitness = false;
float witnessPct = -1.0f; // -1 = no fraction parsed this batch
int witnessRemaining = -1;
int witnessReadTotal = -1; // new daemon: the <total> in "<done> / <total>"
bool witnessRebuilt = false; // new daemon's completion line seen this batch
std::vector<std::string> witnessTxids; // distinct-tx keys seen this batch
int witnessTotalTxs = -1; // max N seen this batch
size_t pos = 0;
while (pos < output.size()) {
size_t eol = output.find('\n', pos);
if (eol == std::string::npos) eol = output.size();
std::string line = output.substr(pos, eol - pos);
pos = eol + 1;
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
lastStatus = line;
}
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9;
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
}
if (numEnd > numStart) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
lastStatus = line;
}
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
finished = true;
}
// This daemon prints no "Done rescanning" line; instead it logs the rescan
// benchmark timing exactly when the scan finishes, e.g.:
// "... rescan 16760577ms"
// Match the lowercase " rescan " bench category ending in "<digits>ms" (the
// progress lines are "Still rescanning"/"Rescanning...", which never end in ms).
if (line.find(" rescan ") != std::string::npos) {
std::string t = line;
while (!t.empty() && (t.back() == '\r' || t.back() == '\n' || t.back() == ' '))
t.pop_back();
if (t.size() >= 3 && t.compare(t.size() - 2, 2, "ms") == 0 &&
std::isdigit(static_cast<unsigned char>(t[t.size() - 3]))) {
finished = true;
}
}
// Sapling note witness rebuild: "Building Witnesses for block <h> <frac>
// complete, <n> remaining". The float before "complete" is a 0..1 fraction.
auto witIdx = line.find("Building Witnesses for block");
if (witIdx != std::string::npos) {
foundWitness = true;
auto compIdx = line.find(" complete", witIdx);
if (compIdx != std::string::npos) {
size_t numEnd = compIdx, numStart = numEnd;
while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) ||
line[numStart - 1] == '.')) numStart--;
if (numStart < numEnd) {
try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
auto remIdx = line.find(" remaining", witIdx);
if (remIdx != std::string::npos) {
size_t numEnd = remIdx, numStart = numEnd;
while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--;
if (numStart < numEnd) {
try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
// Newer multi-threaded daemon: "Reading blocks for witness rebuild: <done> / <total>".
// This replaced the per-block "Building Witnesses" line and is an exact done/total
// count, so capture BOTH (total drives the denominator directly — no peak anchoring).
auto rdIdx = line.find("Reading blocks for witness rebuild:");
if (rdIdx != std::string::npos) {
foundWitness = true;
auto slash = line.find('/', rdIdx);
if (slash != std::string::npos) {
size_t dEnd = slash;
while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--;
size_t dStart = dEnd;
while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--;
size_t tStart = slash + 1;
while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++;
size_t tEnd = tStart;
while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++;
if (dStart < dEnd && tStart < tEnd) {
try {
long done = std::stol(line.substr(dStart, dEnd - dStart));
long total = std::stol(line.substr(tStart, tEnd - tStart));
if (total > 0 && done >= 0 && done <= total) {
witnessRemaining = static_cast<int>(total - done);
witnessReadTotal = static_cast<int>(total);
}
} catch (...) {}
}
}
lastStatus = line;
}
// New daemon's witness-rebuild completion line:
// "... rebuilt <n> note witness cache(s) to height <h> in <ms>ms using <t> thread(s)".
// The parallel rebuild logs no progress, so on completion snap the bar to 100%.
if (line.find("note witness cache") != std::string::npos &&
line.find("thread(s)") != std::string::npos) {
foundWitness = true;
witnessRebuilt = true;
lastStatus = line;
}
// Primary signal: "Setting Initial Sapling Witness for tx <hash>, <i> of <N>".
// <i> is the tx's slot in an unordered map (bounces — useless as progress), so
// we key on <hash> (one per tx) and count distinct txs against <N>. Extract the
// hash between "for tx " and the following comma, and N after " of ".
{
auto setIdx = line.find("Setting Initial Sapling Witness for tx ");
if (setIdx != std::string::npos) {
foundWitness = true;
size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size();
size_t comma = line.find(',', hStart);
if (comma != std::string::npos && comma > hStart) {
witnessTxids.push_back(line.substr(hStart, comma - hStart));
auto ofIdx = line.find(" of ", comma);
if (ofIdx != std::string::npos) {
size_t nStart = ofIdx + 4, nEnd = nStart;
while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++;
if (nEnd > nStart) {
try {
int n = std::stoi(line.substr(nStart, nEnd - nStart));
if (n > witnessTotalTxs) witnessTotalTxs = n;
} catch (...) {}
}
}
}
lastStatus = line;
}
}
}
const auto scan = services::NetworkRefreshService::parseDaemonRescanOutput(std::move(newOutput));
// Apply results directly — we are already on the main thread.
const std::string& status = lastStatus;
if (finished) {
const std::string& status = scan.lastStatus;
if (scan.finished) {
if (state_.sync.rescanning) {
ui::Notifications::instance().success("Blockchain rescan complete");
}
state_.sync.rescanning = false;
rescan_confirmed_active_ = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
// Witness rebuild finishes with the rescan it's part of.
state_.sync.building_witnesses = false;
state_.sync.witness_phase = 0;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 0;
} else if (foundWitness) {
resetWitnessRescanProgress();
state_.sync.rescan_progress = 1.0f;
} else if (scan.foundWitness) {
// Witness rebuild is the tail phase of a rescan — keep rescanning set so
// the broader gating holds, but surface the witness-specific progress.
state_.sync.rescanning = true;
@@ -1074,8 +880,8 @@ void App::update()
// never drops back to the initial pass. Otherwise an interleaved initial
// line would flip the phase back and reset the bar every batch (thrash).
int phase = state_.sync.witness_phase;
if (witnessRemaining >= 0 || witnessRebuilt) phase = 2;
else if (!witnessTxids.empty() && phase < 2) phase = 1;
if (scan.witnessRemaining >= 0 || scan.witnessRebuilt) phase = 2;
else if (!scan.witnessTxids.empty() && phase < 2) phase = 1;
// Entering a higher sub-phase → reset its progress + accumulators so the
// bar restarts from 0 (the two phases have different scales). Upgrade-only,
@@ -1090,24 +896,24 @@ void App::update()
}
if (phase == 2) {
state_.sync.witness_remaining = witnessRemaining;
state_.sync.witness_remaining = scan.witnessRemaining;
float p = -1.0f;
if (witnessRebuilt) {
if (scan.witnessRebuilt) {
// Parallel rebuild finished (it logs no progress) — snap to 100%.
p = 1.0f;
state_.sync.witness_remaining = 0;
} else if (witnessReadTotal > 0) {
} else if (scan.witnessReadTotal > 0) {
// New daemon: exact done/total. Use the reported total as the
// denominator directly (no peak anchoring) for a smooth 0..1 sweep.
int done = witnessReadTotal - (witnessRemaining >= 0 ? witnessRemaining : 0);
p = static_cast<float>(done) / static_cast<float>(witnessReadTotal);
} else if (witnessRemaining >= 0) {
int done = scan.witnessReadTotal - (scan.witnessRemaining >= 0 ? scan.witnessRemaining : 0);
p = static_cast<float>(done) / static_cast<float>(scan.witnessReadTotal);
} else if (scan.witnessRemaining >= 0) {
// Older daemon: bare "<n> remaining" with no total — anchor the % to
// the largest remaining seen (its first/largest value ≈ the full span).
if (witnessRemaining > witness_rebuild_total_blocks_)
witness_rebuild_total_blocks_ = witnessRemaining;
if (scan.witnessRemaining > witness_rebuild_total_blocks_)
witness_rebuild_total_blocks_ = scan.witnessRemaining;
if (witness_rebuild_total_blocks_ > 0)
p = 1.0f - static_cast<float>(witnessRemaining) /
p = 1.0f - static_cast<float>(scan.witnessRemaining) /
static_cast<float>(witness_rebuild_total_blocks_);
}
if (p >= 0.0f) {
@@ -1119,8 +925,8 @@ void App::update()
// Initial pass: count DISTINCT witnessed txs / total. The set only
// grows (dedups the daemon's double-prints), so it's monotonic; the
// raw "<i>" can't be used — it's an unordered-map slot that bounces.
if (witnessTotalTxs > witness_total_txs_) witness_total_txs_ = witnessTotalTxs;
for (const auto& txid : witnessTxids) witness_seen_txids_.insert(txid);
if (scan.witnessTotalTxs > witness_total_txs_) witness_total_txs_ = scan.witnessTotalTxs;
for (const auto& txid : scan.witnessTxids) witness_seen_txids_.insert(txid);
if (witness_total_txs_ > 0 && !witness_seen_txids_.empty()) {
float p = static_cast<float>(witness_seen_txids_.size()) /
static_cast<float>(witness_total_txs_);
@@ -1132,14 +938,14 @@ void App::update()
if (!status.empty()) {
state_.sync.rescan_status = status;
}
} else if (foundRescan) {
} else if (scan.foundRescan) {
state_.sync.rescanning = true;
// Reading "Still rescanning" straight from the daemon log is hard proof the
// rescan is genuinely running — confirm it so the getrescaninfo poll's
// completion check can fire even if it never caught a warmup error.
rescan_confirmed_active_ = true;
if (rescanPct > 0.0f) {
state_.sync.rescan_progress = rescanPct / 100.0f;
if (scan.rescanPct > 0.0f) {
state_.sync.rescan_progress = scan.rescanPct / 100.0f;
}
if (!status.empty()) {
state_.sync.rescan_status = status;
@@ -1305,9 +1111,17 @@ void App::update()
if (network_refresh_.consumeDue(RefreshTimer::Price)) {
if (settings_->getFetchPrices()) {
refreshPrice();
refreshMarketChart(); // self-throttled to ~30 min; keeps sparkline history fresh
}
}
handleGlobalShortcuts();
}
void App::handleGlobalShortcuts()
{
ImGuiIO& io = ImGui::GetIO();
// Keyboard shortcut: Ctrl+, to open Settings page
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
setCurrentPage(ui::NavPage::Settings);
@@ -1429,6 +1243,30 @@ void App::render()
// Process deferred encryption from wizard (runs in background)
processDeferredEncryption();
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
// before the sidebar reads current_page_ (below) so the forced page is reflected.
updateScreenshotSweep();
// While any full-window BLUR overlay is open: (1) suppress panel theme-effects for the whole
// frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay;
// (2) mark a full-window blur overlay active so glass panels use their opaque fallback (they're
// covered by the overlay backdrop, and this leaves the backdrop as the sole acrylic blur caller).
// Driven by the shared framework's last-frame latch (modal-agnostic; timing-equivalent to the old
// PortfolioEditorActive() signal). RAII-restored to the prior state on any return path.
struct ModalRenderGuard {
bool fxOn, wasEffects;
explicit ModalRenderGuard(bool modalOpen) {
wasEffects = ui::effects::ThemeEffects::instance().isEnabled();
fxOn = modalOpen && wasEffects;
if (fxOn) ui::effects::ThemeEffects::instance().setEnabled(false);
ui::material::SetFullWindowBlurOverlayActive(modalOpen);
}
~ModalRenderGuard() {
if (fxOn) ui::effects::ThemeEffects::instance().setEnabled(true);
ui::material::SetFullWindowBlurOverlayActive(false);
}
} modalGuard(ui::material::AnyBlurOverlayActiveLastFrame());
// Main content area - use full window (no menu bar)
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
@@ -2080,6 +1918,10 @@ void App::render()
// Render notifications (toast messages)
ui::Notifications::instance().render();
// Latch whether any blur overlay was drawn this frame (for next frame's ModalRenderGuard) and, on
// the overlay's close transition, invalidate the acrylic capture so glass panels re-blur cleanly.
ui::material::LatchBlurOverlayActive();
}
void App::renderStatusBar()
@@ -2503,7 +2345,7 @@ void App::renderLiteFirstRunPrompt()
creating = false; // back to the buttons so the user can retry
}
} else {
if (ImGui::Button(TR("lite_welcome_create"), ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("lite_welcome_create"), ImVec2(btnW, 0))) {
// Async create with the same server failover as open (no UI freeze; a dead
// server falls through to the next). On success the wizard reveals the seed.
if (lite_wallet_->beginCreateWallet()) {
@@ -2513,11 +2355,11 @@ void App::renderLiteFirstRunPrompt()
}
}
ImGui::SameLine();
if (ImGui::Button(TR("lite_welcome_restore"), ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("lite_welcome_restore"), ImVec2(btnW, 0))) {
step = 3; // inline seed-entry restore (see step 3 below)
}
ImGui::Spacing();
if (ImGui::Button(TR("lite_welcome_later"),
if (ui::material::TactileButton(TR("lite_welcome_later"),
ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) {
finish();
}
@@ -2552,7 +2394,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
if (ImGui::Button("I've written it down", ImVec2(btnW, 0))) {
if (ui::material::TactileButton("I've written it down", ImVec2(btnW, 0))) {
chips.clear();
for (const auto& w : words) chips.emplace_back(w, false);
std::mt19937 rng{std::random_device{}()};
@@ -2561,9 +2403,9 @@ void App::renderLiteFirstRunPrompt()
step = 2;
}
ImGui::SameLine();
if (ImGui::Button("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
ImGui::SameLine();
if (ImGui::Button("Skip", ImVec2(80, 0))) {
if (ui::material::TactileButton("Skip", ImVec2(80, 0))) {
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
finish();
}
@@ -2591,9 +2433,9 @@ void App::renderLiteFirstRunPrompt()
ImGui::PushID((int)i);
if (chips[i].second) {
ImGui::BeginDisabled();
ImGui::Button(chips[i].first.c_str(), ImVec2(125, 0));
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0));
ImGui::EndDisabled();
} else if (ImGui::Button(chips[i].first.c_str(), ImVec2(125, 0))) {
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0))) {
if (progress < (int)words.size() && chips[i].first == words[progress]) {
chips[i].second = true; // correct next word
++progress;
@@ -2608,15 +2450,15 @@ void App::renderLiteFirstRunPrompt()
const bool verified = progress == (int)words.size();
if (!verified) ImGui::BeginDisabled();
if (ImGui::Button("Done", ImVec2(btnW, 0))) {
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) {
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
finish();
}
if (!verified) ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Back", ImVec2(80, 0))) step = 1;
if (ui::material::TactileButton("Back", ImVec2(80, 0))) step = 1;
ImGui::SameLine();
if (ImGui::Button("Skip", ImVec2(80, 0))) {
if (ui::material::TactileButton("Skip", ImVec2(80, 0))) {
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
finish();
}
@@ -2668,7 +2510,7 @@ void App::renderLiteFirstRunPrompt()
while (!seedTrim.empty() && std::isspace((unsigned char)seedTrim.back())) seedTrim.pop_back();
ImGui::BeginDisabled(seedTrim.empty());
if (ImGui::Button(TR("lite_restore_btn"), ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("lite_restore_btn"), ImVec2(btnW, 0))) {
wallet::LiteWalletRestoreRequest req;
req.seedPhrase = seedTrim;
req.birthday = static_cast<unsigned long long>(std::max(0, restoreBirthday));
@@ -2684,7 +2526,7 @@ void App::renderLiteFirstRunPrompt()
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Back", ImVec2(80, 0))) {
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
sodium_memzero(restoreSeed, sizeof(restoreSeed));
restoreErr.clear();
step = 0;
@@ -2719,7 +2561,7 @@ void App::renderLiteUnlockPrompt()
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing();
const float btnW = 130.0f;
bool doUnlock = ImGui::Button(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
if (doUnlock) {
const bool ok = lite_wallet_->unlockWallet(pass);
sodium_memzero(pass, sizeof(pass));
@@ -2732,7 +2574,7 @@ void App::renderLiteUnlockPrompt()
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
sodium_memzero(pass, sizeof(pass));
lite_unlock_prompt_ = false;
ImGui::CloseCurrentPopup();
@@ -2741,41 +2583,6 @@ void App::renderLiteUnlockPrompt()
}
}
void App::renderAboutDialog()
{
auto dlg = ui::schema::UI().drawElement("inline-dialogs", "about");
auto dlgF = [&](const char* key, float fb) -> float {
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
if (!ui::material::BeginOverlayDialog("About ObsidianDragon", &show_about_, dlgF("width", 400.0f), 0.94f)) {
return;
}
ui::material::Type().text(ui::material::TypeStyle::H6, DRAGONX_APP_NAME);
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::Text("Version: %s", DRAGONX_VERSION);
ImGui::Text("ImGui: %s", IMGUI_VERSION);
ImGui::Spacing();
ImGui::TextWrapped("A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::Spacing();
ImGui::Text("Copyright 2024-2026 The Hush Developers");
ImGui::Text("Released under the GPLv3 License");
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont((int)dlgF("button-font", 1)))) {
show_about_ = false;
}
ui::material::EndOverlayDialog();
}
void App::renderImportKeyDialog()
{
@@ -2910,7 +2717,7 @@ void App::renderExportKeyDialog()
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
for (const auto& addr : all_addrs) {
bool selected = (export_address_ == addr);
std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen);
std::string display = util::truncateMiddle(addr, addrFrontLen, addrBackLen);
if (ImGui::Selectable(display.c_str(), selected)) {
export_address_ = addr;
export_result_.clear();
@@ -3622,7 +3429,9 @@ void App::renderShutdownScreen()
ImGui::SetNextWindowPos(vp_pos);
ImGui::SetNextWindowSize(vp_size);
ImGui::SetNextWindowFocus();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
// Shared overlay-scrim tone (matches dialog backdrops); the shutdown screen stays an opaque
// scrim (no blur) so it reads as "closing", not a modal over your work.
ImGui::PushStyleColor(ImGuiCol_WindowBg, OverlayScrimColor(0.92f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGuiWindowFlags shutdownFlags =
@@ -3661,7 +3470,7 @@ void App::renderShutdownScreen()
ImGui::PushFont(Type().h5());
ImVec2 ts = ImGui::CalcTextSize(title);
ImGui::SetCursorPosX(cx - ts.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); // theme amber
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::PopFont();
@@ -3742,7 +3551,7 @@ void App::renderShutdownScreen()
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(forceLabel, btnSize)) {
if (ui::material::TactileButton(forceLabel, btnSize)) {
force_quit_confirm_ = true;
}
if (ImGui::IsItemHovered()) {
@@ -3771,14 +3580,15 @@ void App::renderShutdownScreen()
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) {
// Destructive action — theme error color (alpha-varied for hover/active) instead of hardcoded red.
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
if (ui::material::TactileButton(TR("force_quit_yes"), ImVec2(btnW, 0))) {
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
shutdown_complete_ = true;
ImGui::CloseCurrentPopup();
+71 -2
View File
@@ -12,6 +12,7 @@
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
@@ -294,6 +295,9 @@ public:
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
// near-static); populates state.market.exchanges. Safe to call every frame.
void refreshExchanges();
// 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();
/// @brief Per-category refresh intervals, adjusted by active tab
using RefreshIntervals = services::NetworkRefreshService::Intervals;
@@ -304,7 +308,17 @@ public:
// UI navigation
void setCurrentPage(ui::NavPage page);
ui::NavPage getCurrentPage() const { return current_page_; }
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
void startScreenshotSweep();
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 showExportKeyDialog() { show_export_key_ = true; }
@@ -394,6 +408,12 @@ public:
// Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase);
// Post-encrypt daemon restart: the daemon shuts itself down after
// encryptwallet, so restart it off the main thread. Shared by the
// immediate and deferred encryption continuations. When
// announceRestartStatus is true, connection_status_ is updated first so
// the loading overlay explains the restart.
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
void unlockWallet(const std::string& passphrase, int timeout);
void lockWallet();
void changePassphrase(const std::string& oldPass, const std::string& newPass);
@@ -443,8 +463,16 @@ private:
friend class AppDaemonLifecycleRuntime;
friend class AppDaemonLifecycleTaskContext;
// Global keyboard-shortcut handling, dispatched once per frame from update().
void handleGlobalShortcuts();
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
void maybeFinishTransactionSendProgress();
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
void upsertPendingSendTransaction(const std::string& opid,
const std::string& from,
const std::string& to,
@@ -461,10 +489,28 @@ private:
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool, const std::string&)> callback);
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
// 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.
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);
void markPendingSendTransactionSucceeded(const std::string& opid,
const std::string& txid);
void removePendingSendTransactions(const std::vector<std::string>& opids,
bool restoreBalances);
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
// same clamp. Shared by the three pending-send delta sites so they can't drift.
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
bool includeAggregates);
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
// reaches a terminal status. Returns true if a callback was registered (and fired).
bool invokeSendResultCallback(const std::string& opid, bool ok,
@@ -473,6 +519,14 @@ private:
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
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
// curve on every failed path (a PIN RPC failure used to skip it).
void applyUnlockSuccess(const std::string& passphrase, int timeout);
void applyUnlockFailure(const std::string& errorMessage);
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
// rescan-completion sites so a new witness field can't be forgotten in one copy.
void resetWitnessRescanProgress();
void loadTransactionHistoryCacheIfAvailable();
void storeTransactionHistoryCacheIfAvailable();
void wipePendingTransactionHistoryCachePassphrase();
@@ -526,6 +580,7 @@ private:
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
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
util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
@@ -593,6 +648,21 @@ private:
ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode
// Debug screenshot sweep state.
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
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
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
@@ -794,7 +864,6 @@ private:
// Private methods - rendering
void renderStatusBar();
void renderAboutDialog();
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
void renderImportKeyDialog();
+238 -165
View File
@@ -40,8 +40,10 @@
#include "daemon/xmrig_manager.h"
#include "util/pool_registry.h"
#include "ui/notifications.h"
#include "ui/schema/skin_manager.h"
#include "default_banlist_embedded.h"
#include "util/amount_format.h"
#include "util/http_download.h"
#include "util/platform.h"
#include "util/perf_log.h"
#include "util/i18n.h"
@@ -52,6 +54,7 @@
#include <algorithm>
#include <cmath>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <utility>
@@ -740,6 +743,108 @@ void App::setCurrentPage(ui::NavPage page)
}
}
// ── Debug screenshot sweep ──────────────────────────────────────────────────
// Cycle every skin x every build-enabled tab, saving one PNG per (skin,tab). Driven from
// App::render() (updateScreenshotSweep pins the page + settles); main.cpp reads the framebuffer
// on the settled frame and calls onScreenshotCaptured() to advance.
// Filesystem-safe one-word page name (tracePageName returns "Overview tab" etc.).
static 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::Mining: return "mining";
case ui::NavPage::Market: return "market";
case ui::NavPage::Console: return "console";
case ui::NavPage::Peers: return "network";
case ui::NavPage::Explorer: return "explorer";
case ui::NavPage::Settings: return "settings";
default: return "page";
}
}
std::string App::screenshotDir() const
{
return (std::filesystem::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
}
void App::startScreenshotSweep()
{
if (screenshot_sweep_active_) return;
sweep_skins_.clear();
for (const auto& s : ui::schema::SkinManager::instance().available())
if (s.valid) sweep_skins_.push_back(s.id);
sweep_pages_.clear();
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
ui::NavPage pg = static_cast<ui::NavPage>(p);
if (isPageEnabledForBuild(pg)) sweep_pages_.push_back(pg);
}
if (sweep_skins_.empty() || sweep_pages_.empty()) return;
// Fixed output folder (organized into per-tab subfolders below); subsequent sweeps overwrite the
// existing PNGs in place rather than creating a new folder each time.
sweep_dir_ = screenshotDir();
std::error_code ec; std::filesystem::create_directories(sweep_dir_, ec);
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
sweep_saved_page_ = current_page_;
sweep_skin_idx_ = 0; sweep_page_idx_ = 0;
screenshot_sweep_active_ = true;
sweep_capture_this_frame_ = false;
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
applySweepTarget();
ui::Notifications::instance().info("Screenshot sweep running…");
DEBUG_LOGF("[Sweep] -> %s (%d skins x %d pages)\n",
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_pages_.size());
}
void App::applySweepTarget()
{
current_page_ = sweep_pages_[sweep_page_idx_];
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
// Organized as <screenshots>/<tab>/<skin>.png — one subfolder per tab, one PNG per theme,
// overwritten in place on the next sweep. (writePng in main.cpp creates the parent subfolder.)
sweep_current_path_ = (std::filesystem::path(sweep_dir_) / sweepPageName(current_page_)
/ (sweep_skins_[sweep_skin_idx_] + ".png")).string();
sweep_settle_frames_ = 4; // let the new skin (TOML reload / acrylic recapture) + page settle
sweep_capture_this_frame_ = false;
}
void App::updateScreenshotSweep()
{
if (!screenshot_sweep_active_) return;
current_page_ = sweep_pages_[sweep_page_idx_]; // keep the page pinned each frame
page_alpha_ = 1.0f;
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;
if (++sweep_page_idx_ >= static_cast<int>(sweep_pages_.size())) {
sweep_page_idx_ = 0;
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
// Done — restore the original skin + page.
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
current_page_ = sweep_saved_page_;
page_alpha_ = 1.0f;
screenshot_sweep_active_ = false;
ui::Notifications::instance().success("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();
}
bool App::shouldRefreshTransactions() const
{
// NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new
@@ -800,22 +905,7 @@ void App::upsertPendingSendTransaction(const std::string& opid,
state_.transactions.insert(state_.transactions.begin(), std::move(pending));
}
if (newPending) {
auto applyDelta = [&](std::vector<AddressInfo>& addresses) {
for (auto& address : addresses) {
if (address.address == pendingInfo.from) {
address.balance = std::max(0.0, address.balance - pendingInfo.amount);
return true;
}
}
return false;
};
if (!applyDelta(state_.z_addresses)) applyDelta(state_.t_addresses);
if (!pendingInfo.from.empty() && pendingInfo.from[0] == 'z') {
state_.privateBalance = std::max(0.0, state_.privateBalance - pendingInfo.amount);
} else {
state_.transparentBalance = std::max(0.0, state_.transparentBalance - pendingInfo.amount);
}
state_.totalBalance = std::max(0.0, state_.totalBalance - pendingInfo.amount);
applyPendingSendDelta(pendingInfo.from, -pendingInfo.amount, /*includeAggregates*/ true);
}
state_.last_tx_update = std::time(nullptr);
}
@@ -850,22 +940,8 @@ void App::removePendingSendTransactions(const std::vector<std::string>& opids,
for (const auto& opid : opidSet) {
auto pending = pending_send_info_.find(opid);
if (pending == pending_send_info_.end()) continue;
auto restoreBalance = [&](std::vector<AddressInfo>& addresses) {
for (auto& address : addresses) {
if (address.address == pending->second.from) {
address.balance += pending->second.amount;
return true;
}
}
return false;
};
if (!restoreBalance(state_.z_addresses)) restoreBalance(state_.t_addresses);
if (!pending->second.from.empty() && pending->second.from[0] == 'z') {
state_.privateBalance += pending->second.amount;
} else {
state_.transparentBalance += pending->second.amount;
}
state_.totalBalance += pending->second.amount;
applyPendingSendDelta(pending->second.from, pending->second.amount,
/*includeAggregates*/ true);
}
}
state_.transactions.erase(
@@ -878,6 +954,32 @@ void App::removePendingSendTransactions(const std::vector<std::string>& opids,
state_.last_tx_update = std::time(nullptr);
}
void App::applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
bool includeAggregates)
{
// For a debit (signedAmount < 0) this clamps at >=0 exactly as the debit sites did. For a
// restore (signedAmount > 0) the clamp is a no-op — max(0, bal+amt) == bal+amt when bal,amt>=0 —
// so the single clamped form reproduces the original restore's unclamped bal += amt.
auto applyToAddress = [&](std::vector<AddressInfo>& addresses) {
for (auto& address : addresses) {
if (address.address == fromAddress) {
address.balance = std::max(0.0, address.balance + signedAmount);
return true;
}
}
return false;
};
if (!applyToAddress(state_.z_addresses)) applyToAddress(state_.t_addresses);
if (includeAggregates) {
if (!fromAddress.empty() && fromAddress[0] == 'z') {
state_.privateBalance = std::max(0.0, state_.privateBalance + signedAmount);
} else {
state_.transparentBalance = std::max(0.0, state_.transparentBalance + signedAmount);
}
state_.totalBalance = std::max(0.0, state_.totalBalance + signedAmount);
}
}
void App::trackOperation(const std::string& opid)
{
if (opid.empty()) return;
@@ -903,27 +1005,28 @@ void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances)
{
for (const auto& [opid, pending] : pending_send_info_) {
(void)opid;
auto applyDelta = [&](std::vector<AddressInfo>& addresses) {
for (auto& address : addresses) {
if (address.address == pending.from) {
address.balance = std::max(0.0, address.balance - pending.amount);
return true;
}
}
return false;
};
if (!applyDelta(state_.z_addresses)) applyDelta(state_.t_addresses);
if (includeAggregateBalances) {
if (!pending.from.empty() && pending.from[0] == 'z') {
state_.privateBalance = std::max(0.0, state_.privateBalance - pending.amount);
} else {
state_.transparentBalance = std::max(0.0, state_.transparentBalance - pending.amount);
}
state_.totalBalance = std::max(0.0, state_.totalBalance - pending.amount);
}
applyPendingSendDelta(pending.from, -pending.amount, includeAggregateBalances);
}
}
void App::resetWitnessRescanProgress()
{
// Common core of the four rescan-completion sites. Deliberately does NOT set
// rescan_progress (callers set it to 1.0 only on success) or runtime_rescan_active_
// (only the manual-rescan path uses it), and is distinct from the witness phase-transition
// reset in App::update() which sets witness_phase to the new phase rather than clearing it.
rescan_confirmed_active_ = false;
state_.sync.rescanning = false;
state_.sync.rescan_status.clear();
state_.sync.building_witnesses = false;
state_.sync.witness_phase = 0;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 0;
}
std::string App::transactionHistoryCacheWalletIdentity() const
{
std::vector<std::string> shieldedAddresses;
@@ -1156,17 +1259,8 @@ void App::refreshCoreData()
// rescan_confirmed_active_ ensures we actually observed this rescan running (set
// by the getrescaninfo / daemon-log pollers) before declaring it done.
if (state_.sync.rescanning && rescan_confirmed_active_) {
state_.sync.rescanning = false;
rescan_confirmed_active_ = false;
resetWitnessRescanProgress();
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
state_.sync.building_witnesses = false;
state_.sync.witness_phase = 0;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 0;
// Notes/witnesses were rebuilt — force a fresh history + balance pull.
transactions_dirty_ = true;
last_tx_block_height_ = -1;
@@ -1632,6 +1726,46 @@ void App::refreshMarketData()
{
refreshPrice();
refreshExchanges();
refreshMarketChart();
}
void App::refreshMarketChart()
{
if (!settings_ || !settings_->getFetchPrices()) return;
if (!worker_) return;
if (chart_fetch_in_flight_) return;
// Historical data moves slowly — refresh at most every 30 minutes after the first fetch.
if (state_.market.chart_loaded &&
std::chrono::steady_clock::now() - state_.market.chart_last_fetch_time < std::chrono::minutes(30))
return;
chart_fetch_in_flight_ = true;
worker_->post([this]() -> rpc::RPCWorker::MainCb {
// Two ranges give real granularity across every sparkline interval:
// days=1 -> 5-minute samples (backs the "hour" interval)
// days=365 -> daily samples (backs "day"/"week"/"month")
// httpGetString verifies TLS and returns "" on any failure (parses to an empty series).
std::string intradayBody = util::httpGetString(
"https://api.coingecko.com/api/v3/coins/dragonx-2/market_chart?vs_currency=usd&days=1",
"[market-chart]");
std::string dailyBody = util::httpGetString(
"https://api.coingecko.com/api/v3/coins/dragonx-2/market_chart?vs_currency=usd&days=365",
"[market-chart]");
auto intraday = NetworkRefreshService::parseCoinGeckoMarketChart(intradayBody);
auto daily = NetworkRefreshService::parseCoinGeckoMarketChart(dailyBody);
return [this, intraday = std::move(intraday), daily = std::move(daily)]() mutable {
chart_fetch_in_flight_ = false;
const bool any = !intraday.empty() || !daily.empty();
if (!intraday.empty()) state_.market.price_chart_intraday = std::move(intraday);
if (!daily.empty()) state_.market.price_chart_daily = std::move(daily);
if (any) {
state_.market.chart_loaded = true;
state_.market.chart_last_fetch_time = std::chrono::steady_clock::now();
}
// On total failure, leave chart_loaded=false so the next tick retries.
};
});
}
void App::refreshExchanges()
@@ -1944,64 +2078,29 @@ void App::applyDefaultBanlist()
void App::createNewZAddress(std::function<void(const std::string&)> callback)
{
// Lite build: derive locally via the controller (fast, no network). The backend auto-saves
// new addresses; the next lite refresh lists it with a balance.
if (lite_wallet_) {
const auto result = lite_wallet_->newAddress(/*shielded*/ true);
if (result.ok) {
AddressInfo info;
info.address = result.address;
info.type = "shielded";
info.balance = 0.0;
state_.z_addresses.push_back(info);
state_.addresses.push_back(info);
address_list_dirty_ = true;
}
if (callback) callback(result.ok ? result.address : std::string());
return;
}
if (!state_.connected || !rpc_ || !worker_) return;
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
std::string addr;
try {
rpc::RPCClient::TraceScope trace("Receive tab / New shielded address");
json result = rpc_->call("z_getnewaddress");
addr = result.get<std::string>();
} catch (const std::exception& e) {
DEBUG_LOGF("z_getnewaddress error: %s\n", e.what());
}
return [this, callback, addr]() {
if (!addr.empty()) {
// Inject immediately so UI can select the address next frame
AddressInfo info;
info.address = addr;
info.type = "shielded";
info.balance = 0.0;
state_.z_addresses.push_back(info);
state_.addresses.push_back(info); // keep combined view in sync immediately
address_list_dirty_ = true;
// Also trigger full refresh to get proper balances
addresses_dirty_ = true;
refreshAddresses();
}
if (callback) callback(addr);
};
});
createNewAddress(/*shielded*/ true, std::move(callback));
}
void App::createNewTAddress(std::function<void(const std::string&)> callback)
{
// Lite build: derive locally via the controller (see createNewZAddress).
createNewAddress(/*shielded*/ false, std::move(callback));
}
void App::createNewAddress(bool shielded, std::function<void(const std::string&)> callback)
{
const char* typeStr = shielded ? "shielded" : "transparent";
auto& targetList = shielded ? state_.z_addresses : state_.t_addresses;
// Lite build: derive locally via the controller (fast, no network). The backend auto-saves
// new addresses; the next lite refresh lists it with a balance.
if (lite_wallet_) {
const auto result = lite_wallet_->newAddress(/*shielded*/ false);
const auto result = lite_wallet_->newAddress(shielded);
if (result.ok) {
AddressInfo info;
info.address = result.address;
info.type = "transparent";
info.type = typeStr;
info.balance = 0.0;
state_.t_addresses.push_back(info);
targetList.push_back(info);
state_.addresses.push_back(info);
address_list_dirty_ = true;
}
@@ -2011,23 +2110,26 @@ void App::createNewTAddress(std::function<void(const std::string&)> callback)
if (!state_.connected || !rpc_ || !worker_) return;
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
const char* rpcMethod = shielded ? "z_getnewaddress" : "getnewaddress";
const char* traceLabel = shielded ? "Receive tab / New shielded address"
: "Receive tab / New transparent address";
worker_->post([this, callback, shielded, typeStr, rpcMethod, traceLabel]() -> rpc::RPCWorker::MainCb {
std::string addr;
try {
rpc::RPCClient::TraceScope trace("Receive tab / New transparent address");
json result = rpc_->call("getnewaddress");
rpc::RPCClient::TraceScope trace(traceLabel);
json result = rpc_->call(rpcMethod);
addr = result.get<std::string>();
} catch (const std::exception& e) {
DEBUG_LOGF("getnewaddress error: %s\n", e.what());
DEBUG_LOGF("%s error: %s\n", rpcMethod, e.what());
}
return [this, callback, addr]() {
return [this, callback, addr, shielded, typeStr]() {
if (!addr.empty()) {
// Inject immediately so UI can select the address next frame
AddressInfo info;
info.address = addr;
info.type = "transparent";
info.type = typeStr;
info.balance = 0.0;
state_.t_addresses.push_back(info);
(shielded ? state_.z_addresses : state_.t_addresses).push_back(info);
state_.addresses.push_back(info); // keep combined view in sync immediately
address_list_dirty_ = true;
// Also trigger full refresh to get proper balances
@@ -2405,26 +2507,36 @@ void App::sendTransaction(const std::string& from, const std::string& to,
return;
}
send_progress_active_ = true;
++send_submissions_in_flight_;
// z_sendmany signature is (fromaddress, amounts, minconf, fee). The fee MUST be a JSON number:
// the daemon reads it with get_real(), so a string is rejected ("JSON value is not a number as
// expected"). get_real() parses the double directly and accepts any number notation (incl. the
// "5e-05" form of a small fee), so passing the raw double is correct for every fee value.
// (The recipient "amount" above is intentionally a fixed-decimal STRING — that field is parsed
// with ParseFixedPoint, which a scientific-notation double would break.)
worker_->post([this, from, to, amount, fee, memo, recipients, callback]() -> rpc::RPCWorker::MainCb {
submitZSendMany(from, to, amount, fee, memo, recipients, "Send tab / Submit transaction",
/*markFeeGapRetry*/ false, std::move(callback));
}
void App::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)
{
send_progress_active_ = true;
++send_submissions_in_flight_;
worker_->post([this, from, to, amount, fee, memo, recipients, callback, traceLabel,
markFeeGapRetry]() -> rpc::RPCWorker::MainCb {
bool ok = false;
std::string result_str;
try {
rpc::RPCClient::TraceScope trace("Send tab / Submit transaction");
rpc::RPCClient::TraceScope trace(traceLabel);
auto result = rpc_->call("z_sendmany", {from, recipients, 1, fee});
result_str = result.get<std::string>();
ok = true;
} catch (const std::exception& e) {
result_str = e.what();
}
return [this, callback, ok, result_str, from, to, amount, fee, memo]() {
return [this, callback, ok, result_str, from, to, amount, fee, memo, markFeeGapRetry]() {
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
if (ok) {
// A send changes address balances — refresh on next cycle
@@ -2439,6 +2551,8 @@ void App::sendTransaction(const std::string& from, const std::string& to,
// "sent successfully" for an operation that may still fail.
if (!result_str.empty()) {
pending_opids_.push_back(result_str);
if (markFeeGapRetry)
send_feegap_retried_opids_.insert(result_str); // a retry of a retry is a real error
upsertPendingSendTransaction(result_str, from, to, amount, memo, fee);
if (callback) pending_send_callbacks_[result_str] = callback;
} else if (callback) {
@@ -2528,40 +2642,8 @@ void App::resendWithFeeGapWorkaround(const std::string& from, const std::string&
selfOut["amount"] = util::formatAmountFixed(fee);
recipients.push_back(selfOut);
send_progress_active_ = true;
++send_submissions_in_flight_;
worker_->post([this, from, to, amount, fee, memo, recipients, callback]() -> rpc::RPCWorker::MainCb {
bool ok = false;
std::string result_str;
try {
rpc::RPCClient::TraceScope trace("Send tab / Fee-gap retry");
auto result = rpc_->call("z_sendmany", {from, recipients, 1, fee});
result_str = result.get<std::string>();
ok = true;
} catch (const std::exception& e) {
result_str = e.what();
}
return [this, callback, ok, result_str, from, to, amount, fee, memo]() {
if (send_submissions_in_flight_ > 0) --send_submissions_in_flight_;
if (ok) {
addresses_dirty_ = true;
transactions_dirty_ = true;
last_tx_block_height_ = -1;
network_refresh_.markWalletMutationRefresh();
if (!result_str.empty()) {
pending_opids_.push_back(result_str);
send_feegap_retried_opids_.insert(result_str); // a retry of a retry is a real error
upsertPendingSendTransaction(result_str, from, to, amount, memo, fee);
if (callback) pending_send_callbacks_[result_str] = callback;
} else if (callback) {
callback(true, result_str);
}
} else {
send_progress_active_ = false;
if (callback) callback(false, result_str);
}
};
});
submitZSendMany(from, to, amount, fee, memo, recipients, "Send tab / Fee-gap retry",
/*markFeeGapRetry*/ true, std::move(callback));
}
// --------------------------------------------------------------------------------------------
@@ -2652,16 +2734,7 @@ void App::runtimeRescan(int startHeight)
}
return [this, ok, err]() {
runtime_rescan_active_ = false;
rescan_confirmed_active_ = false;
state_.sync.rescanning = false;
state_.sync.rescan_status.clear();
state_.sync.building_witnesses = false;
state_.sync.witness_phase = 0;
state_.sync.witness_progress = 0.0f;
state_.sync.witness_remaining = 0;
witness_rebuild_total_blocks_ = 0;
witness_seen_txids_.clear();
witness_total_txs_ = 0;
resetWitnessRescanProgress();
if (ok) {
state_.sync.rescan_progress = 1.0f;
transactions_dirty_ = true;
+157 -105
View File
@@ -230,6 +230,34 @@ private:
// Wallet encryption helpers
// ===========================================================================
// The daemon shuts itself down after encryptwallet. Restart the embedded
// daemon off the main thread (to avoid stalling the UI), or ask the user to
// restart an external daemon. Shared by encryptWalletWithPassphrase() and
// processDeferredEncryption(); must be called on the main thread.
void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus) {
if (isUsingEmbeddedDaemon()) {
if (announceRestartStatus) {
// Update connection_status_ so the loading overlay explains why
// the daemon is restarting.
connection_status_ = TR("restarting_after_encryption");
}
// Give daemon a moment to shut down, then restart
// (do this off the main thread to avoid stalling the UI)
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
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
}
}
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true;
@@ -267,23 +295,8 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
// explains why the daemon is restarting.
if (isUsingEmbeddedDaemon()) {
connection_status_ = TR("restarting_after_encryption");
// Give daemon a moment to shut down, then restart
// (do this off the main thread to avoid stalling the UI)
async_tasks_.submit("encrypt-daemon-restart", [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
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
}
restartDaemonAfterEncryption("encrypt-daemon-restart",
/*announceRestartStatus=*/true);
};
} else {
std::string err = result.error;
@@ -366,20 +379,8 @@ void App::processDeferredEncryption() {
wallet_security_.clearDeferredEncryption();
// Restart daemon (it shuts itself down after encryptwallet)
if (isUsingEmbeddedDaemon()) {
async_tasks_.submit("deferred-encrypt-daemon-restart", [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
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
}
restartDaemonAfterEncryption("deferred-encrypt-daemon-restart",
/*announceRestartStatus=*/false);
};
} else {
std::string err = result.error;
@@ -396,6 +397,38 @@ void App::processDeferredEncryption() {
}
}
// Shared main-thread success continuation for both the passphrase and PIN unlock paths.
// walletpassphrase has already succeeded on the worker thread, so set the unlocked state
// directly rather than issuing another RPC round-trip.
void App::applyUnlockSuccess(const std::string& passphrase, int timeout) {
lock_unlock_in_progress_ = false;
lock_error_msg_.clear();
lock_attempts_ = 0;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
last_interaction_ = std::chrono::steady_clock::now();
state_.encrypted = true;
state_.locked = false;
state_.unlocked_until = std::time(nullptr) + timeout;
unlockTransactionHistoryCacheWithPassphrase(passphrase);
}
// Shared main-thread failure continuation: bump the attempt counter, surface the error,
// and apply the escalating lockout curve. Every failed unlock path routes through here so
// none can silently skip the lockout math (the PIN RPC-error path previously did).
void App::applyUnlockFailure(const std::string& errorMessage) {
lock_unlock_in_progress_ = false;
lock_attempts_++;
lock_error_msg_ = errorMessage;
lock_error_timer_ = 3.0f;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
const float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
const int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
if (lock_attempts_ >= maxAttempts) {
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
}
DEBUG_LOGF("[App] Wallet unlock failed (attempt %d)\n", lock_attempts_);
}
void App::unlockWallet(const std::string& passphrase, int timeout) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
lock_unlock_in_progress_ = true;
@@ -411,30 +444,11 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
return [this, ok, err_msg, timeout, passphrase = std::move(cachePassphrase)]() mutable {
lock_unlock_in_progress_ = false;
if (ok) {
lock_error_msg_.clear();
lock_attempts_ = 0;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
last_interaction_ = std::chrono::steady_clock::now();
// Set unlock state immediately — walletpassphrase
// already succeeded, no need for another RPC round-trip.
state_.encrypted = true;
state_.locked = false;
state_.unlocked_until = std::time(nullptr) + timeout;
unlockTransactionHistoryCacheWithPassphrase(passphrase);
applyUnlockSuccess(passphrase, timeout);
} else {
lock_attempts_++;
lock_error_msg_ = TR("incorrect_passphrase");
lock_error_timer_ = 3.0f;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
if (lock_attempts_ >= maxAttempts) {
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
}
DEBUG_LOGF("[App] Wallet unlock failed (attempt %d): %s\n", lock_attempts_, err_msg.c_str());
DEBUG_LOGF("[App] Passphrase unlock RPC error: %s\n", err_msg.c_str());
applyUnlockFailure(TR("incorrect_passphrase"));
}
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
@@ -753,15 +767,55 @@ void App::checkIdleMining() {
void App::renderLockScreen() {
using namespace ui::material;
// Full-window lock overlay: cover the ENTIRE viewport (sidebar included) so the whole screen is
// obscured while locked, not just the tab-content area. A borderless, transparent, focused window
// over the main viewport (same pattern as the modal overlays; opened from within ##ContentArea,
// like the portfolio modal). Ended at the bottom of this function.
ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 winPos = vp->Pos, winSize = vp->Size;
ImGui::SetNextWindowPos(winPos);
ImGui::SetNextWindowSize(winSize);
ImGui::SetNextWindowFocus();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // transparent — we draw the backdrop
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::Begin("##LockOverlay", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings);
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
ImVec2 winSize = ImGui::GetWindowSize();
// Consume input to everything behind (sidebar included) while locked. AllowOverlap so the lock
// card's widgets (drawn later, on top of this full-window button) still receive clicks/hover —
// without it this blocker shadows them (dead clicks + a whole-screen hand cursor).
ImGui::SetNextItemAllowOverlap();
ImGui::SetCursorScreenPos(winPos);
ImGui::InvisibleButton("##LockInputBlocker", winSize,
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle);
// Optional backdrop (0 = no darkening)
float backdropAlpha = ui::schema::UI().drawElement("screens.lock-screen", "backdrop-alpha").opacity;
if (backdropAlpha > 0.0f) {
ImU32 backdropCol = IM_COL32(0, 0, 0, (int)(255 * backdropAlpha));
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), backdropCol);
// Live-blur backdrop over the wallet content while locked — matches the app's modal look AND
// obscures the wallet for privacy (the old backdrop defaulted to 0 = fully visible). Capture-once
// on lock-open + resize; the opaque base inside DrawFullWindowBlurBackdrop guarantees the wallet
// is obscured even when the blur is unavailable (low-spec / acrylic-off / first frame / capture
// failure), so this can't leak content. Purely visual — the auth logic below is untouched.
ImVec2 winMax(winPos.x + winSize.x, winPos.y + winSize.y);
{
BlurCaptureState& cap = BlurCaptureStateFor("##LockScreen");
int frame = ImGui::GetFrameCount();
bool justLocked = (cap.lastSeenFrame < frame - 1);
cap.lastSeenFrame = frame;
if (justLocked || cap.w != winSize.x || cap.h != winSize.y) cap.captureFrames = 3;
cap.w = winSize.x; cap.h = winSize.y;
CapturingBlurBackdropRef() = (cap.captureFrames > 0);
if (cap.captureFrames > 0) {
if (ImDrawCallback liveCap = ui::effects::ImGuiAcrylic::GetLiveCaptureCallback()) {
dl->AddCallback(liveCap, nullptr);
dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
}
cap.captureFrames--;
}
DrawFullWindowBlurBackdrop(dl, winPos, winMax, /*allowBlur=*/!justLocked);
MarkBlurOverlayDrawn(nullptr); // suppress foreground theme-effect bleed + re-capture on unlock
}
// Card
@@ -776,8 +830,14 @@ void App::renderLockScreen() {
ImVec2 cardMin(cardX, cardY);
ImVec2 cardMax(cardX + cardW, cardY + cardH);
ImU32 cardBg = ui::material::SurfaceVariant();
dl->AddRectFilled(cardMin, cardMax, cardBg, 16.0f);
// Match the dialog cards: the same glass panel BeginOverlayDialog draws (renders opaque-dark on
// the blur via the sole-consumer fallback), instead of a flat light SurfaceVariant fill.
GlassPanelSpec cardGlass;
cardGlass.rounding = 16.0f;
cardGlass.fillAlpha = 35;
cardGlass.borderAlpha = 50;
cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
float cy = cardY + 24.0f;
@@ -895,6 +955,12 @@ void App::renderLockScreen() {
ImGui::PopStyleVar();
ImGui::PopItemWidth();
}
// Focus indicator: a clear accent ring around the input while it's focused/being edited — the
// 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);
}
cy += 40.0f + 12.0f;
// Focus the input when the lock screen first appears.
@@ -922,10 +988,8 @@ void App::renderLockScreen() {
float rowH = captionFont->LegacySize + 8.0f;
if (lock_unlock_in_progress_) {
// Animated spinner dots
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char msg[64];
snprintf(msg, sizeof(msg), "Unlocking%s", dotStr[dots]);
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -947,7 +1011,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ImGui::Button("Unlock", ImVec2(unlockW, unlockH));
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -980,21 +1044,15 @@ void App::renderLockScreen() {
if (!vaultOk) {
bool noVault = !vault_ || !vault_->hasVault();
return [this, noVault]() {
lock_unlock_in_progress_ = false;
if (noVault) {
// Vault file missing — switch to passphrase mode
// Vault file missing — switch to passphrase mode. Not a failed
// attempt, so no counter bump / lockout escalation.
lock_unlock_in_progress_ = false;
lock_error_msg_ = TR("pin_not_set");
lock_use_pin_ = false;
lock_error_timer_ = 3.0f;
} else {
lock_attempts_++;
lock_error_msg_ = TR("incorrect_pin");
}
lock_error_timer_ = 3.0f;
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
if (lock_attempts_ >= maxAttempts) {
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
applyUnlockFailure(TR("incorrect_pin"));
}
};
}
@@ -1015,25 +1073,15 @@ void App::renderLockScreen() {
if (rpcOk) {
return [this, timeout, passphrase = std::move(passphrase)]() mutable {
lock_unlock_in_progress_ = false;
lock_error_msg_.clear();
lock_attempts_ = 0;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
last_interaction_ = std::chrono::steady_clock::now();
// Set unlock state immediately — walletpassphrase
// already succeeded, no need for another RPC round-trip.
state_.encrypted = true;
state_.locked = false;
state_.unlocked_until = std::time(nullptr) + timeout;
unlockTransactionHistoryCacheWithPassphrase(passphrase);
applyUnlockSuccess(passphrase, timeout);
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
} else {
// Vault decrypt succeeded but the RPC failed — still a failed unlock,
// so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
lock_unlock_in_progress_ = false;
lock_attempts_++;
lock_error_msg_ = "Unlock failed: " + rpcErr;
lock_error_timer_ = 3.0f;
applyUnlockFailure("Unlock failed: " + rpcErr);
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
}
@@ -1044,6 +1092,10 @@ void App::renderLockScreen() {
unlockWallet(std::string(lock_passphrase_buf_), timeout);
}
}
ImGui::End(); // ##LockOverlay
ImGui::PopStyleVar(3); // WindowRounding, WindowBorderSize, WindowPadding
ImGui::PopStyleColor(); // WindowBg
}
// ===========================================================================
@@ -1124,7 +1176,7 @@ void App::renderEncryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
if (ImGui::Button("Encrypt Wallet", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("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_));
@@ -1136,7 +1188,7 @@ void App::renderEncryptWalletDialog() {
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("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;
@@ -1218,7 +1270,7 @@ void App::renderEncryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
if (ImGui::Button("Set PIN", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("Set PIN", ImVec2(btnW, 40))) {
pin_in_progress_ = true;
enc_dlg_pin_status_.clear();
std::string savedPass = enc_dlg_saved_passphrase_;
@@ -1255,7 +1307,7 @@ void App::renderEncryptWalletDialog() {
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Skip", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("Skip", ImVec2(btnW, 40))) {
if (!enc_dlg_saved_passphrase_.empty()) {
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
enc_dlg_saved_passphrase_.size());
@@ -1310,7 +1362,7 @@ 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 (ImGui::Button("Change Passphrase", ImVec2(-1, 40))) {
if (ui::material::TactileButton("Change Passphrase", ImVec2(-1, 40))) {
changePassphrase(std::string(change_old_pass_buf_),
std::string(change_new_pass_buf_));
}
@@ -1376,7 +1428,7 @@ void App::renderDecryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
ImGui::BeginDisabled(!valid || decryptState.inProgress);
if (ImGui::Button("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
if (ui::material::TactileButton("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());
@@ -1567,7 +1619,7 @@ void App::renderDecryptWalletDialog() {
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("Cancel", ImVec2(btnW, 40))) {
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
show_decrypt_dialog_ = false;
}
@@ -1685,7 +1737,7 @@ void App::renderDecryptWalletDialog() {
"was saved as wallet.dat.encrypted.bak in your data directory.");
ImGui::Spacing();
if (ImGui::Button("Close", ImVec2(-1, 40))) {
if (ui::material::TactileButton("Close", ImVec2(-1, 40))) {
show_decrypt_dialog_ = false;
}
@@ -1702,11 +1754,11 @@ void App::renderDecryptWalletDialog() {
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Try Again", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("Try Again", ImVec2(btnW, 40))) {
wallet_security_workflow_.reset();
}
ImGui::SameLine();
if (ImGui::Button("Close", ImVec2(btnW, 40))) {
if (ui::material::TactileButton("Close", ImVec2(btnW, 40))) {
show_decrypt_dialog_ = false;
}
}
@@ -1762,7 +1814,7 @@ void App::renderPinDialogs() {
strcmp(pin_buf_, pin_confirm_buf_) == 0;
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ImGui::Button("Set PIN", ImVec2(-1, 40))) {
if (ui::material::TactileButton("Set PIN", ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying passphrase...";
@@ -1858,7 +1910,7 @@ void App::renderPinDialogs() {
strcmp(pin_buf_, pin_confirm_buf_) == 0;
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ImGui::Button("Change PIN", ImVec2(-1, 40))) {
if (ui::material::TactileButton("Change PIN", ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Changing PIN...";
std::string oldPin(pin_old_buf_);
@@ -1917,7 +1969,7 @@ void App::renderPinDialogs() {
ImGui::Spacing();
bool valid = strlen(pin_old_buf_) >= 4;
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ImGui::Button("Remove PIN", ImVec2(-1, 40))) {
if (ui::material::TactileButton("Remove PIN", ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying PIN...";
std::string oldPin(pin_old_buf_);
+12 -12
View File
@@ -611,7 +611,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 (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
// Save appearance choices, advance to Bootstrap
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
@@ -763,7 +763,7 @@ void App::renderFirstRunWizard() {
float cancelBX = rightX + (colW - cancelW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
bootstrap_->cancel();
}
ImGui::PopStyleVar();
@@ -812,7 +812,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
@@ -826,7 +826,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -896,7 +896,7 @@ 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 (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true;
wizard_stop_status_ = "Sending stop command...";
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
@@ -929,7 +929,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -995,7 +995,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
@@ -1014,7 +1014,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
@@ -1032,7 +1032,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 (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -1127,7 +1127,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 (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
@@ -1310,7 +1310,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canEncrypt);
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing
wallet_security_.beginDeferredEncryption(
std::string(encrypt_pass_buf_),
@@ -1341,7 +1341,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
File diff suppressed because it is too large Load Diff
+505
View File
@@ -0,0 +1,505 @@
#pragma once
#include "chat_protocol.h"
#include <cstddef>
#include <string>
#include <vector>
// HushChat compatibility-fixture / capture-manifest / seed-projection tooling.
//
// Everything in this header is DEAD AT RUNTIME in the shipping app and test
// binaries — its only caller is the standalone dev CLI
// tools/hushchat_fixture_check.cpp. It is deliberately compiled ONLY into the
// HushChatFixtureCheck target so this validation scaffolding (and its libsodium
// seed-projection path) never reaches the wallet binary. Keep it that way: do
// not add chat_fixture_tooling.cpp to APP_SOURCES or the test target.
//
// It builds on the runtime types declared in chat_protocol.h.
namespace dragonx::chat {
enum class HushChatDecryptPreflightError {
None,
FeatureDisabled,
NonMessageHeader,
InvalidHeaderNumber,
UnsupportedVersion,
MissingReplyAddress,
MissingConversationId,
InvalidSecretstreamHeader,
InvalidPublicKey,
EmptyCiphertext,
OversizedCiphertext,
OddLengthCiphertext,
InvalidCiphertextHex,
TruncatedCiphertext
};
struct HushChatDecryptPreflightInput {
HushChatHeader header;
std::string ciphertext_hex;
};
struct HushChatDecryptPreflightResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
const char* error_name = "None";
std::size_t ciphertext_size = 0;
};
enum class HushChatHexDecodeError {
None,
Empty,
OddLength,
InvalidHex,
UnexpectedByteLength
};
struct HushChatHexDecodeResult {
bool ok = false;
HushChatHexDecodeError error = HushChatHexDecodeError::None;
const char* error_name = "None";
std::vector<unsigned char> bytes;
};
enum class HushChatDecryptDirection {
Incoming,
Outgoing
};
enum class HushChatSessionKeySelection {
ClientRx,
ServerTx
};
enum class HushChatDecryptInputError {
None,
FeatureDisabled,
InvalidStoredChatKey,
DecryptPreflightFailed,
InvalidPeerPublicKey,
InvalidStreamHeader,
InvalidCiphertext
};
struct HushChatDecryptInputMaterial {
std::string stored_chat_key_hex;
HushChatHeader header;
std::string ciphertext_hex;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
std::string peer_public_key_hex;
};
struct HushChatPreparedDecryptInput {
std::vector<unsigned char> stored_chat_key_bytes;
std::vector<unsigned char> seed_bytes;
std::vector<unsigned char> peer_public_key_bytes;
std::vector<unsigned char> stream_header_bytes;
std::vector<unsigned char> ciphertext_bytes;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t plaintext_capacity = 0;
};
struct HushChatDecryptInputPreparationResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptInputError error = HushChatDecryptInputError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatPreparedDecryptInput prepared;
};
struct HushChatDecryptFixtureReadinessResult {
bool ready = false;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t stream_header_size = 0;
std::size_t ciphertext_size = 0;
std::size_t plaintext_capacity = 0;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
};
enum class HushChatCompatibilityFixtureError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidLocalPublicKey,
InvalidPeerPublicKey,
InvalidHeaderMemo,
InvalidMemoPair,
NonMemoHeader,
HeaderPublicKeyMismatch,
DecryptInputFailed,
NotFixtureReady,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
ExpectedPeerPublicKeyLengthMismatch,
ExpectedStreamHeaderLengthMismatch,
ExpectedCiphertextLengthMismatch,
ExpectedPlaintextLengthMismatch,
ExpectedRoleMismatch,
InvalidPlaintextHash
};
struct HushChatCompatibilityFixture {
std::string fixture_id;
std::string stored_chat_key_hex;
std::string local_public_key_hex;
std::string peer_public_key_hex;
std::string header_memo;
std::string ciphertext_memo;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t expected_stored_chat_key_size = 32;
std::size_t expected_seed_size = 32;
std::size_t expected_local_public_key_size = 32;
std::size_t expected_peer_public_key_size = 32;
std::size_t expected_stream_header_size = 24;
std::size_t expected_ciphertext_size = 0;
std::size_t expected_plaintext_size = 0;
std::string expected_plaintext_hash_hex;
};
struct HushChatCompatibilityFixtureVerificationResult {
bool ok = false;
bool feature_enabled = false;
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatHeader header;
HushChatDecryptInputPreparationResult preparation;
HushChatDecryptFixtureReadinessResult readiness;
std::size_t local_public_key_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t plaintext_hash_size = 0;
};
enum class HushChatCompatibilityFixtureKind {
IncomingMemo,
OutgoingMemo,
SeedPublicKeyProjection,
CorruptedAuthFailure,
ContactExclusion
};
enum class HushChatCompatibilityFixtureFileStatus {
Pending,
Ready
};
enum class HushChatCompatibilityFixtureFileError {
None,
FeatureDisabled,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingKind,
UnknownKind,
MissingStatus,
UnknownStatus,
MissingFixtureId,
MissingPendingReason,
MissingFixtureObject,
InvalidFixtureField,
FixtureVerificationFailed,
ContactFixtureNotExcluded,
FileReadFailed
};
struct HushChatCompatibilityFixtureFile {
std::string schema;
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
std::string fixture_id;
std::string pending_reason;
HushChatCompatibilityFixture fixture;
};
struct HushChatCompatibilityFixtureFileParseResult {
bool ok = false;
bool feature_enabled = false;
bool pending = false;
bool verified = false;
bool excluded_from_decrypt = false;
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFile file;
HushChatCompatibilityFixtureVerificationResult verification;
};
enum class HushChatSeedPublicKeyProjectionError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidStoredChatKey,
InvalidLocalPublicKey,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
SodiumInitializationFailed,
KeypairProjectionFailed,
ProjectedPublicKeyMismatch
};
struct HushChatSeedPublicKeyProjectionResult {
bool ok = false;
bool feature_enabled = false;
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t local_public_key_size = 0;
std::size_t projected_public_key_size = 0;
};
enum class HushChatCorruptedAuthFailureReadinessError {
None,
FeatureDisabled,
FixturePending,
WrongFixtureKind,
FixtureNotVerified,
SeedProjectionNotVerified
};
struct HushChatCorruptedAuthFailureReadinessResult {
bool ok = false;
bool feature_enabled = false;
bool structurally_ready_for_future_auth_check = false;
bool requires_future_secretstream_auth_failure = false;
bool decrypted = false;
bool authenticated = false;
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
const char* error_name = "None";
};
enum class HushChatCompatibilityFixtureImportError {
None,
FeatureDisabled,
MissingRequiredKind,
DuplicateKind,
FixtureLoadFailed,
FixtureKindMismatch,
FixturePending,
FixtureInvalid,
FixtureNotVerified,
SeedProjectionFailed,
AuthFailureScaffoldFailed,
ContactFixtureNotExcluded
};
struct HushChatCompatibilityFixtureImportCandidate {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
};
struct HushChatCompatibilityFixtureImportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFileParseResult parsed;
HushChatSeedPublicKeyProjectionResult seed_projection;
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
};
struct HushChatCompatibilityFixtureImportChecklistResult {
bool ok = false;
bool feature_enabled = false;
bool replacement_ready = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureImportItem> items;
};
struct HushChatCompatibilityFixtureReplacementReportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool refused = true;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
bool cont_excluded = false;
bool decrypted = false;
bool authenticated = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
};
struct HushChatCompatibilityFixtureReplacementDryRunResult {
bool ok = false;
bool feature_enabled = false;
bool dry_run_only = true;
bool redacted_report = true;
bool would_replace = false;
bool replacement_refused = true;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
};
enum class HushChatCaptureManifestError {
None,
FeatureDisabled,
FileReadFailed,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingManifestId,
MissingStatus,
UnknownStatus,
MissingFixtureDirectory,
MissingDryRunCommand,
InvalidDryRunCommand,
MissingProvenance,
MissingSourceClient,
InvalidSourceClient,
MissingSourceClientVersion,
MissingCaptureDate,
MissingNetwork,
MissingCaptureMethod,
MissingHandling,
MissingHandlingFlag,
HandlingFlagNotTrue,
MissingCategories,
InvalidCategoryEntry,
UnknownCategory,
DuplicateCategory,
MissingRequiredCategory,
ProhibitedFieldPresent
};
enum class HushChatCaptureManifestStatus {
Staged
};
struct HushChatCaptureManifestCategoryReport {
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string staged_filename;
bool declared = false;
};
struct HushChatCaptureManifestValidationResult {
bool ok = false;
bool feature_enabled = false;
bool redacted_report = true;
bool validates_provenance_only = true;
bool no_sensitive_material_declared = false;
bool has_dry_run_command = false;
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
const char* error_name = "None";
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
std::string manifest_path;
std::string fixture_directory;
std::size_t required_count = 0;
std::size_t declared_count = 0;
std::size_t missing_count = 0;
std::size_t duplicate_count = 0;
std::size_t prohibited_field_count = 0;
std::size_t handling_flag_count = 0;
std::vector<HushChatCaptureManifestCategoryReport> categories;
};
constexpr std::size_t kHushChatSecretstreamABytes = 17;
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
constexpr std::size_t kHushChatSeedByteLength = 32;
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
const HushChatDecryptPreflightInput& input,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
std::size_t expectedByteLength);
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
const HushChatDecryptInputMaterial& material,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
const HushChatPreparedDecryptInput& prepared);
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
const HushChatCompatibilityFixtureFileParseResult& parsed,
const HushChatSeedPublicKeyProjectionResult& seedProjection,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
} // namespace dragonx::chat
File diff suppressed because it is too large Load Diff
+1 -482
View File
@@ -84,438 +84,10 @@ struct HushChatTransactionExtractionResult {
std::size_t ignored_memo_count = 0;
};
enum class HushChatDecryptPreflightError {
None,
FeatureDisabled,
NonMessageHeader,
InvalidHeaderNumber,
UnsupportedVersion,
MissingReplyAddress,
MissingConversationId,
InvalidSecretstreamHeader,
InvalidPublicKey,
EmptyCiphertext,
OversizedCiphertext,
OddLengthCiphertext,
InvalidCiphertextHex,
TruncatedCiphertext
};
struct HushChatDecryptPreflightInput {
HushChatHeader header;
std::string ciphertext_hex;
};
struct HushChatDecryptPreflightResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
const char* error_name = "None";
std::size_t ciphertext_size = 0;
};
enum class HushChatHexDecodeError {
None,
Empty,
OddLength,
InvalidHex,
UnexpectedByteLength
};
struct HushChatHexDecodeResult {
bool ok = false;
HushChatHexDecodeError error = HushChatHexDecodeError::None;
const char* error_name = "None";
std::vector<unsigned char> bytes;
};
enum class HushChatDecryptDirection {
Incoming,
Outgoing
};
enum class HushChatSessionKeySelection {
ClientRx,
ServerTx
};
enum class HushChatDecryptInputError {
None,
FeatureDisabled,
InvalidStoredChatKey,
DecryptPreflightFailed,
InvalidPeerPublicKey,
InvalidStreamHeader,
InvalidCiphertext
};
struct HushChatDecryptInputMaterial {
std::string stored_chat_key_hex;
HushChatHeader header;
std::string ciphertext_hex;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
std::string peer_public_key_hex;
};
struct HushChatPreparedDecryptInput {
std::vector<unsigned char> stored_chat_key_bytes;
std::vector<unsigned char> seed_bytes;
std::vector<unsigned char> peer_public_key_bytes;
std::vector<unsigned char> stream_header_bytes;
std::vector<unsigned char> ciphertext_bytes;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t plaintext_capacity = 0;
};
struct HushChatDecryptInputPreparationResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptInputError error = HushChatDecryptInputError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatPreparedDecryptInput prepared;
};
struct HushChatDecryptFixtureReadinessResult {
bool ready = false;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t stream_header_size = 0;
std::size_t ciphertext_size = 0;
std::size_t plaintext_capacity = 0;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
};
enum class HushChatCompatibilityFixtureError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidLocalPublicKey,
InvalidPeerPublicKey,
InvalidHeaderMemo,
InvalidMemoPair,
NonMemoHeader,
HeaderPublicKeyMismatch,
DecryptInputFailed,
NotFixtureReady,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
ExpectedPeerPublicKeyLengthMismatch,
ExpectedStreamHeaderLengthMismatch,
ExpectedCiphertextLengthMismatch,
ExpectedPlaintextLengthMismatch,
ExpectedRoleMismatch,
InvalidPlaintextHash
};
struct HushChatCompatibilityFixture {
std::string fixture_id;
std::string stored_chat_key_hex;
std::string local_public_key_hex;
std::string peer_public_key_hex;
std::string header_memo;
std::string ciphertext_memo;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t expected_stored_chat_key_size = 32;
std::size_t expected_seed_size = 32;
std::size_t expected_local_public_key_size = 32;
std::size_t expected_peer_public_key_size = 32;
std::size_t expected_stream_header_size = 24;
std::size_t expected_ciphertext_size = 0;
std::size_t expected_plaintext_size = 0;
std::string expected_plaintext_hash_hex;
};
struct HushChatCompatibilityFixtureVerificationResult {
bool ok = false;
bool feature_enabled = false;
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatHeader header;
HushChatDecryptInputPreparationResult preparation;
HushChatDecryptFixtureReadinessResult readiness;
std::size_t local_public_key_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t plaintext_hash_size = 0;
};
enum class HushChatCompatibilityFixtureKind {
IncomingMemo,
OutgoingMemo,
SeedPublicKeyProjection,
CorruptedAuthFailure,
ContactExclusion
};
enum class HushChatCompatibilityFixtureFileStatus {
Pending,
Ready
};
enum class HushChatCompatibilityFixtureFileError {
None,
FeatureDisabled,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingKind,
UnknownKind,
MissingStatus,
UnknownStatus,
MissingFixtureId,
MissingPendingReason,
MissingFixtureObject,
InvalidFixtureField,
FixtureVerificationFailed,
ContactFixtureNotExcluded,
FileReadFailed
};
struct HushChatCompatibilityFixtureFile {
std::string schema;
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
std::string fixture_id;
std::string pending_reason;
HushChatCompatibilityFixture fixture;
};
struct HushChatCompatibilityFixtureFileParseResult {
bool ok = false;
bool feature_enabled = false;
bool pending = false;
bool verified = false;
bool excluded_from_decrypt = false;
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFile file;
HushChatCompatibilityFixtureVerificationResult verification;
};
enum class HushChatSeedPublicKeyProjectionError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidStoredChatKey,
InvalidLocalPublicKey,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
SodiumInitializationFailed,
KeypairProjectionFailed,
ProjectedPublicKeyMismatch
};
struct HushChatSeedPublicKeyProjectionResult {
bool ok = false;
bool feature_enabled = false;
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t local_public_key_size = 0;
std::size_t projected_public_key_size = 0;
};
enum class HushChatCorruptedAuthFailureReadinessError {
None,
FeatureDisabled,
FixturePending,
WrongFixtureKind,
FixtureNotVerified,
SeedProjectionNotVerified
};
struct HushChatCorruptedAuthFailureReadinessResult {
bool ok = false;
bool feature_enabled = false;
bool structurally_ready_for_future_auth_check = false;
bool requires_future_secretstream_auth_failure = false;
bool decrypted = false;
bool authenticated = false;
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
const char* error_name = "None";
};
enum class HushChatCompatibilityFixtureImportError {
None,
FeatureDisabled,
MissingRequiredKind,
DuplicateKind,
FixtureLoadFailed,
FixtureKindMismatch,
FixturePending,
FixtureInvalid,
FixtureNotVerified,
SeedProjectionFailed,
AuthFailureScaffoldFailed,
ContactFixtureNotExcluded
};
struct HushChatCompatibilityFixtureImportCandidate {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
};
struct HushChatCompatibilityFixtureImportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFileParseResult parsed;
HushChatSeedPublicKeyProjectionResult seed_projection;
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
};
struct HushChatCompatibilityFixtureImportChecklistResult {
bool ok = false;
bool feature_enabled = false;
bool replacement_ready = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureImportItem> items;
};
struct HushChatCompatibilityFixtureReplacementReportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool refused = true;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
bool cont_excluded = false;
bool decrypted = false;
bool authenticated = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
};
struct HushChatCompatibilityFixtureReplacementDryRunResult {
bool ok = false;
bool feature_enabled = false;
bool dry_run_only = true;
bool redacted_report = true;
bool would_replace = false;
bool replacement_refused = true;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
};
enum class HushChatCaptureManifestError {
None,
FeatureDisabled,
FileReadFailed,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingManifestId,
MissingStatus,
UnknownStatus,
MissingFixtureDirectory,
MissingDryRunCommand,
InvalidDryRunCommand,
MissingProvenance,
MissingSourceClient,
InvalidSourceClient,
MissingSourceClientVersion,
MissingCaptureDate,
MissingNetwork,
MissingCaptureMethod,
MissingHandling,
MissingHandlingFlag,
HandlingFlagNotTrue,
MissingCategories,
InvalidCategoryEntry,
UnknownCategory,
DuplicateCategory,
MissingRequiredCategory,
ProhibitedFieldPresent
};
enum class HushChatCaptureManifestStatus {
Staged
};
struct HushChatCaptureManifestCategoryReport {
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string staged_filename;
bool declared = false;
};
struct HushChatCaptureManifestValidationResult {
bool ok = false;
bool feature_enabled = false;
bool redacted_report = true;
bool validates_provenance_only = true;
bool no_sensitive_material_declared = false;
bool has_dry_run_command = false;
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
const char* error_name = "None";
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
std::string manifest_path;
std::string fixture_directory;
std::size_t required_count = 0;
std::size_t declared_count = 0;
std::size_t missing_count = 0;
std::size_t duplicate_count = 0;
std::size_t prohibited_field_count = 0;
std::size_t handling_flag_count = 0;
std::vector<HushChatCaptureManifestCategoryReport> categories;
};
constexpr int kHushChatSupportedVersion = 0;
constexpr std::size_t kHushChatMemoByteLimit = 512;
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
constexpr std::size_t kHushChatSecretstreamABytes = 17;
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
constexpr std::size_t kHushChatSeedByteLength = 32;
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
constexpr bool hushChatFeatureEnabledAtBuild()
{
@@ -527,60 +99,7 @@ HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMe
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
const HushChatDecryptPreflightInput& input,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
std::size_t expectedByteLength);
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
const HushChatDecryptInputMaterial& material,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
const HushChatPreparedDecryptInput& prepared);
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
const HushChatCompatibilityFixtureFileParseResult& parsed,
const HushChatSeedPublicKeyProjectionResult& seedProjection,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatHeaderTypeName(HushChatHeaderType type);
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
} // namespace dragonx::chat
} // namespace dragonx::chat
+130 -78
View File
@@ -12,6 +12,8 @@
#include <fstream>
#include <filesystem>
#include <ctime>
#include <algorithm>
#include <type_traits>
#include "../util/logger.h"
#include "../util/platform.h"
@@ -76,37 +78,43 @@ const char* poolSelectModeName(Settings::PoolSelectMode mode)
return "manual";
}
// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
// read so a malformed value (wrong type / missing key) leaves the field's default
// instead of throwing out of the whole load().
template <typename T>
bool jsonHasType(const json& v)
{
if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
else if constexpr (std::is_floating_point_v<T>) return v.is_number();
else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
else return false;
}
// Reads j[key] into field only when present AND of the matching type.
template <typename T>
void loadScalar(const json& j, const char* key, T& field)
{
if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
}
// Same as loadScalar but clamps the read value into [lo, hi].
template <typename T>
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
{
if (j.contains(key) && jsonHasType<T>(j[key]))
field = std::max(lo, std::min(hi, j[key].get<T>()));
}
} // namespace
std::string Settings::getDefaultPath()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\" DRAGONX_APP_NAME;
fs::create_directories(dir);
return dir + "\\settings.json";
}
return "settings.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/" DRAGONX_APP_NAME;
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
const std::string dir = util::Platform::getConfigDir();
fs::create_directories(dir);
return dir + "/settings.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/" DRAGONX_APP_NAME;
fs::create_directories(dir);
return dir + "/settings.json";
#endif
return (fs::path(dir) / "settings.json").string();
}
bool Settings::load()
@@ -127,29 +135,29 @@ bool Settings::load(const std::string& path)
json j;
file >> j;
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
if (j.contains("language")) language_ = j["language"].get<std::string>();
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
loadScalar(j, "theme", theme_);
loadScalar(j, "save_ztxs", save_ztxs_);
loadScalar(j, "auto_shield", auto_shield_);
loadScalar(j, "use_tor", use_tor_);
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
loadScalar(j, "default_fee", default_fee_);
loadScalar(j, "fetch_prices", fetch_prices_);
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
loadScalar(j, "address_explorer_url", address_explorer_url_);
loadScalar(j, "language", language_);
loadScalar(j, "skin_id", skin_id_);
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
loadScalar(j, "acrylic_quality", acrylic_quality_);
loadScalar(j, "blur_multiplier", blur_multiplier_);
loadScalar(j, "noise_opacity", noise_opacity_);
loadScalar(j, "gradient_background", gradient_background_);
// Migrate legacy reduced_transparency bool -> ui_opacity float
if (j.contains("ui_opacity")) {
ui_opacity_ = j["ui_opacity"].get<float>();
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
}
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
loadScalar(j, "window_opacity", window_opacity_);
if (j.contains("balance_layout")) {
if (j["balance_layout"].is_string())
balance_layout_ = j["balance_layout"].get<std::string>();
@@ -163,7 +171,7 @@ bool Settings::load(const std::string& path)
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
}
}
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
loadScalar(j, "scanline_enabled", scanline_enabled_);
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
hidden_addresses_.clear();
for (const auto& a : j["hidden_addresses"])
@@ -189,13 +197,13 @@ bool Settings::load(const std::string& path)
address_meta_[addr] = m;
}
}
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
loadScalar(j, "unlock_duration", unlock_duration_);
loadScalar(j, "pin_enabled", pin_enabled_);
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
loadScalar(j, "max_connections", max_connections_);
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) {
@@ -256,34 +264,36 @@ bool Settings::load(const std::string& path)
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
}
}
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
loadScalar(j, "verbose_logging", verbose_logging_);
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear();
for (const auto& c : j["debug_categories"])
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
}
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
loadScalar(j, "low_spec_mode", low_spec_mode_);
loadScalar(j, "reduce_motion", reduce_motion_);
loadScalar(j, "selected_exchange", selected_exchange_);
loadScalar(j, "selected_pair", selected_pair_);
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";
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
loadScalar(j, "pool_algo", pool_algo_);
loadScalar(j, "pool_worker", pool_worker_);
loadScalar(j, "pool_threads", pool_threads_);
loadScalar(j, "pool_tls", pool_tls_);
loadScalar(j, "pool_hugepages", pool_hugepages_);
loadScalar(j, "pool_mode", pool_mode_);
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
if (j.contains("xmrig_version")) xmrig_version_ = j["xmrig_version"].get<std::string>();
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
loadScalar(j, "mine_when_idle", mine_when_idle_);
loadScalar(j, "xmrig_version", xmrig_version_);
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
loadScalar(j, "idle_threads_active", idle_threads_active_);
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"])
@@ -304,18 +314,45 @@ bool Settings::load(const std::string& path)
if (e.contains("addresses") && e["addresses"].is_array())
for (const auto& a : e["addresses"])
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
if (e.contains("icon") && e["icon"].is_string())
entry.icon = e["icon"].get<std::string>();
if (e.contains("color") && e["color"].is_number())
entry.color = e["color"].get<unsigned int>();
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
entry.outlineOpacity = e["outline_opacity"].get<int>();
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
entry.priceBasis = e["price_basis"].get<int>();
if (e.contains("manual_price") && e["manual_price"].is_number())
entry.manualPrice = e["manual_price"].get<double>();
if (e.contains("manual_currency") && e["manual_currency"].is_string())
entry.manualCurrency = e["manual_currency"].get<std::string>();
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
entry.showDrgx = e["show_drgx"].get<bool>();
if (e.contains("show_value") && e["show_value"].is_boolean())
entry.showValue = e["show_value"].get<bool>();
if (e.contains("show_24h") && e["show_24h"].is_boolean())
entry.show24h = e["show_24h"].get<bool>();
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
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>();
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())
entry.gridRow = e["grid_row"].get<int>();
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
entry.gridW = e["grid_w"].get<int>();
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
entry.gridH = e["grid_h"].get<int>();
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
}
}
if (j.contains("font_scale") && j["font_scale"].is_number())
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
if (j.contains("window_width") && j["window_width"].is_number_integer())
window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>();
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
loadScalar(j, "window_width", window_width_);
loadScalar(j, "window_height", window_height_);
// Version tracking — detect upgrades so we can re-save with new defaults
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
loadScalar(j, "settings_version", settings_version_);
if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
@@ -456,6 +493,21 @@ bool Settings::save(const std::string& path)
entry["label"] = e.label;
entry["addresses"] = json::array();
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
entry["icon"] = e.icon;
entry["color"] = e.color;
entry["outline_opacity"] = e.outlineOpacity;
entry["price_basis"] = e.priceBasis;
entry["manual_price"] = e.manualPrice;
entry["manual_currency"] = e.manualCurrency;
entry["show_drgx"] = e.showDrgx;
entry["show_value"] = e.showValue;
entry["show_24h"] = e.show24h;
entry["show_sparkline"] = e.showSparkline;
entry["sparkline_interval"] = e.sparklineInterval;
entry["grid_col"] = e.gridCol;
entry["grid_row"] = e.gridRow;
entry["grid_w"] = e.gridW;
entry["grid_h"] = e.gridH;
j["portfolio_entries"].push_back(std::move(entry));
}
j["font_scale"] = font_scale_;
+20
View File
@@ -78,6 +78,26 @@ public:
struct PortfolioEntry {
std::string label;
std::vector<std::string> addresses;
std::string icon; // project_icons wallet-icon name; empty = no icon
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
// (show DRGX + USD value).
int priceBasis = 0;
double manualPrice = 0.0; // price per DRGX for the Manual basis
std::string manualCurrency = "USD";
bool showDrgx = true; // show the DRGX amount on the card
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 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
};
// Theme
+4 -33
View File
@@ -11,13 +11,6 @@
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
using json = nlohmann::json;
@@ -29,33 +22,11 @@ AddressBook::~AddressBook() = default;
std::string AddressBook::getDefaultPath()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return dir + "\\addressbook.json";
}
return "addressbook.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
// Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/).
// util::Platform::getConfigDir() owns the per-platform + per-variant path in one place.
const std::string dir = util::Platform::getConfigDir();
fs::create_directories(dir);
return dir + "/addressbook.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/addressbook.json";
#endif
return (fs::path(dir) / "addressbook.json").string();
}
bool AddressBook::load()
+99
View File
@@ -0,0 +1,99 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <vector>
#include <utility>
#include <ctime>
#include "wallet_state.h" // MarketInfo
// Pure (no-I/O, no-ImGui) price-series math backing the market chart and the portfolio-group
// sparklines. Kept header-only + inline so it can be unit-tested without the GUI (see
// tests/test_phase4.cpp), mirroring data/portfolio.h.
namespace dragonx {
namespace data {
// Resample a ~1-sample/minute price history to the given interval by averaging each block of K
// minute-samples into one point. interval: 0=min 1=hour 2=day 3=week 4=month.
inline std::vector<double> resampleHistory(const std::vector<double>& hist, int interval)
{
static const int kMins[5] = {1, 60, 1440, 10080, 43200};
int k = kMins[(interval >= 0 && interval < 5) ? interval : 0];
if (k <= 1) return hist;
std::vector<double> out;
for (size_t i = 0; i < hist.size(); i += (size_t)k) {
double sum = 0.0; size_t cnt = 0;
for (size_t j = i; j < hist.size() && j < i + (size_t)k; ++j) { sum += hist[j]; ++cnt; }
if (cnt) out.push_back(sum / (double)cnt);
}
return out;
}
// Bucket a timestamped (unix-seconds, price) series into fixed-width time windows, averaging the
// samples in each window into one point. Input is oldest->newest; output preserves that order.
inline std::vector<double> bucketBySeconds(
const std::vector<std::pair<std::time_t, double>>& series, long windowSec)
{
std::vector<double> out;
if (series.empty() || windowSec <= 0) return out;
long curBucket = 0; double sum = 0.0; int cnt = 0;
for (const auto& sample : series) {
long b = (long)(sample.first / windowSec);
if (cnt > 0 && b != curBucket) { out.push_back(sum / cnt); sum = 0.0; cnt = 0; }
curBucket = b;
sum += sample.second; ++cnt;
}
if (cnt > 0) out.push_back(sum / cnt);
return out;
}
// Resolve the price series backing a group's sparkline for the chosen interval:
// 0=minute -> the live in-session buffer; 1=hour -> intraday (5-min) data bucketed to hours;
// 2=day / 3=week / 4=month -> the ~1yr daily series bucketed accordingly.
// Falls back to the in-session buffer when the historical fetch hasn't populated yet.
inline std::vector<double> sparklineSeries(const MarketInfo& m, int interval)
{
const long kDay = 86400;
switch (interval) {
case 1: { auto v = bucketBySeconds(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; }
case 2: { auto v = bucketBySeconds(m.price_chart_daily, kDay); if (v.size() >= 2) return v; break; }
case 3: { auto v = bucketBySeconds(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; }
case 4: { auto v = bucketBySeconds(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; }
default: break; // minute interval, or no historical data yet -> live buffer below
}
return resampleHistory(m.price_history, interval);
}
// Timestamped price series backing the main chart for the selected RANGE (Live/1H/1D/1W/1M),
// ending at `now`. The interval buttons select a time window ending at `now`: 1H = last hour,
// 1D = last 24h, 1W = last 7 days, 1M = last 30 days. 1H/1D come from the 5-minute intraday
// series; 1W/1M from the daily series. Live (or an un-fetched/empty range) uses the minute buffer.
inline std::vector<std::pair<std::time_t, double>> chartSeries(const MarketInfo& m, int interval,
std::time_t now)
{
const long kDay = 86400;
auto lastWindow = [now](const std::vector<std::pair<std::time_t, double>>& src, long rangeSec) {
std::vector<std::pair<std::time_t, double>> out;
std::time_t cutoff = now - (std::time_t)rangeSec;
for (const auto& s : src) if (s.first >= cutoff) out.push_back(s);
return out;
};
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
default: break;
}
std::vector<std::pair<std::time_t, double>> out;
const auto& h = m.price_history;
for (size_t i = 0; i < h.size(); i++)
out.push_back({ now - (std::time_t)((h.size() - 1 - i) * 60), h[i] });
return out;
}
} // namespace data
} // namespace dragonx
+14 -2
View File
@@ -8,6 +8,8 @@
#include <vector>
#include <cstdint>
#include <chrono>
#include <ctime>
#include <utility>
#include "exchange_info.h"
@@ -159,9 +161,19 @@ struct MarketInfo {
bool price_loading = false;
std::string price_error;
// Price history for chart
// Live in-session price history: ~1 sample/minute, capped at MAX_HISTORY samples.
// Backs the main chart and the "minute" portfolio-sparkline interval.
std::vector<double> price_history;
static constexpr int MAX_HISTORY = 24; // 24 hours
static constexpr int MAX_HISTORY = 24; // 24 samples (~24 minutes at the 60s refresh)
// Historical USD price series fetched from CoinGecko market_chart, so the portfolio-group
// sparklines can show real hour/day/week/month trends instead of resampling the ~24-minute
// in-session buffer. Timestamped (unix seconds), oldest->newest; empty until the first fetch.
// Refreshed on a slow cadence (~30 min) since historical data moves slowly.
std::vector<std::pair<std::time_t, double>> price_chart_intraday; // ~24h @ 5-minute granularity
std::vector<std::pair<std::time_t, double>> price_chart_daily; // ~1yr @ daily granularity
std::chrono::steady_clock::time_point chart_last_fetch_time{};
bool chart_loaded = false;
// Exchanges/pairs fetched live from CoinGecko (empty until fetched; the Market tab
// falls back to data::getExchangeRegistry() while empty).
+261 -277
View File
@@ -71,11 +71,70 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid,
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <chrono>
#include <miniz.h>
#include "util/logger.h"
// Read the just-rendered framebuffer and write it to a PNG (used by the debug screenshot sweep).
// GL: read FBO 0 (RGBA, bottom-up -> miniz flip). DX11: copy the backbuffer to a staging texture,
// map it (BGRA, top-down -> channel-swap, no flip). Alpha forced opaque (the window is transparent).
namespace {
inline void writePng(const std::string& path, const std::vector<uint8_t>& rgba, int w, int h, bool flip) {
size_t len = 0;
void* png = tdefl_write_image_to_png_file_in_memory_ex(rgba.data(), w, h, 4, &len, 6, flip ? MZ_TRUE : MZ_FALSE);
if (!png) return;
std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (out) out.write(reinterpret_cast<const char*>(png), static_cast<std::streamsize>(len));
mz_free(png);
}
#ifdef DRAGONX_USE_DX11
inline void captureFrameToPng(dragonx::platform::DX11Context& dx, const std::string& path) {
IDXGISwapChain1* sc = dx.swapChain(); ID3D11Device* dev = dx.device(); ID3D11DeviceContext* ctx = dx.deviceContext();
if (!sc || !dev || !ctx) return;
ID3D11Texture2D* back = nullptr;
if (FAILED(sc->GetBuffer(0, IID_PPV_ARGS(&back))) || !back) return;
D3D11_TEXTURE2D_DESC bd; back->GetDesc(&bd);
D3D11_TEXTURE2D_DESC sd = bd; sd.Usage = D3D11_USAGE_STAGING; sd.BindFlags = 0;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.MiscFlags = 0;
ID3D11Texture2D* staging = nullptr;
if (SUCCEEDED(dev->CreateTexture2D(&sd, nullptr, &staging)) && staging) {
ctx->CopyResource(staging, back);
D3D11_MAPPED_SUBRESOURCE m;
if (SUCCEEDED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
int w = (int)bd.Width, h = (int)bd.Height;
std::vector<uint8_t> px((size_t)w * h * 4);
for (int y = 0; y < h; ++y) {
const uint8_t* s = (const uint8_t*)m.pData + (size_t)y * m.RowPitch;
uint8_t* d = px.data() + (size_t)y * w * 4;
for (int x = 0; x < w; ++x) { d[x*4+0]=s[x*4+2]; d[x*4+1]=s[x*4+1]; d[x*4+2]=s[x*4+0]; d[x*4+3]=255; }
}
ctx->Unmap(staging, 0);
writePng(path, px, w, h, /*flip*/false); // DX11 textures are top-down
}
staging->Release();
}
back->Release();
}
#else
inline void captureFrameToPng(const std::string& path, int w, int h) {
if (w <= 0 || h <= 0) return;
std::vector<uint8_t> px((size_t)w * h * 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
for (size_t i = 3; i < px.size(); i += 4) px[i] = 255; // opaque
writePng(path, px, w, h, /*flip*/true); // GL default framebuffer is bottom-up
}
#endif
} // namespace
#ifdef _WIN32
// Windows crash handler — writes a minidump and log entry before exit
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
@@ -382,6 +441,184 @@ static std::string findPaymentURI(int argc, char* argv[])
return "";
}
// ---------------------------------------------------------------
// Shared render-loop helpers (used by both the main loop and the
// resize event-watcher). Extracted verbatim to de-duplicate the
// per-frame code — same calls, same order, same #ifdef branches.
// ---------------------------------------------------------------
// Draw the window backdrop (base gradient / texture tint / acrylic
// capture callback / WindowBg alpha). The main loop passes lowSpec =
// isLowSpecMode() so low-spec skips texture sampling and the capture
// callback; the resize watcher passes lowSpec = false to reproduce its
// current always-sample-and-capture behavior.
static void drawWindowBackdrop(dragonx::App& app, ImDrawList* bgDL, ImVec2 p0, ImVec2 p1,
bool backdrop_active, bool lowSpec)
{
// Read the current gradient texture from the app each frame
// so hot-reloaded / skin-changed images are picked up immediately.
ImTextureID curGradTex = app.getGradientTexture();
// Window opacity: scale only background layer alpha so the
// desktop shows through while UI stays fully opaque.
float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
// Scale a single color's alpha channel by window opacity
auto scaleAlpha = [&](ImU32 col) -> ImU32 {
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
a = (uint8_t)(a * winOpacity);
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
};
if (curGradTex != 0) {
// Base color gradient underneath the texture — read from ui.toml
const auto& S2 = dragonx::ui::schema::UI();
ImU32 baseTop = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
if (lowSpec) {
// Low-spec: skip texture sampling — just draw the base gradient.
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
} else {
int tintAlpha = (int)S2.drawElement("backdrop", "texture-tint-alpha").size;
if (tintAlpha <= 0) tintAlpha = 140;
// Scale background gradient + texture tint alpha by
// window opacity — only affects the backdrop, not UI.
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
int scaledTintAlpha = (int)(tintAlpha * winOpacity);
ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha);
bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol);
}
} else if (backdrop_active) {
// Programmatic gradient tint (fallback when texture unavailable)
const auto& S = dragonx::ui::schema::UI();
auto bde = [&](const char* key, float fb) {
float v = S.drawElement("backdrop", key).size;
return v >= 0 ? v : fb;
};
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity));
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity));
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
}
// Insert acrylic capture callback BEFORE the noise overlay
// so the noise grain is not captured and blurred into the
// glass cards (the noise should only be a visual overlay).
// Skip in low-spec mode — acrylic is disabled so the FBO
// capture/blur callback is unnecessary GPU work.
if (!lowSpec) {
auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
if (captureCb) {
bgDL->AddCallback(captureCb, nullptr);
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
}
}
// WindowBg alpha stays at its base theme value — NOT scaled
// by window opacity. Only the background gradient/texture
// layers fade so UI elements remain fully readable.
if (backdrop_active || curGradTex != 0) {
const auto& S3 = dragonx::ui::schema::UI();
float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha;
}
}
// 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
// is pre-normalized by the caller (<=0 -> 1.0f, and macOS -> 1.0f).
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)
{
auto& typo = dragonx::ui::material::Typography::instance();
float oldScale = typo.getDpiScale();
if (std::abs(newScale - oldScale) > 0.01f) {
float ratio = newScale / oldScale;
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
oldScale, newScale, ratio);
ImGuiIO& io = ImGui::GetIO();
#ifdef DRAGONX_USE_DX11
g_borderlessDpi = newScale;
#endif
// Save the last known size (before auto-resize) under
// the OLD scale. We use lastKnownW/H instead of
// SDL_GetWindowSize() because by now WM_DPICHANGED
// may have already auto-resized the window.
{
int oldPct = (int)lroundf(oldScale * 100.0f);
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
lastKnownW, lastKnownH, oldPct);
}
// Compute new physical size: if we've been to this
// scale before, restore that exact size; otherwise
// proportionally scale from the current size.
int newPct = (int)lroundf(newScale * 100.0f);
int newW, newH;
auto it = savedSizeForScale.find(newPct);
if (it != savedSizeForScale.end()) {
newW = it->second.first;
newH = it->second.second;
} 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);
}
// 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);
if (did) {
SDL_Rect usable;
if (SDL_GetDisplayUsableBounds(did, &usable)) {
newW = std::min(newW, usable.w);
newH = std::min(newH, usable.h);
}
}
dpiResizePending = true;
SDL_SetWindowSize(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
// Arm deferred retry — the SetWindowSize above may
// not stick if the user is mid-drag (modal loop).
dpiTargetW = newW;
dpiTargetH = newH;
dpiResizeRetries = 30; // retry for ~30 frames
SDL_SetWindowMinimumSize(window,
(int)(1024 * newScale),
(int)(720 * newScale));
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
newW, newH, newScale, newPct);
// 2) Rebuild font atlas at the new DPI scale
typo.reload(io, newScale);
// 3) Reset style → reapply base theme → scale by new DPI
ImGui::GetStyle() = ImGuiStyle();
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
if (newScale > 1.01f) {
ImGui::GetStyle().ScaleAllSizes(newScale);
}
DEBUG_LOGF(" DPI transition complete\n");
}
}
int main(int argc, char* argv[])
{
// Ensure ObsidianDragon config directory exists early (before any file I/O)
@@ -941,59 +1178,10 @@ int main(int argc, char* argv[])
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
ImVec2 p0 = vp->Pos;
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
ImTextureID curGradTex2 = ctx->app->getGradientTexture();
float winOp2 = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
// Scale only background layer alpha by window opacity so the
// desktop shows through while UI elements stay fully opaque.
auto scaleA2 = [&](ImU32 col) -> ImU32 {
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
a = (uint8_t)(a * winOp2);
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
};
if (curGradTex2 != 0) {
const auto& S1 = dragonx::ui::schema::UI();
ImU32 baseTop1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
ImU32 baseBottom1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
int tintAlpha1 = (int)S1.drawElement("backdrop", "texture-tint-alpha").size;
if (tintAlpha1 <= 0) tintAlpha1 = 140;
bgDL->AddRectFilledMultiColor(p0, p1, scaleA2(baseTop1), scaleA2(baseTop1),
scaleA2(baseBottom1), scaleA2(baseBottom1));
int scaledTA1 = (int)(tintAlpha1 * winOp2);
ImU32 tintCol1 = IM_COL32(255, 255, 255, scaledTA1);
bgDL->AddImage(curGradTex2, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol1);
} else if (ctx->backdrop_active) {
const auto& S = dragonx::ui::schema::UI();
auto bde = [&](const char* key, float fb) {
float v = S.drawElement("backdrop", key).size;
return v >= 0 ? v : fb;
};
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOp2));
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOp2));
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
}
// Acrylic capture callback — must fire BEFORE the noise
// overlay so the noise grain is not captured and blurred
// into the glass cards.
auto captureCb2 = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
if (captureCb2) {
bgDL->AddCallback(captureCb2, nullptr);
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
}
// WindowBg alpha stays at its base theme value — NOT scaled
// by window opacity. Only the background gradient/texture
// layers are scaled so the desktop shows through while UI
// elements (cards, text, buttons) remain fully readable.
if (ctx->backdrop_active || curGradTex2 != 0) {
const auto& S4 = dragonx::ui::schema::UI();
float baseBgAlpha2 = S4.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha2;
}
// Resize path historically always sampled the texture and always
// added the capture callback — pass lowSpec = false to preserve
// that behavior exactly.
drawWindowBackdrop(*ctx->app, bgDL, p0, p1, ctx->backdrop_active, /*lowSpec=*/false);
}
ImGuiIO& io = ImGui::GetIO();
@@ -1143,76 +1331,9 @@ int main(int argc, char* argv[])
#ifdef __APPLE__
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
#endif
auto& typo = dragonx::ui::material::Typography::instance();
float oldScale = typo.getDpiScale();
if (std::abs(newScale - oldScale) > 0.01f) {
float ratio = newScale / oldScale;
DEBUG_LOGF("Display scale changed (idle): %.2f -> %.2f (ratio %.2f)\n",
oldScale, newScale, ratio);
ImGuiIO& io = ImGui::GetIO();
#ifdef DRAGONX_USE_DX11
g_borderlessDpi = newScale;
#endif
// Save the last known size (before auto-resize) under
// the OLD scale. We use lastKnownW/H instead of
// SDL_GetWindowSize() because by now WM_DPICHANGED
// may have already auto-resized the window.
{
int oldPct = (int)lroundf(oldScale * 100.0f);
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
lastKnownW, lastKnownH, oldPct);
}
// Compute new physical size: if we've been to this
// scale before, restore that exact size; otherwise
// proportionally scale from the current size.
int newPct = (int)lroundf(newScale * 100.0f);
int newW, newH;
auto it = savedSizeForScale.find(newPct);
if (it != savedSizeForScale.end()) {
newW = it->second.first;
newH = it->second.second;
} 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);
}
// 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);
if (did) {
SDL_Rect usable;
if (SDL_GetDisplayUsableBounds(did, &usable)) {
newW = std::min(newW, usable.w);
newH = std::min(newH, usable.h);
}
}
dpiResizePending = true;
SDL_SetWindowSize(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
// Arm deferred retry — the SetWindowSize above may
// not stick if the user is mid-drag (modal loop).
dpiTargetW = newW;
dpiTargetH = newH;
dpiResizeRetries = 30; // retry for ~30 frames
SDL_SetWindowMinimumSize(window,
(int)(1024 * newScale), (int)(720 * newScale));
typo.reload(io, newScale);
ImGui::GetStyle() = ImGuiStyle();
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
if (newScale > 1.01f) {
ImGui::GetStyle().ScaleAllSizes(newScale);
}
}
handleDisplayScaleChange(window, newScale, savedSizeForScale,
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
dpiResizeRetries, dpiResizePending);
}
needsRedraw = true; // Got an event — redraw
}
@@ -1323,84 +1444,9 @@ int main(int argc, char* argv[])
#ifdef __APPLE__
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
#endif
auto& typo = dragonx::ui::material::Typography::instance();
float oldScale = typo.getDpiScale();
if (std::abs(newScale - oldScale) > 0.01f) {
float ratio = newScale / oldScale;
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
oldScale, newScale, ratio);
ImGuiIO& io = ImGui::GetIO();
#ifdef DRAGONX_USE_DX11
g_borderlessDpi = newScale;
#endif
// Save the last known size (before auto-resize) under
// the OLD scale. We use lastKnownW/H instead of
// SDL_GetWindowSize() because by now WM_DPICHANGED
// may have already auto-resized the window.
{
int oldPct = (int)lroundf(oldScale * 100.0f);
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
lastKnownW, lastKnownH, oldPct);
}
// Compute new physical size: if we've been to this
// scale before, restore that exact size; otherwise
// proportionally scale from the current size.
int newPct = (int)lroundf(newScale * 100.0f);
int newW, newH;
auto it = savedSizeForScale.find(newPct);
if (it != savedSizeForScale.end()) {
newW = it->second.first;
newH = it->second.second;
} 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);
}
// 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);
if (did) {
SDL_Rect usable;
if (SDL_GetDisplayUsableBounds(did, &usable)) {
newW = std::min(newW, usable.w);
newH = std::min(newH, usable.h);
}
}
dpiResizePending = true;
SDL_SetWindowSize(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
// Arm deferred retry — the SetWindowSize above may
// not stick if the user is mid-drag (modal loop).
dpiTargetW = newW;
dpiTargetH = newH;
dpiResizeRetries = 30; // retry for ~30 frames
SDL_SetWindowMinimumSize(window,
(int)(1024 * newScale),
(int)(720 * newScale));
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
newW, newH, newScale, newPct);
// 2) Rebuild font atlas at the new DPI scale
typo.reload(io, newScale);
// 3) Reset style → reapply base theme → scale by new DPI
ImGui::GetStyle() = ImGuiStyle();
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
if (newScale > 1.01f) {
ImGui::GetStyle().ScaleAllSizes(newScale);
}
DEBUG_LOGF(" DPI transition complete\n");
}
handleDisplayScaleChange(window, newScale, savedSizeForScale,
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
dpiResizeRetries, dpiResizePending);
}
}
@@ -1503,82 +1549,8 @@ int main(int argc, char* argv[])
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
ImVec2 p0 = vp->Pos;
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
// Read the current gradient texture from the app each frame
// so hot-reloaded / skin-changed images are picked up immediately.
ImTextureID curGradTex = app.getGradientTexture();
// Window opacity: scale only background layer alpha so the
// desktop shows through while UI stays fully opaque.
float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
bool lowSpec = dragonx::ui::effects::isLowSpecMode();
// Scale a single color's alpha channel by window opacity
auto scaleAlpha = [&](ImU32 col) -> ImU32 {
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
a = (uint8_t)(a * winOpacity);
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
};
if (curGradTex != 0) {
// Base color gradient underneath the texture — read from ui.toml
const auto& S2 = dragonx::ui::schema::UI();
ImU32 baseTop = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
if (lowSpec) {
// Low-spec: skip texture sampling — just draw the base gradient.
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
} else {
int tintAlpha = (int)S2.drawElement("backdrop", "texture-tint-alpha").size;
if (tintAlpha <= 0) tintAlpha = 140;
// Scale background gradient + texture tint alpha by
// window opacity — only affects the backdrop, not UI.
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
int scaledTintAlpha = (int)(tintAlpha * winOpacity);
ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha);
bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol);
}
} else if (backdrop_active) {
// Programmatic gradient tint (fallback when texture unavailable)
const auto& S = dragonx::ui::schema::UI();
auto bde = [&](const char* key, float fb) {
float v = S.drawElement("backdrop", key).size;
return v >= 0 ? v : fb;
};
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity));
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity));
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
}
// Insert acrylic capture callback BEFORE the noise overlay
// so the noise grain is not captured and blurred into the
// glass cards (the noise should only be a visual overlay).
// Skip in low-spec mode — acrylic is disabled so the FBO
// capture/blur callback is unnecessary GPU work.
if (!lowSpec) {
auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
if (captureCb) {
bgDL->AddCallback(captureCb, nullptr);
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
}
}
// WindowBg alpha stays at its base theme value — NOT scaled
// by window opacity. Only the background gradient/texture
// layers fade so UI elements remain fully readable.
if (backdrop_active || curGradTex != 0) {
const auto& S3 = dragonx::ui::schema::UI();
float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha;
}
drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active,
dragonx::ui::effects::isLowSpecMode());
}
PERF_END("Backdrop", _perfBackdrop);
@@ -1719,6 +1691,12 @@ int main(int argc, char* argv[])
}
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before present.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(dx, app.screenshotSweepPath());
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
@@ -1742,6 +1720,12 @@ int main(int argc, char* argv[])
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before swap.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y);
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
+53 -111
View File
@@ -290,13 +290,8 @@ json RPCClient::makePayload(const std::string& method, const json& params)
};
}
json RPCClient::call(const std::string& method, const json& params)
std::string RPCClient::performCall(const std::string& method, const json& params, long& httpCodeOut)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
if (!impl_->curl) {
throw std::runtime_error("Not connected");
}
emitRpcTrace(method);
json payload = makePayload(method, params);
@@ -310,22 +305,27 @@ json RPCClient::call(const std::string& method, const json& params)
// Perform request
CURLcode res = curl_easy_perform(impl_->curl);
if (res != CURLE_OK) {
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
}
// Check HTTP response code
long http_code = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
httpCodeOut = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &httpCodeOut);
return response_data;
}
json RPCClient::parseRpcResult(long httpCode, const std::string& body)
{
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
// (insufficient funds, bad params, etc.) with a valid JSON body.
// Parse the body first to extract the real error message.
if (http_code != 200) {
if (httpCode != 200) {
int errCode = 0;
try {
json response = json::parse(response_data);
json response = json::parse(body);
if (response.contains("error") && response["error"].is_object()) {
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
errCode = response["error"]["code"].get<int>();
@@ -337,10 +337,10 @@ json RPCClient::call(const std::string& method, const json& params)
} catch (const json::exception&) {
// Body wasn't valid JSON — fall through to generic HTTP error
}
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code));
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(httpCode));
}
json response = json::parse(response_data);
json response = json::parse(body);
if (response.contains("error") && !response["error"].is_null()) {
int errCode = 0;
@@ -358,6 +358,18 @@ json RPCClient::call(const std::string& method, const json& params)
return response["result"];
}
json RPCClient::call(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);
return parseRpcResult(http_code, response_data);
}
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
@@ -365,65 +377,16 @@ json RPCClient::call(const std::string& method, const json& params, long timeout
throw std::runtime_error("Not connected");
}
emitRpcTrace(method);
// Temporarily override timeout
long prevTimeout = 30L;
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
try {
// Unlock before calling to avoid recursive lock issues — but we already hold it,
// and call() also locks with recursive_mutex, so just delegate to the body directly.
json payload = makePayload(method, params);
std::string body = payload.dump();
std::string response_data;
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);
CURLcode res = curl_easy_perform(impl_->curl);
long http_code = 0;
std::string response_data = performCall(method, params, http_code);
// Restore original timeout
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
if (res != CURLE_OK) {
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
}
long http_code = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) {
int errCode = 0;
try {
json response = json::parse(response_data);
if (response.contains("error") && response["error"].is_object()) {
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
errCode = response["error"]["code"].get<int>();
if (response["error"].contains("message") && response["error"]["message"].is_string())
throw RpcError(errCode, response["error"]["message"].get<std::string>());
throw RpcError(errCode, "RPC error: " + response["error"].dump());
}
} catch (const json::exception&) {}
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code));
}
json response = json::parse(response_data);
if (response.contains("error") && !response["error"].is_null()) {
int errCode = 0;
std::string err_msg;
if (response["error"].is_object()) {
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
errCode = response["error"]["code"].get<int>();
if (response["error"].contains("message") && response["error"]["message"].is_string())
err_msg = response["error"]["message"].get<std::string>();
}
if (err_msg.empty()) err_msg = response["error"].dump();
throw RpcError(errCode, "RPC error: " + err_msg);
}
return response["result"];
return parseRpcResult(http_code, response_data);
} catch (...) {
// Ensure timeout is always restored
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
@@ -762,43 +725,40 @@ void RPCClient::z_listReceivedByAddress(const std::string& address, int minconf,
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
}
// Unified callback versions
void RPCClient::getInfo(UnifiedCallback cb)
// Adapts a UnifiedCallback into the (Callback, ErrorCallback) pair doRPC expects: the
// success lambda forwards (result, ""), the error lambda forwards ({}, error). The cb
// null-check is preserved on each invocation.
std::pair<Callback, ErrorCallback> RPCClient::splitUnified(UnifiedCallback cb)
{
doRPC("getinfo", {},
return {
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
};
}
// Unified callback versions
void RPCClient::getInfo(UnifiedCallback cb)
{
auto handlers = splitUnified(std::move(cb));
doRPC("getinfo", {}, handlers.first, handlers.second);
}
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
{
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
doRPC("rescan", {startHeight},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
auto handlers = splitUnified(std::move(cb));
doRPC("rescan", {startHeight}, handlers.first, handlers.second);
}
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
double fee, int limit, UnifiedCallback cb)
{
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
auto handlers = splitUnified(std::move(cb));
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, handlers.first, handlers.second);
}
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
@@ -808,14 +768,8 @@ void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, cons
for (const auto& addr : fromAddrs) {
addrs.push_back(addr);
}
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
auto handlers = splitUnified(std::move(cb));
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, handlers.first, handlers.second);
}
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
@@ -824,30 +778,18 @@ void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, Unif
for (const auto& id : opids) {
ids.push_back(id);
}
doRPC("z_getoperationstatus", {ids},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
auto handlers = splitUnified(std::move(cb));
doRPC("z_getoperationstatus", {ids}, handlers.first, handlers.second);
}
void RPCClient::getBlock(int height, UnifiedCallback cb)
{
// First get block hash, then get block
getBlockHash(height,
getBlockHash(height,
[this, cb](const json& hashResult) {
std::string hash = hashResult.get<std::string>();
getBlock(hash,
[cb](const json& blockResult) {
if (cb) cb(blockResult, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
auto handlers = splitUnified(cb);
getBlock(hash, handlers.first, handlers.second);
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
+11
View File
@@ -245,6 +245,17 @@ private:
json makePayload(const std::string& method, const json& params = json::array());
void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err);
// Shared blocking-call core: dumps the payload, performs the request (throwing on a
// curl transport error) and returns the response body plus HTTP status. Caller must
// 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);
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
std::string host_;
std::string port_;
std::string auth_; // Base64 encoded "user:password"
+208
View File
@@ -2,6 +2,7 @@
#include "../util/logger.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <map>
@@ -457,6 +458,29 @@ std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::
}
}
std::vector<std::pair<std::time_t, double>> NetworkRefreshService::parseCoinGeckoMarketChart(
const std::string& response)
{
std::vector<std::pair<std::time_t, double>> series;
try {
auto parsed = json::parse(response);
if (!parsed.contains("prices") || !parsed["prices"].is_array()) return series;
const auto& prices = parsed["prices"];
series.reserve(prices.size());
for (const auto& row : prices) {
// Each row is [milliseconds, price]; skip anything malformed.
if (!row.is_array() || row.size() < 2) continue;
if (!row[0].is_number() || !row[1].is_number()) continue;
const double ms = row[0].get<double>();
const double price = row[1].get<double>();
series.emplace_back(static_cast<std::time_t>(ms / 1000.0), price);
}
} catch (...) {
series.clear();
}
return series;
}
NetworkRefreshService::PriceHttpResult NetworkRefreshService::parsePriceHttpResponse(
const PriceHttpResponse& response,
std::time_t fetchedAt)
@@ -1336,6 +1360,190 @@ void NetworkRefreshService::resetJobs()
}
}
NetworkRefreshService::DaemonRescanScan NetworkRefreshService::parseDaemonRescanOutput(const std::string& output)
{
DaemonRescanScan scan;
bool& foundRescan = scan.foundRescan;
bool& finished = scan.finished;
float& rescanPct = scan.rescanPct;
std::string& lastStatus = scan.lastStatus;
// Sapling witness rebuild progress. Two daemon signals:
// "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" (the common one —
// one per wallet tx; <hash> is the stable per-tx key, <N> the total tx count)
// "Building Witnesses for block <h> <frac> complete, <n> remaining" (rarer)
bool& foundWitness = scan.foundWitness;
float& witnessPct = scan.witnessPct; // -1 = no fraction parsed this batch
int& witnessRemaining = scan.witnessRemaining;
int& witnessReadTotal = scan.witnessReadTotal; // new daemon: the <total> in "<done> / <total>"
bool& witnessRebuilt = scan.witnessRebuilt; // new daemon's completion line seen this batch
std::vector<std::string>& witnessTxids = scan.witnessTxids; // distinct-tx keys seen this batch
int& witnessTotalTxs = scan.witnessTotalTxs; // max N seen this batch
size_t pos = 0;
while (pos < output.size()) {
size_t eol = output.find('\n', pos);
if (eol == std::string::npos) eol = output.size();
std::string line = output.substr(pos, eol - pos);
pos = eol + 1;
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
lastStatus = line;
}
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9;
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
}
if (numEnd > numStart) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
lastStatus = line;
}
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
finished = true;
}
// This daemon prints no "Done rescanning" line; instead it logs the rescan
// benchmark timing exactly when the scan finishes, e.g.:
// "... rescan 16760577ms"
// Match the lowercase " rescan " bench category ending in "<digits>ms" (the
// progress lines are "Still rescanning"/"Rescanning...", which never end in ms).
if (line.find(" rescan ") != std::string::npos) {
std::string t = line;
while (!t.empty() && (t.back() == '\r' || t.back() == '\n' || t.back() == ' '))
t.pop_back();
if (t.size() >= 3 && t.compare(t.size() - 2, 2, "ms") == 0 &&
std::isdigit(static_cast<unsigned char>(t[t.size() - 3]))) {
finished = true;
}
}
// Sapling note witness rebuild: "Building Witnesses for block <h> <frac>
// complete, <n> remaining". The float before "complete" is a 0..1 fraction.
auto witIdx = line.find("Building Witnesses for block");
if (witIdx != std::string::npos) {
foundWitness = true;
auto compIdx = line.find(" complete", witIdx);
if (compIdx != std::string::npos) {
size_t numEnd = compIdx, numStart = numEnd;
while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) ||
line[numStart - 1] == '.')) numStart--;
if (numStart < numEnd) {
try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
}
}
auto remIdx = line.find(" remaining", witIdx);
if (remIdx != std::string::npos) {
size_t numEnd = remIdx, numStart = numEnd;
while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--;
if (numStart < numEnd) {
try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {}
}
}
lastStatus = line;
}
// Newer multi-threaded daemon: "Reading blocks for witness rebuild: <done> / <total>".
// This replaced the per-block "Building Witnesses" line and is an exact done/total
// count, so capture BOTH (total drives the denominator directly — no peak anchoring).
auto rdIdx = line.find("Reading blocks for witness rebuild:");
if (rdIdx != std::string::npos) {
foundWitness = true;
auto slash = line.find('/', rdIdx);
if (slash != std::string::npos) {
size_t dEnd = slash;
while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--;
size_t dStart = dEnd;
while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--;
size_t tStart = slash + 1;
while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++;
size_t tEnd = tStart;
while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++;
if (dStart < dEnd && tStart < tEnd) {
try {
long done = std::stol(line.substr(dStart, dEnd - dStart));
long total = std::stol(line.substr(tStart, tEnd - tStart));
if (total > 0 && done >= 0 && done <= total) {
witnessRemaining = static_cast<int>(total - done);
witnessReadTotal = static_cast<int>(total);
}
} catch (...) {}
}
}
lastStatus = line;
}
// New daemon's witness-rebuild completion line:
// "... rebuilt <n> note witness cache(s) to height <h> in <ms>ms using <t> thread(s)".
// The parallel rebuild logs no progress, so on completion snap the bar to 100%.
if (line.find("note witness cache") != std::string::npos &&
line.find("thread(s)") != std::string::npos) {
foundWitness = true;
witnessRebuilt = true;
lastStatus = line;
}
// Primary signal: "Setting Initial Sapling Witness for tx <hash>, <i> of <N>".
// <i> is the tx's slot in an unordered map (bounces — useless as progress), so
// we key on <hash> (one per tx) and count distinct txs against <N>. Extract the
// hash between "for tx " and the following comma, and N after " of ".
{
auto setIdx = line.find("Setting Initial Sapling Witness for tx ");
if (setIdx != std::string::npos) {
foundWitness = true;
size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size();
size_t comma = line.find(',', hStart);
if (comma != std::string::npos && comma > hStart) {
witnessTxids.push_back(line.substr(hStart, comma - hStart));
auto ofIdx = line.find(" of ", comma);
if (ofIdx != std::string::npos) {
size_t nStart = ofIdx + 4, nEnd = nStart;
while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++;
if (nEnd > nStart) {
try {
int n = std::stoi(line.substr(nStart, nEnd - nStart));
if (n > witnessTotalTxs) witnessTotalTxs = n;
} catch (...) {}
}
}
}
lastStatus = line;
}
}
}
return scan;
}
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
{
+21
View File
@@ -250,6 +250,10 @@ public:
std::time_t fetchedAt);
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
std::time_t fetchedAt);
// Parse a CoinGecko /market_chart response ({"prices":[[ms,price],...]}) into a timestamped
// (unix seconds, USD price) series, oldest->newest. Malformed rows are skipped; on any error
// (or a missing "prices" array) an empty vector is returned. Pure/no-I/O.
static std::vector<std::pair<std::time_t, double>> parseCoinGeckoMarketChart(const std::string& response);
static AddressInfo buildShieldedAddressInfo(const std::string& address,
const nlohmann::json& validation,
bool validationSucceeded);
@@ -293,6 +297,23 @@ public:
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
const std::vector<std::string>& requestedOpids);
struct DaemonRescanScan {
bool foundRescan = false;
bool finished = false;
float rescanPct = 0.0f;
std::string lastStatus;
bool foundWitness = false;
float witnessPct = -1.0f;
int witnessRemaining = -1;
int witnessReadTotal = -1;
bool witnessRebuilt = false;
std::vector<std::string> witnessTxids;
int witnessTotalTxs = -1;
};
// Pure line-by-line scan of newly-appended dragonxd stdout for rescan + Sapling
// witness-rebuild progress. No I/O, no state — unit-tested against fixture log snippets.
static DaemonRescanScan parseDaemonRescanOutput(const std::string& output);
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
+35
View File
@@ -355,6 +355,20 @@ void AcrylicMaterial::captureBackgroundDirect()
blurCacheValid_ = false; // force re-blur with fresh capture
}
void AcrylicMaterial::captureLiveFramebuffer()
{
// Like captureBackgroundDirect but with NO dirtyFrames_ gate — refresh every frame the
// overlay is open so the blur tracks the live UI. Fires mid-RenderDrawData (from a callback
// inserted at the overlay window's draw-list start), so FB 0 already holds the app UI drawn
// below the overlay. A paired ImDrawCallback_ResetRenderState restores ImGui's GL state.
if (!initialized_ || shouldSkipEffect()) return;
if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return;
glDisable(GL_SCISSOR_TEST);
captureBuffer_.captureScreen(viewportWidth_, viewportHeight_);
hasValidCapture_ = true;
blurCacheValid_ = false; // force re-blur with the fresh live capture
}
bool AcrylicMaterial::shouldSkipEffect() const
{
return !settings_.enabled ||
@@ -1238,6 +1252,26 @@ void AcrylicMaterial::captureBackgroundDirect()
blurCacheValid_ = false;
}
void AcrylicMaterial::captureLiveFramebuffer()
{
// DX11 twin of the GL live capture: copy the currently-bound backbuffer (which holds the app
// UI drawn below the overlay) into the capture texture, with NO dirtyFrames_ gate.
if (!initialized_ || shouldSkipEffect()) return;
if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return;
if (!dx_captureTex_ || !dx_context_) return;
ID3D11RenderTargetView* curRTV = nullptr;
dx_context_->OMGetRenderTargets(1, &curRTV, nullptr);
if (!curRTV) return;
ID3D11Resource* bbRes = nullptr;
curRTV->GetResource(&bbRes);
curRTV->Release();
if (!bbRes) return;
dx_context_->CopyResource(dx_captureTex_, bbRes);
bbRes->Release();
hasValidCapture_ = true;
blurCacheValid_ = false;
}
void AcrylicMaterial::applyBlur(float radius)
{
if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return;
@@ -1666,6 +1700,7 @@ void AcrylicMaterial::shutdown() {}
void AcrylicMaterial::resize(int, int) {}
void AcrylicMaterial::captureBackground() {}
void AcrylicMaterial::captureBackgroundDirect() {}
void AcrylicMaterial::captureLiveFramebuffer() {}
void AcrylicMaterial::applyBlur(float) {}
ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; }
ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; }
+10
View File
@@ -166,6 +166,16 @@ public:
*/
void captureBackgroundDirect();
/**
* @brief Capture the LIVE framebuffer (whatever has been drawn so far this
* frame), bypassing the dirtyFrames_ gate. Used by a full-window modal
* overlay to blur the live UI behind it: insert GetLiveCaptureCallback()
* at the start of the overlay window's draw list so it fires mid-pass,
* after the app UI is rasterized and before the overlay's own backdrop.
* Always forces a re-blur of the fresh capture.
*/
void captureLiveFramebuffer();
/**
* @brief Returns true once a valid background capture has been made.
*
+17 -405
View File
@@ -107,7 +107,10 @@ int MatchPreset(AcrylicQuality q, float blur)
} } } } // namespace dragonx::ui::effects::ImGuiAcrylic
#ifdef DRAGONX_HAS_GLAD
// Single ImGuiAcrylic frontend for BOTH GL (GLAD) and DX11: the frontend makes no
// direct GL/DX calls — every backend op is delegated to AcrylicMaterial, which has a
// backend for each API — so it must not fork per-backend.
#if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11)
namespace dragonx {
namespace ui {
@@ -233,6 +236,17 @@ ImDrawCallback GetBackgroundCaptureCallback()
return BackgroundCaptureCallback;
}
static void LiveCaptureCallback(const ImDrawList*, const ImDrawCmd*)
{
if (!s_initialized) return;
getAcrylicMaterial().captureLiveFramebuffer();
}
ImDrawCallback GetLiveCaptureCallback()
{
return LiveCaptureCallback;
}
// ============================================================================
// Drawing Functions
// ============================================================================
@@ -638,408 +652,6 @@ void ApplyBlurAmount(float blur)
} // namespace ui
} // namespace dragonx
#endif // DRAGONX_HAS_GLAD
// ============================================================================
// Stub Implementations (No GLAD)
// ============================================================================
#ifndef DRAGONX_HAS_GLAD
#ifdef DRAGONX_USE_DX11
// ============================================================================
// DX11 Implementation — mirrors the GLAD version above, delegating to
// AcrylicMaterial which now has a full DX11 backend.
// ============================================================================
namespace dragonx {
namespace ui {
namespace effects {
namespace ImGuiAcrylic {
static bool s_initialized = false;
static int s_viewportWidth = 0;
static int s_viewportHeight = 0;
static bool s_backgroundCaptured = false;
struct AcrylicWindowState {
ImVec2 windowPos;
ImVec2 windowSize;
AcrylicParams params;
bool active;
};
static AcrylicWindowState s_currentWindow = {};
bool Init()
{
if (s_initialized) return true;
AcrylicMaterial& acrylic = getAcrylicMaterial();
if (!acrylic.init()) {
DEBUG_LOGF("ImGuiAcrylic DX11: Failed to initialize acrylic material\n");
return false;
}
s_initialized = true;
DEBUG_LOGF("ImGuiAcrylic DX11: Initialized successfully\n");
return true;
}
void Shutdown()
{
if (!s_initialized) return;
getAcrylicMaterial().shutdown();
s_initialized = false;
}
bool IsAvailable()
{
return s_initialized && getAcrylicMaterial().isInitialized();
}
void BeginFrame(int width, int height)
{
if (!s_initialized) return;
if (width != s_viewportWidth || height != s_viewportHeight) {
s_viewportWidth = width;
s_viewportHeight = height;
getAcrylicMaterial().resize(width, height);
}
// Capture the first few frames to ensure a clean initial blur,
// then stop. Resize / theme change already sets dirtyFrames_ via markBackgroundDirty().
static int s_warmupFrames = 3;
if (s_warmupFrames > 0) {
getAcrylicMaterial().markBackgroundDirty();
--s_warmupFrames;
}
s_backgroundCaptured = false;
}
void CaptureBackground()
{
if (!s_initialized || s_backgroundCaptured) return;
getAcrylicMaterial().captureBackground();
s_backgroundCaptured = true;
}
void InvalidateCapture()
{
if (!s_initialized) return;
getAcrylicMaterial().markBackgroundDirty();
}
static void BackgroundCaptureCallback(const ImDrawList*, const ImDrawCmd*)
{
if (!s_initialized) return;
getAcrylicMaterial().captureBackgroundDirect();
s_backgroundCaptured = true;
}
ImDrawCallback GetBackgroundCaptureCallback()
{
return BackgroundCaptureCallback;
}
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
const AcrylicParams& params,
float rounding)
{
if (!drawList) return;
if (!IsAvailable() || !IsEnabled() || !params.enabled) {
drawList->AddRectFilled(pMin, pMax,
ImGui::ColorConvertFloat4ToU32(params.fallbackColor), rounding);
return;
}
getAcrylicMaterial().drawRect(drawList, pMin, pMax, params, rounding);
}
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
float rounding)
{
DrawAcrylicRect(drawList, pMin, pMax, AcrylicMaterial::getDarkPreset(), rounding);
}
bool BeginAcrylicWindow(const char* name, bool* p_open,
ImGuiWindowFlags flags,
const AcrylicParams& params)
{
s_currentWindow.params = params;
s_currentWindow.active = true;
if (!(flags & ImGuiWindowFlags_NoBringToFrontOnFocus))
ImGui::SetNextWindowFocus();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::Begin(name, p_open, flags);
if (result) {
s_currentWindow.windowPos = ImGui::GetWindowPos();
s_currentWindow.windowSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = s_currentWindow.windowPos;
ImVec2 pMax = ImVec2(pMin.x + s_currentWindow.windowSize.x,
pMin.y + s_currentWindow.windowSize.y);
float rounding = ImGui::GetStyle().WindowRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
}
return result;
}
void EndAcrylicWindow()
{
ImGui::End();
ImGui::PopStyleColor();
s_currentWindow.active = false;
}
bool BeginAcrylicChild(const char* str_id,
const ImVec2& size,
const AcrylicParams& params,
ImGuiChildFlags child_flags,
ImGuiWindowFlags window_flags)
{
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginChild(str_id, size, child_flags, window_flags);
if (result) {
ImVec2 childPos = ImGui::GetWindowPos();
ImVec2 childSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = childPos;
ImVec2 pMax = ImVec2(pMin.x + childSize.x, pMin.y + childSize.y);
float rounding = ImGui::GetStyle().ChildRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
}
return result;
}
void EndAcrylicChild()
{
ImGui::EndChild();
ImGui::PopStyleColor();
}
bool BeginAcrylicPopup(const char* str_id,
ImGuiWindowFlags flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopup(str_id, flags);
if (result) {
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
void EndAcrylicPopup()
{
ImGui::EndPopup();
ImGui::PopStyleColor();
}
bool BeginAcrylicPopupModal(const char* name, bool* p_open,
ImGuiWindowFlags flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupModal(name, p_open, flags);
ImGui::PopStyleColor(); // ModalWindowDimBg
if (result) {
ImDrawList* drawList = ImGui::GetWindowDrawList();
DrawFrostedScrim(drawList);
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
bool BeginAcrylicContextItem(const char* str_id,
ImGuiPopupFlags popup_flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupContextItem(str_id, popup_flags);
if (result) {
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
bool BeginAcrylicContextWindow(const char* str_id,
ImGuiPopupFlags popup_flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupContextWindow(str_id, popup_flags);
if (result) {
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
void DrawSidebarBackground(float width, const AcrylicParams& params)
{
ImGuiViewport* viewport = ImGui::GetMainViewport();
float menuBarHeight = ImGui::GetFrameHeight();
ImVec2 pMin = ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + menuBarHeight);
ImVec2 pMax = ImVec2(pMin.x + width, viewport->WorkPos.y + viewport->WorkSize.y);
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
DrawAcrylicRect(drawList, pMin, pMax, params, 0.0f);
}
void SetQuality(AcrylicQuality quality)
{
if (s_initialized) getAcrylicMaterial().setQuality(quality);
}
AcrylicQuality GetQuality()
{
if (s_initialized) return getAcrylicMaterial().getQuality();
return AcrylicQuality::Off;
}
void SetEnabled(bool enabled)
{
if (s_initialized) getAcrylicMaterial().setEnabled(enabled);
}
bool IsEnabled()
{
if (s_initialized) return getAcrylicMaterial().isEnabled();
return false;
}
void SetBlurMultiplier(float multiplier)
{
if (s_initialized) getAcrylicMaterial().setBlurMultiplier(multiplier);
}
float GetBlurMultiplier()
{
if (s_initialized) return getAcrylicMaterial().getBlurMultiplier();
return 1.0f;
}
void SetReducedTransparency(bool reduced)
{
if (s_initialized) getAcrylicMaterial().setReducedTransparency(reduced);
}
bool GetReducedTransparency()
{
if (s_initialized) return getAcrylicMaterial().getReducedTransparency();
return false;
}
void SetUIOpacity(float opacity)
{
if (s_initialized) getAcrylicMaterial().setUIOpacity(opacity);
}
float GetUIOpacity()
{
if (s_initialized) return getAcrylicMaterial().getUIOpacity();
return 1.0f;
}
void SetNoiseOpacity(float multiplier)
{
if (s_initialized) getAcrylicMaterial().setNoiseOpacityMultiplier(multiplier);
}
float GetNoiseOpacity()
{
if (s_initialized) return getAcrylicMaterial().getNoiseOpacityMultiplier();
return 1.0f;
}
const AcrylicSettings& GetSettings()
{
return getAcrylicMaterial().getSettings();
}
void SetSettings(const AcrylicSettings& settings)
{
if (s_initialized) getAcrylicMaterial().setSettings(settings);
}
void SetPreset(int preset)
{
if (preset < 0 || preset >= kPresetCount) preset = 3;
SetEnabled(preset != 0);
SetQuality(PresetQuality(preset));
SetBlurMultiplier(PresetBlur(preset));
}
int GetPreset()
{
return MatchPreset(GetQuality(), GetBlurMultiplier());
}
void ApplyBlurAmount(float blur)
{
if (blur < 0.001f) {
SetEnabled(false);
SetQuality(AcrylicQuality::Off);
SetBlurMultiplier(0.0f);
} else {
SetEnabled(true);
SetQuality(AcrylicQuality::Low);
SetBlurMultiplier(blur);
}
}
} // namespace ImGuiAcrylic
} // namespace effects
} // namespace ui
} // namespace dragonx
#else // neither GLAD nor DX11
namespace dragonx {
@@ -1056,6 +668,7 @@ void BeginFrame(int, int) {}
void CaptureBackground() {}
void InvalidateCapture() {}
ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; }
ImDrawCallback GetLiveCaptureCallback() { return nullptr; }
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
@@ -1173,6 +786,5 @@ void ApplyBlurAmount(float) {}
} // namespace ui
} // namespace dragonx
#endif // DRAGONX_USE_DX11
#endif // !DRAGONX_HAS_GLAD
#endif // DRAGONX_HAS_GLAD || DRAGONX_USE_DX11
+13
View File
@@ -89,6 +89,19 @@ void InvalidateCapture();
*/
ImDrawCallback GetBackgroundCaptureCallback();
/**
* @brief Returns a draw callback that captures the LIVE framebuffer (no dirtyFrames_
* gate). Insert it at the START of a full-window modal overlay's draw list — before the
* overlay draws its own backdrop — so the acrylic blurs the live app UI behind the modal:
* @code
* ImDrawList* dl = ImGui::GetWindowDrawList();
* dl->AddCallback(ImGuiAcrylic::GetLiveCaptureCallback(), nullptr);
* dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
* // ... then draw the overlay's acrylic/dim backdrop ...
* @endcode
*/
ImDrawCallback GetLiveCaptureCallback();
// ============================================================================
// Drawing Functions
// ============================================================================
+61 -73
View File
@@ -15,6 +15,23 @@ namespace dragonx {
namespace ui {
namespace effects {
namespace {
// Colors are packed little-endian RGBA (R at bit 0, G at 8, B at 16, A at 24) — the
// same layout IM_COL32 produces. unpackRGB pulls out the three color channels (alpha is
// handled separately at each site).
struct RGB { int r, g, b; };
static inline RGB unpackRGB(ImU32 c) {
return RGB{ (int)(c & 0xFF), (int)((c >> 8) & 0xFF), (int)((c >> 16) & 0xFF) };
}
// Scales a 0..1 base alpha by the background opacity into a clamped 0..255 int.
static inline int scaledAlpha(float base, float bgOpacity) {
return std::clamp((int)(base * bgOpacity * 255.0f), 0, 255);
}
} // namespace
// ============================================================================
// Singleton
// ============================================================================
@@ -327,16 +344,13 @@ void ThemeEffects::drawShimmer(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
if (clLeft >= clRight) return;
float mid = (clLeft + clRight) * 0.5f;
int peakA = (int)(shimmer_.alpha * bgOpacity_ * 255.0f);
peakA = std::clamp(peakA, 0, 255);
int peakA = scaledAlpha(shimmer_.alpha, bgOpacity_);
// Extract shimmer color RGB (ignore alpha, use shimmer_.alpha)
int sR = shimmer_.color & 0xFF;
int sG = (shimmer_.color >> 8) & 0xFF;
int sB = (shimmer_.color >> 16) & 0xFF;
RGB s = unpackRGB(shimmer_.color);
ImU32 clear = IM_COL32(sR, sG, sB, 0);
ImU32 peak = IM_COL32(sR, sG, sB, peakA);
ImU32 clear = IM_COL32(s.r, s.g, s.b, 0);
ImU32 peak = IM_COL32(s.r, s.g, s.b, peakA);
// Inset clip rect by corner rounding to prevent shimmer bleeding into rounded corners
float cr = std::min(rounding, std::min(w, h) * 0.5f) * 0.3f;
@@ -374,9 +388,7 @@ void ThemeEffects::drawSpecularGlare(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float minDim = std::min(w, h);
float glareR = minDim * specular_glare_.radius;
int cR = specular_glare_.color & 0xFF;
int cG = (specular_glare_.color >> 8) & 0xFF;
int cB = (specular_glare_.color >> 16) & 0xFF;
RGB c = unpackRGB(specular_glare_.color);
// Per-panel seed from position — unique drift per panel
float seed = pMin.x * 0.0073f + pMin.y * 0.0137f;
@@ -410,7 +422,7 @@ void ThemeEffects::drawSpecularGlare(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
if (a <= 0) continue;
dl->AddCircleFilled(ImVec2(cx, cy), ringRadius,
IM_COL32(cR, cG, cB, a), 32);
IM_COL32(c.r, c.g, c.b, a), 32);
}
}
@@ -445,18 +457,14 @@ ImU32 ThemeEffects::tintByPosition(ImU32 baseColor, float screenY) const {
float s = positional_hue_.strength;
float inv = 1.0f - s;
int bR = baseColor & 0xFF;
int bG = (baseColor >> 8) & 0xFF;
int bB = (baseColor >> 16) & 0xFF;
RGB base = unpackRGB(baseColor);
int bA = (baseColor >> 24) & 0xFF;
int tR = tint & 0xFF;
int tG = (tint >> 8) & 0xFF;
int tB = (tint >> 16) & 0xFF;
RGB t = unpackRGB(tint);
int r = (int)(bR * inv + tR * s);
int g = (int)(bG * inv + tG * s);
int b = (int)(bB * inv + tB * s);
int r = (int)(base.r * inv + t.r * s);
int g = (int)(base.g * inv + t.g * s);
int b = (int)(base.b * inv + t.b * s);
return IM_COL32(r, g, b, bA);
}
@@ -474,10 +482,8 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float alpha = glow_pulse_.minAlpha + phase * (glow_pulse_.maxAlpha - glow_pulse_.minAlpha);
if (alpha <= 0.001f) return;
int a = std::clamp((int)(alpha * bgOpacity_ * 255.0f), 0, 255);
int cR = glow_pulse_.color & 0xFF;
int cG = (glow_pulse_.color >> 8) & 0xFF;
int cB = (glow_pulse_.color >> 16) & 0xFF;
int a = scaledAlpha(alpha, bgOpacity_);
RGB c = unpackRGB(glow_pulse_.color);
// Border-only glow: concentric outlines with smooth Gaussian falloff.
// Use many fine sub-rings (4× the pixel radius) to avoid visible
@@ -496,7 +502,7 @@ void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
dl->AddRect(
ImVec2(pMin.x - expand, pMin.y - expand),
ImVec2(pMax.x + expand, pMax.y + expand),
IM_COL32(cR, cG, cB, ringAlpha),
IM_COL32(c.r, c.g, c.b, ringAlpha),
rounding + expand * 0.5f, 0, thickness);
}
}
@@ -513,17 +519,13 @@ void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 p
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
// Extract RGBA from both colors and lerp
int aR = gradient_border_.colorA & 0xFF;
int aG = (gradient_border_.colorA >> 8) & 0xFF;
int aB = (gradient_border_.colorA >> 16) & 0xFF;
int bR = gradient_border_.colorB & 0xFF;
int bG = (gradient_border_.colorB >> 8) & 0xFF;
int bB = (gradient_border_.colorB >> 16) & 0xFF;
RGB ca = unpackRGB(gradient_border_.colorA);
RGB cb = unpackRGB(gradient_border_.colorB);
int r = aR + (int)((bR - aR) * phase);
int g = aG + (int)((bG - aG) * phase);
int b = aB + (int)((bB - aB) * phase);
int a = std::clamp((int)(gradient_border_.alpha * bgOpacity_ * 255.0f), 0, 255);
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_);
// Draw the shifting border
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
@@ -640,9 +642,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float headPos = std::fmod(time_ * edge_trace_.speed, 1.0f);
float traceLen = edge_trace_.length;
int cR = edge_trace_.color & 0xFF;
int cG = (edge_trace_.color >> 8) & 0xFF;
int cB = (edge_trace_.color >> 16) & 0xFF;
RGB c = unpackRGB(edge_trace_.color);
// Draw the trace as connected line segments with fading alpha.
// Use enough segments to make curved corners smooth.
@@ -667,16 +667,16 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
aFrac = aFrac * aFrac; // quadratic falloff for natural fade
int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f);
if (a > 0) {
dl->AddLine(prev, pt, IM_COL32(cR, cG, cB, a), edge_trace_.thickness);
dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a), edge_trace_.thickness);
}
prev = pt;
}
// Bright dot at the head
int headA = (int)(edge_trace_.alpha * bgOpacity_ * 255.0f);
int headA = scaledAlpha(edge_trace_.alpha, bgOpacity_);
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.5f,
IM_COL32(cR, cG, cB, headA), 8);
IM_COL32(c.r, c.g, c.b, headA), 8);
}
// ============================================================================
@@ -690,9 +690,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return;
int cR = ember_rise_.color & 0xFF;
int cG = (ember_rise_.color >> 8) & 0xFF;
int cB = (ember_rise_.color >> 16) & 0xFF;
RGB c = unpackRGB(ember_rise_.color);
for (int i = 0; i < ember_rise_.count; i++) {
// Golden-ratio spacing gives evenly distributed phases
@@ -723,11 +721,11 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
// Warm core
dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6);
IM_COL32(c.r, c.g, c.b, a), 6);
// Softer outer glow
if (a > 30) {
dl->AddCircleFilled(ImVec2(x, y), size * 2.0f,
IM_COL32(cR, cG, cB, a / 4), 6);
IM_COL32(c.r, c.g, c.b, a / 4), 6);
}
}
}
@@ -746,9 +744,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
float vpY = vp->WorkPos.y;
if (vpW <= 0 || vpH <= 0) return;
int cR = ember_rise_.color & 0xFF;
int cG = (ember_rise_.color >> 8) & 0xFF;
int cB = (ember_rise_.color >> 16) & 0xFF;
RGB c = unpackRGB(ember_rise_.color);
// Viewport embers: more particles, spread across the whole screen
int vpCount = ember_rise_.count * 3;
@@ -777,10 +773,10 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (a <= 0) continue;
dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6);
IM_COL32(c.r, c.g, c.b, a), 6);
if (a > 20) {
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
IM_COL32(cR, cG, cB, a / 5), 6);
IM_COL32(c.r, c.g, c.b, a / 5), 6);
}
}
}
@@ -799,9 +795,7 @@ void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
float vpY = vp->WorkPos.y;
if (vpW <= 0 || vpH <= 0) return;
int cR = sandstorm_.color & 0xFF;
int cG = (sandstorm_.color >> 8) & 0xFF;
int cB = (sandstorm_.color >> 16) & 0xFF;
RGB c = unpackRGB(sandstorm_.color);
// Wind direction vector from angle (degrees from horizontal)
float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f;
@@ -875,21 +869,21 @@ void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
dl->AddLine(
ImVec2(x, y),
ImVec2(x + sx, y + sy),
IM_COL32(cR, cG, cB, a),
IM_COL32(c.r, c.g, c.b, a),
size * 0.7f);
// Bright head dot
dl->AddCircleFilled(ImVec2(x, y), size * 0.5f,
IM_COL32(cR, cG, cB, std::min(255, a * 3 / 2)), 5);
IM_COL32(c.r, c.g, c.b, std::min(255, a * 3 / 2)), 5);
} else {
// Small/slow particles: simple circle
dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6);
IM_COL32(c.r, c.g, c.b, a), 6);
}
// Occasional larger dust puff (every ~8th particle, double size, half alpha)
if (i % 8 == 0 && a > 15) {
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
IM_COL32(cR, cG, cB, a / 4), 8);
IM_COL32(c.r, c.g, c.b, a / 4), 8);
}
}
}
@@ -916,9 +910,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
float headPos = std::fmod(time_ * edge_trace_.speed + posKey, 1.0f);
float traceLen = edge_trace_.length;
int cR = edge_trace_.color & 0xFF;
int cG = (edge_trace_.color >> 8) & 0xFF;
int cB = (edge_trace_.color >> 16) & 0xFF;
RGB c = unpackRGB(edge_trace_.color);
// Panel edge trace is subtler than sidebar — 60% alpha
float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_;
@@ -935,7 +927,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
aFrac = aFrac * aFrac;
int a = (int)(panelAlpha * aFrac * 255.0f);
if (a > 0) {
dl->AddLine(prev, pt, IM_COL32(cR, cG, cB, a),
dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a),
edge_trace_.thickness * 0.8f);
}
prev = pt;
@@ -945,7 +937,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
int headA = (int)(panelAlpha * 255.0f);
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.2f,
IM_COL32(cR, cG, cB, headA), 6);
IM_COL32(c.r, c.g, c.b, headA), 6);
}
}
@@ -958,9 +950,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
int panelCount = std::max(2, ember_rise_.count / 3);
float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_;
int cR = ember_rise_.color & 0xFF;
int cG = (ember_rise_.color >> 8) & 0xFF;
int cB = (ember_rise_.color >> 16) & 0xFF;
RGB c = unpackRGB(ember_rise_.color);
// Use panel position as seed for unique particle distribution
float seed = pMin.x * 0.013f + pMin.y * 0.031f;
@@ -987,7 +977,7 @@ void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
if (a <= 0) continue;
dl->AddCircleFilled(ImVec2(x, y), size,
IM_COL32(cR, cG, cB, a), 6);
IM_COL32(c.r, c.g, c.b, a), 6);
}
dl->PopClipRect();
@@ -1074,14 +1064,12 @@ void ThemeEffects::drawViewportOverlay(ImDrawList* dl) const {
// === Vignette: edge darkening/tinting (4 gradient strips) ===
// Overlapping strips naturally darken corners more than edges — cinematic look
if (viewport_overlay_.vignetteEnabled) {
int cR = viewport_overlay_.vignetteColor & 0xFF;
int cG = (viewport_overlay_.vignetteColor >> 8) & 0xFF;
int cB = (viewport_overlay_.vignetteColor >> 16) & 0xFF;
int maxA = std::clamp((int)(viewport_overlay_.vignetteAlpha * bgOpacity_ * 255.0f), 0, 255);
RGB c = unpackRGB(viewport_overlay_.vignetteColor);
int maxA = scaledAlpha(viewport_overlay_.vignetteAlpha, bgOpacity_);
if (maxA > 0) {
ImU32 edgeCol = IM_COL32(cR, cG, cB, maxA);
ImU32 clearCol = IM_COL32(cR, cG, cB, 0);
ImU32 edgeCol = IM_COL32(c.r, c.g, c.b, maxA);
ImU32 clearCol = IM_COL32(c.r, c.g, c.b, 0);
float fadeW = vpSize.x * viewport_overlay_.vignetteRadius;
float fadeH = vpSize.y * viewport_overlay_.vignetteRadius;
+16 -8
View File
@@ -372,7 +372,7 @@ void ApplyColorThemeToImGui(const ColorTheme& theme)
colors[ImGuiCol_Border] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0.08f);
} else {
colors[ImGuiCol_Border] = ImVec4(0, 0, 0, 0.12f);
colors[ImGuiCol_Border] = ImVec4(0, 0, 0, 0.22f);
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0.04f);
}
@@ -382,9 +382,12 @@ void ApplyColorThemeToImGui(const ColorTheme& theme)
colors[ImGuiCol_FrameBgHovered] = ImVec4(1.0f, 1.0f, 1.0f, 0.10f);
colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
} else {
colors[ImGuiCol_FrameBg] = ImVec4(0, 0, 0, 0.04f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0, 0, 0, 0.07f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0, 0, 0, 0.10f);
// Light themes: the old 4/7/10% black fills were nearly invisible on a white surface (input
// fields blended in). Darker fills + a 1px frame border (below) so inputs/dropdowns read as
// defined boxes even on the pastel/gradient light skins (iridescent, color-pop-light).
colors[ImGuiCol_FrameBg] = ImVec4(0, 0, 0, 0.11f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0, 0, 0, 0.16f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0, 0, 0, 0.20f);
}
// Title bar
@@ -418,9 +421,11 @@ void ApplyColorThemeToImGui(const ColorTheme& theme)
colors[ImGuiCol_ButtonHovered] = ImVec4(1.0f, 1.0f, 1.0f, 30.0f / 255.0f);
colors[ImGuiCol_ButtonActive] = ImVec4(1.0f, 1.0f, 1.0f, 45.0f / 255.0f);
} else {
colors[ImGuiCol_Button] = ImVec4(0, 0, 0, 0.05f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0, 0, 0, 0.08f);
colors[ImGuiCol_ButtonActive] = ImVec4(0, 0, 0, 0.12f);
// Darker overlays on light themes — the old 5/8/12% were barely-there grey and washed out;
// bumped again so the small/secondary buttons read as clearly as the main ones.
colors[ImGuiCol_Button] = ImVec4(0, 0, 0, 0.16f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0, 0, 0, 0.23f);
colors[ImGuiCol_ButtonActive] = ImVec4(0, 0, 0, 0.30f);
}
// Headers (collapsing headers, tree nodes, selectable, menu items)
@@ -504,7 +509,10 @@ void ApplyColorThemeToImGui(const ColorTheme& theme)
style.WindowBorderSize = bwElem.extraFloats.count("window") ? bwElem.extraFloats.at("window") : 1.0f;
style.ChildBorderSize = bwElem.extraFloats.count("child") ? bwElem.extraFloats.at("child") : 1.0f;
style.PopupBorderSize = bwElem.extraFloats.count("popup") ? bwElem.extraFloats.at("popup") : 1.0f;
style.FrameBorderSize = bwElem.extraFloats.count("frame") ? bwElem.extraFloats.at("frame") : 0.0f;
// Light themes get a 1px frame border so inputs have a defined boundary on white/pastel surfaces;
// dark themes stay borderless (their translucent glass FrameBg is enough). Schema can override.
style.FrameBorderSize = bwElem.extraFloats.count("frame") ? bwElem.extraFloats.at("frame")
: (s_isDarkTheme ? 0.0f : 1.0f);
style.TabBorderSize = 0.0f;
style.AntiAliasedLines = true;
-352
View File
@@ -1,352 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../tooltip_style.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Button Components
// ============================================================================
// Based on https://m2.material.io/components/buttons
//
// Three button variants:
// - Text Button: Low emphasis, no container
// - Outlined Button: Medium emphasis, border only
// - Contained Button: High emphasis, filled background
enum class ButtonStyle {
Text, // Low emphasis - text only
Outlined, // Medium emphasis - border
Contained // High emphasis - filled (default)
};
/**
* @brief Button configuration
*/
struct ButtonSpec {
ButtonStyle style = ButtonStyle::Contained;
bool enabled = true;
bool fullWidth = false;
const char* icon = nullptr; // Leading icon (text glyph)
ImU32 color = 0; // 0 = use primary color
};
/**
* @brief Render a Material Design button
*
* @param label Button text (will be uppercased per Material spec)
* @param spec Button configuration
* @return true if clicked
*/
bool Button(const char* label, const ButtonSpec& spec = ButtonSpec());
/**
* @brief Render a text-only button (low emphasis)
*/
inline bool TextButton(const char* label, bool enabled = true) {
ButtonSpec spec;
spec.style = ButtonStyle::Text;
spec.enabled = enabled;
return Button(label, spec);
}
/**
* @brief Render an outlined button (medium emphasis)
*/
inline bool OutlinedButton(const char* label, bool enabled = true) {
ButtonSpec spec;
spec.style = ButtonStyle::Outlined;
spec.enabled = enabled;
return Button(label, spec);
}
/**
* @brief Render a contained/filled button (high emphasis)
*/
inline bool ContainedButton(const char* label, bool enabled = true) {
ButtonSpec spec;
spec.style = ButtonStyle::Contained;
spec.enabled = enabled;
return Button(label, spec);
}
/**
* @brief Render an icon button (circular touch target)
*
* @param icon Icon glyph/character
* @param tooltip Hover tooltip text
* @param enabled Whether button is enabled
* @return true if clicked
*/
bool IconButton(const char* icon, const char* tooltip = nullptr, bool enabled = true);
/**
* @brief Render a Floating Action Button (FAB)
*
* @param icon Icon glyph
* @param label Optional label for extended FAB
* @param mini Use mini size (40dp instead of 56dp)
* @return true if clicked
*/
bool FAB(const char* icon, const char* label = nullptr, bool mini = false);
// ============================================================================
// Implementation
// ============================================================================
inline bool Button(const char* label, const ButtonSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
// Convert label to uppercase for Material Design
char upperLabel[256];
{
const char* src = label;
char* dst = upperLabel;
char* end = upperLabel + sizeof(upperLabel) - 1;
while (*src && dst < end) {
*dst++ = (char)toupper((unsigned char)*src++);
}
*dst = '\0';
}
// Get button font
ImFont* buttonFont = Typography::instance().button();
ImGui::PushFont(buttonFont);
// Calculate button size
ImVec2 labelSize = ImGui::CalcTextSize(upperLabel);
float iconWidth = spec.icon ? (size::IconSize + size::ButtonIconGap) : 0.0f;
ImVec2 buttonSize;
buttonSize.x = spec.fullWidth ? ImGui::GetContentRegionAvail().x
: ImMax(labelSize.x + iconWidth + size::ButtonPaddingH * 2, size::ButtonMinWidth);
buttonSize.y = size::ButtonHeight;
// Get position
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + buttonSize.x, pos.y + buttonSize.y));
ImGui::ItemSize(buttonSize, style.FramePadding.y);
if (!ImGui::ItemAdd(bb, id)) {
ImGui::PopFont();
return false;
}
// Handle interaction
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (!spec.enabled) {
hovered = held = false;
}
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Get colors
ImU32 primaryColor = spec.color ? spec.color : Primary();
ImU32 textColor;
ImU32 bgColor;
ImU32 borderColor = 0;
switch (spec.style) {
case ButtonStyle::Text:
bgColor = 0; // No background
textColor = spec.enabled ? primaryColor : OnSurfaceDisabled();
if (hovered) bgColor = BlendOverlay(Surface(), StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(Surface(), StatePressed(), 1.0f);
break;
case ButtonStyle::Outlined:
bgColor = 0; // No background
textColor = spec.enabled ? primaryColor : OnSurfaceDisabled();
borderColor = spec.enabled ? primaryColor : OnSurfaceDisabled();
if (hovered) bgColor = BlendOverlay(Surface(), StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(Surface(), StatePressed(), 1.0f);
break;
case ButtonStyle::Contained:
default:
if (spec.enabled) {
bgColor = primaryColor;
textColor = OnPrimary();
if (hovered) bgColor = BlendOverlay(primaryColor, StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(primaryColor, StatePressed(), 1.0f);
} else {
bgColor = WithAlphaF(OnSurface(), 0.12f);
textColor = OnSurfaceDisabled();
}
break;
}
// Render
ImDrawList* drawList = window->DrawList;
// Background
if (bgColor) {
drawList->AddRectFilled(bb.Min, bb.Max, bgColor, size::ButtonCornerRadius);
}
// Border (for outlined)
if (borderColor) {
drawList->AddRect(bb.Min, bb.Max, borderColor, size::ButtonCornerRadius, 0, 1.0f);
}
// Icon
float contentX = bb.Min.x + size::ButtonPaddingH;
float contentY = bb.Min.y + (buttonSize.y - labelSize.y) * 0.5f;
if (spec.icon) {
ImFont* iconFont = Type().iconMed();
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, spec.icon);
ImVec2 iconPos(contentX, bb.Min.y + (buttonSize.y - iconSize.y) * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, textColor, spec.icon);
contentX += iconSize.x + size::ButtonIconGap;
}
// Label
drawList->AddText(ImVec2(contentX, contentY), textColor, upperLabel);
ImGui::PopFont();
return pressed && spec.enabled;
}
inline bool IconButton(const char* icon, const char* tooltip, bool enabled) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiID id = window->GetID(icon);
ImVec2 pos = window->DC.CursorPos;
ImVec2 size(size::IconButtonSize, size::IconButtonSize);
ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
ImGui::ItemSize(size);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (!enabled) {
hovered = held = false;
}
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Render ripple/hover circle
ImDrawList* drawList = window->DrawList;
ImVec2 center = bb.GetCenter();
float radius = size::IconButtonSize * 0.5f;
if (hovered || held) {
ImU32 overlayColor = held ? StatePressed() : StateHover();
drawList->AddCircleFilled(center, radius, overlayColor);
}
// Render icon (use icon font)
ImFont* iconFont = Type().iconMed();
ImU32 iconColor = enabled ? OnSurface() : OnSurfaceDisabled();
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, icon);
ImVec2 iconPos(center.x - iconSize.x * 0.5f, center.y - iconSize.y * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, iconColor, icon);
// Tooltip
if (tooltip && hovered) {
material::Tooltip("%s", tooltip);
}
return pressed && enabled;
}
inline bool FAB(const char* icon, const char* label, bool mini) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiID id = window->GetID(icon);
bool extended = (label != nullptr);
float fabSize = mini ? size::FabMiniSize : size::FabSize;
ImVec2 buttonSize;
if (extended) {
ImVec2 labelSize = ImGui::CalcTextSize(label);
buttonSize.x = size::FabExtendedPadding * 2 + size::IconSize + spacing::Sm + labelSize.x;
buttonSize.y = size::FabExtendedHeight;
} else {
buttonSize.x = fabSize;
buttonSize.y = fabSize;
}
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + buttonSize.x, pos.y + buttonSize.y));
ImGui::ItemSize(buttonSize);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Colors
ImU32 bgColor = Secondary();
ImU32 textColor = OnSecondary();
if (hovered) bgColor = BlendOverlay(bgColor, StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(bgColor, StatePressed(), 1.0f);
// Render
ImDrawList* drawList = window->DrawList;
float radius = extended ? size::FabCornerRadius : (fabSize * 0.5f);
drawList->AddRectFilled(bb.Min, bb.Max, bgColor, radius);
// Icon
ImVec2 center = bb.GetCenter();
ImFont* iconFont = Type().iconMed();
if (extended) {
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, icon);
float iconX = bb.Min.x + size::FabExtendedPadding;
float iconY = center.y - iconSize.y * 0.5f;
drawList->AddText(iconFont, iconFont->LegacySize, ImVec2(iconX, iconY), textColor, icon);
// Label
Typography::instance().pushFont(TypeStyle::Button);
float labelX = iconX + iconSize.x + spacing::Sm;
float labelY = center.y - ImGui::GetFontSize() * 0.5f;
drawList->AddText(ImVec2(labelX, labelY), textColor, label);
Typography::instance().popFont();
} else {
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, icon);
ImVec2 iconPos(center.x - iconSize.x * 0.5f, center.y - iconSize.y * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, textColor, icon);
}
return pressed;
}
} // namespace material
} // namespace ui
} // namespace dragonx
+599 -115
View File
@@ -35,6 +35,22 @@ inline ImU32 ScaleAlpha(ImU32 col, float scale) {
return (col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
}
// True when the active theme is light (app background luminance is high).
inline bool IsLightTheme() {
ImU32 bg = Background();
float r = ((bg >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float g = ((bg >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float b = ((bg >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
return (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f;
}
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
inline const char* LoadingDots() {
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;
static const char* kDots[4] = { "", ".", "..", "..." };
return kDots[n];
}
// ============================================================================
// Text Drop Shadow
// ============================================================================
@@ -107,7 +123,9 @@ inline bool IsCurrentWindowOverlayDialog()
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
for (ImGuiWindow* node = window; node; node = node->ParentWindow) {
if (node->Name && strcmp(node->Name, "##OverlayScrim") == 0)
// Prefix match so both "##OverlayScrim" and suffixed "##OverlayScrim_<id>" blur overlays
// (used by nested dialogs and the unified blur-overlay framework) are recognized.
if (node->Name && strncmp(node->Name, "##OverlayScrim", 14) == 0)
return true;
}
return false;
@@ -210,15 +228,35 @@ inline void DrawTactileOverlay(ImDrawList* dl, const ImVec2& bMin,
dl->PopClipRect();
}
// Convenience: call right after any ImGui::Button() / SmallButton() call
// to add tactile depth. Uses the last item rect automatically.
inline void ApplyTactile(ImDrawList* dl = nullptr)
// Glass-card button surface: frosted-glass hover/idle fill (skipped while
// pressed) + rim light + tactile depth overlay. Shared by the tactile
// button wrappers. Call after computing the button rect / rounding /
// active / hovered state.
inline void DrawButtonGlassOverlay(ImDrawList* dl, const ImVec2& bMin,
const ImVec2& bMax, float rounding,
bool active, bool hovered)
{
if (!dl) dl = ImGui::GetWindowDrawList();
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
// Light themes: flat buttons (like the Manage-portfolio modal's plain buttons). Skip the glass
// sheen + tactile 3D — ImGui::Button already draws the fill/hover/active — and add just a subtle
// defining edge, which reads cleaner on light surfaces than the raised glass look.
if (IsLightTheme()) {
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(0, 0, 0, 50));
dl->AddRect(bMin, bMax, active ? ScaleAlpha(rim, 0.6f) : rim, rounding, 0, 1.0f);
return;
}
// Frosted glass highlight — subtle fill on top
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
// Rim light
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
// Tactile depth
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
}
@@ -241,6 +279,159 @@ inline ImFont* resolveButtonFont(int perButton, int sectionDefault)
return resolveButtonFont(perButton >= 0 ? perButton : sectionDefault);
}
// ── Progress / fill bar ─────────────────────────────────────────────────
// A rounded track with a fraction (0..1) fill, drawn at the current cursor and advancing the layout
// cursor past it (matches the download-dialog usage). For a placement you control, use the pMin/pMax
// overload. Defaults: theme Primary fill, a subtle white track, 4dp corners.
inline void DrawProgressBar(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, float frac01,
ImU32 fill = 0, ImU32 track = IM_COL32(255, 255, 255, 30), float rounding = -1.0f)
{
if (fill == 0) fill = Primary();
if (rounding < 0.0f) rounding = 4.0f * Layout::dpiScale();
frac01 = frac01 < 0.0f ? 0.0f : (frac01 > 1.0f ? 1.0f : frac01);
dl->AddRectFilled(pMin, pMax, track, rounding);
if (frac01 > 0.0f)
dl->AddRectFilled(pMin, ImVec2(pMin.x + (pMax.x - pMin.x) * frac01, pMax.y), fill, rounding);
}
inline void ProgressBar(float width, float height, float frac01,
ImU32 fill = 0, ImU32 track = IM_COL32(255, 255, 255, 30))
{
ImVec2 pMin = ImGui::GetCursorScreenPos();
DrawProgressBar(ImGui::GetWindowDrawList(), pMin, ImVec2(pMin.x + width, pMin.y + height), frac01, fill, track);
ImGui::Dummy(ImVec2(0, height));
}
// ── Click-to-copy affordance ────────────────────────────────────────────
// Turns an already-drawn text run into a click-to-copy control. The caller draws the visible text
// (font/color/position of its choosing); this places an invisible hit button (textSize + hitPad) at
// `pos`, shows a hand cursor + tooltip + a hover underline, and copies `copyValue` on click. Returns
// true the frame it was clicked (already on the clipboard) so the caller can show its own toast.
// Caller manages any cursor save/restore around the call.
inline bool CopyableTextOverlay(const char* id, const ImVec2& pos, const ImVec2& textSize, float fontSize,
const char* copyValue, const char* tooltip, const ImVec2& hitPad)
{
const float dp = Layout::dpiScale();
ImGui::SetCursorScreenPos(pos);
ImGui::InvisibleButton(id, ImVec2(textSize.x + hitPad.x, fontSize + hitPad.y));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (tooltip && tooltip[0]) Tooltip("%s", tooltip);
const float uy = pos.y + fontSize + 1.0f * dp;
ImGui::GetWindowDrawList()->AddLine(ImVec2(pos.x, uy), ImVec2(pos.x + textSize.x, uy),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
bool copied = false;
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(copyValue);
copied = true;
}
return copied;
}
// ── Pill / badge ─────────────────────────────────────────────────────────
// A rounded chip: a filled background sized to `text` + `pad`, an optional 1px border, and the text
// inset by `pad`. Use PillSize first when you need to right/center-anchor (compute the width, place
// the pill, then draw).
inline ImVec2 PillSize(const char* text, ImFont* font, const ImVec2& pad)
{
ImVec2 t = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0.0f, text);
return ImVec2(t.x + pad.x * 2.0f, font->LegacySize + pad.y * 2.0f);
}
// Draw a pill/badge anchored at top-left `pos`. `border` == 0 draws no outline. rounding < 0 => fully
// rounded (height/2). Returns the pill's total size.
inline ImVec2 DrawPill(ImDrawList* dl, const ImVec2& pos, const char* text, ImFont* font,
ImU32 fg, ImU32 bg, ImU32 border = 0,
const ImVec2& pad = ImVec2(4.0f, 2.0f), float rounding = -1.0f)
{
ImVec2 sz = PillSize(text, font, pad);
if (rounding < 0.0f) rounding = sz.y * 0.5f;
ImVec2 pMax(pos.x + sz.x, pos.y + sz.y);
dl->AddRectFilled(pos, pMax, bg, rounding);
if (border != 0) dl->AddRect(pos, pMax, border, rounding, 0, 1.0f * Layout::dpiScale());
dl->AddText(font, font->LegacySize, ImVec2(pos.x + pad.x, pos.y + pad.y), fg, text);
return sz;
}
// ── Labeled form input ───────────────────────────────────────────────────
// The app's common form-field idiom: a plain label above a fixed-width single-line input
// (ImGui::Text label + SetNextItemWidth + InputText / InputTextWithHint). Mirrors the existing
// pattern exactly (no visual change) so input styling can later evolve in ONE place. `width` < 0
// means full width (-1). Pass a non-null `hint` to use InputTextWithHint. Returns the input's
// edited bool. The label is drawn via "%s" so a stray '%' in the text is safe.
inline bool LabeledInput(const char* label, const char* id, char* buf, size_t bufSize,
float width = -1.0f, const char* hint = nullptr,
ImGuiInputTextFlags flags = 0)
{
ImGui::Text("%s", label);
ImGui::SetNextItemWidth(width);
if (hint && hint[0]) return ImGui::InputTextWithHint(id, hint, buf, bufSize, flags);
return ImGui::InputText(id, buf, bufSize, flags);
}
// Horizontal variant: label on the left, input to its right on the same line (settings-style rows).
// `labelPos` is the SameLine offset the input starts at (0 = default item spacing after the label).
inline bool LabeledInputRow(const char* label, float labelPos, const char* id, char* buf, size_t bufSize,
float width = -1.0f, const char* hint = nullptr, ImGuiInputTextFlags flags = 0)
{
ImGui::Text("%s", label);
ImGui::SameLine(labelPos);
ImGui::SetNextItemWidth(width);
if (hint && hint[0]) return ImGui::InputTextWithHint(id, hint, buf, bufSize, flags);
return ImGui::InputText(id, buf, bufSize, flags);
}
// Multiline variant: label above a multiline input of the given `size` (InputTextMultiline governs its
// own width via size, so no SetNextItemWidth). Returns the input's edited bool.
inline bool LabeledInputMultiline(const char* label, const char* id, char* buf, size_t bufSize,
const ImVec2& size, ImGuiInputTextFlags flags = 0)
{
ImGui::Text("%s", label);
return ImGui::InputTextMultiline(id, buf, bufSize, size, flags);
}
// ── Frameless icon button ────────────────────────────────────────────────
// Style for IconButton. All colors default to 0 (= unset): color falls back to OnSurfaceMedium();
// hoverColor==0 means "no hover recolor"; hoverBg/restBg==0 mean "no background".
struct IconButtonStyle {
ImU32 color = 0; // resting glyph color (0 => OnSurfaceMedium())
ImU32 hoverColor = 0; // glyph color while hovered (0 => same as color)
ImU32 hoverBg = 0; // fill drawn behind the glyph on hover only (0 => none)
ImU32 restBg = 0; // fill drawn every frame, e.g. an "active" pill (0 => none)
float bgRounding = -1.0f; // <0 => size.y*0.5 (pill/circle for a square); else that radius
bool handCursor = true;
const char* tooltip = nullptr;
};
// A frameless / pill icon button: an InvisibleButton(id, size) at the current cursor with a centered
// `glyph` (in `font`), optional resting + hover backgrounds, hover recolor, hand cursor + tooltip.
// Returns true the frame it is clicked. The caller resolves any state-dependent glyph/colors/tooltip
// before the call and positions the cursor (SetCursorScreenPos / SameLine) as needed.
inline bool IconButton(const char* id, const char* glyph, ImFont* font, const ImVec2& size,
const IconButtonStyle& st)
{
const ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(id, size);
const bool hov = ImGui::IsItemHovered();
const bool clk = ImGui::IsItemClicked();
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImVec2 pMax(pos.x + size.x, pos.y + size.y);
const float round = st.bgRounding < 0.0f ? size.y * 0.5f : st.bgRounding;
if (st.restBg != 0) dl->AddRectFilled(pos, pMax, st.restBg, round);
if (hov && st.hoverBg != 0) dl->AddRectFilled(pos, pMax, st.hoverBg, round);
if (hov) {
if (st.handCursor) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (st.tooltip && st.tooltip[0]) Tooltip("%s", st.tooltip);
}
const ImU32 gcol = (hov && st.hoverColor != 0) ? st.hoverColor
: (st.color != 0 ? st.color : OnSurfaceMedium());
const ImVec2 gsz = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, glyph);
dl->AddText(font, font->LegacySize,
ImVec2(pos.x + (size.x - gsz.x) * 0.5f, pos.y + (size.y - gsz.y) * 0.5f), gcol, glyph);
return clk;
}
// ── Tactile wrappers ────────────────────────────────────────────────────
// Drop-in replacements for ImGui::Button / SmallButton that automatically
// add the tactile highlight overlay after rendering.
@@ -278,20 +469,7 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0),
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
bool hovered = ImGui::IsItemHovered();
// Frosted glass highlight — subtle fill on top
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
// Rim light
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
// Tactile depth
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered);
return pressed;
}
@@ -306,20 +484,44 @@ inline bool TactileSmallButton(const char* label, ImFont* font = nullptr)
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
bool hovered = ImGui::IsItemHovered();
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered);
return pressed;
}
// ============================================================================
// Collapsible section header
// ============================================================================
// Renders a full-width transparent clickable header row: `label` drawn on the
// left and an expand/collapse chevron on the right. Toggles `expanded` when
// clicked and returns the (possibly toggled) state. The label font governs
// the row height (frame height) and text metrics; the chevron uses the small
// icon font. `arrowInset` shifts the chevron left of the right edge.
inline bool CollapsibleHeader(ImDrawList* dl, const char* id, const char* label,
bool& expanded, float width, ImFont* labelFont,
ImU32 labelCol, float arrowInset = 0.0f)
{
const char* arrow = expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(labelFont);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button(id, ImVec2(width, ImGui::GetFrameHeight()))) {
expanded = !expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - labelFont->LegacySize) * 0.5f;
dl->AddText(labelFont, labelFont->LegacySize, ImVec2(hdrPos.x, textY), labelCol, label);
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = labelFont;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + width - arrowW - arrowInset, textY), labelCol, arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
return expanded;
}
// ============================================================================
// Glass Panel (glassmorphism card)
// ============================================================================
@@ -335,11 +537,19 @@ struct GlassPanelSpec {
float borderWidth = 1.0f;
};
// When a full-window blur overlay (e.g. the portfolio modal) is active, glass panels use their
// opaque fallback instead of the acrylic blur. They're covered by the overlay's backdrop anyway, and
// this leaves the backdrop as the SOLE applyBlur caller — so it can use its own (stronger) blur
// radius without thrashing the shared, radius-keyed blur cache. Toggled from App::render().
inline bool& FullWindowBlurOverlayActiveRef() { static bool v = false; return v; }
inline void SetFullWindowBlurOverlayActive(bool v) { FullWindowBlurOverlayActiveRef() = v; }
inline bool IsFullWindowBlurOverlayActive() { return FullWindowBlurOverlayActiveRef(); }
inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
const ImVec2& pMax,
const GlassPanelSpec& spec = GlassPanelSpec())
{
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()) {
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && !IsFullWindowBlurOverlayActive()) {
// --- Cached color lookups (invalidated on theme change) ---
// These 3 resolveColor() calls do string parsing + map lookup
// each time. Cache them per-frame using the schema generation
@@ -434,6 +644,84 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
}
}
// Full-window backdrop for a modal overlay. Lays an OPAQUE base first (so the sheet reads as
// opaque regardless of the UI-opacity slider — the acrylic path multiplies its alpha by
// GetUIOpacity(), so it can't guarantee opacity on its own), then — only when the acrylic backdrop
// is active and we are NOT in low-performance mode — draws a strong blur of the (static) window
// backdrop on top plus a dim. In low-perf mode only the opaque base is drawn (no blur cost),
// mirroring DrawGlassPanel's gating.
// `allowBlur=false` draws only the opaque base (used on the overlay's first frame, before the live
// content has been captured, to avoid a one-frame blur of stale/background pixels).
inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax,
bool allowBlur = true)
{
// Theme-aware tones: a light theme gets a light frosted backdrop, a dark theme a dark one — the
// backdrop tints toward the app background rather than a hardcoded dark.
dl->AddRectFilled(pMin, pMax, WithAlpha(Background(), 255)); // opaque base (fallback / faint-blur show-through)
if (allowBlur && IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()
&& effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) {
// Safe to use a strong custom radius here: while a full-window overlay is active every glass
// 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.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
}
}
// ============================================================================
// Glass Card Scope (RAII) — channel-split glass-card scaffold
// ============================================================================
// Captures the current cursor as the card's top-left, splits the draw list
// into two channels (content on 1, glass on 0), insets the content by `pad`,
// and — on scope exit — appends the bottom pad, sizes the card to its content,
// draws the glass panel behind it on channel 0, merges, and advances the
// cursor past the card. `width` is the full card width, `bottomPad` the
// trailing pad. Reproduces the hand-written prologue/epilogue exactly.
//
// {
// material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
// ... card content ...
// }
struct GlassCardScope {
ImDrawList* dl;
ImVec2 cardMin;
float width;
float pad;
float bottomPad;
GlassPanelSpec spec;
GlassCardScope(ImDrawList* dl_, float width_, float pad_, float bottomPad_,
const GlassPanelSpec& spec_)
: dl(dl_), width(width_), pad(pad_), bottomPad(bottomPad_), spec(spec_)
{
cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
}
~GlassCardScope()
{
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + width, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, spec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(width, 0));
}
GlassCardScope(const GlassCardScope&) = delete;
GlassCardScope& operator=(const GlassCardScope&) = delete;
};
// ============================================================================
// Stat Card — reusable card with overline / value / subtitle + accent stripe
// ============================================================================
@@ -656,17 +944,7 @@ inline bool TactileButton(const char* label, const schema::ButtonStyle& style,
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
bool hovered = ImGui::IsItemHovered();
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered);
ImGui::PopStyleVar(styleCount);
ImGui::PopStyleColor(colorCount);
@@ -911,6 +1189,94 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
return closeClicked;
}
// ============================================================================
// Blur-backdrop modal primitives
// ============================================================================
// Shared building blocks for the "Manage Portfolio"-style overlay (live-blur backdrop, floating
// content, segmented section nav, edge-fade scroll). Promoted verbatim from the Market tab so every
// modal can adopt the same look. The overlay begin/end (below) drives these.
// "A blur overlay was drawn this frame" flag + a 1-frame latch. App::render builds its
// effect-suppression guard at the TOP of the frame, before dialogs dispatch, so it reads the
// LAST-frame latch (a stable signal) rather than this frame's in-progress state.
inline bool& BlurOverlayDrawnThisFrameRef() { static bool v = false; return v; }
inline bool& BlurOverlayActiveLastFrameRef() { static bool v = false; return v; }
inline bool*& BlurOverlayOpenFlagRef() { static bool* p = nullptr; return p; }
// Record that a blur overlay drew this frame, along with its open flag (so the latch below can tell
// "still open next frame" from "closed this frame" and match the modal's actual open state — no
// extra frame of suppression on close).
inline void MarkBlurOverlayDrawn(bool* openFlag) { BlurOverlayDrawnThisFrameRef() = true; BlurOverlayOpenFlagRef() = openFlag; }
inline bool AnyBlurOverlayActiveLastFrame() { return BlurOverlayActiveLastFrameRef(); }
// Latch whether an overlay will still be up next frame (drew this frame AND its open flag is still
// set after this frame's Esc/Close/outside-click handling); on the open->closed transition,
// invalidate the acrylic capture so the other glass panels re-capture the (overlay-free)
// background. Call once at the end of App::render.
inline void LatchBlurOverlayActive() {
bool* pf = BlurOverlayOpenFlagRef();
bool stillOpen = BlurOverlayDrawnThisFrameRef() && (!pf || *pf);
if (BlurOverlayActiveLastFrameRef() && !stillOpen) effects::ImGuiAcrylic::InvalidateCapture();
BlurOverlayActiveLastFrameRef() = stillOpen;
BlurOverlayDrawnThisFrameRef() = false;
BlurOverlayOpenFlagRef() = nullptr;
}
// True during the frames a blur overlay is (re)capturing the live backdrop — the scroll-fade shader
// is suppressed then, since the acrylic capture rebinds render state on those frames.
inline bool& CapturingBlurBackdropRef() { static bool v = false; return v; }
inline bool IsCapturingBlurBackdrop() { return CapturingBlurBackdropRef(); }
// Per-overlay capture-arming state (keyed by the scrim window id) so each blur overlay arms its
// capture-once independently on open/resize, detected via frame-count gaps (no close sweep needed).
struct BlurCaptureState { int lastSeenFrame = -2; int captureFrames = 0; float w = 0.0f, h = 0.0f; };
inline BlurCaptureState& BlurCaptureStateFor(const std::string& key) {
static std::unordered_map<std::string, BlurCaptureState> m; return m[key];
}
// Right-align the next item(s) of total width `w` within the remaining content region (cursor form).
inline void RightAlignX(float w)
{
float avail = ImGui::GetContentRegionAvail().x;
if (avail > w) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - w);
}
// Equal-width segmented control (rounded track + N cells, each a centered label + an active pill) at
// screen-space `origin`, spanning `totalW` x `height`. Each cell is a hit-tested InvisibleButton
// (Hand cursor on hover), disambiguated by `idBase`. Does NOT move the layout cursor. Returns the
// clicked cell index, or -1.
inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float height,
const char* const* labels, int count, int selected,
ImFont* font, const char* idBase, float dp)
{
int clicked = -1;
float cellW = totalW / (float)count;
dl->AddRectFilled(origin, ImVec2(origin.x + totalW, origin.y + height),
WithAlpha(OnSurface(), 20), height * 0.5f);
for (int i = 0; i < count; i++) {
ImVec2 cMin(origin.x + i * cellW, origin.y), cMax(cMin.x + cellW, origin.y + height);
bool active = (selected == i);
bool hov = ImGui::IsMouseHoveringRect(cMin, cMax);
if (active)
dl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp),
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]);
dl->AddText(font, font->LegacySize,
ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (height - ts.y) * 0.5f),
active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()),
labels[i]);
ImGui::PushID(i);
ImGui::SetCursorScreenPos(cMin);
if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i;
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
}
return clicked;
}
// The edge-fade scroll child (BeginFadeScrollChild/EndFadeScrollChild) depends on
// effects::ScrollFadeShader, whose GL/DX headers must not be pulled into this widely-included base
// header — it lives in material/overlay_scroll.h, included only by modal implementations.
// ============================================================================
// Fullscreen Overlay Dialog
// ============================================================================
@@ -923,39 +1289,64 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
inline std::unordered_map<std::string, float> g_overlayCardHeights;
inline std::string g_overlayCurrentKey;
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr)
// 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).
inline ImVec4 OverlayScrimColor(float opacity) { return ImVec4(0.04f, 0.04f, 0.06f, opacity); }
// Style of an overlay dialog. GlassCard = the classic scrim + glass card + boxed title bar.
// BlurFloat = the "Manage Portfolio" look: live-blur backdrop + floating content + plain heading.
enum class OverlayStyle { GlassCard, BlurFloat };
// Spec-driven overlay. The flags below are orthogonal modifiers on one scaffold; OverlayStyle is a
// convenience preset (GlassCard = all off; BlurFloat = blurBackdrop+floatingContent+plainHeading).
struct OverlayDialogSpec {
const char* title = nullptr;
bool* p_open = nullptr;
OverlayStyle style = OverlayStyle::GlassCard;
bool blurBackdrop = false; // live-blur backdrop instead of the opaque scrim
bool floatingContent = false; // transparent card (no glass panel — content floats on the blur)
bool plainHeading = false; // Type().h6() heading instead of the boxed DrawDialogTitleBar
float cardWidth = 460.0f; // scaled by dpiScale; clamped to viewport
float cardHeight = 0.0f; // 0 => auto-height (content-sized); else fixed height (large modals)
float scrimOpacity = 0.92f; // GlassCard scrim only
float cardBottomViewportRatio = 0.85f; // auto-height cap
const char* idSuffix = nullptr; // distinct window/child ids for nesting + capture keying
};
inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
{
const bool blur = spec.blurBackdrop || spec.style == OverlayStyle::BlurFloat;
const bool floating = spec.floatingContent || spec.style == OverlayStyle::BlurFloat;
const bool plainHd = spec.plainHeading || spec.style == OverlayStyle::BlurFloat;
const float dp = Layout::dpiScale();
MarkOverlayDialogActive();
ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 vp_pos = vp->Pos;
ImVec2 vp_size = vp->Size;
ImVec2 vp_pos = vp->Pos, vp_size = vp->Size;
// Dialog widths are authored as raw pixels, but the fonts/spacing inside scale with
// Layout::dpiScale() (which includes the user's font-size setting). Scale the card by the same
// factor so the content doesn't outgrow a fixed card and overflow/misalign at non-default
// scales. No-op at the default scale (dpiScale() == 1). Clamped to the viewport so a large scale
// can't push the card off-screen.
cardWidth *= Layout::dpiScale();
// Cards are authored in raw px but scale with dpiScale (font-size setting); clamp to viewport.
float cardWidth = spec.cardWidth * dp;
cardWidth = std::min(cardWidth, vp_size.x - 32.0f);
// Fullscreen scrim overlay
ImGui::SetNextWindowPos(vp_pos);
ImGui::SetNextWindowSize(vp_size);
ImGui::SetNextWindowFocus();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.04f, 0.04f, 0.06f, scrimOpacity));
// Focus the scrim so the dialog owns input — but not while a popup (combo/color picker) opened
// from the dialog is showing, or forcing focus every frame would immediately close that popup.
const bool overlayPopupOpen =
ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
if (!overlayPopupOpen) ImGui::SetNextWindowFocus();
// Blur overlays draw their own backdrop, so the window itself is transparent.
ImGui::PushStyleColor(ImGuiCol_WindowBg,
blur ? ImVec4(0, 0, 0, 0) : OverlayScrimColor(spec.scrimOpacity));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
std::string scrimId = "##OverlayScrim";
std::string childId = "##OverlayDialogContent";
if (idSuffix && idSuffix[0] != '\0') {
scrimId += "_";
scrimId += idSuffix;
childId += "_";
childId += idSuffix;
if (spec.idSuffix && spec.idSuffix[0] != '\0') {
scrimId += "_"; scrimId += spec.idSuffix;
childId += "_"; childId += spec.idSuffix;
}
bool opened = ImGui::Begin(scrimId.c_str(), nullptr,
@@ -963,79 +1354,129 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoSavedSettings);
if (!opened) {
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
return false;
}
ImDrawList* dl = ImGui::GetWindowDrawList();
// Consume pointer input on the scrim so the overlay owns clicks and wheel
// events even when the click lands outside the card content.
// Live-blur backdrop with capture-once: (re)capture the content drawn BELOW this overlay for a
// few frames on open/resize, then reuse the frozen blurred snapshot. Low-spec / acrylic-off falls
// back to an opaque base inside DrawFullWindowBlurBackdrop (no blur cost).
if (blur) {
MarkBlurOverlayDrawn(spec.p_open);
BlurCaptureState& st = BlurCaptureStateFor(scrimId);
int frame = ImGui::GetFrameCount();
bool justOpened = (st.lastSeenFrame < frame - 1);
st.lastSeenFrame = frame;
if (justOpened || st.w != vp_size.x || st.h != vp_size.y) st.captureFrames = 3;
st.w = vp_size.x; st.h = vp_size.y;
CapturingBlurBackdropRef() = (st.captureFrames > 0);
if (st.captureFrames > 0) {
if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) {
dl->AddCallback(liveCap, nullptr);
dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
}
st.captureFrames--;
}
DrawFullWindowBlurBackdrop(dl, vp_pos, ImVec2(vp_pos.x + vp_size.x, vp_pos.y + vp_size.y),
/*allowBlur=*/!justOpened); // opaque the first frame (nothing captured yet)
} else {
CapturingBlurBackdropRef() = false;
}
// Consume pointer input on the scrim so the overlay owns clicks/wheel even outside the card.
ImGui::SetCursorScreenPos(vp_pos);
ImGui::InvisibleButton("##OverlayInputBlocker", vp_size,
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonRight |
ImGuiButtonFlags_MouseButtonMiddle);
// Calculate card position (centered)
// 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.
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
float cardY = vp_pos.y + vp_size.y * 0.15f;
float cardY, cardBottomY;
const bool fixedHeight = (spec.cardHeight > 0.0f);
if (fixedHeight) {
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
cardY = vp_pos.y + (vp_size.y - cardH) * 0.5f;
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;
}
ImVec2 cardMin(cardX, cardY), cardMax(cardX + cardWidth, cardBottomY);
// Size the card height to its content. The content child below is AutoResizeY, so a glass card
// drawn to a fixed viewport ratio left a tall band of empty glass under short dialogs. Reuse the
// height the child reported LAST frame (content is stable frame-to-frame, so no visible lag) and
// fall back to the ratio on the first frame. Still capped at the ratio so a very tall dialog can't
// run off-screen (its content spills/scrolls as before).
g_overlayCurrentKey = childId;
float ratioMaxY = vp_pos.y + vp_size.y * cardBottomViewportRatio;
auto prevHeightIt = g_overlayCardHeights.find(childId);
float cardBottomY = (prevHeightIt != g_overlayCardHeights.end() && prevHeightIt->second > 0.0f)
? std::min(cardY + prevHeightIt->second, ratioMaxY)
: ratioMaxY;
// Card background — a glass panel unless the content floats directly on the backdrop.
if (!floating) {
GlassPanelSpec cardGlass;
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
}
// Draw glass card background
ImVec2 cardMin(cardX, cardY);
ImVec2 cardMax(cardX + cardWidth, cardBottomY);
// Card background with glass effect
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 the dialog — but NOT on the frame it first appears, otherwise
// the very click that opened it (a button fired the same frame) is read as an outside-click and
// the dialog flashes open then instantly closes.
if (p_open && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
// 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() &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
*p_open = false;
*spec.p_open = false;
}
// Set up child region for card content
// Content child.
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28, 24));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // Transparent - glass already drawn
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
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);
bool childVisible = ImGui::BeginChild(childId.c_str(),
ImVec2(cardWidth, 0), // 0 height = auto-size
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
ImGuiWindowFlags_NoScrollbar);
// Draw title bar with close button
if (childVisible && title) {
DrawDialogTitleBar(title, p_open);
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
cflags, ImGuiWindowFlags_NoScrollbar);
// 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.
if (floating) {
ImGui::PopStyleVar(); // WindowPadding
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f));
}
if (childVisible && spec.title) {
if (plainHd) {
ImGui::PushFont(Type().h6());
ImGui::TextUnformatted(spec.title);
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
} else {
DrawDialogTitleBar(spec.title, spec.p_open);
}
}
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.
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)
{
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;
return BeginOverlayDialog(s);
}
inline void EndOverlayDialog()
{
ImGui::EndChild();
@@ -1069,6 +1510,49 @@ inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator
PlaceOverlayDialogActions(totalActionWidth);
}
// ── Confirmation dialog header / footer ─────────────────────────────────
// Shared scaffolding for the "warning + Cancel/confirm" overlay dialogs.
// Warning icon + "Warning" label row, drawn in `col` (the caller owns the
// exact accent colour; new callers can omit it to use the theme warning).
// Reproduces: PushFont(iconLarge) + ICON_MD_WARNING, PopFont, SameLine,
// TextColored(label). The label text is passed in (already translated).
inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4())
{
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(col, ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(col, "%s", warningLabel);
}
// 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` /
// `outConfirm` when the respective button is pressed (the caller runs the
// action bodies). When `danger`, the confirm button gets the red danger
// style. Button height comes from ui.toml (falls back to 40). Reproduces
// the hand-written footer exactly (btnW split + SameLine + optional danger
// PushStyleColor/PopStyleColor(2)).
inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
bool danger, bool& outCancel, bool& outConfirm)
{
float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f);
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) {
outCancel = true;
}
ImGui::SameLine();
if (danger) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
}
if (ImGui::Button(confirmLabel, ImVec2(btnW, btnH))) {
outConfirm = true;
}
if (danger) {
ImGui::PopStyleColor(2);
}
}
} // namespace material
} // namespace ui
} // namespace dragonx
+68
View File
@@ -0,0 +1,68 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// Edge-fade scroll region for the blur-backdrop modals (smooth scroll + Settings-tab edge fade +
// a thicker, inset, rounded scrollbar). Split out of draw_helpers.h because it depends on
// effects::ScrollFadeShader, whose GL/DX headers must not be pulled into the widely-included base
// header (they clash with SDL's GL headers in some TUs). Include this only from modal .cpp files.
#include "draw_helpers.h" // IsCapturingBlurBackdrop, ApplySmoothScroll
#include "../effects/scroll_fade_shader.h"
#include "../effects/low_spec.h"
#include "imgui.h"
#include <algorithm>
#include <string>
namespace dragonx {
namespace ui {
namespace material {
// Scroll region with smooth scrolling + edge fade + a thicker, inset, rounded scrollbar. Uses a
// bordered/rounded OUTER frame whose padding insets the scrollbar, holding an INNER scrolling child.
// `fade` must persist across frames (one instance per scroll region); `pad` is the inner content
// inset. Returns the inner draw list; pair with EndFadeScrollChild().
inline ImDrawList* BeginFadeScrollChild(const char* id, effects::ScrollFadeShader& fade,
const ImVec2& pad, float dp)
{
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f * dp, 6.0f * dp));
ImGui::BeginChild(id, ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(2); // ChildRounding + outer WindowPadding (captured by the outer child)
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, pad);
std::string innerId = std::string(id) + "_i";
ImGui::BeginChild(innerId.c_str(), ImVec2(0, 0), false, ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(); // inner WindowPadding — captured by this child; don't cascade to tooltips
ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
const float fadeH = 22.0f * dp;
if (!effects::isLowSpecMode() && !IsCapturingBlurBackdrop() && fade.init()) {
ImVec2 cMin = ImGui::GetWindowPos();
ImVec2 cMax(cMin.x + ImGui::GetWindowSize().x, cMin.y + ImGui::GetWindowSize().y);
float sY = ImGui::GetScrollY(), sMax = ImGui::GetScrollMaxY();
fade.fadeTopY = cMin.y;
fade.fadeBottomY = cMax.y;
fade.fadeZoneTop = (sY > 1.0f) ? fadeH : 0.0f;
fade.fadeZoneBottom = (sMax > 0.0f && sY < sMax - 1.0f) ? fadeH : 0.0f;
fade.addBind(dl);
}
return dl;
}
inline void EndFadeScrollChild(effects::ScrollFadeShader& fade, ImDrawList* dl)
{
if (!effects::isLowSpecMode() && !IsCapturingBlurBackdrop() && fade.ready)
effects::ScrollFadeShader::addUnbind(dl);
ImGui::EndChild(); // inner
ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding
ImGui::EndChild(); // outer
}
} // namespace material
} // namespace ui
} // namespace dragonx
+174 -346
View File
@@ -170,6 +170,51 @@ struct SettingsPageState {
static SettingsPageState s_settingsState;
// Low-spec mode enter: snapshot the current effects prefs, then force the
// low-cost overrides. When applyEffects is true, also push the overrides to
// the live effects subsystems (the runtime-sync path passes false since it
// only mirrors an already-applied hotkey toggle into the checkbox state).
static void enterLowSpec(bool applyEffects) {
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.scanline_enabled = false;
if (applyEffects) {
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
effects::ThemeEffects::instance().setEnabled(false);
ConsoleTab::s_scanline_enabled = false;
}
}
// Low-spec mode exit: restore the previously snapshotted effects prefs and
// clear the snapshot. When applyEffects is true, also push them to the live
// effects subsystems. Callers guard this on low_spec_snapshot.valid.
static void exitLowSpec(bool applyEffects) {
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
if (applyEffects) {
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
}
s_settingsState.low_spec_snapshot.valid = false;
}
// Count whitespace-separated words in a (seed) buffer — used to validate/guide restore input.
static int liteSeedWordCount(const char* s) {
int words = 0;
@@ -274,7 +319,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
}
if (started) {
s_settingsState.lite_lifecycle_pending = true;
s_settingsState.lite_lifecycle_status = "Working…";
s_settingsState.lite_lifecycle_status = TR("lite_working");
s_settingsState.lite_lifecycle_summary.clear();
} else {
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
@@ -287,16 +332,12 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
return;
}
const auto result = wallet::executeLiteWalletLifecycleUiRequest(*app->settings(), input);
s_settingsState.lite_lifecycle_summary = result.requestSummaryRedacted;
if (result.ok) {
s_settingsState.lite_lifecycle_status = "Ready";
} else {
s_settingsState.lite_lifecycle_status = result.error.empty()
? wallet::liteWalletLifecycleUiExecutionStatusName(result.status)
: result.error;
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
// No linked lite backend to execute against (full-node build, or the lite backend
// failed to load / rollout-disabled). The live path above returns when a backend is
// present, so reaching here means there is nothing to run.
s_settingsState.lite_lifecycle_summary.clear();
s_settingsState.lite_lifecycle_status = "Lite wallet backend unavailable";
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
// (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -432,28 +473,10 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.low_spec_mode != runtimeLowSpec) {
if (runtimeLowSpec) {
// Hotkey turned low-spec ON — save snapshot, override statics
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.scanline_enabled = false;
enterLowSpec(false);
} else if (s_settingsState.low_spec_snapshot.valid) {
// Hotkey turned low-spec OFF — restore snapshot
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
s_settingsState.low_spec_snapshot.valid = false;
exitLowSpec(false);
} else if (app->settings()) {
// No snapshot — read prefs from settings file
s_settingsState.blur_amount = app->settings()->getBlurMultiplier();
@@ -565,12 +588,7 @@ void RenderSettingsPage(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
float contentW = availWidth - pad * 2;
float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size;
@@ -771,27 +789,9 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Advanced Effects... ---
{
const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
s_settingsState.effects_expanded = !s_settingsState.effects_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
material::CollapsibleHeader(dl, "##EffectsToggle", TR("advanced_effects"),
s_settingsState.effects_expanded, contentW,
body2, OnSurfaceMedium());
if (s_settingsState.effects_expanded) {
ImGui::PushFont(body2);
@@ -800,35 +800,9 @@ void RenderSettingsPage(App* app) {
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) {
effects::setLowSpecMode(s_settingsState.low_spec_mode);
if (s_settingsState.low_spec_mode) {
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.scanline_enabled = false;
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
effects::ThemeEffects::instance().setEnabled(false);
ConsoleTab::s_scanline_enabled = false;
enterLowSpec(true);
} else if (s_settingsState.low_spec_snapshot.valid) {
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.low_spec_snapshot.valid = false;
exitLowSpec(true);
}
saveSettingsPageState(app->settings());
}
@@ -1069,26 +1043,10 @@ void RenderSettingsPage(App* app) {
// --- Collapsible: Advanced Effects... ---
{
const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
float narrowContentW = availWidth - pad * 2;
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) {
s_settingsState.effects_expanded = !s_settingsState.effects_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
float narrowContentW = availWidth - pad * 2;
material::CollapsibleHeader(dl, "##EffectsToggleN", TR("advanced_effects"),
s_settingsState.effects_expanded, narrowContentW,
body2, OnSurfaceMedium());
}
if (s_settingsState.effects_expanded) {
@@ -1097,35 +1055,9 @@ void RenderSettingsPage(App* app) {
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) {
effects::setLowSpecMode(s_settingsState.low_spec_mode);
if (s_settingsState.low_spec_mode) {
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.scanline_enabled = false;
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
effects::ThemeEffects::instance().setEnabled(false);
ConsoleTab::s_scanline_enabled = false;
enterLowSpec(true);
} else if (s_settingsState.low_spec_snapshot.valid) {
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.low_spec_snapshot.valid = false;
exitLowSpec(true);
}
saveSettingsPageState(app->settings());
}
@@ -1223,19 +1155,6 @@ void RenderSettingsPage(App* app) {
ImGui::PopFont();
} // s_settingsState.effects_expanded
}
// Bottom padding
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
// Draw glass panel behind content (auto-sized)
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
@@ -1247,11 +1166,8 @@ void RenderSettingsPage(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
const ImVec2& cardMin = card.cardMin;
float contentW = availWidth - pad * 2;
@@ -1311,27 +1227,9 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Tools & Actions... ---
{
const char* arrow = s_settingsState.tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
{
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
s_settingsState.tools_expanded = !s_settingsState.tools_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"),
s_settingsState.tools_expanded, contentW,
body2, OnSurfaceMedium());
if (s_settingsState.tools_expanded) {
float btnSpacing = Layout::spacingMd();
@@ -1439,17 +1337,6 @@ void RenderSettingsPage(App* app) {
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
}
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
@@ -1462,11 +1349,7 @@ void RenderSettingsPage(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
float contentW = availWidth - pad * 2;
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
@@ -1622,18 +1505,18 @@ void RenderSettingsPage(App* app) {
// Lite-server selection lives in the dedicated Network tab now.
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
"Lite servers are managed in the Network tab.");
TR("lite_servers_network_tab"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) {
if (material::TactileButton(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) {
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
}
if (s_settingsState.lite_lifecycle_expanded) {
const char* lifecycleLabels[] = {"Create", "Open", "Restore"};
const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")};
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Action");
ImGui::TextUnformatted(TR("lite_action"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteLifecycleOperation",
@@ -1654,7 +1537,7 @@ void RenderSettingsPage(App* app) {
s_settingsState.lite_lifecycle_operation == 2) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Wallet");
ImGui::TextUnformatted(TR("lite_wallet_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
@@ -1664,7 +1547,7 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.lite_lifecycle_operation == 2) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Seed");
ImGui::TextUnformatted(TR("lite_seed_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
@@ -1674,38 +1557,38 @@ void RenderSettingsPage(App* app) {
{
const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed);
char wbuf[32];
snprintf(wbuf, sizeof(wbuf), "%d / 24 words", wc);
snprintf(wbuf, sizeof(wbuf), TR("lite_word_count"), wc);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, wc == 24 ? Success() : Warning(), wbuf);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Birthday");
ImGui::TextUnformatted(TR("lite_birthday_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
"Block height to start scanning from. Leave 0 if unknown (slower full scan).");
TR("lite_birthday_hint"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Account");
ImGui::TextUnformatted(TR("lite_account_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::Checkbox("Overwrite##LiteRestoreOverwrite",
ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(),
&s_settingsState.lite_restore_overwrite);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::TextUnformatted(TR("lite_passphrase_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteLifecyclePassphrase",
@@ -1723,7 +1606,7 @@ void RenderSettingsPage(App* app) {
const auto& result = lite->lastLifecycleResult();
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
if (result.walletReady) {
s_settingsState.lite_lifecycle_status = "Wallet ready";
s_settingsState.lite_lifecycle_status = TR("lite_wallet_ready");
} else {
s_settingsState.lite_lifecycle_status = result.error.empty()
? result.status.message
@@ -1739,7 +1622,7 @@ void RenderSettingsPage(App* app) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
if (liteLifecycleBusy) ImGui::BeginDisabled();
if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
evaluateLiteLifecycleRequestFromPageState(app);
}
if (liteLifecycleBusy) ImGui::EndDisabled();
@@ -1769,17 +1652,15 @@ void RenderSettingsPage(App* app) {
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().text(TypeStyle::Body2, "Backup & keys");
Type().text(TypeStyle::Body2, TR("lite_backup_keys"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_show_seed", "LiteExportSeed").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
auto r = app->liteWallet()->exportSeed();
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
if (r.ok) {
s_settingsState.lite_export_secret = r.seedPhrase;
s_settingsState.lite_export_label =
"Seed phrase — the ONLY way to restore your wallet. "
"Write it down, store it offline, never share it.";
s_settingsState.lite_export_label = TR("lite_seed_warning");
s_settingsState.lite_export_is_seed = true;
s_settingsState.lite_export_birthday = r.birthday;
s_settingsState.lite_backup_status.clear();
@@ -1791,13 +1672,13 @@ void RenderSettingsPage(App* app) {
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_show_private_keys", "LiteExportKeys").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
auto r = app->liteWallet()->exportPrivateKeys();
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
s_settingsState.lite_export_is_seed = false; // keys, not a seed
if (r.ok) {
s_settingsState.lite_export_secret = r.privateKeysJson;
s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds";
s_settingsState.lite_export_label = TR("lite_private_keys_warning");
s_settingsState.lite_backup_status.clear();
} else {
s_settingsState.lite_export_label.clear();
@@ -1818,19 +1699,19 @@ void RenderSettingsPage(App* app) {
// The seed's birthday is needed to restore quickly — show + back it up too.
if (s_settingsState.lite_export_is_seed) {
char bday[64];
snprintf(bday, sizeof(bday), "Birthday: %llu (back this up too)",
snprintf(bday, sizeof(bday), TR("lite_birthday_backup"),
s_settingsState.lite_export_birthday);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), bday);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
app->copySecretToClipboard(s_settingsState.lite_export_secret);
}
ImGui::SameLine(0, Layout::spacingSm());
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
if (s_settingsState.lite_export_is_seed &&
TactileButton("Save to file##LiteExportSave", ImVec2(0, 0), S.resolveFont("button"))) {
TactileButton(TrId("lite_save_to_file", "LiteExportSave").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
std::string path = util::Platform::getConfigDir() + "/lite-seed-backup.txt";
std::string content = s_settingsState.lite_export_secret + "\nBirthday: " +
std::to_string(s_settingsState.lite_export_birthday) + "\n";
@@ -1838,11 +1719,11 @@ void RenderSettingsPage(App* app) {
path, content, /*restrictPermissions=*/true);
wallet::secureWipeLiteSecret(content);
s_settingsState.lite_backup_status = ok
? ("Saved (plaintext, owner-only) to " + path)
: ("Could not write " + path);
? (std::string(TR("lite_saved_to")) + path)
: (std::string(TR("lite_could_not_write")) + path);
}
if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
s_settingsState.lite_export_label.clear();
s_settingsState.lite_export_is_seed = false;
@@ -1852,18 +1733,18 @@ void RenderSettingsPage(App* app) {
// Import a spending/viewing key (history appears after the next sync).
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Import key");
ImGui::TextUnformatted(TR("lite_import_key_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
sizeof(s_settingsState.lite_import_key),
ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
s_settingsState.lite_backup_status =
r.ok ? "Key imported — run a sync to scan its history" : r.error;
r.ok ? TR("lite_key_imported") : r.error;
}
if (!s_settingsState.lite_backup_status.empty()) {
@@ -1875,57 +1756,57 @@ void RenderSettingsPage(App* app) {
// ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ----
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().text(TypeStyle::Body2, "Security");
Type().text(TypeStyle::Body2, TR("lite_security"));
const auto& wstate = app->getWalletState();
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
if (!wstate.isEncrypted()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::TextUnformatted(TR("lite_passphrase_label"));
ImGui::SameLine(encLabelX);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Encrypt wallet##LiteEncrypt", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = r.ok ? "Wallet encrypted" : r.error;
s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error;
}
} else {
if (wstate.isLocked()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Unlock");
ImGui::TextUnformatted(TR("lite_unlock"));
ImGui::SameLine(encLabelX);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Unlock##LiteUnlock", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = ok ? "Wallet unlocked" : "Unlock failed";
s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed");
}
} else {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
s_settingsState.lite_encryption_status =
app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed";
app->liteWallet()->lockWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed");
}
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::TextUnformatted(TR("lite_passphrase_label"));
ImGui::SameLine(encLabelX);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Remove encryption##LiteDecrypt", ImVec2(0, 0), S.resolveFont("button"))) {
if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
s_settingsState.lite_encryption_status = r.ok ? "Encryption removed" : r.error;
s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error;
}
}
if (!s_settingsState.lite_encryption_status.empty()) {
@@ -2351,17 +2232,6 @@ void RenderSettingsPage(App* app) {
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
}
}
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
@@ -2373,11 +2243,7 @@ void RenderSettingsPage(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer_section"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
float contentW = availWidth - pad * 2;
ImGui::PushFont(body2);
@@ -2417,17 +2283,6 @@ void RenderSettingsPage(App* app) {
util::Platform::openUrl("https://explorer.dragonx.is");
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_block_explorer"));
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
ImGui::Dummy(ImVec2(0, gap));
@@ -2570,7 +2425,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// DEBUG LOGGING — collapsible card (full-node only: these are dragonxd
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
// daemon debug= categories written to DRAGONX.conf; lite has no daemon)
// ====================================================================
if (app->supportsFullNodeLifecycleActions()) {
@@ -2601,11 +2456,19 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.debug_expanded) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos();
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
// Screenshot sweep — capture every theme x every tab (above the daemon debug categories).
ImGui::TextWrapped("%s", TR("screenshot_sweep_desc"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button")))
app->startScreenshotSweep();
ImGui::SameLine();
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
util::Platform::openFolder(app->screenshotDir());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select"));
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));
@@ -2688,17 +2551,6 @@ void RenderSettingsPage(App* app) {
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
}
ImGui::Dummy(ImVec2(0, bottomPad));
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
}
@@ -2743,12 +2595,8 @@ void RenderSettingsPage(App* app) {
// Confirmation dialog for clearing z-tx history
if (s_settingsState.confirm_clear_ztx) {
if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
ImGui::Spacing();
@@ -2756,15 +2604,14 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_btn").c_str(),
true, doCancel, doConfirm);
if (doCancel) {
s_settingsState.confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_btn").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
@@ -2773,7 +2620,6 @@ void RenderSettingsPage(App* app) {
}
s_settingsState.confirm_clear_ztx = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
@@ -2781,12 +2627,8 @@ void RenderSettingsPage(App* app) {
// Confirmation dialog for deleting blockchain data
if (s_settingsState.confirm_delete_blockchain) {
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
ImGui::Spacing();
@@ -2794,20 +2636,18 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TR("cancel"), TrId("delete_blockchain_confirm", "del_bc_btn").c_str(),
true, doCancel, doConfirm);
if (doCancel) {
s_settingsState.confirm_delete_blockchain = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
if (app->supportsFullNodeLifecycleActions())
app->deleteBlockchainData();
s_settingsState.confirm_delete_blockchain = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
@@ -2829,11 +2669,7 @@ void RenderSettingsPage(App* app) {
}
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
ImGui::Spacing();
@@ -2860,12 +2696,12 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
s_settingsState.confirm_rescan = false;
}
ImGui::SameLine();
ImGui::BeginDisabled(detecting);
if (ImGui::Button(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
if (bootstrapped) {
app->runtimeRescan(s_settingsState.rescan_start_height);
} else {
@@ -2881,11 +2717,7 @@ void RenderSettingsPage(App* app) {
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
if (s_settingsState.confirm_repair_wallet) {
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
@@ -2895,12 +2727,14 @@ void RenderSettingsPage(App* app) {
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TrId("cancel", "repair_wallet_cancel").c_str(), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TrId("cancel", "repair_wallet_cancel").c_str(),
TrId("repair_wallet", "repair_wallet_confirm").c_str(),
false, doCancel, doConfirm);
if (doCancel) {
s_settingsState.confirm_repair_wallet = false;
}
ImGui::SameLine();
if (ImGui::Button(TrId("repair_wallet", "repair_wallet_confirm").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
app->repairWallet();
s_settingsState.confirm_repair_wallet = false;
}
@@ -2911,11 +2745,7 @@ void RenderSettingsPage(App* app) {
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
if (s_settingsState.confirm_reinstall_daemon) {
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
@@ -2925,12 +2755,14 @@ void RenderSettingsPage(App* app) {
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TrId("cancel", "reinstall_daemon_cancel").c_str(),
TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(),
false, doCancel, doConfirm);
if (doCancel) {
s_settingsState.confirm_reinstall_daemon = false;
}
ImGui::SameLine();
if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
app->reinstallBundledDaemon();
s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap
s_settingsState.confirm_reinstall_daemon = false;
@@ -2942,11 +2774,7 @@ void RenderSettingsPage(App* app) {
// Confirm: restart daemon (briefly drops the connection to apply changed options)
if (s_settingsState.confirm_restart_daemon) {
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
@@ -2954,12 +2782,14 @@ void RenderSettingsPage(App* app) {
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TrId("cancel", "restartd_cancel").c_str(), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TrId("cancel", "restartd_cancel").c_str(),
TrId("settings_restart_daemon", "restartd_confirm").c_str(),
false, doCancel, doConfirm);
if (doCancel) {
s_settingsState.confirm_restart_daemon = false;
}
ImGui::SameLine();
if (ImGui::Button(TrId("settings_restart_daemon", "restartd_confirm").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
s_settingsState.debug_cats_dirty = false;
app->restartDaemon();
s_settingsState.confirm_restart_daemon = false;
@@ -2971,11 +2801,7 @@ void RenderSettingsPage(App* app) {
// Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe)
if (s_settingsState.confirm_lite_redownload) {
if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg"));
@@ -2985,12 +2811,14 @@ void RenderSettingsPage(App* app) {
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TrId("cancel", "lite_redl_cancel").c_str(), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TrId("cancel", "lite_redl_cancel").c_str(),
TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(),
false, doCancel, doConfirm);
if (doCancel) {
s_settingsState.confirm_lite_redownload = false;
}
ImGui::SameLine();
if (ImGui::Button(TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
if (auto* lite = app->liteWallet()) lite->startRescan();
s_settingsState.confirm_lite_redownload = false;
}
+8 -12
View File
@@ -6,6 +6,7 @@
#include "../../app.h"
#include "../../data/address_book.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
@@ -100,15 +101,13 @@ void AddressBookDialog::render(App* app)
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
Layout::kDialogCompactBottomRatio(), id)) {
ImGui::Text("%s", TR("label"));
ImGui::SetNextItemWidth(formW);
ImGui::InputText(isEdit ? "##EditLabel" : "##AddLabel", s_edit_label, sizeof(s_edit_label));
material::LabeledInput(TR("label"), isEdit ? "##EditLabel" : "##AddLabel",
s_edit_label, sizeof(s_edit_label), formW);
ImGui::Spacing();
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(formW);
ImGui::InputText(isEdit ? "##EditAddress" : "##AddAddress", s_edit_address, sizeof(s_edit_address));
material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress",
s_edit_address, sizeof(s_edit_address), formW);
if (!isEdit) {
ImGui::SameLine();
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
@@ -119,10 +118,8 @@ void AddressBookDialog::render(App* app)
ImGui::Spacing();
ImGui::Text("%s", TR("notes_optional"));
ImGui::SetNextItemWidth(formW);
ImGui::InputTextMultiline(isEdit ? "##EditNotes" : "##AddNotes",
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
float totalActionsW = actionW * 2.0f + actionGap;
@@ -253,8 +250,7 @@ void AddressBookDialog::render(App* app)
std::string addr_display = entry.address;
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40;
if (addr_display.length() > static_cast<size_t>(addrTruncLen)) {
addr_display = addr_display.substr(0, addrFrontLbl.truncate) + "..." +
addr_display.substr(addr_display.length() - addrBackLbl.truncate);
addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate);
}
ImGui::TextDisabled("%s", addr_display.c_str());
if (ImGui::IsItemHovered()) {
+3 -2
View File
@@ -11,6 +11,7 @@
#include <cstddef>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../material/draw_helpers.h"
#include "../material/project_icons.h"
#include "../theme.h"
@@ -69,7 +70,7 @@ public:
ImGui::Spacing();
{
std::string display = s_address;
if (display.size() > 42) display = display.substr(0, 20) + "..." + display.substr(display.size() - 16);
if (display.size() > 42) display = util::truncateMiddle(display, 20, 16);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str());
}
ImGui::Spacing();
@@ -188,7 +189,7 @@ public:
// "No icon" option
if (showClearIcon) {
ImGui::Spacing();
if (ImGui::SmallButton(TR("clear_icon"))) {
if (material::TactileSmallButton(TR("clear_icon"))) {
s_selectedIcon = -1;
}
}
+1 -1
View File
@@ -119,7 +119,7 @@ public:
// Max button
ImGui::SameLine();
if (ImGui::SmallButton(TR("max"))) {
if (material::TactileSmallButton(TR("max"))) {
snprintf(s_amount, sizeof(s_amount), "%.8f",
maxSendableAmount(s_info.fromBalance, s_fee));
}
-189
View File
@@ -1,189 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "backup_wallet_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "imgui.h"
#include <string>
#include <ctime>
#include <fstream>
#include <filesystem>
namespace dragonx {
namespace ui {
namespace fs = std::filesystem;
using json = nlohmann::json;
// Static state
static bool s_open = false;
static char s_destination[512] = "";
static std::string s_status;
static bool s_backing_up = false;
void BackupWalletDialog::show()
{
s_open = true;
s_status.clear();
s_backing_up = false;
// Generate default destination with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
// Default to home directory
std::string home = util::Platform::getHomeDir();
snprintf(s_destination, sizeof(s_destination), "%s/wallet_backup_%s.dat", home.c_str(), timebuf);
}
bool BackupWalletDialog::isOpen()
{
return s_open;
}
void BackupWalletDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.backup-wallet");
auto backupBtn = S.button("dialogs.backup-wallet", "backup-button");
auto closeBtn = S.button("dialogs.backup-wallet", "close-button");
if (material::BeginOverlayDialog(TR("backup_title"), &s_open, win.width, 0.94f)) {
ImGui::TextWrapped("%s", TR("backup_description"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (s_backing_up) {
ImGui::BeginDisabled();
}
// Destination path
ImGui::Text("%s", TR("backup_destination"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Destination", s_destination, sizeof(s_destination));
ImGui::Spacing();
// Show wallet.dat location
std::string walletPath = util::Platform::getDataDir() + "/wallet.dat";
ImGui::TextDisabled(TR("backup_source"), walletPath.c_str());
// Check if source exists
bool sourceExists = fs::exists(walletPath);
if (!sourceExists) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::Text("%s", TR("backup_wallet_not_found"));
ImGui::PopStyleColor();
}
if (s_backing_up) {
ImGui::EndDisabled();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Backup button - use RPC backupwallet
if (s_backing_up) {
ImGui::BeginDisabled();
}
if (material::StyledButton(TR("backup_create"), ImVec2(backupBtn.width, 0), S.resolveFont(backupBtn.font))) {
if (strlen(s_destination) == 0) {
Notifications::instance().warning("Please enter a destination path");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon");
} else {
s_backing_up = true;
s_status = "Creating backup...";
// Run backup on worker thread to avoid freezing UI
std::string dest(s_destination);
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), dest]() -> rpc::RPCWorker::MainCb {
bool success = false;
std::string statusMsg;
try {
rpc::RPCClient::TraceScope trace("Settings / Backup wallet");
rpc->call("backupwallet", json::array({dest}));
// Check if file was created
if (fs::exists(dest)) {
auto size = fs::file_size(dest);
char sizebuf[32];
if (size > 1024 * 1024) {
snprintf(sizebuf, sizeof(sizebuf), "%.2f MB", size / (1024.0 * 1024.0));
} else if (size > 1024) {
snprintf(sizebuf, sizeof(sizebuf), "%.2f KB", size / 1024.0);
} else {
snprintf(sizebuf, sizeof(sizebuf), "%zu bytes", size);
}
statusMsg = std::string("Backup created successfully (") + sizebuf + ")";
success = true;
} else {
statusMsg = "Backup may have failed - file not found";
}
} catch (const std::exception& e) {
statusMsg = std::string("Backup failed: ") + e.what();
}
return [success, statusMsg]() {
s_status = statusMsg;
s_backing_up = false;
if (success) {
Notifications::instance().success(TR("backup_created"), 5.0f);
} else {
Notifications::instance().warning(statusMsg);
}
};
});
}
}
}
if (s_backing_up) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("%s", TR("backup_backing_up"));
}
ImGui::SameLine();
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Tips
ImGui::TextDisabled("%s", TR("backup_tips"));
ImGui::BulletText("%s", TR("backup_tip_external"));
ImGui::BulletText("%s", TR("backup_tip_multiple"));
ImGui::BulletText("%s", TR("backup_tip_test"));
material::EndOverlayDialog();
}
}
} // namespace ui
} // namespace dragonx
-31
View File
@@ -1,31 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for backing up wallet.dat
*/
class BackupWalletDialog {
public:
// Show the dialog
static void show();
// Render the dialog (call each frame)
static void render(App* app);
// Check if dialog is open
static bool isOpen();
};
} // namespace ui
} // namespace dragonx
+4 -6
View File
@@ -200,10 +200,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(sharedAddrSyncing);
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots());
TactileButton(genLabel, ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
@@ -774,9 +772,9 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
const auto& state = app->state();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions"));
ImGui::SameLine();
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
if (TactileSmallButton(TR("view_all"), S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
app->setCurrentPage(NavPage::History);
}
ImGui::Spacing();
@@ -791,7 +789,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
int count = (int)txs.size();
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
+1 -8
View File
@@ -10,13 +10,6 @@ namespace dragonx {
namespace ui {
namespace {
std::string truncateRecentAddress(const std::string& address, int maxLen)
{
if (maxLen <= 3 || address.length() <= static_cast<size_t>(maxLen)) return address;
int half = (maxLen - 3) / 2;
return address.substr(0, half) + "..." + address.substr(address.length() - half);
}
std::string formatRecentAmount(const std::string& type, double amount)
{
char buffer[32];
@@ -36,7 +29,7 @@ RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLe
{
return {
tx.getTypeDisplay(),
truncateRecentAddress(tx.address, addressMaxLen),
util::truncateMiddle(tx.address, addressMaxLen),
formatRecentAmount(tx.type, tx.amount),
recentTimeAgo(tx.timestamp)
};
+16 -575
View File
@@ -218,17 +218,11 @@ static void RenderBalanceClassic(App* app)
{
using namespace material;
const auto& S = schema::UISchema::instance();
const auto addrBtn = S.button("tabs.balance", "address-button");
const auto actionBtn = S.button("tabs.balance", "action-button");
const auto searchIn = S.input("tabs.balance", "search-input");
const auto addrTable = S.table("tabs.balance", "address-table");
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
// Read layout properties from schema
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
const float kHeroPadTop = S.drawElement("tabs.balance", "hero-pad-top").sizeOr(12.0f);
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
const float kButtonRowRightMargin = S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f);
const auto& state = app->state();
@@ -241,17 +235,6 @@ static void RenderBalanceClassic(App* app)
const float cGap = Layout::cardGap();
const float dp = Layout::dpiScale();
// Responsive constants (scale with window size)
const float kSparklineHeight = std::max(S.drawElement("tabs.balance", "sparkline-min-height").size, S.drawElement("tabs.balance", "sparkline-height").size * vs);
const float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size, S.drawElement("tabs.balance", "buttons-position").size * hs);
// Dynamic recent tx count — fit as many as space allows
const float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
// At minimum size ~601px avail, reserve ~300px for hero+cards+addr header,
// rest for address list + recent txs. Show 3-5 rows depending on space.
const int kRecentTxCount = std::clamp(
(int)((contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f)) / scaledRowH), 2, 5);
// Lerp displayed balances toward actual values
{
float dt = ImGui::GetIO().DeltaTime;
@@ -634,561 +617,19 @@ static void RenderBalanceClassic(App* app)
}
// ================================================================
// Address list — DrawList-based rows (matches recent tx style)
// Address list + Recent transactions — shared renderers (feature-complete:
// set-label / add-to-portfolio context items, custom address icons, drag-to-reorder,
// keyboard nav, copy-flash, i18n). Classic keeps its own address-table-height override
// (see tabs.balance.classic) so the card sizing is unchanged.
// ================================================================
{
// Header row: title only
Type().text(TypeStyle::H6, TR("your_addresses"));
ImGui::Spacing();
// Static filter state (declared here, UI rendered below with ADDRESSES overline)
static char addr_search[128] = "";
static bool s_hideZeroBalances = true;
static bool s_showHidden = false;
// Build a merged + sorted list of all addresses
struct AddrRow {
const AddressInfo* info;
bool isZ;
bool hidden;
bool favorite;
bool mining;
};
std::vector<AddrRow> rows;
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
// The search text is constant across the loop and containsIgnoreCase is now allocation-free,
// so build the filter view ONCE (was a per-address std::string + two tolower-copies/frame).
const std::string_view addrFilter(addr_search);
for (const auto& a : state.z_addresses) {
if (!util::containsIgnoreCase(a.address, addrFilter) &&
!util::containsIgnoreCase(a.label, addrFilter))
continue;
bool isHidden = app->isAddressHidden(a.address);
if (isHidden && !s_showHidden) continue;
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
continue;
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
}
for (const auto& a : state.t_addresses) {
if (!util::containsIgnoreCase(a.address, addrFilter) &&
!util::containsIgnoreCase(a.label, addrFilter))
continue;
bool isHidden = app->isAddressHidden(a.address);
if (isHidden && !s_showHidden) continue;
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
continue;
rows.push_back({&a, false, isHidden, isFav, app->isMiningAddress(a.address)});
}
// Sort: favorites first, then Z addresses, then by balance descending
static int s_sortCol = 3; // default sort by balance
static bool s_sortAsc = false;
std::sort(rows.begin(), rows.end(),
[](const AddrRow& a, const AddrRow& b) -> bool {
if (a.favorite != b.favorite) return a.favorite > b.favorite;
if (a.isZ != b.isZ) return a.isZ > b.isZ; // Z first
if (s_sortAsc)
return a.info->balance < b.info->balance;
else
return a.info->balance > b.info->balance;
});
// Recent TX gets sizing priority — compute its reserve first,
// then the address list gets whatever remains.
float scaledTxRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
// Recent TX section: header + kRecentTxCount rows + status label + gaps
float recentTxReserve = S.drawElement("tabs.balance", "recent-tx-header-height").size * vs + kRecentTxCount * scaledTxRowH + Layout::spacingXl();
// Search + create buttons row
float avail = ImGui::GetContentRegionAvail().x;
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
ImGui::SetNextItemWidth(searchW);
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox(TrId("hide_zero_balances", "hide0").c_str(), &s_hideZeroBalances);
{
int hc = app->getHiddenAddressCount();
if (hc > 0) {
ImGui::SameLine(0, Layout::spacingLg());
char hlbl[64];
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
ImGui::Checkbox(hlbl, &s_showHidden);
} else {
s_showHidden = false;
}
}
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
float totalButtonsWidth = buttonWidth * 2 + spacing;
ImGui::SameLine(std::max(kMinButtonsPosition,
ImGui::GetWindowWidth() - totalButtonsWidth - kButtonRowRightMargin));
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(addrSyncing);
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}
ImGui::SameLine();
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
app->createNewTAddress([](const std::string& addr) {
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
});
}
ImGui::EndDisabled();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float availWidth = ImGui::GetContentRegionAvail().x;
// Address list gets whatever height remains after recent TX reserve
float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
float addrListH;
if (classicAddrH >= 0.0f) {
addrListH = classicAddrH * dp; // scaled for DPI + font scale
} else {
addrListH = ImGui::GetContentRegionAvail().y - recentTxReserve;
}
// Keep address list at a reasonable minimum; if too tight,
// shrink recent-tx reserve instead so both remain visible.
float addrListMin = S.drawElement("tabs.balance", "addr-list-min-height").sizeOr(40.0f);
if (addrListH < addrListMin) {
addrListH = addrListMin;
}
// Glass panel wrapping the list area (matching tx list)
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
ImVec2 listPanelMax(listPanelMin.x + availWidth, listPanelMin.y + addrListH);
GlassPanelSpec addrGlassSpec;
addrGlassSpec.rounding = glassRound;
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
// Scroll-edge mask state
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
int addrParentVtx = dlPanel->VtxBuffer.Size;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
ImGui::BeginChild("AddressList", ImVec2(availWidth, addrListH), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
int addrChildVtx = addrChildDL->VtxBuffer.Size;
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 16 * dp));
float cw = ImGui::GetContentRegionAvail().x;
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
ImGui::TextDisabled("%s", TR("not_connected"));
} else if (rows.empty()) {
// Empty state
float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
if (addr_search[0]) {
ImVec2 textSz = ImGui::CalcTextSize("No matching addresses");
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("No matching addresses");
} else {
const char* msg = "No addresses yet";
ImVec2 msgSz = ImGui::CalcTextSize(msg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("%s", msg);
}
} else {
// DrawList-based address rows (matching transaction list style)
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
static int selected_row = -1;
addrScrollY = ImGui::GetScrollY();
addrScrollMaxY = ImGui::GetScrollMaxY();
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
float rowPadLeft = Layout::cardInnerPadding();
float rowPadRight = Layout::cardInnerPadding();
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, S.drawElement("tabs.balance", "address-icon-size").size * hs);
float innerW = ImGui::GetContentRegionAvail().x;
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
const auto& row = rows[row_idx];
const auto& addr = *row.info;
ImVec2 rowPos = ImGui::GetCursorScreenPos();
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
ImU32 typeCol = row.isZ ? greenCol : goldCol;
if (row.hidden) typeCol = OnSurfaceDisabled();
// Golden border for favorites
if (row.favorite) {
ImU32 favBorder = IM_COL32(255, 200, 50, 120);
dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp);
}
// Selected indicator (left accent bar)
if (selected_row == row_idx) {
ImDrawFlags accentFlags = 0;
float accentRound = 2.0f * dp;
if (row_idx == 0) {
accentFlags = ImDrawFlags_RoundCornersTopLeft;
accentRound = glassRound;
}
if (row_idx == (int)rows.size() - 1) {
accentFlags |= ImDrawFlags_RoundCornersBottomLeft;
accentRound = glassRound;
}
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp);
}
// Hover glow
bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered && selected_row != row_idx) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
float cx = rowPos.x + rowPadLeft;
float cy = rowPos.y + Layout::spacingMd();
// --- Button zone (right edge): [eye] [star] ---
float btnH = rowH - Layout::spacingSm() * 2.0f;
float btnW = btnH;
float btnGap = Layout::spacingSm();
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
float rightEdge = rowPos.x + innerW;
float starX = rightEdge - btnW - rowPadRight;
float eyeX = starX - btnGap - btnW;
float btnRound = 6.0f * dp;
bool btnClicked = false;
// Star button (always shown, rightmost)
{
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12);
ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25);
dl->AddRectFilled(bMin, bMax, starFill, btnRound);
dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = material::Type().iconSmall();
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled());
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
if (bHov && ImGui::IsMouseClicked(0)) {
if (row.favorite) app->unfavoriteAddress(addr.address);
else app->favoriteAddress(addr.address);
btnClicked = true;
}
if (bHov) material::Tooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
}
// Eye button (zero balance or hidden)
if (addr.balance < 1e-9 || row.hidden) {
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12);
ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25);
dl->AddRectFilled(bMin, bMax, eyeFill, btnRound);
dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = material::Type().iconSmall();
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
if (bHov && ImGui::IsMouseClicked(0)) {
if (row.hidden) app->unhideAddress(addr.address);
else app->hideAddress(addr.address);
btnClicked = true;
}
if (bHov) material::Tooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
}
// Content zone ends before buttons
float contentRight = eyeX - Layout::spacingSm();
// Type icon (shield for Z, circle for T)
float iconCx = cx + rowIconSz;
float iconCy = cy + body2->LegacySize * 0.5f;
if (row.isZ) {
ImFont* iconFont = material::Type().iconSmall();
const char* shieldIcon = ICON_MD_SHIELD;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon);
} else {
ImFont* iconFont = material::Type().iconSmall();
const char* circIcon = ICON_MD_CIRCLE;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon);
}
// Type label (first line, next to icon)
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
const char* hiddenTag = row.hidden ? " (hidden)" : "";
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
const char* miningTag = row.mining ? TR("mining_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// Label (if present, next to type)
if (!addr.label.empty()) {
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
OnSurfaceMedium(), addr.label.c_str());
}
// Address (second line) — show full if it fits, otherwise truncate
float addrAvailW = contentRight - labelX;
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
std::string display_addr;
if (fullAddrSz.x <= addrAvailW) {
display_addr = addr.address;
} else {
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
? addrTable.columns.at("address").truncate : 32;
display_addr = truncateAddress(addr.address, addrTruncLen);
}
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), display_addr.c_str());
// Balance (right-aligned within content zone)
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
float balX = contentRight - balSz.x;
ImU32 balCol = addr.balance > 0.0
? (row.isZ ? greenCol : OnSurface())
: OnSurfaceDisabled();
if (row.hidden) balCol = OnSurfaceDisabled();
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
// USD equivalent (right-aligned, second line)
double priceUsd = state.market.price_usd;
if (priceUsd > 0.0 && addr.balance > 0.0) {
char usdBuf[32];
double usdVal = addr.balance * priceUsd;
if (usdVal >= 1.0)
snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
else if (usdVal >= 0.01)
snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
else
snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(contentRight - usdSz.x,
cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), usdBuf);
}
// Click to copy + select
if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) {
ImGui::SetClipboardText(addr.address.c_str());
selected_row = row_idx;
}
// Invisible button for context menu + tooltip
ImGui::PushID(row_idx);
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
// Tooltip with full address
if (ImGui::IsItemHovered() && !btnClicked) {
material::Tooltip("%s", addr.address.c_str());
}
// Right-click context menu
const auto& acrTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
if (ImGui::MenuItem(TR("copy_address"))) {
ImGui::SetClipboardText(addr.address.c_str());
}
if (ImGui::MenuItem(TR("send_from_this_address"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::Separator();
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
app->setMiningAddress(addr.address, !row.mining);
}
if (ImGui::MenuItem(TR("export_private_key"))) {
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
}
if (row.isZ) {
if (ImGui::MenuItem(TR("export_viewing_key"))) {
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
}
}
if (ImGui::MenuItem(TR("show_qr_code"))) {
QRPopupDialog::show(addr.address,
row.isZ ? "Z-Address" : "T-Address");
}
ImGui::Separator();
if (row.hidden) {
if (ImGui::MenuItem(TR("restore_address")))
app->unhideAddress(addr.address);
} else if (addr.balance < 1e-9) {
if (ImGui::MenuItem(TR("hide_address")))
app->hideAddress(addr.address);
}
if (row.favorite) {
if (ImGui::MenuItem(TR("remove_favorite")))
app->unfavoriteAddress(addr.address);
} else {
if (ImGui::MenuItem(TR("favorite_address")))
app->favoriteAddress(addr.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
ImGui::PopID();
// Subtle divider between rows (matching tx list)
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(
ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
IM_COL32(255, 255, 255, 15));
}
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::EndChild();
ImGui::PopStyleVar(); // WindowPadding for address list
// CSS-style clipping mask (same as history list)
{
float fadeZone = std::min(
(Type().body2()->LegacySize + Type().caption()->LegacySize + Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
addrListH * 0.18f);
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
}
// Status line (matching tx list)
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
char countBuf[128];
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses",
(int)rows.size(), totalAddrs);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
}
}
// ================================================================
// Recent Transactions
// ================================================================
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
ImGui::SameLine();
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
app->setCurrentPage(NavPage::History);
}
ImGui::Spacing();
ImFont* capFont = Type().caption();
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
ImGuiWindowFlags_NoBackground);
const auto& txs = state.transactions;
int count = (int)txs.size();
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
"No transactions yet");
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
for (int i = 0; i < count; i++) {
const auto& tx = txs[i];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowY = rowPos.y + rowH * 0.5f;
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
// Type label
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize,
ImVec2(tx_x, rowPos.y + 2 * dp),
OnSurfaceMedium(), display.typeText.c_str());
// Address (truncated)
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2 * dp),
OnSurfaceDisabled(), display.addressText.c_str());
// Amount (right-aligned area)
ImVec2 amtSz = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, display.amountText.c_str());
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, S.drawElement("tabs.balance", "amount-right-margin").size * hs);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2 * dp),
recentTxAmountColor(tx.type),
display.amountText.c_str());
// Time ago
ImVec2 agoSz = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, display.timeText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
OnSurfaceDisabled(), display.timeText.c_str());
// Clickable row — hover highlight + navigate to History
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
if (material::IsRectHovered(rowPos, rowEnd)) {
dl->AddRectFilled(rowPos, rowEnd,
IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Show the full, untruncated address — two z-addresses can truncate to the
// same first/last window, so the truncated text alone can't disambiguate.
if (!tx.address.empty())
material::Tooltip("%s", tx.address.c_str());
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::History);
}
ImGui::Dummy(ImVec2(0, rowH));
}
}
ImGui::EndChild();
float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp
: ImGui::GetContentRegionAvail().y - recentReserve
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs);
RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs);
}
}
@@ -2189,9 +1630,9 @@ static void RenderBalanceShield(App* app) {
// Label below gauge
ImFont* capFont = Type().caption();
const char* statusMsg;
if (privPct >= 80.0f) statusMsg = "Great privacy!";
else if (privPct >= 50.0f) statusMsg = "Consider shielding more";
else statusMsg = "Low privacy — shield funds";
if (privPct >= 80.0f) statusMsg = TR("privacy_great");
else if (privPct >= 50.0f) statusMsg = TR("privacy_medium");
else statusMsg = TR("privacy_low");
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000, 0, statusMsg);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(gaugeCx - msgSz.x * 0.5f, gaugeCy + 4 * dp),
@@ -2307,7 +1748,7 @@ static void RenderBalanceTimeline(App* app) {
DrawGlassPanel(dl, chartMin, chartMax, spec);
ImFont* capFont = Type().caption();
const char* msg = "Balance history — collecting data...";
const char* msg = TR("balance_history_collecting");
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, msg);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(chartMin.x + (availW - msgSz.x) * 0.5f,
@@ -2629,10 +2070,10 @@ static void RenderBalanceMinimal(App* app) {
ImGui::Dummy(heroSz);
// Line 2: Shielded + Transparent
snprintf(buf, sizeof(buf), "Shielded: %.8f", s_dispShielded);
snprintf(buf, sizeof(buf), TR("balance_shielded_fmt"), s_dispShielded);
Type().textColored(TypeStyle::Caption, Success(), buf);
ImGui::SameLine(0, Layout::spacingLg());
snprintf(buf, sizeof(buf), "Transparent: %.8f", s_dispTransparent);
snprintf(buf, sizeof(buf), TR("balance_transparent_fmt"), s_dispTransparent);
Type().textColored(TypeStyle::Caption, Warning(), buf);
// USD value
+1 -3
View File
@@ -36,9 +36,7 @@ std::string timeAgo(int64_t timestamp)
std::string truncateAddress(const std::string& address, int maxLen)
{
if (maxLen <= 3 || address.length() <= static_cast<size_t>(maxLen)) return address;
int half = (maxLen - 3) / 2;
return address.substr(0, half) + "..." + address.substr(address.length() - half);
return util::truncateMiddle(address, maxLen);
}
ImU32 recentTxIconColor(const std::string& type)
+9 -3
View File
@@ -7,6 +7,7 @@
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
@@ -100,7 +101,12 @@ void BlockInfoDialog::render(App* app)
auto hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
auto closeBtn = S.button("dialogs.block-info", "close-button");
if (material::BeginOverlayDialog(TR("block_info_title"), &s_open, win.width, 0.94f)) {
material::OverlayDialogSpec ov;
ov.title = TR("block_info_title"); ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = win.width; // keep the authored width
ov.cardBottomViewportRatio = 0.94f;
if (material::BeginOverlayDialog(ov)) {
auto* rpc = app->rpc();
const auto& state = app->getWalletState();
@@ -251,7 +257,7 @@ void BlockInfoDialog::render(App* app)
// Truncate for display
std::string prev_short = s_prev_hash;
if (prev_short.length() > static_cast<size_t>(hashLbl.truncate)) {
prev_short = prev_short.substr(0, hashFrontLbl.truncate) + "..." + prev_short.substr(prev_short.length() - hashBackLbl.truncate);
prev_short = util::truncateMiddle(prev_short, hashFrontLbl.truncate, hashBackLbl.truncate);
}
ImGui::Text("%s", prev_short.c_str());
ImGui::PopStyleColor();
@@ -273,7 +279,7 @@ void BlockInfoDialog::render(App* app)
// Truncate for display
std::string next_short = s_next_hash;
if (next_short.length() > static_cast<size_t>(hashLbl.truncate)) {
next_short = next_short.substr(0, hashFrontLbl.truncate) + "..." + next_short.substr(next_short.length() - hashBackLbl.truncate);
next_short = util::truncateMiddle(next_short, hashFrontLbl.truncate, hashBackLbl.truncate);
}
ImGui::Text("%s", next_short.c_str());
ImGui::PopStyleColor();
+5 -13
View File
@@ -53,7 +53,10 @@ public:
using namespace material;
const float dp = Layout::dpiScale();
if (BeginOverlayDialog(TR("download_bootstrap"), &s_open, 500.0f, 0.94f)) {
OverlayDialogSpec ov;
ov.title = TR("download_bootstrap"); ov.p_open = &s_open;
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 500.0f; ov.cardBottomViewportRatio = 0.94f;
if (BeginOverlayDialog(ov)) {
if (s_state == State::Confirm) {
renderConfirm(dp);
@@ -159,18 +162,7 @@ private:
ImGui::Spacing();
// Progress bar
float barH = 8.0f * dp;
float barW = ImGui::GetContentRegionAvail().x;
ImVec2 barMin = ImGui::GetCursorScreenPos();
ImVec2 barMax(barMin.x + barW, barMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(barMin, barMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
float fillW = barW * (prog.percent / 100.0f);
if (fillW > 0) {
dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y),
Primary(), 4.0f * dp);
}
ImGui::Dummy(ImVec2(0, barH));
material::ProgressBar(ImGui::GetContentRegionAvail().x, 8.0f * dp, prog.percent / 100.0f);
ImGui::Spacing();
// Percent + status text
+20 -10
View File
@@ -111,12 +111,15 @@ void ConsoleTab::refreshColors()
ImU32 defInf = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
ImU32 defRpc = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
COLOR_COMMAND = !cmd.color.empty() ? S.resolveColor(cmd.color, defCmd) : defCmd;
COLOR_RESULT = !res.color.empty() ? S.resolveColor(res.color, defRes) : defRes;
COLOR_ERROR = !err.color.empty() ? S.resolveColor(err.color, defErr) : defErr;
COLOR_DAEMON = !dmn.color.empty() ? S.resolveColor(dmn.color, defDmn) : defDmn;
COLOR_INFO = !inf.color.empty() ? S.resolveColor(inf.color, defInf) : defInf;
COLOR_RPC = !rpc.color.empty() ? S.resolveColor(rpc.color, defRpc) : defRpc;
// The schema console colors are authored for the dark terminal (light-on-dark); honor them only
// in dark themes. Light themes always use the dark-text defaults so log text stays legible on the
// now near-white console surface (otherwise the schema's light-blue text sat pale-on-white ~1.2:1).
COLOR_COMMAND = (dark && !cmd.color.empty()) ? S.resolveColor(cmd.color, defCmd) : defCmd;
COLOR_RESULT = (dark && !res.color.empty()) ? S.resolveColor(res.color, defRes) : defRes;
COLOR_ERROR = (dark && !err.color.empty()) ? S.resolveColor(err.color, defErr) : defErr;
COLOR_DAEMON = (dark && !dmn.color.empty()) ? S.resolveColor(dmn.color, defDmn) : defDmn;
COLOR_INFO = (dark && !inf.color.empty()) ? S.resolveColor(inf.color, defInf) : defInf;
COLOR_RPC = (dark && !rpc.color.empty()) ? S.resolveColor(rpc.color, defRpc) : defRpc;
} else {
// No schema — use hardcoded defaults per theme
COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
@@ -145,7 +148,7 @@ ImU32 ConsoleTab::channelTextColor(ConsoleChannel channel) const
case ConsoleChannel::JsonKey: return WithAlpha(Secondary(), 255);
case ConsoleChannel::JsonString: return WithAlpha(Success(), 255);
case ConsoleChannel::JsonNumber: return WithAlpha(Warning(), 255);
case ConsoleChannel::JsonBrace: return IM_COL32(200, 200, 200, 150);
case ConsoleChannel::JsonBrace: return IsLightTheme() ? IM_COL32(90, 90, 90, 180) : IM_COL32(200, 200, 200, 150);
case ConsoleChannel::None:
default: return COLOR_RESULT;
}
@@ -266,7 +269,10 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
// Terminal-dark overlay: dim the glass so console text reads like a real terminal.
{
int darkA = static_cast<int>(schema::UI().drawElement("tabs.console", "bg-darken-alpha").sizeOr(110.0f));
dlOut->AddRectFilled(outPanelMin, outPanelMax, IM_COL32(0, 0, 0, darkA), outGlass.rounding);
// Light skins get a near-white terminal surface (not a black darken) so the dark, theme-aware
// log text stays legible instead of sitting dark-on-dark-grey.
ImU32 termOverlay = IsLightTheme() ? IM_COL32(255, 255, 255, 205) : IM_COL32(0, 0, 0, darkA);
dlOut->AddRectFilled(outPanelMin, outPanelMax, termOverlay, outGlass.rounding);
}
int consoleParentVtx = dlOut->VtxBuffer.Size;
@@ -1341,7 +1347,11 @@ void ConsoleTab::renderCommandsPopup()
using namespace material;
float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
if (!material::BeginOverlayDialog(TR("console_rpc_reference"), &show_commands_popup_, popW, 0.94f)) {
material::OverlayDialogSpec ov;
ov.title = TR("console_rpc_reference"); ov.p_open = &show_commands_popup_;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = popW; ov.cardBottomViewportRatio = 0.94f; // keep authored width
if (!material::BeginOverlayDialog(ov)) {
return;
}
@@ -1463,7 +1473,7 @@ void ConsoleTab::renderCommandsPopup()
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Close button
if (ImGui::Button(TR("console_close"), ImVec2(-1, 0))) {
if (material::TactileButton(TR("console_close"), ImVec2(-1, 0))) {
cmdFilter[0] = '\0';
show_commands_popup_ = false;
}
+5 -11
View File
@@ -62,7 +62,10 @@ public:
}
using namespace material;
const float dp = Layout::dpiScale();
if (BeginOverlayDialog(TR("daemon_update_title"), &s_open, 480.0f, 0.94f)) {
OverlayDialogSpec ov;
ov.title = TR("daemon_update_title"); ov.p_open = &s_open;
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
if (BeginOverlayDialog(ov)) {
const auto p = s_updater->getProgress();
using St = util::DaemonUpdater::State;
switch (p.state) {
@@ -186,16 +189,7 @@ private:
: TR("daemon_update_installing");
Type().text(TypeStyle::Subtitle2, title);
ImGui::Spacing();
const float barH = 8.0f * dp;
const float barW = fullW();
const ImVec2 bMin = ImGui::GetCursorScreenPos();
const ImVec2 bMax(bMin.x + barW, bMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
const float fillW = barW * (p.percent / 100.0f);
if (fillW > 0)
dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp);
ImGui::Dummy(ImVec2(0, barH));
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
ImGui::Spacing();
ImGui::Text("%s", p.status_text.c_str());
ImGui::Spacing();
+18 -39
View File
@@ -8,6 +8,8 @@
#include "../../rpc/rpc_worker.h"
#include "../../data/wallet_state.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "mining_tab_helpers.h"
#include "../explorer/explorer_block_cache.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
@@ -117,9 +119,7 @@ static const char* relativeTime(int64_t timestamp) {
}
static std::string truncateHash(const std::string& hash, int front, int back) {
if (hash.length() <= static_cast<size_t>(front + back + 3))
return hash;
return hash.substr(0, front) + "..." + hash.substr(hash.length() - back);
return util::truncateMiddle(hash, front, back);
}
// Adaptive hash truncation based on available pixel width
@@ -176,21 +176,6 @@ static std::string formatSize(int bytes) {
return b;
}
static std::string formatHashrate(double hashrate) {
char b[32];
if (hashrate >= 1e12)
snprintf(b, sizeof(b), "%.2f TH/s", hashrate / 1e12);
else if (hashrate >= 1e9)
snprintf(b, sizeof(b), "%.2f GH/s", hashrate / 1e9);
else if (hashrate >= 1e6)
snprintf(b, sizeof(b), "%.2f MH/s", hashrate / 1e6);
else if (hashrate >= 1e3)
snprintf(b, sizeof(b), "%.2f KH/s", hashrate / 1e3);
else
snprintf(b, sizeof(b), "%.0f H/s", hashrate);
return b;
}
// Copy icon button — draws a small icon that copies text to clipboard on click
static void copyButton(const char* id, const std::string& text, float x, float y) {
ImFont* iconFont = Type().iconSmall();
@@ -615,17 +600,11 @@ static void renderChainStats(App* app, float availWidth) {
float maxTextW = cardW * 0.34f;
statusText = truncateHashToFit(statusText, capFont, maxTextW);
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, statusText.c_str());
float pillPadX = Layout::spacingSm();
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
float pillW = textSz.x + pillPadX * 2.0f;
ImVec2 pillMin(cardMin.x + cardW - pad - pillW, cardMin.y + pad * 0.5f);
ImVec2 pillMax(pillMin.x + pillW, pillMin.y + pillH);
dl->AddRectFilled(pillMin, pillMax, WithAlpha(pillCol, 32), pillH * 0.5f);
dl->AddRect(pillMin, pillMax, WithAlpha(pillCol, 110), pillH * 0.5f, 0, 1.0f * dp);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(pillMin.x + pillPadX, pillMin.y + Layout::spacingXs()), pillCol, statusText.c_str());
ImVec2 pillPad(Layout::spacingSm(), Layout::spacingXs());
ImVec2 pillSz = material::PillSize(statusText.c_str(), capFont, pillPad);
ImVec2 pillMin(cardMin.x + cardW - pad - pillSz.x, cardMin.y + pad * 0.5f);
material::DrawPill(dl, pillMin, statusText.c_str(), capFont, pillCol,
WithAlpha(pillCol, 32), WithAlpha(pillCol, 110), pillPad);
};
auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {
@@ -698,7 +677,7 @@ static void renderChainStats(App* app, float availWidth) {
MetricSpec metrics[4] = {
{ ICON_MD_SPEED, TR("difficulty"), difficulty, "", Warning() },
{ ICON_MD_SHOW_CHART, TR("peers_hashrate"), formatHashrate(state.mining.networkHashrate), "", Success() },
{ ICON_MD_SHOW_CHART, TR("peers_hashrate"), FormatHashrate(state.mining.networkHashrate), "", Success() },
{ ICON_MD_CONFIRMATION_NUMBER, TR("peers_notarized"), notarized, "", Primary() },
{ ICON_MD_STORAGE, TR("explorer_mempool"), mempoolTxs, s_mempool_loading ? std::string("") : formatSize(static_cast<int>(s_mempool_size)), Secondary() },
};
@@ -1030,10 +1009,8 @@ static void renderBlockDetailModal(App* app) {
// Show a loading modal while fetching
if (BeginOverlayDialog(TR("loading"), &s_detail_loading, 300.0f, 0.85f)) {
ImGui::Spacing();
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char loadBuf[64];
snprintf(loadBuf, sizeof(loadBuf), "%s%s", TR("loading"), dotStr[dots]);
snprintf(loadBuf, sizeof(loadBuf), "%s%s", TR("loading"), material::LoadingDots());
ImGui::TextDisabled("%s", loadBuf);
ImGui::Spacing();
EndOverlayDialog();
@@ -1041,7 +1018,11 @@ static void renderBlockDetailModal(App* app) {
return;
}
if (!BeginOverlayDialog(TR("explorer_block_detail"), &s_show_detail_modal, modalW, 0.90f))
OverlayDialogSpec ov;
ov.title = TR("explorer_block_detail"); ov.p_open = &s_show_detail_modal;
ov.style = OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = modalW; ov.cardBottomViewportRatio = 0.90f; // keep authored width
if (!BeginOverlayDialog(ov))
return;
float contentW = ImGui::GetContentRegionAvail().x;
@@ -1062,7 +1043,7 @@ static void renderBlockDetailModal(App* app) {
if (s_detail_height > 1) {
ImGui::PushFont(Type().iconMed());
ImGui::PushID("prevBlock");
if (ImGui::SmallButton(ICON_MD_CHEVRON_LEFT)) {
if (material::TactileSmallButton(ICON_MD_CHEVRON_LEFT)) {
if (app->rpc() && app->rpc()->isConnected())
fetchBlockDetail(app, s_detail_height - 1);
}
@@ -1076,7 +1057,7 @@ static void renderBlockDetailModal(App* app) {
if (!s_detail_next_hash.empty()) {
ImGui::PushFont(Type().iconMed());
ImGui::PushID("nextBlock");
if (ImGui::SmallButton(ICON_MD_CHEVRON_RIGHT)) {
if (material::TactileSmallButton(ICON_MD_CHEVRON_RIGHT)) {
if (app->rpc() && app->rpc()->isConnected())
fetchBlockDetail(app, s_detail_height + 1);
}
@@ -1091,9 +1072,7 @@ static void renderBlockDetailModal(App* app) {
// Show loading state inside the modal when navigating between blocks
if (s_detail_loading) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
snprintf(buf, sizeof(buf), "%s%s", TR("loading"), dotStr[dots]);
snprintf(buf, sizeof(buf), "%s%s", TR("loading"), material::LoadingDots());
ImGui::TextDisabled("%s", buf);
EndOverlayDialog();
return;
+6 -4
View File
@@ -69,7 +69,11 @@ void ExportAllKeysDialog::render(App* app)
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
if (material::BeginOverlayDialog(TR("export_keys_title"), &s_open, win.width, 0.94f)) {
material::OverlayDialogSpec ov;
ov.title = TR("export_keys_title"); ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = win.width; ov.cardBottomViewportRatio = 0.94f; // keep authored width
if (material::BeginOverlayDialog(ov)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
@@ -95,9 +99,7 @@ void ExportAllKeysDialog::render(App* app)
ImGui::Spacing();
// Filename
ImGui::Text("%s", TR("output_filename"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("file_save_location"));
@@ -82,9 +82,7 @@ void ExportTransactionsDialog::render(App* app)
ImGui::Spacing();
// Filename
ImGui::Text("%s", TR("output_filename"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
material::LabeledInput(TR("output_filename"), "##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("%s", TR("file_save_location"));
-412
View File
@@ -1,412 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "import_key_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <sstream>
namespace dragonx {
namespace ui {
using json = nlohmann::json;
// Static state
static bool s_open = false;
static char s_key_input[4096] = "";
static bool s_rescan = true;
static int s_rescan_height = 0; // 0 = full rescan
static bool s_importing = false;
static std::string s_status;
static int s_total_keys = 0;
static int s_imported_keys = 0;
static int s_failed_keys = 0;
static bool s_paste_previewing = false;
static std::string s_preview_text;
// Helper to detect key type
static std::string detectKeyType(const std::string& key)
{
if (key.empty()) return "unknown";
// Z-address spending keys start with "secret-extended-key-" or "SK" prefix patterns
if (key.substr(0, 20) == "secret-extended-key-") {
return "z-spending";
}
// Legacy z-addr keys (SK prefix)
if (key.length() >= 2 && key[0] == 'S' && key[1] == 'K') {
return "z-spending";
}
// T-address private keys (WIF format) - start with 5, K, or L for Bitcoin-derived
// DragonX/HUSH uses different prefixes
if (key.length() >= 51 && key.length() <= 52) {
char first = key[0];
if (first == '5' || first == 'K' || first == 'L' || first == 'U') {
return "t-privkey";
}
}
return "unknown";
}
// Helper to split input into individual keys
static std::vector<std::string> splitKeys(const std::string& input)
{
std::vector<std::string> keys;
std::istringstream stream(input);
std::string line;
while (std::getline(stream, line)) {
// Trim whitespace
size_t start = line.find_first_not_of(" \t\r\n");
size_t end = line.find_last_not_of(" \t\r\n");
if (start != std::string::npos && end != std::string::npos) {
std::string key = line.substr(start, end - start + 1);
if (!key.empty() && key[0] != '#') { // Skip comments
keys.push_back(key);
}
}
}
return keys;
}
void ImportKeyDialog::show()
{
s_open = true;
s_key_input[0] = '\0';
s_rescan = true;
s_rescan_height = 0;
s_importing = false;
s_status.clear();
s_total_keys = 0;
s_imported_keys = 0;
s_failed_keys = 0;
s_paste_previewing = false;
s_preview_text.clear();
}
bool ImportKeyDialog::isOpen()
{
return s_open;
}
void ImportKeyDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.import-key");
auto keyInput = S.input("dialogs.import-key", "key-input");
auto rescanInput = S.input("dialogs.import-key", "rescan-height-input");
auto importBtn = S.button("dialogs.import-key", "import-button");
auto closeBtn = S.button("dialogs.import-key", "close-button");
if (material::BeginOverlayDialog(TR("import_key_title"), &s_open, win.width, 0.94f)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("%s", TR("import_key_warning"));
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Key input
ImGui::Text("%s", TR("import_key_label"));
ImGui::SameLine();
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("import_key_tooltip"));
}
// Validation indicator inline with title — check preview text during hover,
// otherwise check the actual input
{
const char* checkSrc = s_key_input;
if (s_paste_previewing && !s_preview_text.empty())
checkSrc = s_preview_text.c_str();
if (checkSrc[0] != '\0') {
auto previewKeys = splitKeys(checkSrc);
int pz = 0, pt = 0, pu = 0;
for (const auto& k : previewKeys) {
std::string kt = detectKeyType(k);
if (kt == "z-spending") pz++;
else if (kt == "t-privkey") pt++;
else pu++;
}
if (pz > 0 || pt > 0) {
ImGui::SameLine();
char vbuf[128];
if (pz > 0 && pt > 0)
snprintf(vbuf, sizeof(vbuf), "%d shielded, %d transparent", pz, pt);
else if (pz > 0)
snprintf(vbuf, sizeof(vbuf), "%d shielded key(s)", pz);
else
snprintf(vbuf, sizeof(vbuf), "%d transparent key(s)", pt);
material::Type().textColored(material::TypeStyle::Caption, material::Success(), vbuf);
if (pu > 0) {
ImGui::SameLine();
snprintf(vbuf, sizeof(vbuf), "(%d unrecognized)", pu);
material::Type().textColored(material::TypeStyle::Caption, material::Error(), vbuf);
}
} else if (pu > 0 && !s_paste_previewing) {
ImGui::SameLine();
material::Type().textColored(material::TypeStyle::Caption, material::Error(), "Unrecognized key format");
}
}
}
if (s_importing) {
ImGui::BeginDisabled();
}
ImGui::SetNextItemWidth(-1);
ImGui::InputTextMultiline("##KeyInput", s_key_input, sizeof(s_key_input),
ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput);
ImVec2 inputMin = ImGui::GetItemRectMin();
ImVec2 inputMax = ImGui::GetItemRectMax();
// Detect paste button hover before drawing it
ImVec2 pasteBtnPos = ImGui::GetCursorScreenPos();
// Estimate button size for hover detection
ImFont* btnFont = S.resolveFont(importBtn.font);
ImVec2 pasteBtnSize = btnFont
? ImVec2(btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("paste_from_clipboard")).x
+ ImGui::GetStyle().FramePadding.x * 2,
ImGui::GetFrameHeight())
: ImVec2(150, ImGui::GetFrameHeight());
bool paste_hovered = material::IsRectHovered(pasteBtnPos,
ImVec2(pasteBtnPos.x + pasteBtnSize.x, pasteBtnPos.y + pasteBtnSize.y));
// Handle preview state
if (paste_hovered && !s_paste_previewing) {
const char* clip = ImGui::GetClipboardText();
if (clip && clip[0] != '\0') {
std::string trimmed(clip);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
if (!trimmed.empty() && s_key_input[0] == '\0') {
s_preview_text = trimmed;
s_paste_previewing = true;
}
}
} else if (!paste_hovered && s_paste_previewing) {
s_paste_previewing = false;
s_preview_text.clear();
}
// Draw transparent preview text overlay on the input field
if (s_paste_previewing && !s_preview_text.empty()) {
ImVec2 textPos(inputMin.x + ImGui::GetStyle().FramePadding.x,
inputMin.y + ImGui::GetStyle().FramePadding.y);
ImVec4 previewCol = ImGui::GetStyleColorVec4(ImGuiCol_Text);
previewCol.w = S.drawElement("dialogs.import-key", "paste-preview-alpha").sizeOr(0.3f);
size_t maxChars = (size_t)S.drawElement("dialogs.import-key", "paste-preview-max-chars").sizeOr(200.0f);
// Clip to input rect
ImGui::GetWindowDrawList()->PushClipRect(inputMin, inputMax, true);
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::ColorConvertFloat4ToU32(previewCol),
s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), maxChars));
ImGui::GetWindowDrawList()->PopClipRect();
}
// Paste button
if (material::StyledButton(TR("paste_from_clipboard"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
if (s_paste_previewing) {
snprintf(s_key_input, sizeof(s_key_input), "%s", s_preview_text.c_str());
s_paste_previewing = false;
s_preview_text.clear();
} else {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
std::string trimmed(clipboard);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' ||
trimmed.front() == '\n' || trimmed.front() == '\r'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' ||
trimmed.back() == '\n' || trimmed.back() == '\r'))
trimmed.pop_back();
snprintf(s_key_input, sizeof(s_key_input), "%s", trimmed.c_str());
}
}
}
ImGui::SameLine();
if (material::StyledButton(TR("clear"), ImVec2(0,0), S.resolveFont(importBtn.font))) {
s_key_input[0] = '\0';
s_paste_previewing = false;
s_preview_text.clear();
}
ImGui::Spacing();
// Rescan options
ImGui::Checkbox(TR("import_key_rescan"), &s_rescan);
if (s_rescan) {
ImGui::Indent();
ImGui::Text("%s", TR("import_key_start_height"));
ImGui::SameLine();
ImGui::SetNextItemWidth(rescanInput.width);
ImGui::InputInt("##RescanHeight", &s_rescan_height);
if (s_rescan_height < 0) s_rescan_height = 0;
ImGui::SameLine();
ImGui::TextDisabled("%s", TR("import_key_full_rescan"));
ImGui::Unindent();
}
if (s_importing) {
ImGui::EndDisabled();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Import button
if (s_importing) {
ImGui::BeginDisabled();
}
if (material::StyledButton(TR("import_key_btn"), ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
auto keys = splitKeys(s_key_input);
if (keys.empty()) {
Notifications::instance().warning(TR("import_key_no_valid"));
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error(TR("not_connected"));
} else {
s_importing = true;
s_total_keys = static_cast<int>(keys.size());
s_imported_keys = 0;
s_failed_keys = 0;
s_status = "Importing...";
// Import keys on worker thread to avoid freezing UI
bool rescan = s_rescan;
int rescanHeight = s_rescan_height;
if (app->worker()) {
app->worker()->post([app, rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
int imported = 0;
int failed = 0;
for (const auto& key : keys) {
std::string keyType = detectKeyType(key);
try {
rpc::RPCClient::TraceScope trace("Settings / Import private keys");
if (keyType == "z-spending") {
// z_importkey "key" "yes"|"no" startheight
if (rescan && rescanHeight > 0) {
rpc->call("z_importkey", {key, "yes", rescanHeight});
} else {
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
}
imported++;
} else if (keyType == "t-privkey") {
// importprivkey does not accept a startheight;
// import without rescan, then rescanblockchain
// from the requested height afterward.
if (rescan && rescanHeight > 0) {
rpc->call("importprivkey", {key, "", false});
} else {
rpc->call("importprivkey", {key, "", rescan});
}
imported++;
} else {
failed++;
}
} catch (...) {
failed++;
}
}
// If t-keys were imported without rescan (because a
// specific start height was requested), trigger a
// single rescanblockchain from that height now.
if (rescan && rescanHeight > 0 && imported > 0) {
try {
rpc::RPCClient::TraceScope trace("Settings / Import private keys");
rpc->call("rescanblockchain", {rescanHeight});
} catch (...) {
// rescan failure is non-fatal; user can retry
}
}
return [app, imported, failed]() {
s_imported_keys = imported;
s_failed_keys = failed;
s_importing = false;
char buf[128];
snprintf(buf, sizeof(buf), "Import complete: %d success, %d failed",
imported, failed);
s_status = buf;
if (imported > 0) {
app->invalidateAddressValidationCache();
app->refreshNow();
Notifications::instance().success(TR("import_key_success"), 5.0f);
}
};
});
}
}
}
if (s_importing) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled(TR("import_key_progress"), s_imported_keys + s_failed_keys, s_total_keys);
}
ImGui::SameLine();
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
bool success = s_failed_keys == 0 && !s_importing;
ImGui::TextColored(
success ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : ImVec4(1.0f, 0.8f, 0.0f, 1.0f),
"%s", s_status.c_str()
);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Help text
ImGui::TextDisabled("%s", TR("import_key_formats"));
ImGui::BulletText("%s", TR("import_key_z_format"));
ImGui::BulletText("%s", TR("import_key_t_format"));
material::EndOverlayDialog();
}
}
} // namespace ui
} // namespace dragonx
-36
View File
@@ -1,36 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for importing private keys
*
* Supports importing:
* - Z-address private keys (z_importkey)
* - T-address private keys (importprivkey)
* - Batch import (multiple keys, one per line)
*/
class ImportKeyDialog {
public:
// Show the dialog
static void show();
// Render the dialog (call each frame)
static void render(App* app);
// Check if dialog is open
static bool isOpen();
};
} // namespace ui
} // namespace dragonx
File diff suppressed because it is too large Load Diff
+4
View File
@@ -15,5 +15,9 @@ namespace ui {
*/
void RenderMarketTab(App* app);
// True while the full-window Manage-portfolio modal is open (used to suppress panel theme-effects
// frame-wide so their foreground borders don't bleed over the overlay).
bool PortfolioEditorActive();
} // namespace ui
} // namespace dragonx
+23 -82
View File
@@ -16,7 +16,6 @@
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../material/components/buttons.h"
#include "../layout.h"
#include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h"
@@ -125,31 +124,17 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
float btnX = idleRightEdge - btnSz;
float btnY = curY + (headerH - btnSz) * 0.5f;
// Pill background when active
if (idleOn) {
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
WithAlpha(Primary(), 60), btnSz * 0.5f);
}
// Icon centered in button
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
icoCol, idleIcon);
// Click target (save/restore cursor so layout is unaffected)
// Idle toggle: active pill + state-colored glyph (cursor saved/restored at block end).
material::IconButtonStyle st;
st.color = idleOn ? Primary() : OnSurfaceDisabled();
st.restBg = idleOn ? WithAlpha(Primary(), 60) : 0;
st.tooltip = idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip");
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
if (material::IconButton("##IdleMining", idleIcon, icoFont, ImVec2(btnSz, btnSz), st)) {
app->settings()->setMineWhenIdle(!idleOn);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
}
idleRightEdge = btnX - 4.0f * dp;
@@ -158,30 +143,16 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
float sBtnX = idleRightEdge - btnSz;
float sBtnY = btnY;
if (threadScaling) {
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
sIcoCol, scaleIcon);
material::IconButtonStyle st;
st.color = threadScaling ? Primary() : OnSurfaceMedium();
st.restBg = threadScaling ? WithAlpha(Primary(), 40) : 0;
st.tooltip = threadScaling ? TR("mining_idle_scale_on_tooltip")
: TR("mining_idle_scale_off_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
if (material::IconButton("##IdleScaleMode", scaleIcon, icoFont, ImVec2(btnSz, btnSz), st)) {
app->settings()->setIdleThreadScaling(!threadScaling);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", threadScaling
? TR("mining_idle_scale_on_tooltip")
: TR("mining_idle_scale_off_tooltip"));
}
idleRightEdge = sBtnX - 4.0f * dp;
}
@@ -190,33 +161,18 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
// When OFF: unrestricted mode, only keyboard/mouse input matters
if (idleOn) {
bool gpuAware = app->settings()->getIdleGpuAware();
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
float gBtnX = idleRightEdge - btnSz;
float gBtnY = btnY;
if (gpuAware) {
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
gIcoCol, gpuIcon);
material::IconButtonStyle st;
st.color = gpuAware ? Primary() : OnSurfaceDisabled();
st.restBg = gpuAware ? WithAlpha(Primary(), 40) : 0;
st.tooltip = gpuAware ? TR("mining_idle_gpu_on_tooltip")
: TR("mining_idle_gpu_off_tooltip");
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
if (material::IconButton("##IdleGpuAware", ICON_MD_MONITOR, icoFont, ImVec2(btnSz, btnSz), st)) {
app->settings()->setIdleGpuAware(!gpuAware);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", gpuAware
? TR("mining_idle_gpu_on_tooltip")
: TR("mining_idle_gpu_off_tooltip"));
}
idleRightEdge = gBtnX - 4.0f * dp;
}
@@ -488,27 +444,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
float btnX = idleRightEdge - btnSz;
float btnY = curY + (headerH - btnSz) * 0.5f;
material::IconButtonStyle st;
st.color = OnSurfaceMedium();
st.hoverBg = StateHover();
st.tooltip = TR("mining_benchmark_tooltip");
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
bool benchHovered = ImGui::IsItemHovered();
bool benchClicked = ImGui::IsItemClicked();
// Hover highlight
if (benchHovered) {
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
StateHover(), btnSz * 0.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_benchmark_tooltip"));
}
const char* benchIcon = ICON_MD_SPEED;
ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f,
btnY + (btnSz - bIcoSz.y) * 0.5f),
OnSurfaceMedium(), benchIcon);
if (benchClicked) {
if (material::IconButton("##BenchStart", ICON_MD_SPEED, icoFont, ImVec2(btnSz, btnSz), st)) {
// Require a wallet address for pool mining
std::string worker(s_pool_worker);
if (!worker.empty()) {
+10 -33
View File
@@ -283,17 +283,9 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
ImGui::SetCursorScreenPos(ImVec2(col1X + valOffX, cy));
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_click_copy_difficulty"));
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
if (material::CopyableTextOverlay("##DiffCopy", ImVec2(col1X + valOffX, cy), diffSz,
capFont->LegacySize, buf, TR("mining_click_copy_difficulty"),
ImVec2(Layout::spacingMd(), 4 * dp))) {
Notifications::instance().info(TR("mining_difficulty_copied"));
}
}
@@ -304,17 +296,9 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
snprintf(buf, sizeof(buf), "%d", mining.blocks);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
ImGui::SetCursorScreenPos(ImVec2(col2X + valOffX, cy));
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_click_copy_block"));
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
if (material::CopyableTextOverlay("##BlockCopy", ImVec2(col2X + valOffX, cy), blkSz,
capFont->LegacySize, buf, TR("mining_click_copy_block"),
ImVec2(Layout::spacingMd(), 4 * dp))) {
Notifications::instance().info(TR("mining_block_copied"));
}
}
@@ -342,17 +326,10 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurface(), truncAddr.c_str());
ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str());
ImGui::SetCursorScreenPos(ImVec2(col3X + valOffX, cy));
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_click_copy_address"));
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(mining_address.c_str());
if (material::CopyableTextOverlay("##MiningAddrCopy", ImVec2(col3X + valOffX, cy), addrTextSz,
capFont->LegacySize, mining_address.c_str(),
TR("mining_click_copy_address"),
ImVec2(Layout::spacingMd(), 4 * dp))) {
Notifications::instance().info(TR("mining_address_copied"));
}
}
+37 -107
View File
@@ -16,7 +16,6 @@
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../material/components/buttons.h"
#include "../layout.h"
#include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h"
@@ -191,26 +190,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
// --- URL: Dropdown arrow button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##PoolDropdown", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_saved_pools"));
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
OnSurfaceMedium(), dropIcon);
if (btnClk) {
material::IconButtonStyle st;
st.color = OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = TR("mining_saved_pools");
if (material::IconButton("##PoolDropdown", ICON_MD_ARROW_DROP_DOWN, Type().iconSmall(),
ImVec2(iconBtnW, inputFrameH2), st)) {
ImGui::OpenPopup("##SavedPoolsPopup");
}
}
@@ -218,28 +204,16 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
// --- URL: Bookmark button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##SavePoolUrl", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
std::string currentUrl(s_pool_url);
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
if (btnClk && !currentUrl.empty() && !alreadySaved) {
material::IconButtonStyle st;
st.color = alreadySaved ? Primary() : OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url");
if (material::IconButton("##SavePoolUrl", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER,
Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st)
&& !currentUrl.empty() && !alreadySaved) {
app->settings()->addSavedPoolUrl(currentUrl);
app->settings()->save();
}
@@ -413,26 +387,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
// --- Worker: Dropdown arrow button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##WorkerDropdown", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_saved_addresses"));
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
OnSurfaceMedium(), dropIcon);
if (btnClk) {
material::IconButtonStyle st;
st.color = OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = TR("mining_saved_addresses");
if (material::IconButton("##WorkerDropdown", ICON_MD_ARROW_DROP_DOWN, Type().iconSmall(),
ImVec2(iconBtnW, inputFrameH2), st)) {
ImGui::OpenPopup("##SavedWorkersPopup");
}
}
@@ -440,28 +401,16 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
// --- Worker: Bookmark button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##SaveWorkerAddr", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
std::string currentWorker(s_pool_worker);
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
material::IconButtonStyle st;
st.color = alreadySaved ? Primary() : OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address");
if (material::IconButton("##SaveWorkerAddr", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER,
Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st)
&& !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
app->settings()->addSavedPoolWorker(currentWorker);
app->settings()->save();
}
@@ -580,32 +529,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
// === Reset to defaults button ===
ImGui::SameLine(0, Layout::spacingSm());
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(inputFrameH2, inputFrameH2);
ImGui::InvisibleButton("##ResetPoolDefaults", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
// Hover highlight
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_reset_defaults"));
}
// Icon
ImFont* iconFont = Type().iconSmall();
const char* resetIcon = ICON_MD_REFRESH;
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, resetIcon);
dl2->AddText(iconFont, iconFont->LegacySize,
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
OnSurfaceMedium(), resetIcon);
if (btnClk) {
material::IconButtonStyle st;
st.color = OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 4.0f * dp;
st.tooltip = TR("mining_reset_defaults");
if (material::IconButton("##ResetPoolDefaults", ICON_MD_REFRESH, Type().iconSmall(),
ImVec2(inputFrameH2, inputFrameH2), st)) {
strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1);
// Default to user's first shielded (z) address for pool payouts.
// Leave blank if no z-address exists yet.
+7 -15
View File
@@ -258,24 +258,16 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
(int)util::knownPools().size());
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr);
ImFont* icoFont = Type().iconSmall();
const char* icon = ICON_MD_REFRESH;
ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, icon);
float btnS = ovFont->LegacySize + 6 * dp;
ImVec2 rbMin(x + w - btnS, y - 2 * dp);
ImVec2 rbMax(x + w, y - 2 * dp + btnS);
material::IconButtonStyle st;
st.color = OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 3 * dp;
st.tooltip = TR("mining_refresh");
ImGui::SetCursorScreenPos(rbMin);
ImGui::InvisibleButton("##PoolRefresh", ImVec2(btnS, btnS));
bool rhov = ImGui::IsItemHovered();
if (rhov) {
dl->AddRectFilled(rbMin, rbMax, StateHover(), 3 * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_refresh"));
}
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2((rbMin.x + rbMax.x - iSz.x) * 0.5f, (rbMin.y + rbMax.y - iSz.y) * 0.5f),
OnSurfaceMedium(), icon);
if (ImGui::IsItemClicked()) app->requestPoolBalanceRefresh();
if (material::IconButton("##PoolRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(btnS, btnS), st))
app->requestPoolBalanceRefresh();
}
y += ovFont->LegacySize + 4 * dp;
-1
View File
@@ -22,7 +22,6 @@
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../material/components/buttons.h"
#include "../effects/low_spec.h"
#include "../layout.h"
#include "../notifications.h"
+10 -18
View File
@@ -285,9 +285,8 @@ void RenderLiteNetworkTab(App* app)
ImVec2 tagSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, tag);
float tagX = rx - tagSz.x;
float tagY = cardMin.y + 9.0f * dp;
dl->AddRectFilled(ImVec2(tagX - 6 * dp, tagY - 1 * dp), ImVec2(rx + 6 * dp, tagY + tagSz.y + 1 * dp),
tagBg, 4.0f * dp);
dl->AddText(cap, cap->LegacySize, ImVec2(tagX, tagY), tagFg, tag);
material::DrawPill(dl, ImVec2(tagX - 6 * dp, tagY - 1 * dp), tag, cap, tagFg, tagBg, 0,
ImVec2(6 * dp, 1 * dp), 4.0f * dp);
if (selected) {
const char* inUse = TR("lite_net_in_use");
ImVec2 uSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse);
@@ -295,25 +294,18 @@ void RenderLiteNetworkTab(App* app)
}
// Hide / unhide button (far-right strip).
ImVec2 hideMin(cardMin.x + cardBtnW, cardMin.y);
ImGui::SetCursorScreenPos(hideMin);
ImGui::InvisibleButton(("##litehide_" + sv.url).c_str(), ImVec2(hideW, cardH));
bool hideHov = ImGui::IsItemHovered();
if (hideHov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"));
}
if (ImGui::IsItemClicked()) {
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + cardBtnW, cardMin.y));
material::IconButtonStyle hideSt;
hideSt.color = OnSurfaceDisabled();
hideSt.hoverColor = OnSurface();
hideSt.tooltip = hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide");
if (material::IconButton(("##litehide_" + sv.url).c_str(),
hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF,
Type().iconSmall(), ImVec2(hideW, cardH), hideSt)) {
if (hiddenList) st->unhideLiteServer(sv.url);
else st->hideLiteServer(sv.url);
st->save();
}
ImFont* ico = Type().iconSmall();
const char* eye = hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
ImVec2 icoSz = ico->CalcTextSizeA(ico->LegacySize, FLT_MAX, 0, eye);
dl->AddText(ico, ico->LegacySize,
ImVec2(hideMin.x + (hideW - icoSz.x) * 0.5f, cardMin.y + (cardH - icoSz.y) * 0.5f),
hideHov ? OnSurface() : OnSurfaceDisabled(), eye);
// Advance to the next card via a real item (Dummy) below the card, so the scroll region's
// content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.
+90 -159
View File
@@ -6,6 +6,7 @@
#include "../../app.h"
#include "../../data/wallet_state.h"
#include "../../util/i18n.h"
#include "mining_tab_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../effects/low_spec.h"
@@ -63,6 +64,30 @@ static bool IsSeedNode(const std::string& addr) {
return seeds.count(ExtractIP(addr)) > 0;
}
// Draw one label/value stat cell in an info card. The caption label sits on the first line
// (OnSurfaceMedium); the value sits below it (valueFont) — or an em-dash "—" in the disabled
// color when the value is null/empty. Matches the hand-positioned blocks it replaces exactly:
// value baseline is ry + capFont->LegacySize + spacingXs().
static void drawStatCell(ImDrawList* dl, float cx, float ry, ImFont* capFont, ImFont* valueFont,
const char* label, const char* valueText, ImU32 valueCol)
{
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), label);
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (valueText && valueText[0]) {
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), valueCol, valueText);
} else {
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
}
// Draw a subtle horizontal divider between info-card rows, inset to the card's rounded corners.
static void drawCardDivider(ImDrawList* dl, float leftX, float rightX, float y, float rounding, float dp)
{
dl->AddLine(ImVec2(leftX + rounding * 0.5f, y),
ImVec2(rightX - rounding * 0.5f, y),
WithAlpha(OnSurface(), 15), 1.0f * dp);
}
void RenderPeersTab(App* app)
{
auto& S = schema::UI();
@@ -152,20 +177,22 @@ void RenderPeersTab(App* app)
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
float colW = (cardW - pad * 2) / 2.0f;
float leftX = cardMin.x + pad;
float rightX = cardMin.x + pad + colW;
float ry = cardMin.y + pad * 0.5f + headerH;
// Helper to draw a subtle horizontal divider
auto drawDivider = [&](float y) {
float rnd = glassSpec.rounding;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
ImVec2(cardMax.x - rnd * 0.5f, y),
WithAlpha(OnSurface(), 15), 1.0f * dp);
// Advance ry past a row and draw a divider before the next one. Keeps the row
// stepping + divider in one place (was a per-row copy + a duplicate divider lambda).
auto stepRow = [&]() {
ry += rowH + Layout::spacingSm() * 0.5f;
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
ry += Layout::spacingSm() * 0.5f + dividerH;
};
// Row 1: Blocks | Longest Chain
{
// Blocks
float cx = cardMin.x + pad;
// Blocks — bespoke: block number + "(X left)" in a second color
float cx = leftX;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
int blocks = state.sync.blocks;
if (blocks > 0) {
@@ -173,7 +200,6 @@ void RenderPeersTab(App* app)
int blocksLeft = chainTip - blocks;
if (blocksLeft < 0) blocksLeft = 0;
if (blocksLeft > 0) {
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
float valY = ry + capFont->LegacySize + Layout::spacingXs();
// Draw block number in normal color
char blockStr[32];
@@ -195,145 +221,85 @@ void RenderPeersTab(App* app)
}
// Longest Chain
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest_chain"));
if (state.longestchain > 0) {
snprintf(buf, sizeof(buf), "%d", state.longestchain);
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), chainCol, buf);
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), buf, chainCol);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), nullptr, OnSurface());
}
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 2: Hashrate | Difficulty
{
// Hashrate
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (mining.networkHashrate > 0) {
if (mining.networkHashrate >= 1e12)
snprintf(buf, sizeof(buf), "%.2f TH/s", mining.networkHashrate / 1e12);
else if (mining.networkHashrate >= 1e9)
snprintf(buf, sizeof(buf), "%.2f GH/s", mining.networkHashrate / 1e9);
else if (mining.networkHashrate >= 1e6)
snprintf(buf, sizeof(buf), "%.2f MH/s", mining.networkHashrate / 1e6);
else if (mining.networkHashrate >= 1e3)
snprintf(buf, sizeof(buf), "%.2f KH/s", mining.networkHashrate / 1e3);
else
snprintf(buf, sizeof(buf), "%.2f H/s", mining.networkHashrate);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
std::string hr = mining.networkHashrate > 0 ? FormatHashrate(mining.networkHashrate) : std::string();
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_hashrate"),
hr.empty() ? nullptr : hr.c_str(), Success());
// Difficulty
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (mining.difficulty > 0) {
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
if (mining.difficulty > 0) snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
drawStatCell(dl, rightX, ry, capFont, sub1, TR("difficulty"),
mining.difficulty > 0 ? buf : nullptr, OnSurface());
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 3: Notarized | Protocol
{
// Notarized
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.notarized > 0) {
snprintf(buf, sizeof(buf), "%d", state.notarized);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
if (state.notarized > 0) snprintf(buf, sizeof(buf), "%d", state.notarized);
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_notarized"),
state.notarized > 0 ? buf : nullptr, OnSurface());
// Protocol
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_protocol"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.protocol_version > 0) {
snprintf(buf, sizeof(buf), "%d", state.protocol_version);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
if (state.protocol_version > 0) snprintf(buf, sizeof(buf), "%d", state.protocol_version);
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_protocol"),
state.protocol_version > 0 ? buf : nullptr, OnSurface());
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 4: Version | Memory
{
// Version
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_version"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.daemon_version > 0) {
int major = state.daemon_version / 1000000;
int minor = (state.daemon_version / 10000) % 100;
int patch = (state.daemon_version / 100) % 100;
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_version"),
state.daemon_version > 0 ? buf : nullptr, OnSurface());
// Memory
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_memory"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
double memMb = state.mining.daemon_memory_mb;
if (memMb > 0) {
if (memMb >= 1024.0)
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
else
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_memory"),
memMb > 0 ? buf : nullptr, OnSurface());
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 5: Longest Chain | Best Block
{
// Longest Chain
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.longestchain > 0) {
snprintf(buf, sizeof(buf), "%d", state.longestchain);
// Color green if local matches longest, warning if behind
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), chainCol, buf);
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), buf, chainCol);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), nullptr, OnSurface());
}
// Best Block (truncated hash)
cx = cardMin.x + pad + colW;
// Best Block — bespoke: truncated hash with click-to-copy + hover underline
float cx = rightX;
float valY = ry + capFont->LegacySize + Layout::spacingXs();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (!state.sync.best_blockhash.empty()) {
// Truncate hash to fit: first 6 + "..." + last 6
std::string hash = state.sync.best_blockhash;
@@ -348,17 +314,10 @@ void RenderPeersTab(App* app)
// Click to copy full hash
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(cx, valY));
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s %s", TR("peers_click_copy"), hash.c_str());
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(hash.c_str());
std::string copyTip = std::string(TR("peers_click_copy")) + " " + hash;
if (material::CopyableTextOverlay("##BestBlockCopy", ImVec2(cx, valY), hashSz,
sub1->LegacySize, hash.c_str(), copyTip.c_str(),
ImVec2(Layout::spacingSm(), 2 * dp))) {
ui::Notifications::instance().info(TR("peers_hash_copied"));
}
ImGui::SetCursorScreenPos(savedCursor);
@@ -380,39 +339,32 @@ void RenderPeersTab(App* app)
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
float colW = (cardW - pad * 2) / 2.0f;
float leftX = cardMin.x + pad;
float rightX = cardMin.x + pad + colW;
float ry = cardMin.y + pad * 0.5f + headerH;
// Helper to draw a subtle horizontal divider
auto drawPeerDivider = [&](float y) {
float rnd = glassSpec.rounding;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
ImVec2(cardMax.x - rnd * 0.5f, y),
WithAlpha(OnSurface(), 15), 1.0f * dp);
// Advance ry past a row and draw a divider before the next one (see blockchain card).
auto stepRow = [&]() {
ry += rowH + Layout::spacingSm() * 0.5f;
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
ry += Layout::spacingSm() * 0.5f + dividerH;
};
// Row 1: Connected | In/Out
{
// Connected
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_connected"));
snprintf(buf, sizeof(buf), "%d", totalPeers);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_connected"), buf, OnSurface());
// In / Out
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_in_out"));
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_in_out"), buf, OnSurface());
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawPeerDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 2: TLS | Avg Ping
{
// TLS
float cx = cardMin.x + pad;
// TLS — bespoke: appends a check icon when every peer is TLS
float cx = leftX;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (totalPeers > 0) {
@@ -428,60 +380,39 @@ void RenderPeersTab(App* app)
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Avg Ping
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_avg_ping"));
// Avg Ping — color by latency
ImU32 pingCol;
if (avgPing < 100) pingCol = Success();
else if (avgPing < 500) pingCol = Warning();
else pingCol = Error();
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), pingCol, buf);
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_avg_ping"), buf, pingCol);
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawPeerDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 3: Received | Sent
{
// Received
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_received"));
std::string recvStr = fmtBytes(totalBytesRecv);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str());
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_received"), recvStr.c_str(), OnSurface());
// Sent
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_sent"));
std::string sentStr = fmtBytes(totalBytesSent);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str());
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_sent"), sentStr.c_str(), OnSurface());
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawPeerDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
stepRow();
// Row 4: P2P Port | Banned
{
// P2P Port
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_p2p_port"));
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.p2p_port > 0) {
snprintf(buf, sizeof(buf), "%d", state.p2p_port);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Banned count
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_banned"));
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.p2p_port > 0) snprintf(buf, sizeof(buf), "%d", state.p2p_port);
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_p2p_port"),
state.p2p_port > 0 ? buf : nullptr, OnSurface());
size_t bannedCount = state.bannedPeers.size();
snprintf(buf, sizeof(buf), "%zu", bannedCount);
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), bannedCol, buf); }
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_banned"), buf, bannedCol);
}
}
ImGui::SetCursorScreenPos(basePos);
+14 -33
View File
@@ -13,6 +13,7 @@
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../util/text_format.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../ui/widgets/qr_code.h"
@@ -71,12 +72,6 @@ static bool s_generating_address = false;
// ============================================================================
// Helpers
// ============================================================================
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) {
if (addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
static void OpenExplorerURL(App* app, const std::string& address) {
std::string url = app->settings()->getAddressExplorerUrl() + address;
dragonx::util::Platform::openUrl(url);
@@ -137,27 +132,15 @@ static void RenderAddressDropdown(App* app, float width) {
float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
float segH = ImGui::GetFrameHeight();
ImGui::SameLine(width - toggleTotalW);
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
for (int i = 0; i < 3; i++) {
bool isActive = (s_addr_type_filter == i);
if (isActive) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0.06f));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
}
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.receive", "toggle-rounding").size);
char toggleId[32];
snprintf(toggleId, sizeof(toggleId), "%s##addrFilter", filterLabels[i]);
if (ImGui::Button(toggleId, ImVec2(toggleBtnW, 0))) {
s_addr_type_filter = i;
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
if (i < 2) ImGui::SameLine(0, toggleGap);
}
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, toggleTotalW, segH,
filterLabels, 3, s_addr_type_filter, Type().caption(),
"##addrFilter", Layout::dpiScale());
if (segClk >= 0) s_addr_type_filter = segClk;
ImGui::Dummy(ImVec2(toggleTotalW, segH));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
@@ -199,8 +182,8 @@ static void RenderAddressDropdown(App* app, float width) {
const auto& addr = state.addresses[s_selected_address_idx];
bool isZ = addr.type == "shielded";
const char* tag = isZ ? "[Z]" : "[T]";
std::string trunc = TruncateAddress(addr.address,
static_cast<size_t>(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size)));
std::string trunc = util::truncateMiddle(addr.address,
static_cast<int>(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size)));
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
s_source_preview = buf;
@@ -256,7 +239,7 @@ static void RenderAddressDropdown(App* app, float width) {
auto lblIt = s_address_labels.find(addr.address);
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
std::string trunc = TruncateAddress(addr.address, addrTruncLen);
std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
if (hasLabel) {
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
tag, lblIt->second.c_str(), trunc.c_str(),
@@ -302,10 +285,8 @@ static void RenderAddressDropdown(App* app, float width) {
ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
if (s_generating_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), dotStr[dots]);
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots());
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
s_generating_address = true;
@@ -416,8 +397,8 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
std::string addrDisplay = TruncateAddress(tx.address,
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
std::string addrDisplay = util::truncateMiddle(tx.address,
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
+5 -10
View File
@@ -197,9 +197,7 @@ void RequestPaymentDialog::render(App* app)
ImGui::Spacing();
// Label (optional)
ImGui::Text("%s", TR("request_label"));
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##Label", s_label, sizeof(s_label))) {
if (material::LabeledInput(TR("request_label"), "##Label", s_label, sizeof(s_label))) {
s_uri_dirty = true;
}
@@ -208,9 +206,8 @@ void RequestPaymentDialog::render(App* app)
// Memo (optional, only for z-addr)
bool is_zaddr = (s_address[0] == 'z');
if (is_zaddr) {
ImGui::Text("%s", TR("request_memo"));
ImGui::SetNextItemWidth(-1);
if (ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo), ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
if (material::LabeledInputMultiline(TR("request_memo"), "##Memo", s_memo, sizeof(s_memo),
ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
s_uri_dirty = true;
}
} else {
@@ -242,13 +239,11 @@ void RequestPaymentDialog::render(App* app)
// Payment URI display
if (!s_payment_uri.empty()) {
ImGui::Text("%s", TR("request_payment_uri"));
ImGui::SetNextItemWidth(-1);
// Use a selectable text area for the URI
char uri_buf[1024];
strncpy(uri_buf, s_payment_uri.c_str(), sizeof(uri_buf) - 1);
ImGui::InputText("##URI", uri_buf, sizeof(uri_buf), ImGuiInputTextFlags_ReadOnly);
material::LabeledInput(TR("request_payment_uri"), "##URI", uri_buf, sizeof(uri_buf),
-1.0f, nullptr, ImGuiInputTextFlags_ReadOnly);
ImGui::Spacing();
+14 -16
View File
@@ -147,12 +147,6 @@ static double GetAvailableBalance(App* app) {
return 0.0;
}
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) {
if (addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
// Recipient validity = prefix/length pre-filter AND a real encoding-checksum check, so a
// transcription error that still matches the prefix/length is no longer labelled "Valid".
// The checksum verifiers are version-agnostic, so they never reject a genuine address.
@@ -254,8 +248,8 @@ static void RenderSourceDropdown(App* app, float width) {
const auto& addr = state.addresses[s_selected_from_idx];
bool isZ = addr.type == "shielded";
const char* tag = isZ ? "[Z]" : "[T]";
std::string trunc = TruncateAddress(addr.address,
static_cast<size_t>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
std::string trunc = util::truncateMiddle(addr.address,
static_cast<int>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
s_source_preview = buf;
@@ -285,7 +279,7 @@ static void RenderSourceDropdown(App* app, float width) {
bool isZ = addr.type == "shielded";
const char* tag = isZ ? "[Z]" : "[T]";
std::string trunc = TruncateAddress(addr.address, addrTruncLen);
std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
@@ -345,7 +339,7 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
for (size_t si = 0; si < suggestions.size(); si++) {
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
std::string dispSug = TruncateAddress(suggestions[si], sugTrunc > 0 ? sugTrunc : 50);
std::string dispSug = util::truncateMiddle(suggestions[si], sugTrunc > 0 ? sugTrunc : 50);
ImGui::PushID((int)si);
if (ImGui::Selectable(dispSug.c_str())) {
snprintf(s_to_address, sizeof(s_to_address), "%s", suggestions[si].c_str());
@@ -708,7 +702,7 @@ void RenderSendConfirmPopup(App* app) {
DrawGlassPanel(popDl, cMin, cMax, gs);
popDl->AddText(body2, body2->LegacySize,
ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()),
Success(), TruncateAddress(s_from_address, (size_t)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str());
Success(), util::truncateMiddle(s_from_address, (int)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str());
ImGui::Dummy(ImVec2(popW, addrCardH));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
@@ -722,7 +716,7 @@ void RenderSendConfirmPopup(App* app) {
DrawGlassPanel(popDl, cMin, cMax, gs);
popDl->AddText(body2, body2->LegacySize,
ImVec2(cMin.x + Layout::spacingMd(), cMin.y + Layout::spacingSm()),
Success(), TruncateAddress(s_to_address, (size_t)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str());
Success(), util::truncateMiddle(s_to_address, (int)S.drawElement("tabs.send", "confirm-addr-trunc-len").size).c_str());
ImGui::Dummy(ImVec2(popW, addrCardH));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
}
@@ -910,8 +904,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
// ---- Send button ----
if (!can_send) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "disabled-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "disabled-btn-bg-alpha").size)));
// The disabled fill must contrast the surface: a white overlay reads on dark skins but vanished
// on the light/pastel skins (white-on-white). Use a dark overlay for light themes.
int dAlpha = (int)S.drawElement("tabs.send", "disabled-btn-bg-alpha").size;
ImU32 disFill = IsLightTheme() ? IM_COL32(0, 0, 0, dAlpha) : IM_COL32(255, 255, 255, dAlpha);
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(disFill));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(disFill));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
}
@@ -1058,8 +1056,8 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
std::string addrDisplay = TruncateAddress(tx.address,
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
std::string addrDisplay = util::truncateMiddle(tx.address,
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
+17 -31
View File
@@ -396,28 +396,20 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("%s", TR("rpc_host"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
material::LabeledInputRow(TR("rpc_host"), connLbl.position, "##RPCHost",
s_rpc_host, sizeof(s_rpc_host), cmb.width);
ImGui::Text("%s", TR("rpc_port"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(portInput.width);
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
material::LabeledInputRow(TR("rpc_port"), connLbl.position, "##RPCPort",
s_rpc_port, sizeof(s_rpc_port), portInput.width);
ImGui::Spacing();
ImGui::Text("%s", TR("rpc_user"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
material::LabeledInputRow(TR("rpc_user"), connLbl.position, "##RPCUser",
s_rpc_user, sizeof(s_rpc_user), cmb.width);
ImGui::Text("%s", TR("rpc_pass"));
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
ImGuiInputTextFlags_Password);
material::LabeledInputRow(TR("rpc_pass"), connLbl.position, "##RPCPassword",
s_rpc_password, sizeof(s_rpc_password), cmb.width, nullptr,
ImGuiInputTextFlags_Password);
ImGui::Spacing();
ImGui::Separator();
@@ -487,12 +479,8 @@ void RenderSettingsWindow(App* app, bool* p_open)
// Confirmation dialog
if (s_confirm_clear_ztx) {
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(material::Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
ImGui::Spacing();
@@ -500,15 +488,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
bool doCancel = false, doConfirm = false;
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_w").c_str(),
true, doCancel, doConfirm);
if (doCancel) {
s_confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_w").c_str(), ImVec2(btnW, 40))) {
if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
@@ -517,7 +504,6 @@ void RenderSettingsWindow(App* app, bool* p_open)
}
s_confirm_clear_ztx = false;
}
ImGui::PopStyleColor(2);
material::EndOverlayDialog();
}
}
+1 -3
View File
@@ -95,9 +95,7 @@ void ShieldDialog::render(App* app)
// From address (for shield coinbase)
if (s_mode == Mode::ShieldCoinbase) {
ImGui::Text("%s", TR("shield_from_address"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##FromAddr", s_from_address, sizeof(s_from_address));
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
ImGui::Spacing();
}
@@ -45,7 +45,12 @@ void TransactionDetailsDialog::render(App* app)
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
auto bottomBtn = S.button("dialogs.transaction-details", "bottom-button");
if (material::BeginOverlayDialog(TR("tx_details_title"), &s_open, win.width, 0.94f)) {
material::OverlayDialogSpec ov;
ov.title = TR("tx_details_title"); ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
ov.cardWidth = win.width; // keep the authored width (content is laid out to it)
ov.cardBottomViewportRatio = 0.94f;
if (material::BeginOverlayDialog(ov)) {
const auto& tx = s_transaction;
// Type indicator with color
+65 -109
View File
@@ -37,13 +37,6 @@ static std::string TrId(const char* key, const char* id) {
return std::string(TR(key)) + "##" + id;
}
// Helper to truncate strings
static std::string truncateString(const std::string& str, int maxLen = 16) {
if (str.length() <= static_cast<size_t>(maxLen)) return str;
int half = (maxLen - 3) / 2;
return str.substr(0, half) + "..." + str.substr(str.length() - half);
}
// Case-insensitive string search
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
return dragonx::util::containsIgnoreCase(str, search);
@@ -103,6 +96,44 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
}
// Draw one Received/Sent/Mined summary card: icon + label + count + signed total, with a
// hover outline and click-to-toggle type filter. All three cards are identical apart from the
// per-card data passed here. cMin is the card's top-left; filterValue is the type_filter value
// this card selects (clicking again clears it).
static void drawSummaryCard(ImDrawList* dl, ImVec2 cMin, float cardW, float cardH,
float innerPad, float iconSz, const GlassPanelSpec& glassSpec,
ImFont* ovFont, ImFont* capFont, ImFont* body2,
const char* iconType, ImU32 accentCol, const char* labelKey,
int count, double total, const char* signPrefix,
int filterValue, int& type_filter)
{
char buf[128];
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
DrawTxIcon(dl, iconType, cx + iconSz, cy + iconSz * 1.33f, iconSz, accentCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR(labelKey));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), TR("txs_count"), count);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "%s%.4f %s", signPrefix, total, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), accentCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == filterValue) ? 0 : filterValue;
}
}
void RenderTransactionsTab(App* app)
{
auto& S = schema::UISchema::instance();
@@ -210,95 +241,17 @@ void RenderTransactionsTab(App* app)
// Sort mode: 0 = newest first, 1 = oldest first, 2 = largest amount, 3 = smallest amount.
static int s_sort_mode = 0;
// --- Received card ---
{
ImVec2 cMin = origin;
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
// Icon
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "+%.4f %s", recvTotal, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), greenCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 2) ? 0 : 2;
}
}
// --- Sent card ---
{
float xOff = cardW + cardGap;
ImVec2 cMin(origin.x + xOff, origin.y);
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "-%.4f %s", sendTotal, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), redCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 1) ? 0 : 1;
}
}
// --- Mined card ---
{
float xOff = 2 * (cardW + cardGap);
ImVec2 cMin(origin.x + xOff, origin.y);
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "+%.4f %s", minedTotal, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), goldCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 3) ? 0 : 3;
}
}
// --- Received / Sent / Mined cards ---
// filter values: Received = 2, Sent = 1, Mined = 3 (see idx_map for the accent bar below)
drawSummaryCard(dl, origin, cardW, cardH, innerPad, iconSz, glassSpec,
ovFont, capFont, body2, "receive", greenCol, "received_upper",
recvCount, recvTotal, "+", 2, type_filter);
drawSummaryCard(dl, ImVec2(origin.x + (cardW + cardGap), origin.y), cardW, cardH,
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "send", redCol, "sent_upper",
sendCount, sendTotal, "-", 1, type_filter);
drawSummaryCard(dl, ImVec2(origin.x + 2 * (cardW + cardGap), origin.y), cardW, cardH,
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper",
minedCount, minedTotal, "+", 3, type_filter);
// Selected card accent
if (type_filter > 0) {
@@ -358,9 +311,7 @@ void RenderTransactionsTab(App* app)
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingXs());
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]);
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), material::LoadingDots());
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
@@ -679,9 +630,7 @@ void RenderTransactionsTab(App* app)
} else if (state.transactions.empty()) {
ImGui::Dummy(ImVec2(0, 20));
if (txLoading) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]);
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), material::LoadingDots());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
@@ -756,7 +705,7 @@ void RenderTransactionsTab(App* app)
OnSurfaceDisabled(), ago.c_str());
// Address (second line, left side)
std::string addr_display = truncateString(tx.address, (addrLabel.truncate > 0) ? addrLabel.truncate : 20);
std::string addr_display = util::truncateMiddle(tx.address, (addrLabel.truncate > 0) ? addrLabel.truncate : 20);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), addr_display.c_str());
@@ -799,7 +748,7 @@ void RenderTransactionsTab(App* app)
} else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
statusStr = TR("mature"); statusCol = greenCol;
} else {
statusStr = TR("confirmed"); statusCol = WithAlpha(Success(), 140);
statusStr = TR("confirmed"); statusCol = Success();
}
// Position status badge in the middle-right area
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
@@ -813,23 +762,30 @@ void RenderTransactionsTab(App* app)
float minStatusX = cx + innerW * 0.25f; // don't overlap address
if (statusX < minStatusX) statusX = minStatusX;
float statusTextX = statusX + (stackW - sSz.x) * 0.5f;
// Pills use a visible tinted fill + a 1px border so each state separates from the
// row background on every skin (faint alpha-30 fills used to vanish on dark-red /
// near-white / gradient skins). Fill 48, border 90 of the state color's RGB.
const float pillRound = schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size;
if (shieldedDisplay) {
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
ImU32 shieldCol = Primary();
ImU32 shieldBg = (shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
dl->AddRectFilled(shieldPillMin, shieldPillMax, shieldBg,
schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
dl->AddRectFilled(shieldPillMin, shieldPillMax,
(shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(48) << 24), pillRound);
dl->AddRect(shieldPillMin, shieldPillMax,
(shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(90) << 24), pillRound, 0, 1.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(shieldX, cy), shieldCol, shieldedStr);
}
// Background pill
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
ImVec2 pillMax(statusTextX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
dl->AddRectFilled(pillMin, pillMax,
(statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(48) << 24), pillRound);
dl->AddRect(pillMin, pillMax,
(statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(90) << 24), pillRound, 0, 1.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
}
+3 -4
View File
@@ -61,10 +61,9 @@ void ValidateAddressDialog::render(App* app)
ImGui::Spacing();
// Address input
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(-1);
bool enter_pressed = ImGui::InputText("##ValidateAddr", s_address_input, sizeof(s_address_input),
ImGuiInputTextFlags_EnterReturnsTrue);
bool enter_pressed = material::LabeledInput(TR("address_label"), "##ValidateAddr", s_address_input,
sizeof(s_address_input), -1.0f, nullptr,
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing();
+5 -11
View File
@@ -51,7 +51,10 @@ public:
}
using namespace material;
const float dp = Layout::dpiScale();
if (BeginOverlayDialog(TR("xmrig_update_title"), &s_open, 480.0f, 0.94f)) {
OverlayDialogSpec ov;
ov.title = TR("xmrig_update_title"); ov.p_open = &s_open;
ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 480.0f; ov.cardBottomViewportRatio = 0.94f;
if (BeginOverlayDialog(ov)) {
const auto p = s_updater->getProgress();
using St = util::XmrigUpdater::State;
switch (p.state) {
@@ -179,16 +182,7 @@ private:
: TR("xmrig_installing");
Type().text(TypeStyle::Subtitle2, title);
ImGui::Spacing();
const float barH = 8.0f * dp;
const float barW = fullW();
const ImVec2 bMin = ImGui::GetCursorScreenPos();
const ImVec2 bMax(bMin.x + barW, bMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
const float fillW = barW * (p.percent / 100.0f);
if (fillW > 0)
dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp);
ImGui::Dummy(ImVec2(0, barH));
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
ImGui::Spacing();
ImGui::Text("%s", p.status_text.c_str());
ImGui::Spacing();
+4 -72
View File
@@ -10,6 +10,7 @@
#include "daemon_updater.h"
#include "xmrig_updater.h" // util::sha256Hex, util::verifyXmrigSignature (shared crypto)
#include "http_download.h"
#include "logger.h"
#include <curl/curl.h>
@@ -35,24 +36,6 @@ namespace {
constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024; // 256 MiB
constexpr std::size_t kMaxMemberBytes = 128u * 1024 * 1024; // 128 MiB per extracted file
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
{
return std::fwrite(contents, size, nmemb, static_cast<FILE*>(userp));
}
// libcurl progress callback: publish live byte counts and abort the transfer on cancel().
int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t)
{
auto* up = static_cast<DaemonUpdater*>(clientp);
return up->onDownloadProgress(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
}
std::string baseName(const std::string& path)
{
const auto slash = path.find_last_of("/\\");
@@ -120,64 +103,13 @@ void DaemonUpdater::cancel()
std::string DaemonUpdater::httpGet(const std::string& url)
{
CURL* curl = curl_easy_init();
if (!curl) return {};
std::string result;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("[daemon-updater] GET %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
return {};
}
return result;
return httpGetString(url, "[daemon-updater]");
}
bool DaemonUpdater::downloadToFile(const std::string& url, const std::string& destPath)
{
FILE* fp = std::fopen(destPath.c_str(), "wb");
if (!fp) return false;
CURL* curl = curl_easy_init();
if (!curl) { std::fclose(fp); return false; }
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, kMaxArchiveBytes); // refuse oversized bodies
// Live progress + cancellation: the callback publishes byte counts and aborts on cancel().
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
std::fclose(fp);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("[daemon-updater] download %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
std::error_code ec; fs::remove(destPath, ec);
return false;
}
return true;
return httpDownloadToFile(url, destPath, "[daemon-updater]", kMaxArchiveBytes,
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
}
void DaemonUpdater::startCheck(const std::string& installedVersion)
+108
View File
@@ -0,0 +1,108 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "http_download.h"
#include "logger.h"
#include <curl/curl.h>
#include <cstdio>
#include <filesystem>
#include <system_error>
namespace fs = std::filesystem;
namespace dragonx {
namespace util {
namespace {
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
{
return std::fwrite(contents, size, nmemb, static_cast<FILE*>(userp));
}
// Bridge libcurl's C progress callback to the caller's std::function: publish live byte counts
// and abort the transfer (return non-zero) when the callback returns false (cancellation).
int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t)
{
auto* fn = static_cast<const std::function<bool(double, double)>*>(clientp);
return (*fn)(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
}
} // namespace
std::string httpGetString(const std::string& url, const char* logTag)
{
CURL* curl = curl_easy_init();
if (!curl) return {};
std::string result;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("%s GET %s failed: %s (HTTP %ld)\n",
logTag, url.c_str(), curl_easy_strerror(res), httpCode);
return {};
}
return result;
}
bool httpDownloadToFile(const std::string& url, const std::string& destPath,
const char* logTag, long long maxBytes,
const std::function<bool(double, double)>& onProgress)
{
FILE* fp = std::fopen(destPath.c_str(), "wb");
if (!fp) return false;
CURL* curl = curl_easy_init();
if (!curl) { std::fclose(fp); return false; }
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(maxBytes)); // refuse oversized bodies
// Live progress + cancellation: the callback publishes byte counts and aborts on cancel().
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &onProgress);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
std::fclose(fp);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("%s download %s failed: %s (HTTP %ld)\n",
logTag, url.c_str(), curl_easy_strerror(res), httpCode);
std::error_code ec; fs::remove(destPath, ec);
return false;
}
return true;
}
} // namespace util
} // namespace dragonx
+34
View File
@@ -0,0 +1,34 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Shared libcurl download primitives for the in-app updaters (miner + daemon). Both the
// XmrigUpdater and DaemonUpdater workers issued byte-identical libcurl GET/download code —
// including the security-critical TLS-verify and max-body-size options. Sharing it here means
// those options are defined exactly once, so a curl/TLS change can't silently drift between the
// two signature-verified download-and-execute paths. The header stays curl-free (callers that
// only download don't need <curl/curl.h>).
#pragma once
#include <functional>
#include <string>
namespace dragonx {
namespace util {
// GET `url` over TLS (peer + host verified) into a string. Follows redirects; 30s transfer /
// 15s connect timeout; browser-agnostic User-Agent. Returns "" on ANY failure (curl error or
// non-2xx status). `logTag` (e.g. "[xmrig-updater]") prefixes the debug log on failure.
std::string httpGetString(const std::string& url, const char* logTag);
// Download `url` over TLS (peer + host verified) to `destPath`, refusing bodies larger than
// `maxBytes` (a defense ahead of the checksum/signature check). `onProgress(downloaded, total)`
// is invoked live during the transfer; returning false aborts it (used for cancellation). On
// any failure the partial file is removed and false is returned. `logTag` prefixes debug logs.
bool httpDownloadToFile(const std::string& url, const std::string& destPath,
const char* logTag, long long maxBytes,
const std::function<bool(double downloaded, double total)>& onProgress);
} // namespace util
} // namespace dragonx
+99 -2
View File
@@ -253,6 +253,9 @@ void I18n::loadBuiltinEnglish()
strings_["keep_daemon"] = "Keep daemon running";
strings_["stop_external"] = "Stop external daemon";
strings_["verbose_logging"] = "Verbose logging";
strings_["screenshot_sweep"] = "Run screenshot sweep";
strings_["screenshot_open_dir"] = "Open location";
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds.";
strings_["mine_when_idle"] = "Mine when idle";
strings_["setup_wizard"] = "Run Setup Wizard...";
@@ -452,6 +455,49 @@ void I18n::loadBuiltinEnglish()
strings_["confirm_lite_redownload_title"] = "Redownload Blocks";
strings_["confirm_lite_redownload_msg"] = "This clears the wallet's downloaded block data and re-fetches and re-scans every block from the lite server. It can take a while; the wallet shows sync progress until it finishes.";
strings_["confirm_lite_redownload_safe"] = "Your wallet, keys, and seed are not affected — only the block data is re-downloaded.";
// Lite wallet lifecycle / backup / security section
strings_["lite_servers_network_tab"] = "Lite servers are managed in the Network tab.";
strings_["lite_wallet_request"] = "Lite wallet request";
strings_["lite_action"] = "Action";
strings_["lite_op_create"] = "Create";
strings_["lite_op_open"] = "Open";
strings_["lite_op_restore"] = "Restore";
strings_["lite_wallet_label"] = "Wallet";
strings_["lite_seed_label"] = "Seed";
strings_["lite_word_count"] = "%d / 24 words";
strings_["lite_birthday_label"] = "Birthday";
strings_["lite_birthday_hint"] = "Block height to start scanning from. Leave 0 if unknown (slower full scan).";
strings_["lite_account_label"] = "Account";
strings_["lite_overwrite"] = "Overwrite";
strings_["lite_passphrase_label"] = "Passphrase";
strings_["lite_validate"] = "Validate";
strings_["lite_working"] = "Working…";
strings_["lite_wallet_ready"] = "Wallet ready";
strings_["lite_backup_keys"] = "Backup & keys";
strings_["lite_show_seed"] = "Show seed";
strings_["lite_seed_warning"] = "Seed phrase — the ONLY way to restore your wallet. Write it down, store it offline, never share it.";
strings_["lite_show_private_keys"] = "Show private keys";
strings_["lite_private_keys_warning"] = "Private keys — anyone with these can spend your funds";
strings_["lite_birthday_backup"] = "Birthday: %llu (back this up too)";
strings_["lite_copy"] = "Copy";
strings_["lite_save_to_file"] = "Save to file";
strings_["lite_saved_to"] = "Saved (plaintext, owner-only) to ";
strings_["lite_could_not_write"] = "Could not write ";
strings_["lite_hide_wipe"] = "Hide & wipe";
strings_["lite_import_key_label"] = "Import key";
strings_["lite_import"] = "Import";
strings_["lite_key_imported"] = "Key imported — run a sync to scan its history";
strings_["lite_security"] = "Security";
strings_["lite_encrypt_wallet"] = "Encrypt wallet";
strings_["lite_wallet_encrypted"] = "Wallet encrypted";
strings_["lite_unlock"] = "Unlock";
strings_["lite_wallet_unlocked"] = "Wallet unlocked";
strings_["lite_unlock_failed"] = "Unlock failed";
strings_["lite_lock_now"] = "Lock now";
strings_["lite_wallet_locked"] = "Wallet locked";
strings_["lite_lock_failed"] = "Lock failed";
strings_["lite_remove_encryption"] = "Remove encryption";
strings_["lite_encryption_removed"] = "Encryption removed";
strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase";
strings_["tt_change_pass"] = "Change the wallet encryption passphrase";
strings_["tt_lock"] = "Lock the wallet immediately";
@@ -483,7 +529,7 @@ void I18n::loadBuiltinEnglish()
// Settings: about / debug
strings_["settings_about_text"] = "A shielded cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui for a lightweight, portable experience.";
strings_["settings_copyright"] = "Copyright 2024-2026 DragonX Developers | GPLv3 License";
strings_["debug_logging"] = "DEBUG LOGGING";
strings_["debug_logging"] = "DEBUG OPTIONS";
strings_["settings_debug_select"] = "Select categories to enable daemon debug logging (-debug= flags).";
strings_["settings_debug_restart_note"] = "Changes take effect after restarting the daemon.";
@@ -652,6 +698,15 @@ void I18n::loadBuiltinEnglish()
strings_["transparent"] = "Transparent";
strings_["total"] = "Total";
strings_["unconfirmed"] = "Unconfirmed";
strings_["recent_transactions"] = "RECENT TRANSACTIONS";
strings_["view_all"] = "View All";
strings_["no_transactions_yet"] = "No transactions yet";
strings_["privacy_great"] = "Great privacy!";
strings_["privacy_medium"] = "Consider shielding more";
strings_["privacy_low"] = "Low privacy — shield funds";
strings_["balance_history_collecting"] = "Balance history — collecting data...";
strings_["balance_shielded_fmt"] = "Shielded: %.8f";
strings_["balance_transparent_fmt"] = "Transparent: %.8f";
strings_["your_addresses"] = "Your Addresses";
strings_["z_addresses"] = "Z-Addresses";
strings_["t_addresses"] = "T-Addresses";
@@ -1059,15 +1114,53 @@ void I18n::loadBuiltinEnglish()
strings_["market_24h"] = "24h";
strings_["market_24h_volume"] = "24H VOLUME";
strings_["market_6h"] = "6h";
strings_["market_iv_live"] = "Live";
strings_["market_iv_1h"] = "1H";
strings_["market_iv_1d"] = "1D";
strings_["market_iv_1w"] = "1W";
strings_["market_iv_1m"] = "1M";
strings_["market_updated"] = "\xc2\xb7 Updated %s";
strings_["market_vol_short"] = "Vol";
strings_["market_cap_short"] = "Cap";
strings_["market_attribution"] = "Price data from CoinGecko";
strings_["market_btc_price"] = "BTC PRICE";
strings_["market_no_history"] = "No price history available";
strings_["market_no_price"] = "No price data";
strings_["market_now"] = "Now";
strings_["market_pct_shielded"] = "%.0f%% Shielded";
strings_["market_portfolio"] = "MY DRGX";
strings_["market_portfolio"] = "PORTFOLIO";
strings_["portfolio_all_funds"] = "All funds";
strings_["portfolio_manage"] = "Manage\xE2\x80\xA6";
strings_["portfolio_icon"] = "Icon";
strings_["portfolio_color"] = "Color";
strings_["portfolio_no_icon"] = "None";
strings_["portfolio_custom_color"] = "Custom color\xE2\x80\xA6";
strings_["portfolio_outline_opacity"] = "Outline opacity";
strings_["portfolio_price"] = "Price";
strings_["portfolio_price_usd"] = "Market \xC2\xB7 USD";
strings_["portfolio_price_btc"] = "Market \xC2\xB7 BTC";
strings_["portfolio_price_drgx"] = "DRGX only";
strings_["portfolio_price_manual"] = "Manual";
strings_["portfolio_manual_price"] = "Price / DRGX";
strings_["portfolio_currency"] = "Currency";
strings_["portfolio_show"] = "Show:";
strings_["portfolio_show_value"] = "Value";
strings_["portfolio_show_24h"] = "24h";
strings_["portfolio_show_sparkline"] = "Sparkline";
strings_["portfolio_spark_min"] = "Minute";
strings_["portfolio_spark_hour"] = "Hour";
strings_["portfolio_spark_day"] = "Day";
strings_["portfolio_spark_week"] = "Week";
strings_["portfolio_spark_month"] = "Month";
strings_["portfolio_appearance"] = "Appearance";
strings_["portfolio_addresses_hdr"] = "Addresses";
strings_["portfolio_search"] = "Search addresses\xE2\x80\xA6";
strings_["portfolio_search_icons"] = "Search icons\xE2\x80\xA6";
strings_["portfolio_funded"] = "Funded";
strings_["portfolio_select_all"] = "All";
strings_["portfolio_select_shown"] = "Select all";
strings_["portfolio_group_name"] = "Group name";
strings_["portfolio_no_addr_match"] = "No addresses match";
strings_["portfolio_manage_title"] = "Manage portfolio";
strings_["portfolio_add_entry"] = "Add entry";
strings_["portfolio_new_entry"] = "New entry";
@@ -1081,6 +1174,10 @@ void I18n::loadBuiltinEnglish()
strings_["portfolio_delete"] = "Delete";
strings_["portfolio_save"] = "Save";
strings_["portfolio_cancel"] = "Cancel";
strings_["portfolio_revert"] = "Revert";
strings_["portfolio_close"] = "Close";
strings_["portfolio_untitled"] = "Untitled";
strings_["portfolio_detail_empty"] = "Select a group on the left, or add one, to edit it.";
strings_["portfolio_add_to"] = "Add to portfolio";
strings_["portfolio_remove_from"] = "Remove from portfolio";
strings_["market_24h_change"] = "24h";
+3
View File
@@ -245,6 +245,9 @@ std::string Platform::getConfigDir()
return std::string(path) + "\\" DRAGONX_APP_NAME "\\";
}
return getHomeDir() + "\\AppData\\Roaming\\" DRAGONX_APP_NAME "\\";
#elif defined(__APPLE__)
// macOS-native config location (matches what settings.json / addressbook.json use).
return getHomeDir() + "/Library/Application Support/" DRAGONX_APP_NAME "/";
#else
return getHomeDir() + "/.config/" DRAGONX_APP_NAME "/";
#endif
+15
View File
@@ -45,6 +45,21 @@ std::string formatTimeAgoShort(std::int64_t timestamp)
return buf;
}
std::string truncateMiddle(const std::string& s, int maxLen)
{
// Guard maxLen <= 3 before the unsigned (maxLen - 3): otherwise it wraps and the
// trailing substr(length - half) throws std::out_of_range on short inputs.
if (maxLen <= 3 || s.length() <= static_cast<std::size_t>(maxLen)) return s;
int half = (maxLen - 3) / 2;
return s.substr(0, half) + "..." + s.substr(s.length() - half);
}
std::string truncateMiddle(const std::string& s, int front, int back)
{
if (s.length() <= static_cast<std::size_t>(front + back + 3)) return s;
return s.substr(0, front) + "..." + s.substr(s.length() - back);
}
bool containsIgnoreCase(std::string_view haystack, std::string_view needle)
{
if (needle.empty()) return true;
+9
View File
@@ -24,5 +24,14 @@ std::string formatTimeAgoShort(std::int64_t timestamp);
// An empty needle matches everything. Safe for per-frame filtering of large lists.
bool containsIgnoreCase(std::string_view haystack, std::string_view needle);
// Middle-ellipsis truncation, half/half form: "prefix...suffix" so the total (including "...")
// is at most maxLen. Guards maxLen <= 3 (returns s unchanged) to avoid unsigned underflow on the
// (maxLen - 3) split. Strings already at/under maxLen are returned unchanged.
std::string truncateMiddle(const std::string& s, int maxLen);
// Middle-ellipsis truncation with explicit prefix/suffix lengths: "<front chars>...<back chars>".
// Returns s unchanged when it is no longer than front + back + 3 (the "..." would not save space).
std::string truncateMiddle(const std::string& s, int front, int back);
} // namespace util
} // namespace dragonx
+4 -72
View File
@@ -8,6 +8,7 @@
#include "xmrig_updater.h"
#include "http_download.h"
#include "logger.h"
#include <curl/curl.h>
@@ -31,24 +32,6 @@ namespace {
constexpr curl_off_t kMaxArchiveBytes = 64LL * 1024 * 1024; // 64 MiB
constexpr std::size_t kMaxMemberBytes = 64u * 1024 * 1024; // 64 MiB per extracted file
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
{
return std::fwrite(contents, size, nmemb, static_cast<FILE*>(userp));
}
// libcurl progress callback: publish live byte counts and abort the transfer on cancel().
int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t)
{
auto* up = static_cast<XmrigUpdater*>(clientp);
return up->onDownloadProgress(static_cast<double>(dlnow), static_cast<double>(dltotal)) ? 0 : 1;
}
std::string baseName(const std::string& path)
{
const auto slash = path.find_last_of("/\\");
@@ -109,64 +92,13 @@ void XmrigUpdater::cancel()
std::string XmrigUpdater::httpGet(const std::string& url)
{
CURL* curl = curl_easy_init();
if (!curl) return {};
std::string result;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("[xmrig-updater] GET %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
return {};
}
return result;
return httpGetString(url, "[xmrig-updater]");
}
bool XmrigUpdater::downloadToFile(const std::string& url, const std::string& destPath)
{
FILE* fp = std::fopen(destPath.c_str(), "wb");
if (!fp) return false;
CURL* curl = curl_easy_init();
if (!curl) { std::fclose(fp); return false; }
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, kMaxArchiveBytes); // refuse oversized bodies
// Live progress + cancellation: the callback publishes byte counts and aborts on cancel().
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
std::fclose(fp);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("[xmrig-updater] download %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
std::error_code ec; fs::remove(destPath, ec);
return false;
}
return true;
return httpDownloadToFile(url, destPath, "[xmrig-updater]", kMaxArchiveBytes,
[this](double downloaded, double total) { return onDownloadProgress(downloaded, total); });
}
void XmrigUpdater::startCheck(const std::string& installedTag)
+16 -16
View File
@@ -13,17 +13,6 @@ namespace wallet {
namespace {
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
bool startsWith(const std::string& value, const char* prefix)
{
const std::string prefixValue(prefix);
@@ -34,7 +23,7 @@ bool startsWith(const std::string& value, const char* prefix)
LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint)
{
LiteServerEndpoint normalized = endpoint;
normalized.url = trimCopy(normalized.url);
normalized.url = liteTrimCopy(normalized.url);
return normalized;
}
@@ -52,12 +41,23 @@ std::vector<std::pair<std::size_t, LiteServerEndpoint>> usableConfiguredServers(
std::string normalizedChainName(const std::string& chainName)
{
const std::string normalized = trimCopy(chainName);
const std::string normalized = liteTrimCopy(chainName);
return normalized.empty() ? kDragonXLiteChainName : normalized;
}
} // namespace
std::string liteTrimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::vector<LiteServerEndpoint> defaultDragonXLiteServers()
{
return {
@@ -83,13 +83,13 @@ LiteConnectionSettings defaultLiteConnectionSettings()
bool isLiteServerUrlUsable(const std::string& serverUrl)
{
const std::string normalized = trimCopy(serverUrl);
const std::string normalized = liteTrimCopy(serverUrl);
return startsWith(normalized, "https://") || startsWith(normalized, "http://");
}
bool isOfficialLiteServer(const std::string& serverUrl)
{
const std::string url = trimCopy(serverUrl);
const std::string url = liteTrimCopy(serverUrl);
for (const auto& s : defaultDragonXLiteServers())
if (s.url == url) return true;
return false;
@@ -126,7 +126,7 @@ const char* liteConnectionAvailabilityName(LiteConnectionAvailability availabili
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
{
auto usableServers = usableConfiguredServers(settings);
const std::string stickyServerUrl = trimCopy(settings.stickyServerUrl);
const std::string stickyServerUrl = liteTrimCopy(settings.stickyServerUrl);
if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
isLiteServerUrlUsable(stickyServerUrl)) {
+2
View File
@@ -84,6 +84,8 @@ struct LiteServerHealthCheckResult {
std::string error;
};
// Trims leading/trailing whitespace, returning a copy. Shared across the lite slice.
std::string liteTrimCopy(const std::string& value);
std::vector<LiteServerEndpoint> defaultDragonXLiteServers();
LiteConnectionSettings defaultLiteConnectionSettings();
bool isLiteServerUrlUsable(const std::string& serverUrl);
+1 -13
View File
@@ -1,24 +1,12 @@
#include "wallet/lite_sync_service.h"
#include <algorithm>
#include <cctype>
#include <limits>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimSyncCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::uint64_t effectiveStagnantThreshold(std::uint64_t threshold)
{
return threshold == 0 ? 1 : threshold;
@@ -283,7 +271,7 @@ LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest
LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const
{
const auto overrideUrl = trimSyncCopy(serverUrl);
const auto overrideUrl = liteTrimCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"};
+9 -13
View File
@@ -5,6 +5,7 @@
#include "lite_wallet_controller.h"
#include "lite_diagnostics.h"
#include "lite_result_parsers.h"
#include "../data/wallet_state.h"
#include <algorithm>
@@ -695,19 +696,14 @@ std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModel()
if (bridge_) {
const auto h = bridge_->execute("height", "");
if (h.ok) {
try {
const auto j = nlohmann::json::parse(h.value);
if (j.is_object() && j.contains("height") && j["height"].is_number_unsigned()) {
const unsigned long long height = j["height"].get<unsigned long long>();
if (height > 0) {
model.hasSyncStatus = true;
model.sync.walletHeight = height;
model.sync.chainHeight = height; // synced: wallet height == chain tip
model.sync.complete = true;
model.sync.progress = 1.0;
}
}
} catch (...) { /* leave the mapped sync fields as-is */ }
const auto parsed = parseLiteHeightResponse(h.value);
if (parsed.ok && parsed.height.height > 0) {
model.hasSyncStatus = true;
model.sync.walletHeight = parsed.height.height;
model.sync.chainHeight = parsed.height.height; // synced: wallet height == chain tip
model.sync.complete = true;
model.sync.progress = 1.0;
}
}
}
return model;
+1 -13
View File
@@ -1,22 +1,10 @@
#include "wallet/lite_wallet_gateway.h"
#include <cctype>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimGatewayCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
WalletBackendStatus makeGatewayStatus(WalletBackendState state, std::string message)
{
WalletBackendStatus status;
@@ -262,7 +250,7 @@ LiteWalletRefreshResult LiteWalletGateway::refresh(const LiteWalletRefreshReques
LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const
{
const auto overrideUrl = trimGatewayCopy(serverUrl);
const auto overrideUrl = liteTrimCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"};
+2 -14
View File
@@ -4,7 +4,6 @@
#include "lite_wallet_lifecycle_service.h"
#include <cctype>
#include <utility>
namespace dragonx {
@@ -12,17 +11,6 @@ namespace wallet {
namespace {
std::string trimLifecycleCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value)
{
return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)};
@@ -172,7 +160,7 @@ LiteWalletLifecyclePlan LiteWalletLifecycleService::planOpenWallet(
LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet(
const LiteWalletRestoreRequest& request) const
{
const std::string validationError = trimLifecycleCopy(request.seedPhrase).empty()
const std::string validationError = liteTrimCopy(request.seedPhrase).empty()
? "restore seed phrase is required"
: std::string();
return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed,
@@ -220,7 +208,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::restoreWallet(
LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest(
const std::string& serverUrl) const
{
const std::string overrideUrl = trimLifecycleCopy(serverUrl);
const std::string overrideUrl = liteTrimCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"};
+5 -426
View File
@@ -1,428 +1,7 @@
#include "lite_wallet_lifecycle_ui_adapter.h"
#include <sstream>
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
void addIssue(std::vector<LiteWalletLifecycleUiExecutionIssueInfo>& issues,
LiteWalletLifecycleUiExecutionIssue issue,
std::string message)
{
issues.push_back(LiteWalletLifecycleUiExecutionIssueInfo{issue, std::move(message)});
}
LiteWalletLifecycleUiExecutionResult stoppedResult(
LiteWalletLifecycleUiExecutionResult result,
LiteWalletLifecycleUiExecutionStatus status,
LiteWalletLifecycleUiExecutionIssue issue,
const std::string& message)
{
result.status = status;
result.error = message;
result.ok = false;
addIssue(result.issues, issue, message);
return result;
}
LiteWalletLifecycleUiExecutionStatus statusFromServerSelection(
LiteWalletServerSelectionUiExecutionStatus status)
{
switch (status) {
case LiteWalletServerSelectionUiExecutionStatus::ReadyForFutureLifecycle:
return LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLiteBuild:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLiteBuild;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForBackendCapability:
return LiteWalletLifecycleUiExecutionStatus::WaitingForBackendCapability;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection:
return LiteWalletLifecycleUiExecutionStatus::WaitingForServerSelection;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPersistenceOwner:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForDisplayStatus:
return LiteWalletLifecycleUiExecutionStatus::WaitingForDisplayStatus;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLifecycleUi:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLifecycleUi;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPrivateDataRedaction:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPrivateDataRedaction;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSyncPlannerFeed:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSyncPlannerFeed;
case LiteWalletServerSelectionUiExecutionStatus::SettingsSaveFailed:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
case LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled:
return LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled;
}
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
}
LiteWalletLifecycleUiExecutionStatus statusFromLifecycle(
LiteWalletServerLifecycleReadinessStatus status)
{
switch (status) {
case LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle:
return LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLiteBuild;
case LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability:
return LiteWalletLifecycleUiExecutionStatus::WaitingForBackendCapability;
case LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection:
return LiteWalletLifecycleUiExecutionStatus::WaitingForServerSelection;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection;
case LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus:
return LiteWalletLifecycleUiExecutionStatus::WaitingForDisplayStatus;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLifecycleUi;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPrivateDataRedaction;
case LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSyncPlannerFeed;
case LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled:
return LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled;
}
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
}
LiteWalletLifecycleUiExecutionIssue issueFromLifecycle(
LiteWalletServerLifecycleReadinessIssue issue)
{
switch (issue) {
case LiteWalletServerLifecycleReadinessIssue::FullNodeBuild:
return LiteWalletLifecycleUiExecutionIssue::FullNodeBuild;
case LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing:
return LiteWalletLifecycleUiExecutionIssue::LiteBackendCapabilityMissing;
case LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded:
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing:
return LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing;
case LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing:
return LiteWalletLifecycleUiExecutionIssue::ServerSelectionMissing;
case LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing;
case LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing:
return LiteWalletLifecycleUiExecutionIssue::SelectedServerDisplayMissing;
case LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing;
case LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed:
return LiteWalletLifecycleUiExecutionIssue::LifecycleOperationUnconfirmed;
case LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing:
return LiteWalletLifecycleUiExecutionIssue::OpenWalletPathMissing;
case LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing:
return LiteWalletLifecycleUiExecutionIssue::RestoreSeedMissing;
case LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing:
return LiteWalletLifecycleUiExecutionIssue::PrivateDataRedactionMissing;
case LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing:
return LiteWalletLifecycleUiExecutionIssue::SyncPlannerFeedMissing;
case LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested;
}
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
}
LiteWalletLifecycleUiExecutionIssue issueFromServerSelection(
LiteWalletServerSelectionUiExecutionIssue issue)
{
switch (issue) {
case LiteWalletServerSelectionUiExecutionIssue::FullNodeBuild:
return LiteWalletLifecycleUiExecutionIssue::FullNodeBuild;
case LiteWalletServerSelectionUiExecutionIssue::LiteBackendCapabilityMissing:
return LiteWalletLifecycleUiExecutionIssue::LiteBackendCapabilityMissing;
case LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded:
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing:
return LiteWalletLifecycleUiExecutionIssue::ServerSelectionMissing;
case LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing;
case LiteWalletServerSelectionUiExecutionIssue::SelectedServerDisplayMissing:
return LiteWalletLifecycleUiExecutionIssue::SelectedServerDisplayMissing;
case LiteWalletServerSelectionUiExecutionIssue::LifecycleUiOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing;
case LiteWalletServerSelectionUiExecutionIssue::PrivateDataRedactionMissing:
return LiteWalletLifecycleUiExecutionIssue::PrivateDataRedactionMissing;
case LiteWalletServerSelectionUiExecutionIssue::SyncPlannerFeedMissing:
return LiteWalletLifecycleUiExecutionIssue::SyncPlannerFeedMissing;
case LiteWalletServerSelectionUiExecutionIssue::SettingsSaveFailed:
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested:
return LiteWalletLifecycleUiExecutionIssue::ServerConnectivityCheckRequested;
case LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested;
case LiteWalletServerSelectionUiExecutionIssue::SyncRequested:
return LiteWalletLifecycleUiExecutionIssue::SyncRequested;
case LiteWalletServerSelectionUiExecutionIssue::SyncStatusPollingRequested:
return LiteWalletLifecycleUiExecutionIssue::SyncStatusPollingRequested;
case LiteWalletServerSelectionUiExecutionIssue::WorkerQueueRequested:
return LiteWalletLifecycleUiExecutionIssue::WorkerQueueRequested;
case LiteWalletServerSelectionUiExecutionIssue::WalletStateMutationRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletStateMutationRequested;
case LiteWalletServerSelectionUiExecutionIssue::WalletFilePersistenceRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletFilePersistenceRequested;
case LiteWalletServerSelectionUiExecutionIssue::SendImportExportRequested:
return LiteWalletLifecycleUiExecutionIssue::SendImportExportRequested;
}
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
}
void copyServerSelectionIssues(LiteWalletLifecycleUiExecutionResult& result)
{
for (const auto& issue : result.serverSelectionReport.issues) {
addIssue(result.issues, issueFromServerSelection(issue.issue), issue.message);
}
}
void copyLifecycleIssues(LiteWalletLifecycleUiExecutionResult& result)
{
for (const auto& issue : result.lifecycleReadiness.issues) {
addIssue(result.issues, issueFromLifecycle(issue.issue), issue.message);
}
}
LiteWalletServerSelectionUiExecutionInput makeServerSelectionInput(
const LiteWalletLifecycleUiExecutionInput& input,
const config::Settings& settings)
{
LiteWalletServerSelectionUiExecutionInput selection;
selection.capabilities = input.capabilities;
selection.persistence.settingsLoaded = input.settingsLoaded;
selection.persistence.havePersistedSelectionIntent =
!input.requirePersistedServerSelectionIntent || settings.getLitePersistSelectedServer();
selection.persistence.persistSelectedServer = false;
selection.persistence.persistenceOwnerReady = true;
selection.persistence.writeSettings = false;
selection.ui = input.ui;
selection.operation = input.request.operation;
selection.createRequest = input.request.createRequest;
selection.openRequest = input.request.openRequest;
selection.restoreRequest = input.request.restoreRequest;
selection.requireLifecycleReadiness = true;
return selection;
}
std::string buildDiagnosticSummary(const LiteWalletLifecycleUiExecutionResult& result)
{
std::ostringstream out;
out << "operation=" << liteWalletLifecycleOperationName(result.lifecycleInput.operation)
<< ";server=" << result.selectedServerUrlRedacted
<< ";request=" << result.requestSummaryRedacted
<< ";network=false;bridge=false;lifecycle=false;sync=false;wallet_state=false;wallet_file=false";
return out.str();
}
} // namespace
const char* liteWalletLifecycleUiExecutionStatusName(
LiteWalletLifecycleUiExecutionStatus status)
{
switch (status) {
case LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest:
return "ReadyForFutureLifecycleRequest";
case LiteWalletLifecycleUiExecutionStatus::WaitingForLiteBuild:
return "WaitingForLiteBuild";
case LiteWalletLifecycleUiExecutionStatus::WaitingForBackendCapability:
return "WaitingForBackendCapability";
case LiteWalletLifecycleUiExecutionStatus::WaitingForSettings:
return "WaitingForSettings";
case LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection:
return "WaitingForPersistedServerSelection";
case LiteWalletLifecycleUiExecutionStatus::WaitingForServerSelection:
return "WaitingForServerSelection";
case LiteWalletLifecycleUiExecutionStatus::WaitingForDisplayStatus:
return "WaitingForDisplayStatus";
case LiteWalletLifecycleUiExecutionStatus::WaitingForLifecycleUi:
return "WaitingForLifecycleUi";
case LiteWalletLifecycleUiExecutionStatus::WaitingForRequest:
return "WaitingForRequest";
case LiteWalletLifecycleUiExecutionStatus::WaitingForPrivateDataRedaction:
return "WaitingForPrivateDataRedaction";
case LiteWalletLifecycleUiExecutionStatus::WaitingForSyncPlannerFeed:
return "WaitingForSyncPlannerFeed";
case LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled:
return "RuntimeExecutionDisabled";
}
return "WaitingForSettings";
}
const char* liteWalletLifecycleUiExecutionIssueName(
LiteWalletLifecycleUiExecutionIssue issue)
{
switch (issue) {
case LiteWalletLifecycleUiExecutionIssue::FullNodeBuild:
return "FullNodeBuild";
case LiteWalletLifecycleUiExecutionIssue::LiteBackendCapabilityMissing:
return "LiteBackendCapabilityMissing";
case LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded:
return "SettingsNotLoaded";
case LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing:
return "PersistedServerSelectionIntentMissing";
case LiteWalletLifecycleUiExecutionIssue::ServerSelectionMissing:
return "ServerSelectionMissing";
case LiteWalletLifecycleUiExecutionIssue::SelectedServerDisplayMissing:
return "SelectedServerDisplayMissing";
case LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing:
return "LifecycleUiOwnerMissing";
case LiteWalletLifecycleUiExecutionIssue::LifecycleOperationUnconfirmed:
return "LifecycleOperationUnconfirmed";
case LiteWalletLifecycleUiExecutionIssue::OpenWalletPathMissing:
return "OpenWalletPathMissing";
case LiteWalletLifecycleUiExecutionIssue::RestoreSeedMissing:
return "RestoreSeedMissing";
case LiteWalletLifecycleUiExecutionIssue::PrivateDataRedactionMissing:
return "PrivateDataRedactionMissing";
case LiteWalletLifecycleUiExecutionIssue::SyncPlannerFeedMissing:
return "SyncPlannerFeedMissing";
case LiteWalletLifecycleUiExecutionIssue::ServerConnectivityCheckRequested:
return "ServerConnectivityCheckRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletExistsCheckRequested:
return "WalletExistsCheckRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested:
return "WalletLifecycleExecutionRequested";
case LiteWalletLifecycleUiExecutionIssue::SyncRequested:
return "SyncRequested";
case LiteWalletLifecycleUiExecutionIssue::SyncStatusPollingRequested:
return "SyncStatusPollingRequested";
case LiteWalletLifecycleUiExecutionIssue::WorkerQueueRequested:
return "WorkerQueueRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletStateMutationRequested:
return "WalletStateMutationRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletFilePersistenceRequested:
return "WalletFilePersistenceRequested";
case LiteWalletLifecycleUiExecutionIssue::SendImportExportRequested:
return "SendImportExportRequested";
case LiteWalletLifecycleUiExecutionIssue::SettingsWriteRequested:
return "SettingsWriteRequested";
}
return "SettingsNotLoaded";
}
LiteWalletLifecycleUiExecutionResult executeLiteWalletLifecycleUiRequest(
config::Settings& settings,
const LiteWalletLifecycleUiExecutionInput& input)
{
LiteWalletLifecycleUiExecutionResult result;
auto stopForRuntime = [&](LiteWalletLifecycleUiExecutionIssue issue,
const std::string& message) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled,
issue,
message);
};
if (input.settingsWriteRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SettingsWriteRequested,
"lite lifecycle request adapter cannot write settings");
}
if (input.serverConnectivityCheckRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::ServerConnectivityCheckRequested,
"lite lifecycle request adapter cannot check server connectivity");
}
if (input.walletExistsCheckRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletExistsCheckRequested,
"lite lifecycle request adapter cannot check wallet existence");
}
if (input.walletLifecycleExecutionRequested || input.ui.realLifecycleExecutionRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested,
"lite lifecycle request adapter cannot execute wallet lifecycle actions");
}
if (input.syncStartRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SyncRequested,
"lite lifecycle request adapter cannot start sync");
}
if (input.syncStatusPollingRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SyncStatusPollingRequested,
"lite lifecycle request adapter cannot poll syncstatus");
}
if (input.workerQueueEnqueueRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WorkerQueueRequested,
"lite lifecycle request adapter cannot enqueue wallet workers");
}
if (input.walletStateMutationRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletStateMutationRequested,
"lite lifecycle request adapter cannot mutate WalletState");
}
if (input.walletFilePersistenceRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletFilePersistenceRequested,
"lite lifecycle request adapter cannot persist wallet files");
}
if (input.sendImportExportExecutionRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SendImportExportRequested,
"lite lifecycle request adapter cannot execute send/import/export flows");
}
if (!input.settingsLoaded) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::WaitingForSettings,
LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded,
"lite lifecycle settings are not loaded");
}
if (!input.request.requestProvided) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::WaitingForRequest,
LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing,
"lite lifecycle request intent is missing");
}
if (input.requirePersistedServerSelectionIntent && !settings.getLitePersistSelectedServer()) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection,
LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing,
"lite lifecycle requires persisted selected-server intent");
}
result.connectionSettings = liteConnectionSettingsFromAppSettings(settings);
result.selectedServerSettingsLoaded = true;
LiteWalletServerSelectionUiExecutionInput selectionInput = makeServerSelectionInput(input, settings);
result.serverSelectionReport = executeLiteWalletServerSelectionUi(settings, selectionInput);
result.connectionSettings = result.serverSelectionReport.connectionSettings;
result.selectedServer = result.serverSelectionReport.selectedServer;
result.selectedServerUrlRedacted = result.serverSelectionReport.selectedServerUrlRedacted;
result.selectedServerIntentAccepted = result.serverSelectionReport.selectedServerIntentAccepted;
result.status = statusFromServerSelection(result.serverSelectionReport.status);
result.lifecycleInput = result.serverSelectionReport.lifecycleInput;
result.lifecycleReadiness = result.serverSelectionReport.lifecycleReadiness;
result.lifecycleReadinessAccepted = result.lifecycleReadiness.ok;
result.lifecycleReportCouldFeedSyncPlanners =
result.lifecycleReadiness.lifecycleReportCouldFeedSyncPlanners;
result.requestAccepted = result.lifecycleReadiness.requestAccepted;
result.privateDataRedacted = result.lifecycleReadiness.requestPlan.privateInputsRedacted;
result.privateData = result.lifecycleReadiness.requestPlan.privateData;
result.requestSummaryRedacted = result.lifecycleReadiness.requestPlan.requestSummary;
result.diagnosticSummary = buildDiagnosticSummary(result);
copyServerSelectionIssues(result);
copyLifecycleIssues(result);
const bool lifecycleReadinessEvaluated = result.lifecycleReadiness.ok ||
!result.lifecycleReadiness.issues.empty() ||
!result.lifecycleReadiness.error.empty() ||
result.lifecycleReadiness.serverSelectionAccepted ||
result.lifecycleReadiness.requestAccepted;
if (!result.serverSelectionReport.ok && !lifecycleReadinessEvaluated) {
result.ok = false;
result.status = statusFromServerSelection(result.serverSelectionReport.status);
result.error = result.serverSelectionReport.error;
return result;
}
if (!result.lifecycleReadiness.ok) {
result.ok = false;
result.status = statusFromLifecycle(result.lifecycleReadiness.status);
result.error = result.lifecycleReadiness.error.empty()
? result.serverSelectionReport.error
: result.lifecycleReadiness.error;
return result;
}
result.ok = true;
result.status = LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest;
return result;
}
} // namespace wallet
} // namespace dragonx
// The lite-wallet lifecycle UI adapter is now header-only: it declares the live
// LiteWalletLifecycleUiExecutionInput DTO (and its request-intent / prerequisites
// sub-structs) that Settings drives the App-owned controller with. The former
// dry-run "readiness" executor and its status/issue reporting were removed as dead
// scaffolding, leaving no out-of-line definitions in this translation unit.
+10 -96
View File
@@ -1,53 +1,21 @@
#pragma once
#include "lite_wallet_server_selection_adapter.h"
#include "lite_wallet_lifecycle_service.h"
#include "wallet_capabilities.h"
#include "../config/settings.h"
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
enum class LiteWalletLifecycleUiExecutionStatus {
ReadyForFutureLifecycleRequest,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForSettings,
WaitingForPersistedServerSelection,
WaitingForServerSelection,
WaitingForDisplayStatus,
WaitingForLifecycleUi,
WaitingForRequest,
WaitingForPrivateDataRedaction,
WaitingForSyncPlannerFeed,
RuntimeExecutionDisabled
};
enum class LiteWalletLifecycleUiExecutionIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
SettingsNotLoaded,
PersistedServerSelectionIntentMissing,
ServerSelectionMissing,
SelectedServerDisplayMissing,
LifecycleUiOwnerMissing,
LifecycleOperationUnconfirmed,
OpenWalletPathMissing,
RestoreSeedMissing,
PrivateDataRedactionMissing,
SyncPlannerFeedMissing,
ServerConnectivityCheckRequested,
WalletExistsCheckRequested,
WalletLifecycleExecutionRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletFilePersistenceRequested,
SendImportExportRequested,
SettingsWriteRequested
struct LiteWalletLifecycleUiPrerequisites {
bool selectedServerDisplayReady = false;
bool lifecycleUiOwnerReady = false;
bool operationConfirmed = false;
bool privateDataRedactionReady = false;
bool syncPlannerFeedReady = false;
bool realLifecycleExecutionRequested = false;
};
struct LiteWalletLifecycleUiRequestIntent {
@@ -77,59 +45,5 @@ struct LiteWalletLifecycleUiExecutionInput {
bool sendImportExportExecutionRequested = false;
};
struct LiteWalletLifecycleUiExecutionIssueInfo {
LiteWalletLifecycleUiExecutionIssue issue = LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
std::string message;
};
struct LiteWalletLifecycleUiExecutionResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noServerHealthChecked = true;
bool noWalletExistsChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool noSettingsPersistence = true;
bool noSendImportExportExecution = true;
bool settingsWritten = false;
bool workerQueueEnqueued = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool selectedServerSettingsLoaded = false;
bool selectedServerIntentAccepted = false;
bool lifecycleReadinessAccepted = false;
bool requestAccepted = false;
bool privateDataRedacted = false;
bool lifecycleReportCouldFeedSyncPlanners = false;
LiteWalletLifecycleUiExecutionStatus status = LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
std::string error;
std::string selectedServerUrlRedacted;
std::string requestSummaryRedacted;
std::string diagnosticSummary;
LiteConnectionSettings connectionSettings;
LiteServerSelectionResult selectedServer;
LiteWalletServerSelectionUiExecutionResult serverSelectionReport;
LiteWalletServerLifecycleReadinessInput lifecycleInput;
LiteWalletServerLifecycleReadinessResult lifecycleReadiness;
std::vector<LiteRedactedPrivateData> privateData;
std::vector<LiteWalletLifecycleUiExecutionIssueInfo> issues;
};
const char* liteWalletLifecycleUiExecutionStatusName(
LiteWalletLifecycleUiExecutionStatus status);
const char* liteWalletLifecycleUiExecutionIssueName(
LiteWalletLifecycleUiExecutionIssue issue);
LiteWalletLifecycleUiExecutionResult executeLiteWalletLifecycleUiRequest(
config::Settings& settings,
const LiteWalletLifecycleUiExecutionInput& input);
} // namespace wallet
} // namespace dragonx
} // namespace dragonx
@@ -1,386 +0,0 @@
#include "wallet/lite_wallet_server_lifecycle_readiness.h"
#include <cctype>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
void addIssue(LiteWalletServerLifecycleReadinessResult& result,
LiteWalletServerLifecycleReadinessIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletServerLifecycleReadinessIssueInfo{issue, std::move(message)});
}
LiteWalletServerLifecycleReadinessResult stoppedResult(
LiteWalletServerLifecycleReadinessResult result,
LiteWalletServerLifecycleReadinessStatus status,
LiteWalletServerLifecycleReadinessIssue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
result.lifecycleStatus = WalletBackendStatus{WalletBackendState::Unavailable, result.error, {}, {}, 0.0};
return result;
}
LiteRedactedPrivateData redactedPrivateField(LitePrivateDataKind kind, const std::string& value)
{
return LiteRedactedPrivateData{kind, !trimCopy(value).empty(), redactLitePrivateDataValue(value)};
}
bool privateDataIsRedacted(const std::vector<LiteRedactedPrivateData>& privateData)
{
for (const auto& item : privateData) {
if (!item.present && item.redactedValue != "<empty>") return false;
if (item.present && item.redactedValue != "<redacted>") return false;
}
return true;
}
LiteWalletSelectedServerDisplayReport displayReportForSelection(
const LiteServerSelectionResult& selection)
{
LiteWalletSelectedServerDisplayReport report;
if (!selection.ok) {
report.status = LiteWalletSelectedServerDisplayStatus::Missing;
report.message = selection.error.empty() ? "no usable lite server is selected" : selection.error;
return report;
}
report.ok = true;
report.status = selection.customServer
? LiteWalletSelectedServerDisplayStatus::CustomServer
: LiteWalletSelectedServerDisplayStatus::SelectedServer;
report.label = selection.server.label;
report.url = selection.server.url;
report.serverIndex = selection.serverIndex;
report.customServer = selection.customServer;
report.message = selection.customServer
? "custom lite server selected for display"
: "configured lite server selected for display";
return report;
}
LiteWalletLifecyclePlan lifecyclePlanForRequest(
LiteWalletLifecycleOperation operation,
const LiteServerSelectionResult& selection)
{
LiteWalletLifecyclePlan plan;
plan.operation = operation;
plan.bridgeExecutionAllowed = false;
if (!selection.ok) {
plan.error = selection.error.empty() ? "no usable lite server is selected" : selection.error;
return plan;
}
plan.ok = true;
plan.server = selection.server;
plan.serverIndex = selection.serverIndex;
plan.customServer = selection.customServer;
return plan;
}
LiteWalletLifecycleUiRequestPlan requestPlanForInput(
const LiteWalletServerLifecycleReadinessInput& input,
const LiteServerSelectionResult& selection)
{
LiteWalletLifecycleUiRequestPlan plan;
plan.operation = input.operation;
plan.lifecyclePlan = lifecyclePlanForRequest(input.operation, selection);
if (!plan.lifecyclePlan.ok) {
plan.error = plan.lifecyclePlan.error;
return plan;
}
switch (input.operation) {
case LiteWalletLifecycleOperation::CreateNew:
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::Passphrase,
input.createRequest.passphrase));
plan.requestSummary = "operation=create;server=<selected>;passphrase=" +
std::string(input.createRequest.passphrase.empty() ? "<empty>" : "<redacted>");
break;
case LiteWalletLifecycleOperation::OpenExisting:
if (trimCopy(input.openRequest.walletPath).empty()) {
plan.error = "lite wallet open requires a wallet path selected by the UI";
return plan;
}
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::WalletPath,
input.openRequest.walletPath));
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::Passphrase,
input.openRequest.passphrase));
plan.requestSummary = "operation=open;server=<selected>;wallet_path=<redacted>;passphrase=" +
std::string(input.openRequest.passphrase.empty() ? "<empty>" : "<redacted>");
break;
case LiteWalletLifecycleOperation::RestoreFromSeed:
if (trimCopy(input.restoreRequest.seedPhrase).empty()) {
plan.error = "lite wallet restore requires redacted seed material metadata";
return plan;
}
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::SeedPhrase,
input.restoreRequest.seedPhrase));
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::WalletPath,
input.restoreRequest.walletPath));
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::Passphrase,
input.restoreRequest.passphrase));
plan.requestSummary = "operation=restore;server=<selected>;seed=<redacted>;wallet_path=" +
std::string(input.restoreRequest.walletPath.empty() ? "<empty>" : "<redacted>") +
";passphrase=" +
std::string(input.restoreRequest.passphrase.empty() ? "<empty>" : "<redacted>");
break;
}
plan.privateInputsRedacted = privateDataIsRedacted(plan.privateData);
plan.lifecyclePlan.privateData = plan.privateData;
plan.ok = plan.privateInputsRedacted;
if (!plan.ok) plan.error = "lite wallet lifecycle private inputs are not redacted";
return plan;
}
LiteWalletServerSelectionPersistencePlan persistencePlanForInput(
const LiteWalletServerLifecycleReadinessInput& input,
const LiteServerSelectionResult& selection)
{
LiteWalletServerSelectionPersistencePlan plan;
plan.settingsLoaded = input.persistence.settingsLoaded;
plan.persistedSelectionIntentAccepted = input.persistence.havePersistedSelectionIntent;
plan.wouldPersistSelectedServer = input.persistence.persistSelectedServer;
plan.persistenceOwnerAccepted = input.persistence.persistenceOwnerReady;
plan.selectionMode = input.settings.selectionMode;
if (selection.ok) {
plan.selectedServerUrl = selection.server.url;
plan.selectedServerIndex = selection.serverIndex;
plan.selectedServerCustom = selection.customServer;
}
plan.settingsWritten = false;
plan.ok = true;
return plan;
}
WalletBackendStatus readyLifecycleStatus(const LiteWalletSelectedServerDisplayReport& display)
{
const std::string serverLabel = display.label.empty() ? display.url : display.label;
return WalletBackendStatus{
WalletBackendState::Disconnected,
"lite lifecycle UI readiness accepted for " + serverLabel + "; wallet lifecycle execution is still disabled",
{},
{},
0.0
};
}
} // namespace
const char* liteWalletServerLifecycleReadinessStatusName(
LiteWalletServerLifecycleReadinessStatus status)
{
switch (status) {
case LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle: return "ReadyForFutureLifecycle";
case LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild: return "WaitingForLiteBuild";
case LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability: return "WaitingForBackendCapability";
case LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection: return "WaitingForServerSelection";
case LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent: return "WaitingForPersistenceIntent";
case LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus: return "WaitingForDisplayStatus";
case LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi: return "WaitingForLifecycleUi";
case LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction: return "WaitingForPrivateDataRedaction";
case LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed: return "WaitingForSyncPlannerFeed";
case LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled: return "RuntimeExecutionDisabled";
}
return "Unknown";
}
const char* liteWalletServerLifecycleReadinessIssueName(
LiteWalletServerLifecycleReadinessIssue issue)
{
switch (issue) {
case LiteWalletServerLifecycleReadinessIssue::FullNodeBuild: return "FullNodeBuild";
case LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing: return "LiteBackendCapabilityMissing";
case LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded: return "PersistedSettingsNotLoaded";
case LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing: return "PersistedServerSelectionIntentMissing";
case LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing: return "ServerSelectionMissing";
case LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing: return "ServerPersistenceOwnerMissing";
case LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing: return "SelectedServerDisplayMissing";
case LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing: return "LifecycleUiOwnerMissing";
case LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed: return "LifecycleOperationUnconfirmed";
case LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing: return "OpenWalletPathMissing";
case LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing: return "RestoreSeedMissing";
case LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing: return "PrivateDataRedactionMissing";
case LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing: return "SyncPlannerFeedMissing";
case LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested: return "RealLifecycleExecutionRequested";
}
return "Unknown";
}
const char* liteWalletSelectedServerDisplayStatusName(
LiteWalletSelectedServerDisplayStatus status)
{
switch (status) {
case LiteWalletSelectedServerDisplayStatus::Hidden: return "Hidden";
case LiteWalletSelectedServerDisplayStatus::SelectedServer: return "SelectedServer";
case LiteWalletSelectedServerDisplayStatus::CustomServer: return "CustomServer";
case LiteWalletSelectedServerDisplayStatus::Missing: return "Missing";
}
return "Unknown";
}
LiteWalletServerLifecycleReadinessResult evaluateLiteWalletServerLifecycleReadiness(
const LiteWalletServerLifecycleReadinessInput& input,
LiteWalletServerLifecycleReadinessOptions options)
{
LiteWalletServerLifecycleReadinessResult result;
result.capabilities = input.capabilities;
result.persistencePlan = persistencePlanForInput(input, {});
if (options.requireLiteBuild && !isLiteBuild(input.capabilities)) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild,
LiteWalletServerLifecycleReadinessIssue::FullNodeBuild,
"lite server lifecycle readiness requires a lite build");
}
result.liteBuildAccepted = true;
if (options.requireLiteBackendCapability && !supportsLiteBackend(input.capabilities)) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability,
LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing,
"lite backend capability is required before lifecycle UI readiness can feed sync planners");
}
result.backendCapabilityAccepted = true;
if (options.requirePersistedSettingsLoaded && !input.persistence.settingsLoaded) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent,
LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded,
"lite server settings must be loaded before lifecycle UI readiness is evaluated");
}
if (options.requirePersistedSelectionIntent && !input.persistence.havePersistedSelectionIntent) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent,
LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing,
"lite server selection persistence intent is missing");
}
result.selectedServer = selectLiteServer(input.settings);
result.persistencePlan = persistencePlanForInput(input, result.selectedServer);
if (!result.selectedServer.ok) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection,
LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing,
result.selectedServer.error.empty()
? "no usable lite server is selected"
: result.selectedServer.error);
}
result.serverSelectionAccepted = true;
if (input.persistence.persistSelectedServer && !input.persistence.persistenceOwnerReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent,
LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing,
"lite server selection persistence owner is not ready");
}
result.persistenceIntentAccepted = true;
result.persistencePlan.ok = true;
result.selectedServerDisplay = displayReportForSelection(result.selectedServer);
if (options.requireSelectedServerDisplay && !input.ui.selectedServerDisplayReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus,
LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing,
"selected lite server display status is not ready");
}
result.selectedServerDisplayAccepted = true;
if (options.requireLifecycleUiOwner && !input.ui.lifecycleUiOwnerReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi,
LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing,
"lite wallet lifecycle UI owner is not ready");
}
result.lifecycleUiOwnerAccepted = true;
if (input.ui.realLifecycleExecutionRequested) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled,
LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested,
"real lite wallet lifecycle execution is disabled in this scaffold");
}
if (options.requireOperationConfirmation && !input.ui.operationConfirmed) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi,
LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed,
"lite wallet lifecycle operation requires explicit UI confirmation");
}
result.requestPlan = requestPlanForInput(input, result.selectedServer);
if (!result.requestPlan.ok) {
const auto issue = input.operation == LiteWalletLifecycleOperation::OpenExisting
? LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing
: LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing;
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi,
issue,
result.requestPlan.error.empty()
? "lite wallet lifecycle UI request is incomplete"
: result.requestPlan.error);
}
result.requestAccepted = true;
if (options.requirePrivateDataRedaction && !input.ui.privateDataRedactionReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction,
LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing,
"lite wallet lifecycle private-data redaction owner is not ready");
}
result.privateDataRedactionAccepted = true;
if (options.requireSyncPlannerFeed && !input.ui.syncPlannerFeedReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed,
LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing,
"lite lifecycle readiness feed for sync planners is not ready");
}
result.syncPlannerFeedAccepted = true;
result.ok = true;
result.status = LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle;
result.futureLifecycleCouldBeEnabled = true;
result.lifecycleReportCouldFeedSyncPlanners = true;
result.lifecycleStatus = readyLifecycleStatus(result.selectedServerDisplay);
return result;
}
} // namespace dragonx::wallet
@@ -1,181 +0,0 @@
#pragma once
#include "lite_wallet_lifecycle_service.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletServerLifecycleReadinessStatus {
ReadyForFutureLifecycle,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForServerSelection,
WaitingForPersistenceIntent,
WaitingForDisplayStatus,
WaitingForLifecycleUi,
WaitingForPrivateDataRedaction,
WaitingForSyncPlannerFeed,
RuntimeExecutionDisabled,
};
enum class LiteWalletServerLifecycleReadinessIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
PersistedSettingsNotLoaded,
PersistedServerSelectionIntentMissing,
ServerSelectionMissing,
ServerPersistenceOwnerMissing,
SelectedServerDisplayMissing,
LifecycleUiOwnerMissing,
LifecycleOperationUnconfirmed,
OpenWalletPathMissing,
RestoreSeedMissing,
PrivateDataRedactionMissing,
SyncPlannerFeedMissing,
RealLifecycleExecutionRequested,
};
enum class LiteWalletSelectedServerDisplayStatus {
Hidden,
SelectedServer,
CustomServer,
Missing,
};
struct LiteWalletServerSelectionPersistenceInput {
bool settingsLoaded = false;
bool havePersistedSelectionIntent = false;
bool persistSelectedServer = false;
bool persistenceOwnerReady = false;
};
struct LiteWalletLifecycleUiPrerequisites {
bool selectedServerDisplayReady = false;
bool lifecycleUiOwnerReady = false;
bool operationConfirmed = false;
bool privateDataRedactionReady = false;
bool syncPlannerFeedReady = false;
bool realLifecycleExecutionRequested = false;
};
struct LiteWalletServerLifecycleReadinessInput {
WalletCapabilities capabilities;
LiteConnectionSettings settings;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletServerSelectionPersistenceInput persistence;
LiteWalletLifecycleUiPrerequisites ui;
LiteWalletCreateRequest createRequest;
LiteWalletOpenRequest openRequest;
LiteWalletRestoreRequest restoreRequest;
};
struct LiteWalletServerLifecycleReadinessOptions {
bool requireLiteBuild = true;
bool requireLiteBackendCapability = true;
bool requirePersistedSettingsLoaded = true;
bool requirePersistedSelectionIntent = true;
bool requireSelectedServerDisplay = true;
bool requireLifecycleUiOwner = true;
bool requireOperationConfirmation = true;
bool requirePrivateDataRedaction = true;
bool requireSyncPlannerFeed = true;
};
struct LiteWalletSelectedServerDisplayReport {
bool ok = false;
LiteWalletSelectedServerDisplayStatus status = LiteWalletSelectedServerDisplayStatus::Hidden;
std::string label;
std::string url;
std::size_t serverIndex = 0;
bool customServer = false;
std::string message;
};
struct LiteWalletServerSelectionPersistencePlan {
bool ok = false;
bool settingsLoaded = false;
bool persistedSelectionIntentAccepted = false;
bool wouldPersistSelectedServer = false;
bool persistenceOwnerAccepted = false;
bool settingsWritten = false;
LiteServerSelectionMode selectionMode = LiteServerSelectionMode::Sticky;
std::string selectedServerUrl;
std::size_t selectedServerIndex = 0;
bool selectedServerCustom = false;
std::string error;
};
struct LiteWalletLifecycleUiRequestPlan {
bool ok = false;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletLifecyclePlan lifecyclePlan;
std::vector<LiteRedactedPrivateData> privateData;
bool privateInputsRedacted = true;
std::string requestSummary;
std::string error;
};
struct LiteWalletServerLifecycleReadinessIssueInfo {
LiteWalletServerLifecycleReadinessIssue issue = LiteWalletServerLifecycleReadinessIssue::FullNodeBuild;
std::string message;
};
struct LiteWalletServerLifecycleReadinessResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noServerHealthChecked = true;
bool noWalletExistsChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool noSettingsPersistence = true;
bool settingsWritten = false;
bool workerQueueEnqueued = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool walletReady = false;
bool liteBuildAccepted = false;
bool backendCapabilityAccepted = false;
bool serverSelectionAccepted = false;
bool persistenceIntentAccepted = false;
bool selectedServerDisplayAccepted = false;
bool lifecycleUiOwnerAccepted = false;
bool requestAccepted = false;
bool privateDataRedactionAccepted = false;
bool syncPlannerFeedAccepted = false;
bool futureLifecycleCouldBeEnabled = false;
bool lifecycleReportCouldFeedSyncPlanners = false;
LiteWalletServerLifecycleReadinessStatus status = LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild;
WalletCapabilities capabilities;
LiteServerSelectionResult selectedServer;
LiteWalletSelectedServerDisplayReport selectedServerDisplay;
LiteWalletServerSelectionPersistencePlan persistencePlan;
LiteWalletLifecycleUiRequestPlan requestPlan;
WalletBackendStatus lifecycleStatus;
std::vector<LiteWalletServerLifecycleReadinessIssueInfo> issues;
std::string error;
};
const char* liteWalletServerLifecycleReadinessStatusName(
LiteWalletServerLifecycleReadinessStatus status);
const char* liteWalletServerLifecycleReadinessIssueName(
LiteWalletServerLifecycleReadinessIssue issue);
const char* liteWalletSelectedServerDisplayStatusName(
LiteWalletSelectedServerDisplayStatus status);
LiteWalletServerLifecycleReadinessResult evaluateLiteWalletServerLifecycleReadiness(
const LiteWalletServerLifecycleReadinessInput& input,
LiteWalletServerLifecycleReadinessOptions options = {});
} // namespace dragonx::wallet
@@ -1,27 +1,10 @@
#include "lite_wallet_server_selection_adapter.h"
#include <algorithm>
#include <cctype>
#include <sstream>
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
std::string trimCopy(const std::string& value)
{
const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
});
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}).base();
if (first >= last) return {};
return std::string(first, last);
}
LiteServerSelectionMode walletModeFromSettings(
config::Settings::LiteServerSelectionPreferenceMode mode)
{
@@ -46,146 +29,6 @@ config::Settings::LiteServerSelectionPreferenceMode settingsModeFromWallet(
return config::Settings::LiteServerSelectionPreferenceMode::Sticky;
}
void addIssue(std::vector<LiteWalletServerSelectionUiExecutionIssueInfo>& issues,
LiteWalletServerSelectionUiExecutionIssue issue,
std::string message)
{
issues.push_back(LiteWalletServerSelectionUiExecutionIssueInfo{issue, std::move(message)});
}
LiteWalletServerSelectionUiExecutionStatus statusFromLifecycle(
LiteWalletServerLifecycleReadinessStatus status)
{
switch (status) {
case LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle:
return LiteWalletServerSelectionUiExecutionStatus::ReadyForFutureLifecycle;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForLiteBuild;
case LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForBackendCapability;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings;
case LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection;
case LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForDisplayStatus;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForLifecycleUi;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForPrivateDataRedaction;
case LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForSyncPlannerFeed;
case LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled:
return LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled;
}
return LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings;
}
LiteWalletServerSelectionUiExecutionIssue issueFromLifecycle(
LiteWalletServerLifecycleReadinessIssue issue)
{
switch (issue) {
case LiteWalletServerLifecycleReadinessIssue::FullNodeBuild:
return LiteWalletServerSelectionUiExecutionIssue::FullNodeBuild;
case LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing:
return LiteWalletServerSelectionUiExecutionIssue::LiteBackendCapabilityMissing;
case LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded:
case LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing:
return LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing:
return LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing;
case LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing:
return LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing;
case LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing:
return LiteWalletServerSelectionUiExecutionIssue::SelectedServerDisplayMissing;
case LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing:
case LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed:
case LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing:
case LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing:
return LiteWalletServerSelectionUiExecutionIssue::LifecycleUiOwnerMissing;
case LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing:
return LiteWalletServerSelectionUiExecutionIssue::PrivateDataRedactionMissing;
case LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing:
return LiteWalletServerSelectionUiExecutionIssue::SyncPlannerFeedMissing;
case LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested:
return LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested;
}
return LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded;
}
void copyLifecycleIssues(LiteWalletServerSelectionUiExecutionResult& result)
{
for (const auto& issue : result.lifecycleReadiness.issues) {
addIssue(result.issues, issueFromLifecycle(issue.issue), issue.message);
}
}
LiteWalletServerSelectionUiExecutionResult stoppedResult(
LiteWalletServerSelectionUiExecutionResult result,
LiteWalletServerSelectionUiExecutionStatus status,
LiteWalletServerSelectionUiExecutionIssue issue,
const std::string& message)
{
result.status = status;
result.error = message;
result.ok = false;
addIssue(result.issues, issue, message);
return result;
}
std::string buildDiagnosticSummary(const LiteWalletServerSelectionUiExecutionResult& result)
{
std::ostringstream out;
out << "mode=" << liteServerSelectionModeName(result.connectionSettings.selectionMode)
<< ";server=" << result.selectedServerUrlRedacted
<< ";settings_written=" << (result.settingsWritten ? "true" : "false")
<< ";network=false;bridge=false;lifecycle=false;sync=false;wallet_state=false";
return out.str();
}
LiteConnectionSettings settingsWithIntent(const LiteConnectionSettings& current,
const LiteWalletServerSelectionUiIntent& intent)
{
LiteConnectionSettings settings = current;
if (!intent.selectedServerIntentProvided) return settings;
settings.selectionMode = intent.selectionMode;
if (!intent.chainName.empty()) settings.chainName = trimCopy(intent.chainName);
if (intent.replaceServers) settings.servers = intent.servers;
switch (intent.selectionMode) {
case LiteServerSelectionMode::Sticky:
settings.stickyServerUrl = trimCopy(intent.selectedServerUrl);
break;
case LiteServerSelectionMode::Random:
settings.randomSelectionSeed = intent.randomSelectionSeed;
break;
}
return settings;
}
LiteWalletServerLifecycleReadinessInput makeLifecycleInput(
const LiteWalletServerSelectionUiExecutionInput& input,
const LiteConnectionSettings& connectionSettings)
{
LiteWalletServerLifecycleReadinessInput lifecycle;
lifecycle.capabilities = input.capabilities;
lifecycle.settings = connectionSettings;
lifecycle.operation = input.operation;
lifecycle.persistence.settingsLoaded = input.persistence.settingsLoaded;
lifecycle.persistence.havePersistedSelectionIntent =
input.persistence.havePersistedSelectionIntent || input.intent.selectedServerIntentProvided;
lifecycle.persistence.persistSelectedServer = input.persistence.persistSelectedServer;
lifecycle.persistence.persistenceOwnerReady = input.persistence.persistenceOwnerReady;
lifecycle.ui = input.ui;
lifecycle.ui.realLifecycleExecutionRequested =
lifecycle.ui.realLifecycleExecutionRequested || input.walletLifecycleExecutionRequested;
lifecycle.createRequest = input.createRequest;
lifecycle.openRequest = input.openRequest;
lifecycle.restoreRequest = input.restoreRequest;
return lifecycle;
}
} // namespace
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings)
@@ -194,14 +37,14 @@ LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Setti
connectionSettings.servers.clear();
for (const auto& server : settings.getLiteServers()) {
connectionSettings.servers.push_back(LiteServerEndpoint{
trimCopy(server.url),
liteTrimCopy(server.url),
server.label,
server.enabled
});
}
connectionSettings.selectionMode = walletModeFromSettings(settings.getLiteServerSelectionMode());
connectionSettings.stickyServerUrl = trimCopy(settings.getLiteStickyServerUrl());
connectionSettings.chainName = trimCopy(settings.getLiteChainName());
connectionSettings.stickyServerUrl = liteTrimCopy(settings.getLiteStickyServerUrl());
connectionSettings.chainName = liteTrimCopy(settings.getLiteChainName());
if (connectionSettings.chainName.empty()) connectionSettings.chainName = kDragonXLiteChainName;
connectionSettings.randomSelectionSeed = settings.getLiteRandomSelectionSeed();
return connectionSettings;
@@ -214,238 +57,29 @@ void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
servers.reserve(connectionSettings.servers.size());
for (const auto& server : connectionSettings.servers) {
servers.push_back(config::Settings::LiteServerPreference{
trimCopy(server.url),
liteTrimCopy(server.url),
server.label,
server.enabled
});
}
settings.setLiteServerSelectionMode(settingsModeFromWallet(connectionSettings.selectionMode));
settings.setLiteStickyServerUrl(trimCopy(connectionSettings.stickyServerUrl));
settings.setLiteChainName(trimCopy(connectionSettings.chainName).empty()
settings.setLiteStickyServerUrl(liteTrimCopy(connectionSettings.stickyServerUrl));
settings.setLiteChainName(liteTrimCopy(connectionSettings.chainName).empty()
? kDragonXLiteChainName
: trimCopy(connectionSettings.chainName));
: liteTrimCopy(connectionSettings.chainName));
settings.setLiteRandomSelectionSeed(connectionSettings.randomSelectionSeed);
settings.setLiteServers(servers);
}
const char* liteWalletServerSelectionUiExecutionStatusName(
LiteWalletServerSelectionUiExecutionStatus status)
{
switch (status) {
case LiteWalletServerSelectionUiExecutionStatus::ReadyForFutureLifecycle:
return "ReadyForFutureLifecycle";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLiteBuild:
return "WaitingForLiteBuild";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForBackendCapability:
return "WaitingForBackendCapability";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings:
return "WaitingForSettings";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection:
return "WaitingForServerSelection";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPersistenceOwner:
return "WaitingForPersistenceOwner";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForDisplayStatus:
return "WaitingForDisplayStatus";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLifecycleUi:
return "WaitingForLifecycleUi";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPrivateDataRedaction:
return "WaitingForPrivateDataRedaction";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSyncPlannerFeed:
return "WaitingForSyncPlannerFeed";
case LiteWalletServerSelectionUiExecutionStatus::SettingsSaveFailed:
return "SettingsSaveFailed";
case LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled:
return "RuntimeExecutionDisabled";
}
return "WaitingForSettings";
}
const char* liteWalletServerSelectionUiExecutionIssueName(
LiteWalletServerSelectionUiExecutionIssue issue)
{
switch (issue) {
case LiteWalletServerSelectionUiExecutionIssue::FullNodeBuild:
return "FullNodeBuild";
case LiteWalletServerSelectionUiExecutionIssue::LiteBackendCapabilityMissing:
return "LiteBackendCapabilityMissing";
case LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded:
return "SettingsNotLoaded";
case LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing:
return "ServerSelectionMissing";
case LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing:
return "ServerPersistenceOwnerMissing";
case LiteWalletServerSelectionUiExecutionIssue::SelectedServerDisplayMissing:
return "SelectedServerDisplayMissing";
case LiteWalletServerSelectionUiExecutionIssue::LifecycleUiOwnerMissing:
return "LifecycleUiOwnerMissing";
case LiteWalletServerSelectionUiExecutionIssue::PrivateDataRedactionMissing:
return "PrivateDataRedactionMissing";
case LiteWalletServerSelectionUiExecutionIssue::SyncPlannerFeedMissing:
return "SyncPlannerFeedMissing";
case LiteWalletServerSelectionUiExecutionIssue::SettingsSaveFailed:
return "SettingsSaveFailed";
case LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested:
return "ServerConnectivityCheckRequested";
case LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested:
return "WalletLifecycleExecutionRequested";
case LiteWalletServerSelectionUiExecutionIssue::SyncRequested:
return "SyncRequested";
case LiteWalletServerSelectionUiExecutionIssue::SyncStatusPollingRequested:
return "SyncStatusPollingRequested";
case LiteWalletServerSelectionUiExecutionIssue::WorkerQueueRequested:
return "WorkerQueueRequested";
case LiteWalletServerSelectionUiExecutionIssue::WalletStateMutationRequested:
return "WalletStateMutationRequested";
case LiteWalletServerSelectionUiExecutionIssue::WalletFilePersistenceRequested:
return "WalletFilePersistenceRequested";
case LiteWalletServerSelectionUiExecutionIssue::SendImportExportRequested:
return "SendImportExportRequested";
}
return "SettingsNotLoaded";
}
std::string redactLiteServerSelectionValue(const std::string& value)
{
const std::string trimmed = trimCopy(value);
const std::string trimmed = liteTrimCopy(value);
if (trimmed.empty()) return "<empty>";
const auto scheme = trimmed.find("://");
if (scheme == std::string::npos) return "<redacted>";
return trimmed.substr(0, scheme + 3) + "<redacted>";
}
LiteWalletServerSelectionUiExecutionResult executeLiteWalletServerSelectionUi(
config::Settings& settings,
const LiteWalletServerSelectionUiExecutionInput& input)
{
LiteWalletServerSelectionUiExecutionResult result;
auto stopForRuntime = [&](LiteWalletServerSelectionUiExecutionIssue issue,
const std::string& message) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled,
issue,
message);
};
if (input.serverConnectivityCheckRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested,
"lite server selection settings adapter cannot check server connectivity");
}
if (input.walletExistsCheckRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested,
"lite server selection settings adapter cannot check wallet existence");
}
if (input.walletLifecycleExecutionRequested || input.ui.realLifecycleExecutionRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested,
"lite server selection settings adapter cannot execute wallet lifecycle actions");
}
if (input.syncStartRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::SyncRequested,
"lite server selection settings adapter cannot start sync");
}
if (input.syncStatusPollingRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::SyncStatusPollingRequested,
"lite server selection settings adapter cannot poll syncstatus");
}
if (input.workerQueueEnqueueRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WorkerQueueRequested,
"lite server selection settings adapter cannot enqueue wallet workers");
}
if (input.walletStateMutationRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WalletStateMutationRequested,
"lite server selection settings adapter cannot mutate WalletState");
}
if (input.walletFilePersistenceRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WalletFilePersistenceRequested,
"lite server selection settings adapter cannot persist wallet files");
}
if (input.sendImportExportExecutionRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::SendImportExportRequested,
"lite server selection settings adapter cannot execute send/import/export flows");
}
if (!input.persistence.settingsLoaded) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings,
LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded,
"lite server settings are not loaded");
}
result.connectionSettings = settingsWithIntent(
liteConnectionSettingsFromAppSettings(settings), input.intent);
if (input.intent.selectedServerIntentProvided &&
input.intent.selectionMode == LiteServerSelectionMode::Sticky &&
!isLiteServerUrlUsable(trimCopy(input.intent.selectedServerUrl))) {
result.selectedServerUrlRedacted = redactLiteServerSelectionValue(input.intent.selectedServerUrl);
result.selectedServerRedacted = true;
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection,
LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing,
"lite server selection URL is not usable");
}
result.selectedServer = selectLiteServer(result.connectionSettings);
result.selectedServerUrlRedacted = result.selectedServer.ok
? redactLiteServerSelectionValue(result.selectedServer.server.url)
: redactLiteServerSelectionValue(result.connectionSettings.stickyServerUrl);
result.selectedServerRedacted = true;
if (!result.selectedServer.ok) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection,
LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing,
result.selectedServer.error.empty()
? "lite server selection is missing"
: result.selectedServer.error);
}
result.selectedServerIntentAccepted = true;
if ((input.persistence.persistSelectedServer || input.persistence.writeSettings) &&
!input.persistence.persistenceOwnerReady) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForPersistenceOwner,
LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing,
"lite server settings persistence owner is not ready");
}
result.settingsPersistenceAccepted = true;
if (input.persistence.persistSelectedServer || input.persistence.writeSettings) {
applyLiteConnectionSettingsToAppSettings(settings, result.connectionSettings);
settings.setLitePersistSelectedServer(input.persistence.persistSelectedServer);
result.settingsMutated = true;
}
if (input.persistence.writeSettings) {
result.noSettingsPersistence = false;
const bool saved = input.persistence.settingsSavePath.empty()
? settings.save()
: settings.save(input.persistence.settingsSavePath);
if (!saved) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::SettingsSaveFailed,
LiteWalletServerSelectionUiExecutionIssue::SettingsSaveFailed,
"failed to save lite server settings");
}
result.settingsWritten = true;
}
result.lifecycleInput = makeLifecycleInput(input, result.connectionSettings);
result.lifecycleReadiness = evaluateLiteWalletServerLifecycleReadiness(result.lifecycleInput);
result.lifecycleReadinessAccepted = result.lifecycleReadiness.ok;
result.lifecycleReportCouldFeedSyncPlanners = result.lifecycleReadiness.lifecycleReportCouldFeedSyncPlanners;
result.status = statusFromLifecycle(result.lifecycleReadiness.status);
copyLifecycleIssues(result);
result.diagnosticSummary = buildDiagnosticSummary(result);
if (input.requireLifecycleReadiness && !result.lifecycleReadiness.ok) {
result.ok = false;
result.error = result.lifecycleReadiness.error;
return result;
}
result.ok = true;
return result;
}
} // namespace wallet
} // namespace dragonx
} // namespace dragonx
@@ -1,150 +1,19 @@
#pragma once
#include "lite_connection_service.h"
#include "lite_wallet_server_lifecycle_readiness.h"
#include "wallet_capabilities.h"
#include "../config/settings.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
enum class LiteWalletServerSelectionUiExecutionStatus {
ReadyForFutureLifecycle,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForSettings,
WaitingForServerSelection,
WaitingForPersistenceOwner,
WaitingForDisplayStatus,
WaitingForLifecycleUi,
WaitingForPrivateDataRedaction,
WaitingForSyncPlannerFeed,
SettingsSaveFailed,
RuntimeExecutionDisabled
};
enum class LiteWalletServerSelectionUiExecutionIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
SettingsNotLoaded,
ServerSelectionMissing,
ServerPersistenceOwnerMissing,
SelectedServerDisplayMissing,
LifecycleUiOwnerMissing,
PrivateDataRedactionMissing,
SyncPlannerFeedMissing,
SettingsSaveFailed,
ServerConnectivityCheckRequested,
WalletLifecycleExecutionRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletFilePersistenceRequested,
SendImportExportRequested
};
struct LiteWalletServerSelectionUiIntent {
bool selectedServerIntentProvided = false;
LiteServerSelectionMode selectionMode = LiteServerSelectionMode::Sticky;
std::string selectedServerUrl;
std::size_t randomSelectionSeed = 0;
std::string chainName;
bool replaceServers = false;
std::vector<LiteServerEndpoint> servers;
};
struct LiteWalletServerSelectionPersistenceExecutionInput {
bool settingsLoaded = true;
bool havePersistedSelectionIntent = true;
bool persistSelectedServer = true;
bool persistenceOwnerReady = true;
bool writeSettings = false;
std::string settingsSavePath;
};
struct LiteWalletServerSelectionUiExecutionInput {
WalletCapabilities capabilities;
LiteWalletServerSelectionUiIntent intent;
LiteWalletServerSelectionPersistenceExecutionInput persistence;
LiteWalletLifecycleUiPrerequisites ui;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletCreateRequest createRequest;
LiteWalletOpenRequest openRequest;
LiteWalletRestoreRequest restoreRequest;
bool requireLifecycleReadiness = false;
bool serverConnectivityCheckRequested = false;
bool walletExistsCheckRequested = false;
bool walletLifecycleExecutionRequested = false;
bool syncStartRequested = false;
bool syncStatusPollingRequested = false;
bool workerQueueEnqueueRequested = false;
bool walletStateMutationRequested = false;
bool walletFilePersistenceRequested = false;
bool sendImportExportExecutionRequested = false;
};
struct LiteWalletServerSelectionUiExecutionIssueInfo {
LiteWalletServerSelectionUiExecutionIssue issue = LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded;
std::string message;
};
struct LiteWalletServerSelectionUiExecutionResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noServerHealthChecked = true;
bool noWalletExistsChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool noSendImportExportExecution = true;
bool workerQueueEnqueued = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool settingsMutationAllowed = true;
bool settingsMutated = false;
bool noSettingsPersistence = true;
bool settingsWritten = false;
bool settingsPersistenceAccepted = false;
bool selectedServerIntentAccepted = false;
bool lifecycleReadinessAccepted = false;
bool lifecycleReportCouldFeedSyncPlanners = false;
bool selectedServerRedacted = false;
LiteWalletServerSelectionUiExecutionStatus status = LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings;
std::string error;
std::string selectedServerUrlRedacted;
std::string diagnosticSummary;
LiteConnectionSettings connectionSettings;
LiteServerSelectionResult selectedServer;
LiteWalletServerLifecycleReadinessInput lifecycleInput;
LiteWalletServerLifecycleReadinessResult lifecycleReadiness;
std::vector<LiteWalletServerSelectionUiExecutionIssueInfo> issues;
};
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings);
void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
const LiteConnectionSettings& connectionSettings);
const char* liteWalletServerSelectionUiExecutionStatusName(
LiteWalletServerSelectionUiExecutionStatus status);
const char* liteWalletServerSelectionUiExecutionIssueName(
LiteWalletServerSelectionUiExecutionIssue issue);
std::string redactLiteServerSelectionValue(const std::string& value);
LiteWalletServerSelectionUiExecutionResult executeLiteWalletServerSelectionUi(
config::Settings& settings,
const LiteWalletServerSelectionUiExecutionInput& input);
} // namespace wallet
} // namespace dragonx
} // namespace dragonx
+184
View File
@@ -40,6 +40,7 @@
#include "config/settings.h"
#include "data/wallet_state.h"
#include "data/portfolio.h"
#include "data/market_series.h"
#include "fake_lite_backend.h"
#include <chrono>
@@ -1051,6 +1052,99 @@ void testNetworkRefreshSnapshotHelpers()
EXPECT_EQ(snapshot.sendTxids.count("pending-send"), static_cast<size_t>(1));
}
void testParseDaemonRescanOutput()
{
using dragonx::services::NetworkRefreshService;
// "Still rescanning ... Progress=<frac>" — fraction scaled to a percentage.
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
"2026-07-01 Still rescanning at height 500000. Progress=0.42 tip=600000\n");
EXPECT_TRUE(scan.foundRescan);
EXPECT_NEAR(scan.rescanPct, 42.0f, 0.01f);
}
// "Rescanning... <n>%" — percentage read directly (already 0..100).
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
"Rescanning... 37% done\n");
EXPECT_TRUE(scan.foundRescan);
EXPECT_NEAR(scan.rescanPct, 37.0f, 0.01f);
}
// Explicit completion line marks finished.
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput("Done rescanning\n");
EXPECT_TRUE(scan.finished);
}
// The daemon that prints no "Done rescanning" logs a rescan benchmark ending in "<digits>ms".
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
" rescan 16760577ms\n");
EXPECT_TRUE(scan.finished);
}
// "Building Witnesses for block <h> <frac> complete, <n> remaining".
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
"Building Witnesses for block 123 0.5 complete, 40 remaining\n");
EXPECT_TRUE(scan.foundWitness);
EXPECT_NEAR(scan.witnessPct, 50.0f, 0.01f);
EXPECT_EQ(scan.witnessRemaining, 40);
}
// "Reading blocks for witness rebuild: <done> / <total>" — captures total and remaining.
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
"Reading blocks for witness rebuild: 10 / 200\n");
EXPECT_TRUE(scan.foundWitness);
EXPECT_EQ(scan.witnessReadTotal, 200);
EXPECT_EQ(scan.witnessRemaining, 190); // total - done
}
// Parallel-rebuild completion line snaps witnessRebuilt=true.
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
"rebuilt 3 note witness cache(s) to height 99 in 12ms using 4 thread(s)\n");
EXPECT_TRUE(scan.foundWitness);
EXPECT_TRUE(scan.witnessRebuilt);
}
// "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" — distinct hashes, max N.
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput(
"Setting Initial Sapling Witness for tx abc123, 2 of 7\n"
"Setting Initial Sapling Witness for tx def456, 3 of 7\n"
"Setting Initial Sapling Witness for tx abc123, 5 of 7\n");
EXPECT_TRUE(scan.foundWitness);
// The parser collects every occurrence (dedup happens in the apply step's set), so the
// vector has all three keys but only two distinct values.
EXPECT_EQ(scan.witnessTxids.size(), static_cast<size_t>(3));
std::set<std::string> distinct(scan.witnessTxids.begin(), scan.witnessTxids.end());
EXPECT_EQ(distinct.size(), static_cast<size_t>(2));
EXPECT_EQ(distinct.count("abc123"), static_cast<size_t>(1));
EXPECT_EQ(distinct.count("def456"), static_cast<size_t>(1));
EXPECT_EQ(scan.witnessTotalTxs, 7);
}
// Empty input yields all defaults.
{
auto scan = NetworkRefreshService::parseDaemonRescanOutput("");
EXPECT_FALSE(scan.foundRescan);
EXPECT_FALSE(scan.finished);
EXPECT_NEAR(scan.rescanPct, 0.0f, 0.0001f);
EXPECT_TRUE(scan.lastStatus.empty());
EXPECT_FALSE(scan.foundWitness);
EXPECT_NEAR(scan.witnessPct, -1.0f, 0.0001f);
EXPECT_EQ(scan.witnessRemaining, -1);
EXPECT_EQ(scan.witnessReadTotal, -1);
EXPECT_FALSE(scan.witnessRebuilt);
EXPECT_TRUE(scan.witnessTxids.empty());
EXPECT_EQ(scan.witnessTotalTxs, -1);
}
}
void testNetworkRefreshRpcCollectors()
{
using Refresh = dragonx::services::NetworkRefreshService;
@@ -1803,6 +1897,26 @@ void testNetworkRefreshResultModels()
EXPECT_FALSE(priceHttpParseResult.price.has_value());
EXPECT_EQ(priceHttpParseResult.errorMessage, std::string("Price fetch returned an unrecognized response"));
// CoinGecko market_chart parser: [ms, price] rows -> (seconds, price), oldest->newest.
auto chart = Refresh::parseCoinGeckoMarketChart(
R"({"prices":[[1700000000000,1.5],[1700003600000,1.6],[1700007200000,1.55]],)"
R"("market_caps":[[1700000000000,100]],"total_volumes":[[1700000000000,10]]})");
EXPECT_EQ(chart.size(), static_cast<size_t>(3));
if (chart.size() == 3) {
EXPECT_EQ(chart[0].first, static_cast<std::time_t>(1700000000));
EXPECT_NEAR(chart[0].second, 1.5, 0.00000001);
EXPECT_EQ(chart[2].first, static_cast<std::time_t>(1700007200));
EXPECT_NEAR(chart[2].second, 1.55, 0.00000001);
}
// Malformed rows are skipped; a good row still comes through.
auto chartSkip = Refresh::parseCoinGeckoMarketChart(
R"({"prices":[[1700000000000],["x",1.0],[1700003600000,2.0]]})");
EXPECT_EQ(chartSkip.size(), static_cast<size_t>(1));
if (chartSkip.size() == 1) EXPECT_NEAR(chartSkip[0].second, 2.0, 0.00000001);
// Missing/!array "prices" and invalid JSON both yield an empty series.
EXPECT_EQ(Refresh::parseCoinGeckoMarketChart(R"({"nope":[]})").size(), static_cast<size_t>(0));
EXPECT_EQ(Refresh::parseCoinGeckoMarketChart("not json").size(), static_cast<size_t>(0));
Refresh::AddressRefreshResult addresses;
dragonx::AddressInfo zAddr;
zAddr.address = "zs-refresh";
@@ -2622,6 +2736,74 @@ void testPortfolioHelpers()
EXPECT_EQ(group[0], std::string("R-t2"));
}
// Market price-series math (data/market_series.h) — minute-resampling + time-window bucketing
// that feed the price chart and the portfolio-group sparklines.
void testMarketSeries()
{
using dragonx::MarketInfo;
using dragonx::data::resampleHistory;
using dragonx::data::bucketBySeconds;
using dragonx::data::sparklineSeries;
using dragonx::data::chartSeries;
// resampleHistory: interval 0 (minute) returns the input untouched.
std::vector<double> minute = {1.0, 2.0, 3.0, 4.0};
EXPECT_EQ(resampleHistory(minute, 0).size(), static_cast<size_t>(4));
EXPECT_NEAR(resampleHistory(minute, 0)[3], 4.0, 1e-9);
// interval 1 (hour = 60 minute-samples): 120 samples -> 2 averaged points.
std::vector<double> longHist(120, 0.0);
for (int i = 0; i < 120; i++) longHist[i] = (i < 60) ? 10.0 : 20.0;
auto hourly = resampleHistory(longHist, 1);
EXPECT_EQ(hourly.size(), static_cast<size_t>(2));
EXPECT_NEAR(hourly[0], 10.0, 1e-9);
EXPECT_NEAR(hourly[1], 20.0, 1e-9);
// A trailing partial block still averages (61 samples -> 2 points).
EXPECT_EQ(resampleHistory(std::vector<double>(61, 5.0), 1).size(), static_cast<size_t>(2));
// Empty input -> empty output.
EXPECT_TRUE(resampleHistory({}, 2).empty());
// bucketBySeconds: guards + averaging within fixed windows.
EXPECT_TRUE(bucketBySeconds({}, 3600).empty());
EXPECT_TRUE(bucketBySeconds({{0, 1.0}}, 0).empty());
std::vector<std::pair<std::time_t, double>> s = {
{0, 10.0}, {1000, 20.0}, // bucket 0 (< 3600s) -> avg 15
{3600, 40.0}, {7000, 60.0}, // bucket 1 -> avg 50
};
auto b = bucketBySeconds(s, 3600);
EXPECT_EQ(b.size(), static_cast<size_t>(2));
EXPECT_NEAR(b[0], 15.0, 1e-9);
EXPECT_NEAR(b[1], 50.0, 1e-9);
// sparklineSeries: hour interval buckets the intraday series...
MarketInfo m;
m.price_history = {1.0, 2.0, 3.0};
m.price_chart_intraday = { {0, 10.0}, {100, 12.0}, {3600, 20.0}, {3700, 22.0} }; // 2 hourly buckets
EXPECT_EQ(sparklineSeries(m, 1).size(), static_cast<size_t>(2));
// ...interval 0 (minute) always uses the live buffer...
EXPECT_EQ(sparklineSeries(m, 0).size(), static_cast<size_t>(3));
// ...and with no historical series, an interval falls back to the resampled live buffer.
MarketInfo bare; bare.price_history = std::vector<double>(120, 7.0);
EXPECT_EQ(sparklineSeries(bare, 1).size(), static_cast<size_t>(2));
// chartSeries: the 1H window keeps only samples within the last hour of `now`.
std::time_t now = 100000;
MarketInfo cm;
cm.price_chart_intraday = {
{now - 7200, 1.0}, // 2h ago -> excluded from 1H
{now - 1800, 2.0}, // 30m ago -> included
{now - 60, 3.0}, // 1m ago -> included
};
auto oneH = chartSeries(cm, 1, now);
EXPECT_EQ(oneH.size(), static_cast<size_t>(2));
EXPECT_NEAR(oneH[0].second, 2.0, 1e-9);
// Live (interval 0) synthesizes timestamps from the minute buffer, newest at `now`.
MarketInfo lm; lm.price_history = {1.0, 2.0, 3.0};
auto live = chartSeries(lm, 0, now);
EXPECT_EQ(live.size(), static_cast<size_t>(3));
EXPECT_EQ(static_cast<long>(live.back().first), static_cast<long>(now));
EXPECT_NEAR(live.back().second, 3.0, 1e-9);
}
// Regression: the Market-tab portfolio (and other consumers) read the combined
// WalletState::addresses view, which the full-node refresh path builds from the
// authoritative z/t lists via rebuildAddressList(). Before the fix that rebuild
@@ -5381,6 +5563,7 @@ int main()
testRefreshScheduler();
testNetworkRefreshService();
testNetworkRefreshSnapshotHelpers();
testParseDaemonRescanOutput();
testNetworkRefreshRpcCollectors();
testNetworkRefreshResultModels();
testOperationStatusPollParsing();
@@ -5393,6 +5576,7 @@ int main()
testConsoleTextLayout();
testConsoleModel();
testPortfolioHelpers();
testMarketSeries();
testWalletStateAddressListRebuild();
testConsoleFoldSpans();
testConsoleScrollController();
+1 -1
View File
@@ -1,4 +1,4 @@
#include "chat/chat_protocol.h"
#include "chat/chat_fixture_tooling.h"
#include <algorithm>
#include <filesystem>