Compare commits
103
Commits
530bf24abf
...
ee6903ae56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6903ae56 | ||
|
|
1c5bc0d4d6 | ||
|
|
dfca9bbd92 | ||
|
|
840ead3645 | ||
|
|
1c8e97c0a4 | ||
|
|
f1e66c08d1 | ||
|
|
c83348ab63 | ||
|
|
da56bb73a0 | ||
|
|
ad59d62b82 | ||
|
|
07f6d2efc9 | ||
|
|
e84cea27d9 | ||
|
|
a457f0262e | ||
|
|
4c466d78d1 | ||
|
|
0d4eaa02e7 | ||
|
|
dddf1b6ec7 | ||
|
|
a523611779 | ||
|
|
7c90d72ded | ||
|
|
bb2337b552 | ||
|
|
19e31388d9 | ||
|
|
30139a5698 | ||
|
|
94b57e91e3 | ||
|
|
10289b33b5 | ||
|
|
42df658275 | ||
|
|
9d2c4a8ad3 | ||
|
|
65c06d0e6b | ||
|
|
c7f89e5cd3 | ||
|
|
34ded6b441 | ||
|
|
0ea423dcea | ||
|
|
61738ef24a | ||
|
|
ccbb0783b5 | ||
|
|
c6b32e86c0 | ||
|
|
5caffc84b0 | ||
|
|
ebf01bd858 | ||
|
|
d91be3b34e | ||
|
|
4b46d4a20c | ||
|
|
69cd8ba048 | ||
|
|
4543cdc495 | ||
|
|
71fb81670f | ||
|
|
a092097141 | ||
|
|
50873d1112 | ||
|
|
7e8628f94c | ||
|
|
0ae52ee095 | ||
|
|
529a74f6a7 | ||
|
|
88f1415bcc | ||
|
|
b8cfc1c79c | ||
|
|
a0fd9aff82 | ||
|
|
14b08d1358 | ||
|
|
d689b02de9 | ||
|
|
84498b1312 | ||
|
|
8c2b0521a6 | ||
|
|
4411cb45e2 | ||
|
|
7db6564a0e | ||
|
|
faf8e4e430 | ||
|
|
1affea0273 | ||
|
|
32d8d5663a | ||
|
|
3568b12dbf | ||
|
|
e74989ec2c | ||
|
|
fe18628216 | ||
|
|
4dc6b2ef8f | ||
|
|
5afb339ba7 | ||
|
|
0e8816e220 | ||
|
|
2035f71c65 | ||
|
|
d007f3de39 | ||
|
|
1fdb6656aa | ||
|
|
3305be6318 | ||
|
|
19482214f1 | ||
|
|
4aed72c73b | ||
|
|
3e6987d72f | ||
|
|
42c511b13a | ||
|
|
ef9141c248 | ||
|
|
8c88afe16e | ||
|
|
d48c6db21c | ||
|
|
ced1773e70 | ||
|
|
d76c109063 | ||
|
|
5aa3cc4d51 | ||
|
|
4e15a2b1ba | ||
|
|
021ba88522 | ||
|
|
c1d5f79502 | ||
|
|
db212a81c6 | ||
|
|
67f6156030 | ||
|
|
76ed09fad0 | ||
|
|
77219c5ce7 | ||
|
|
b26aff392b | ||
|
|
b0ded528b1 | ||
|
|
301b3df77e | ||
|
|
ae4e6486c6 | ||
|
|
d838243caa | ||
|
|
75f33e41eb | ||
|
|
1263058ba7 | ||
|
|
9bcdf51895 | ||
|
|
67feaea767 | ||
|
|
3e1cdd15f3 | ||
|
|
af2db4366c | ||
|
|
cb74fbbff0 | ||
|
|
121a79e768 | ||
|
|
262891229a | ||
|
|
dcd09dc542 | ||
|
|
4635a56e89 | ||
|
|
9c533fa485 | ||
|
|
de11850c74 | ||
|
|
280a71e973 | ||
|
|
7891c689fb | ||
|
|
f8150434d4 |
+3
-7
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+1
-1572
File diff suppressed because it is too large
Load Diff
+1
-482
@@ -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
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1860
-645
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,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"};
|
||||
|
||||
@@ -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,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"};
|
||||
|
||||
@@ -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"};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
#include "chat/chat_protocol.h"
|
||||
#include "chat/chat_fixture_tooling.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
Reference in New Issue
Block a user